Compare commits

..

1445 Commits

Author SHA1 Message Date
Peter Steinberger 936f2449bd chore(release): prep 2026.2.23-beta.1 changelog
Docker Release / build-amd64 (push) Has been cancelled
Docker Release / build-arm64 (push) Has been cancelled
Docker Release / create-manifest (push) Has been cancelled
2026-02-24 05:02:40 +00:00
Peter Steinberger fd10286819 docs(changelog): mark allowFrom id-only default as breaking 2026-02-24 04:47:36 +00:00
Peter Steinberger 91ea6ad8ec docs(changelog): reorder unreleased fixes by user impact 2026-02-24 04:46:19 +00:00
Arturo 10cd4b5e68 chore: credit PR #24705 contributor attribution
Attribution-only commit for the bot-authored upstream patch landed from #24705.
2026-02-24 04:44:11 +00:00
Peter Steinberger ee42381951 chore: add mailmap mappings for cherry-picked contributors 2026-02-24 04:43:28 +00:00
Peter Steinberger 31f2bf9519 test: fix gate regressions 2026-02-24 04:39:53 +00:00
Peter Steinberger 2d6d6797d8 test: fix post-merge config and tui command-handler tests 2026-02-24 04:38:21 +00:00
justinhuangcode 6ea1607f1c test(discord): add regression tests for reasoning tag stripping in stream
Verify that partial stream updates containing <thinking> tags are stripped
before reaching the draft preview, and that pure "Reasoning:\n" partials
are suppressed entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:37:30 +00:00
justinhuangcode e8a4d5d9bd fix(discord): strip reasoning tags from partial stream preview
When streamMode is "partial", reasoning/thinking block content can leak
into the Discord draft preview because the partial text is forwarded to
the draft stream without filtering.  Apply `stripReasoningTagsFromText`
before updating the draft and skip pure-reasoning messages (those
starting with "Reasoning:\n") so internal thinking traces never reach
the user-visible preview.

Fixes #24532

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:37:30 +00:00
justinhuangcode 0ded77ca7d test(matrix): add regression tests for reasoning-only reply filtering
Verify that deliverMatrixReplies skips replies whose text starts with
"Reasoning:\n" or opens with <thinking>/<think>/<antthinking> tags, while
still delivering all normal replies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:36:27 +00:00
justinhuangcode 1298bd4e1b fix(matrix): skip reasoning-only messages in reply delivery
When `includeReasoning` is active (or `reasoningLevel` falls back to the
model default), the agent emits reasoning blocks as separate reply
payloads prefixed with "Reasoning:\n".  Matrix has no dedicated reasoning
lane, so these internal thinking traces leak into the chat as regular
user-visible messages.

Filter out pure-reasoning payloads (those starting with "Reasoning:\n" or
a `<thinking>` tag) before delivery so internal reasoning never reaches
the Matrix room.

Fixes #24411

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:36:27 +00:00
Peter Steinberger 5ac70b36a4 test: make shell-env trust-path test platform-safe (#24991) (thanks @stakeswky) 2026-02-24 04:34:49 +00:00
Peter Steinberger d3ecc234da test: align flaky CI expectations after main changes (#24991) (thanks @stakeswky) 2026-02-24 04:34:49 +00:00
Peter Steinberger cb450fd31f fix: align lockfile with diagnostics-otel proto deps (#24991) (thanks @stakeswky) 2026-02-24 04:34:49 +00:00
Peter Steinberger 2880fb3cb8 fix: sync lockfile for diagnostics-otel deps (#24991) (thanks @stakeswky) 2026-02-24 04:34:49 +00:00
Peter Steinberger 19d0ddc679 fix: regenerate protocol swift models for nodeId (#24991) (thanks @stakeswky) 2026-02-24 04:34:49 +00:00
Peter Steinberger d427d09b5e fix: align reasoning payload typing for #24991 (thanks @stakeswky) 2026-02-24 04:34:49 +00:00
User 7d76c241f8 fix: suppress reasoning payloads from generic channel dispatch path
When reasoningLevel is 'on', reasoning content was being sent as a
visible message to WhatsApp and other non-Telegram channels via two
paths:
1. Block reply: emitted via onBlockReply in handleMessageEnd
2. Final payloads: added to replyItems in buildEmbeddedRunPayloads

Telegram has its own dispatch path (bot-message-dispatch.ts) that
splits reasoning into a dedicated lane and handles suppression.
The generic dispatch-from-config.ts path used by WhatsApp, web, etc.
had no such filtering.

Fix:
- Add isReasoning?: boolean flag to ReplyPayload
- Tag reasoning payloads at both emission points
- Filter isReasoning payloads in dispatch-from-config.ts for both
  block reply and final reply paths

Telegram is unaffected: it uses its own deliver callback that detects
reasoning via the 'Reasoning:\n' prefix and routes to a separate lane.

Fixes #24954
2026-02-24 04:34:49 +00:00
Workweaver Ralph b9e587fb63 fix(tui): guard sendMessage when disconnected; reset readyPromise on close
(cherry picked from commit df827c3eef34ca02cfe5c57a1eabcd9c8e5a4ec1)
2026-02-24 04:33:51 +00:00
Shennan a7518b7589 fix(feishu): pass parentPeer for topic session binding inheritance
(cherry picked from commit bddeb1fd95d10cf18da9dca129b58828eae84cba)
2026-02-24 04:33:51 +00:00
Marco Di Dionisio 83689fc838 fix: include trusted-proxy in sharedAuthOk check
In trusted-proxy mode, sharedAuthResult is null because hasSharedAuth
only triggers for token/password in connectParams.auth. But the primary
auth (authResult) already validated the trusted-proxy — the connection
came from a CIDR in trustedProxies with a valid userHeader. This IS
shared auth semantically (the proxy vouches for identity), so operator
connections should be able to skip device identity.

Without this fix, trusted-proxy operator connections are rejected with
"device identity required" because roleCanSkipDeviceIdentity() sees
sharedAuthOk=false.

(cherry picked from commit e87048a6a650d391e1eb5704546eb49fac5f0091)
2026-02-24 04:33:51 +00:00
zerone0x bc52d4a459 fix(openrouter): skip reasoning effort injection for 'auto' routing model
The 'auto' model on OpenRouter dynamically routes to any underlying model
OpenRouter selects, including reasoning-required endpoints. Previously,
OpenClaw would unconditionally inject `reasoning.effort: "none"` into
every request when the thinking level was "off", which causes a 400 error
on models where reasoning is mandatory and cannot be disabled.

Root cause:
- openrouter/auto has reasoning: false in the built-in catalog
- With thinking level "off", createOpenRouterWrapper injects
  `reasoning: { effort: "none" }` via mapThinkingLevelToOpenRouterReasoningEffort
- For any OpenRouter-routed model that requires reasoning this results in:
  "400 Reasoning is mandatory for this endpoint and cannot be disabled"
- The reasoning: false is then persisted back to models.json on every
  ensureOpenClawModelsJson call, so manually removing it has no lasting effect

Fix:
- In applyExtraParamsToAgent, when provider is "openrouter" and the model
  id is "auto", pass undefined as thinkingLevel to createOpenRouterWrapper
  so no reasoning.effort is injected at all, letting OpenRouter's upstream
  model handle it natively
- Add an explanatory comment in buildOpenrouterProvider clarifying that the
  reasoning: false catalog value does NOT cause effort injection for "auto"

Users who need explicit reasoning control should target a specific model
id (e.g. openrouter/deepseek/deepseek-r1) rather than the auto router.

Fixes #24851

(cherry picked from commit aa554397980972d917dece09ab03c4cc15f5d100)
2026-02-24 04:33:51 +00:00
Ben Marvell eae13d9367 test(agents): update test to match universal tool-result repair for OpenAI
The previous test asserted that OpenAI-responses sessions would NOT get
synthetic tool results for orphaned tool calls. With repairToolUseResultPairing
now running universally, the correct behavior is that orphaned tool calls
get a synthetic tool_result — matching what OpenAI actually requires.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 2edb0ffe0bf96e9e415c03458ff9cee6bf29bcbe)
2026-02-24 04:33:51 +00:00
Ben Marvell 252079f001 fix(agents): repair orphaned tool results for OpenAI after history truncation
repairToolUseResultPairing was gated behind !isOpenAi, skipping orphaned
tool_result cleanup for OpenAI providers. When limitHistoryTurns truncated
conversation history, tool_result messages whose matching tool_call was
before the truncation point survived and were sent as function_call_output
items with stale call_id references. OpenAI rejects these with:
"No tool call found for function call output with call_id ..."

Enable the repair universally — all providers need it after truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 97b065aa6e56fff97414bee26a6b6fc5a33f019a)
2026-02-24 04:33:50 +00:00
chilu18 424ba72cad fix(config): add actionable guidance for dmPolicy open allowFrom mismatch
(cherry picked from commit d3bfbdec5dc5c85305caa0f129f5d4b3c504f559)
2026-02-24 04:33:50 +00:00
chilu18 8c8374defa fix(cron): treat embedded error payloads as run failures
(cherry picked from commit 50fd31c070e8b466db6d81c70b285fd631df1c05)
2026-02-24 04:33:50 +00:00
Marc Gratch 75969ed5c4 fix(plugins): pass session context to before_compaction hook in subscribe handler
The handleAutoCompactionStart handler was calling runBeforeCompaction with
only messageCount and an empty hook context. Plugins receiving this hook
could not identify the session or snapshot the transcript during
auto-compaction.

The other call site in compact.ts already passes the full payload
(messages, sessionFile, sessionKey). This aligns the subscribe handler
to do the same using ctx.params.session and ctx.params.sessionKey.

(cherry picked from commit 318a19d1a1a428ff1be2e03f51777c3829c6e322)
2026-02-24 04:33:50 +00:00
Marcus Castro 58ce0a89ec fix(cli): load plugin registry for configure and onboard commands (#17266)
(cherry picked from commit 644badd40df6eb36847ee7baf36e02ae07bdac74)
2026-02-24 04:33:50 +00:00
JackyWay 792bd6195c fix: recognize Bedrock as Anthropic-compatible in transcript policy
(cherry picked from commit 3b5154081cdd6f9ff94b35c50b8f57714f9ad381)
2026-02-24 04:33:50 +00:00
github-actions[bot] 3823587ada fix(agents): allow empty edit replacement text
(cherry picked from commit 3c21fc30d38ae69f59c7200cfae76642473b2f03)
2026-02-24 04:33:50 +00:00
Glucksberg fd7ca4c394 fix: normalize input peer.kind in resolveAgentRoute (#22730)
The input peer.kind from channel plugins was used as-is without
normalization via normalizeChatType(), while the binding side correctly
normalized. This caused "dm" !== "direct" mismatches in
matchesBindingScope, making plugins that use "dm" as peerKind fail to
match bindings configured with "direct".

Normalize both peer.kind and parentPeer.kind through normalizeChatType()
so that "dm" and "direct" are treated equivalently on both sides.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b0c96702f5531287f857410303b2c3cc698a1441)
2026-02-24 04:33:50 +00:00
HCL 24e52f53e4 fix(cli): resolve --url option collision in browser cookies set
When addGatewayClientOptions registers --url on the parent browser
command, Commander.js captures it before the cookies set subcommand
can receive it. Switch from requiredOption to option and resolve
via inheritOptionFromParent, matching the existing pattern used
for --target-id.

Fixes #24811

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 96fcb963ec6ef4254898aa2afa91d85b61ce677a)
2026-02-24 04:33:50 +00:00
Brian Mendonca d51a4695f0 Deny cron tool on /tools/invoke by default
(cherry picked from commit 816a6b3a4df5bf8436f08e3fc8fa82411e3543ac)
2026-02-24 04:33:50 +00:00
Peter Steinberger f9de17106a refactor(browser): share relay token + options validation tests 2026-02-24 04:23:22 +00:00
Peter Steinberger 8c5cf2d5b2 docs(subagents): document default runTimeoutSeconds config (#24594) (thanks @mitchmcalister) 2026-02-24 04:22:43 +00:00
Mitch McAlister 8bcd405b1c fix: add .int() to runTimeoutSeconds zod schema for consistency
Matches convention used by all other *Seconds/*Ms timeout fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:22:43 +00:00
Mitch McAlister 5710d72527 feat(agents): configurable default runTimeoutSeconds for subagent spawns
When sessions_spawn is called without runTimeoutSeconds, subagents
previously defaulted to 0 (no timeout). This adds a config key at
agents.defaults.subagents.runTimeoutSeconds so operators can set a
global default timeout for all subagent runs.

The agent-provided value still takes precedence when explicitly passed.
When neither the agent nor the config specifies a timeout, behavior is
unchanged (0 = no timeout), preserving backwards compatibility.

Updated for the subagent-spawn.ts refactor (logic moved from
sessions-spawn-tool.ts to spawnSubagentDirect).

Closes #19288

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 04:22:43 +00:00
Peter Steinberger 803e02d8df fix: adapt landed fixups to current type and approval constraints 2026-02-24 04:20:30 +00:00
Peter Steinberger dd14daab15 fix(telegram): allowlist api.telegram.org in media SSRF policy 2026-02-24 04:20:30 +00:00
zerone0x ac6cec7677 fix(providers): strip trailing /v1 from Anthropic baseUrl to prevent double-path
The pi-ai Anthropic provider constructs the full API endpoint as
`${baseUrl}/v1/messages`. If a user configures
`models.providers.anthropic.baseUrl` with a trailing `/v1`
(e.g. "https://api.anthropic.com/v1"), the resolved URL becomes
"https://api.anthropic.com/v1/v1/messages" which the Anthropic API
rejects with a 404 / connection failure.

This regression appeared in v2026.2.22 when @mariozechner/pi-ai bumped
from 0.54.0 to 0.54.1, which started appending the /v1 segment where
the previous version did not.

Fix: in normalizeModelCompat(), detect anthropic-messages models and
strip a single trailing /v1 (with optional trailing slash) from the
configured baseUrl before it is handed to pi-ai. Models with baseUrls
that do not end in /v1 are unaffected. Non-anthropic-messages models
are not touched.

Adds 6 unit tests covering the normalisation scenarios.

Fixes #24709

(cherry picked from commit 4c4857fdcb3506dc277f9df75d4df5879dca8d41)
2026-02-24 04:20:30 +00:00
Marcus Castro 01c1f68ab3 fix(hooks): decouple message:sent internal hook from mirror param
(cherry picked from commit 1afd7030f8e5e9dda682f1de5942a9662ac7dbcf)
2026-02-24 04:20:30 +00:00
User c7bf0dacb8 chore: remove unused isMinimal param from buildSkillsSection
Address review feedback: isMinimal is no longer referenced after the
early-return guard was removed in the parent commit.

(cherry picked from commit 2efe04d301c386f1c7dc93d4ae60de8fac8a63b2)
2026-02-24 04:20:30 +00:00
User 2398b51378 fix: include available_skills in isolated cron agentTurn sessions (closes #24888)
buildSkillsSection() had an early-return guard on isMinimal that silently
dropped the entire <available_skills> block for any session using
promptMode="minimal" — which includes all isolated cron agentTurn sessions
(isCronSessionKey → promptMode="minimal" in attempt.ts:497-500).

Fix: remove the isMinimal guard from buildSkillsSection so that skills are
emitted whenever a non-empty skillsPrompt is provided, regardless of mode.
Memory, docs, reply-tags, and other verbose sections remain gated on isMinimal.

Tests added:
- "includes skills in minimal prompt mode when skillsPrompt is provided (cron regression)"
- "omits skills in minimal prompt mode when skillsPrompt is absent"
- Updated existing minimal-mode test expectation to match corrected behaviour.

(cherry picked from commit 66af86e7eede75721a0439cff595209aa4548eff)
2026-02-24 04:20:30 +00:00
zerone0x c69fc383b9 fix(config): surface helpful chown hint on EACCES when reading config
When the gateway is deployed in a Docker/container environment using a
1-click hosting template, the openclaw.json config file can end up owned
by root (mode 600) while the gateway process runs as the non-root 'node'
user. This causes a silent EACCES failure: the gateway starts with an
empty config and Telegram/Discord bots stop responding.

Before this fix the error was logged as a generic 'read failed: ...'
message with no indication of how to recover.

After this fix:
- EACCES errors log a clear, actionable error to stderr (visible in
  docker logs) with the exact chown command to run
- The config snapshot issue message also includes the chown hint so
  'openclaw gateway status' / Control UI surface the fix path
- process.getuid() is used to include the current UID in the hint;
  falls back to '1001' on platforms where it is unavailable

Fixes #24853

(cherry picked from commit 0a3c572c4175953b0d1284993642b1689678fce4)
2026-02-24 04:20:30 +00:00
SidQin-cyber f3459d71e8 fix(exec): treat shell exit codes 126/127 as failures instead of completed
When a command exits with code 127 (command not found) or 126 (not
executable), the exec tool previously returned status "completed" with
the error buried in the output text. This caused cron jobs to report
status "ok" and never increment consecutiveErrors, silently swallowing
failures like `python: command not found` across multiple daily cycles.

Now these shell-reserved exit codes are classified as "failed", which
propagates through the cron pipeline to properly increment
consecutiveErrors and surface the issue for operator attention.

Fixes #24587

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 2b1d1985ef09000977131bbb1a5c2d732b6cd6e4)
2026-02-24 04:20:30 +00:00
damaozi c6bb7b0c04 fix(whatsapp): groupAllowFrom sender filter bypassed when groupPolicy is allowlist (#24670)
(cherry picked from commit af06ebd9a63c5fb91d7481a61fcdd60dac955b59)
2026-02-24 04:20:30 +00:00
Brian Mendonca 3f5e7f8156 fix(gateway): consume allow-once approvals to prevent replay
(cherry picked from commit 6adacd447c61b7b743d49e8fabab37fb0b2694c5)
2026-02-24 04:20:30 +00:00
Peter Steinberger ffc22778f3 fix(subagents): prune orphaned restored runs + status wording (#24244) (thanks @HeMuling) 2026-02-24 04:17:56 +00:00
HeMuling 3c13f4c2b4 test(subagents): mock sessions store in steer-restart coverage 2026-02-24 04:17:56 +00:00
HeMuling d0e008d460 chore(status): clarify bootstrap file semantics 2026-02-24 04:17:56 +00:00
HeMuling c3b3065cc9 fix(subagents): reconcile orphaned restored runs 2026-02-24 04:17:56 +00:00
Peter Steinberger cd3927ad67 fix(sessions): preserve allow-any subagent model overrides (#21088) (thanks @Slats24) 2026-02-24 04:16:32 +00:00
Slats 87dd896963 fix: sessions_sspawn model override ignored for sub-agents
Fix bug where sessions_spawn model parameter was ignored, causing sub-agents
   to always use the parent's default model.

   The allowAny flag from buildAllowedModelSet() was not being captured or used.

   🤖 AI-assisted (Claude) - fully tested locally

   Fixes #17479, #6295, #10963
2026-02-24 04:16:32 +00:00
Peter Steinberger f6b4baa776 test(telegram): align stop-phrase sequential key expectation (#25034) 2026-02-24 04:16:17 +00:00
Peter Steinberger 1237516ae8 fix(chrome-extension): finalize relay endpoint validation flow (#22252) (thanks @krizpoon) 2026-02-24 04:16:08 +00:00
Kriz Poon b7949d317f Chrome extension: simplify validation logic
Use OR operator to require both Browser and Protocol-Version fields. Simplified catch block to generic error message since specific wrong-port cases are already handled by the validation blocks above.
2026-02-24 04:16:08 +00:00
Kriz Poon 0a53a77dd6 Chrome extension: validate relay endpoint response format
Options page now validates that /json/version returns valid CDP JSON (with Browser/Protocol-Version fields) rather than accepting any HTTP 200 response. This prevents false success when users mistakenly configure the gateway port instead of the relay port (gateway + 3).

Helpful error messages now guide users to use "gateway port + 3" when they configure the wrong port.
2026-02-24 04:16:08 +00:00
Kriz Poon 1fdaaaedd3 Docs: clarify Chrome extension relay port derivation (gateway + 3) 2026-02-24 04:16:08 +00:00
Keith b2719d00ff fix(subagents): restore isInternalMessageChannel guard in resolveAnnounceOrigin
Restores the narrower internal-channel guard from PR #22223 (fe57bea08) that was
inadvertently reverted by f555835b0.

The original !isDeliverableMessageChannel() check strips the requester's channel
whenever it is not in the registered deliverable set. This causes delivery
failures for plugin channels whose adapter ID differs from their plugin ID (e.g.
"gmail" vs "openclaw-gmail"): the requester origin is discarded and the announce
falls back to stale session routes — typically WhatsApp — resulting in a timeout
followed by an E.164 format error.

Replacing with isInternalMessageChannel() limits stripping to explicitly internal
channels (webchat), preserving the requester origin for all external channels
regardless of whether they are currently in the deliverable list.

Fixes: #22223 regression introduced in f555835b0

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 04:13:40 +00:00
Sahil Satralkar 420d8c663c Tests/Typing: stabilize subagent completion routing changes 2026-02-24 04:12:25 +00:00
Sahil Satralkar 8796c78b3d Gateway: propagate message target and thread headers into tools invoke context 2026-02-24 04:12:25 +00:00
Sahil Satralkar f9ffd41cfa Subagents: fallback completion announce to internal session when outbound route is incomplete 2026-02-24 04:12:25 +00:00
Sahil Satralkar 28d658e178 Tests: verify tools invoke propagates route headers for subagent spawn context 2026-02-24 04:12:25 +00:00
Sahil Satralkar 3eabd53898 Tests: add regressions for subagent completion fallback and explicit direct route 2026-02-24 04:12:25 +00:00
Peter Steinberger 004a61056c docs(changelog): note relay nav auto-reattach fix (#19766) (thanks @nishantkabra77) 2026-02-24 04:11:13 +00:00
NK 7c028e8c09 fix: respect canceled_by_user and replaced_with_devtools detach reasons
Skip re-attach when user explicitly dismisses debugger bar or opens
DevTools. Prevents frustrating re-attach loop that fights user intent.

Addresses review feedback from greptile-apps.
2026-02-24 04:11:13 +00:00
NK 67bac62c2c fix: Chrome relay extension auto-reattach after SPA navigation
When Chrome's debugger detaches during page navigation (common in SPAs
like Gmail, Google Calendar), the extension now automatically re-attaches
instead of permanently losing the connection.

Changes:
- onDebuggerDetach: detect navigation vs tab close, attempt re-attach
  with 3 retries and exponential backoff (300ms, 700ms, 1500ms)
- Add reattachPending guard to prevent concurrent re-attach races
- connectOrToggleForActiveTab: handle pending re-attach state
- onRelayClosed: clear reattachPending on relay disconnect
- Add chrome.tabs.onRemoved listener for proper cleanup

Fixes #19744
2026-02-24 04:11:13 +00:00
Peter Steinberger 721d8b2278 test(discord): stabilize parent-info + doctor migration assertions (#25028) 2026-02-24 04:10:52 +00:00
Marcus Castro dd41a78458 fix(bluebubbles): pass SSRF policy for localhost attachment downloads (#24457)
(cherry picked from commit aff64567c757ac46aad320b53406b5036361ff65)
2026-02-24 04:06:57 +00:00
Peter Steinberger 113545f005 docs(changelog): note browser control startup import fix (#23974) (thanks @ieaves) 2026-02-24 04:06:03 +00:00
Ian Eaves 3129d1c489 fix(gateway): start browser HTTP control server module 2026-02-24 04:06:03 +00:00
root 8d2035633b fix(agents): include SOUL.md, IDENTITY.md, USER.md in subagent/cron bootstrap allowlist
Subagent and isolated cron sessions only loaded AGENTS.md and TOOLS.md,
causing subagents to lose their role personality, identity, and user
preferences. Expand MINIMAL_BOOTSTRAP_ALLOWLIST to include the three
missing identity files.

Closes #24852

(cherry picked from commit c33377150eeddb42c2a24f4a48c2d01b5cdf8d3e)
2026-02-24 04:04:35 +00:00
SidQin-cyber 9d3bd50990 fix(otel): use protobuf OTLP exporters instead of JSON/HTTP
The diagnostics-otel extension validates that protocol is "http/protobuf"
but was importing JSON-based `-http` exporters. This caused silent failures
with backends like VictoriaMetrics that only accept protobuf-encoded OTLP.

Switch all three exporter imports (metrics, traces, logs) from
`@opentelemetry/exporter-*-otlp-http` to `@opentelemetry/exporter-*-otlp-proto`.

Fixes #24942

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit f5c0bf0497bff4c9c0748472ad5a63742af43374)
2026-02-24 04:04:35 +00:00
Peter Steinberger aea28e26fb fix(auto-reply): expand standalone stop phrases 2026-02-24 04:02:43 +00:00
Peter Steinberger 588a188d6f fix: replace stale plugin webhook routes on re-registration 2026-02-24 04:01:41 +00:00
Peter Steinberger d76742ff88 fix: normalize manifest plugin ids during install 2026-02-24 03:56:34 +00:00
Peter Steinberger a388fbb6c3 fix: harden custom-provider verification probes (#24743) (thanks @Glucksberg) 2026-02-24 03:56:30 +00:00
Peter Steinberger ebde897bb8 fix: add dmScope route guard regression tests (#24949) (thanks @kevinWangSheng) 2026-02-24 03:55:29 +00:00
shenghui kevin 57783680ad fix(whatsapp): guard updateLastRoute when dmScope isolates DM sessions
When session.dmScope is set to 'per-channel-peer', WhatsApp DMs correctly
resolve isolated session keys, but updateLastRouteInBackground unconditionally
wrote lastTo to the main session key. This caused reply routing corruption
and privacy violations.

Only update main session's lastRoute when the DM session actually IS
the main session (sessionKey === mainSessionKey).

Fixes #24912
2026-02-24 03:55:29 +00:00
shenghui kevin 6f44d92d76 docs: update PR_STATUS.md - all 11 PRs CI passed 2026-02-24 03:55:29 +00:00
Peter Steinberger de0e01259a fix: expand openrouter thinking-off regression coverage (#24863) (thanks @DevSecTim) 2026-02-24 03:54:29 +00:00
Tim Jones b96d32c1c2 chore: fix oxfmt formatting in extraparams test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 03:54:29 +00:00
Tim Jones 3e974dc93f fix: don't inject reasoning: { effort: "none" } for OpenRouter when thinking is off
"off" is a truthy string, so the existing guard `if (thinkingLevel && ...)`
was always entering the injection block and sending `reasoning: { effort: "none" }`
to every OpenRouter request — even when thinking wasn't enabled. Models that
require reasoning (e.g. deepseek/deepseek-r1) reject this with:
  400 Reasoning is mandatory for this endpoint and cannot be disabled.

Fix: skip the reasoning injection entirely when thinkingLevel is "off".
The reasoning_effort flat-field cleanup still runs. Omitting the reasoning
field lets each model use its own default behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 03:54:29 +00:00
Peter Steinberger 69a541c3f0 fix: sanitize pairing recovery requestId hints (#24771) (thanks @markmusson) 2026-02-24 03:53:45 +00:00
Mark Musson b902d5ade0 fix(status): show pairing approval recovery hints 2026-02-24 03:53:45 +00:00
Peter Steinberger 6c1ed9493c fix: harden queue retry debounce and add regression tests 2026-02-24 03:52:49 +00:00
Peter Steinberger a216f2dabe fix: extend discord thread parent fallback coverage (#24897) (thanks @z-x-yang) 2026-02-24 03:52:43 +00:00
Zongxin Yang d883ecade6 fix(discord): fallback thread parent lookup when parentId missing 2026-02-24 03:52:43 +00:00
Peter Steinberger fd24b35449 fix: cover startup locale hydration path (#24795) (thanks @chilu18) 2026-02-24 03:51:58 +00:00
chilu18 053b0df7d4 fix(ui): load saved locale on startup 2026-02-24 03:51:58 +00:00
Peter Steinberger 7a42558a3e fix: harden legacy plugin schema compatibility tests (#24933) (thanks @pandego) 2026-02-24 03:50:53 +00:00
pandego 9f4764cd41 fix(plugins): guard legacy zod schemas without toJSONSchema 2026-02-24 03:50:53 +00:00
Peter Steinberger dd145f1346 fix: suppress sessions_send warning leakage coverage (#24740) (thanks @Glucksberg) 2026-02-24 03:49:52 +00:00
Glucksberg 947883d2e0 fix: suppress sessions_send error warnings from leaking to chat (#23989)
sessions_send timeout/error results were being surfaced as raw warning
messages in Telegram chats because the tool is classified as mutating,
which forces error warnings to always be shown. However, sessions_send
failures are transient inter-session communication issues where the
message may still have been delivered, so they should not leak to users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:49:52 +00:00
Peter Steinberger 9cc7450edf docs(changelog): add missing unreleased fixes and reorder 2026-02-24 03:48:49 +00:00
Glucksberg 1565d7e7b3 fix: increase verification max_tokens to 1024 for Poe API compatibility
Poe API's Extended Thinking models (e.g. claude-sonnet-4.6) require
budget_tokens >= 1024. The previous values (5 for OpenAI, 16 for
Anthropic) caused HTTP 400 errors during provider verification.

Fixes #23433

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:47:49 +00:00
Peter Steinberger b5881d9ef4 fix: avoid WhatsApp silent turns with final-only delivery (#24962) (thanks @SidQin-cyber) 2026-02-24 03:47:20 +00:00
SidQin-cyber 3d22af692c fix(whatsapp): suppress reasoning/thinking content from WhatsApp delivery
The deliver callback in process-message.ts was forwarding all payload
kinds (tool, block, final) to WhatsApp. Block payloads contain the
model's reasoning/thinking content, which should only be visible in
the internal web UI. This caused chain-of-thought to leak to end users
as separate WhatsApp messages.

Add an early return for non-final payloads so only the actual response
is delivered to the WhatsApp channel, matching how Telegram already
filters by info.kind === "final".

Fixes #24954
Fixes #24605

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 03:47:20 +00:00
Peter Steinberger 3a653082d8 fix(config): align whatsapp enabled schema with auto-enable 2026-02-24 03:39:41 +00:00
Coy Geek aef45b2abb fix(logging): redact phone numbers and message content from WhatsApp logs
Apply redactIdentifier() (SHA-256 hashing) to all recipient JIDs and
phone numbers logged by sendMessageWhatsApp, sendReactionWhatsApp,
sendPollWhatsApp, and runWebHeartbeatOnce. Remove poll question text
and message preview content from log entries, replacing with character
counts where useful for debugging.

The existing redactIdentifier() utility in src/logging/redact-identifier.ts
was already implemented but not wired into any WhatsApp logging path.
This commit connects it to all affected call sites while leaving
functional parameters (actual send calls, event emitters) untouched.

Closes #24957
2026-02-24 03:36:29 +00:00
Peter Steinberger 0bdcca2f35 test(whatsapp): add log redaction coverage 2026-02-24 03:34:31 +00:00
Sid d95ee859f8 fix(cron): use full prompt mode for isolated cron sessions to include skills (#24944)
Isolated cron sessions (agentTurn) were grouped with subagent sessions
under the "minimal" prompt mode, which causes buildSkillsSection to
return an empty array. This meant <available_skills> was never included
in the system prompt for isolated cron runs.

Subagent sessions legitimately need minimal prompts (reduced context),
but isolated cron sessions are full agent turns that should have access
to all configured skills, matching the behavior of normal chat sessions
and non-isolated cron runs.

Remove isCronSessionKey from the minimal prompt condition so only
subagent sessions use "minimal" mode.

Fixes openclaw#24888

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 03:33:54 +00:00
zerone0x bf91b347c1 fix(plugins): use manifest id as config entry key instead of npm package name (#24796)
* fix(plugins): use manifest id as config key instead of npm package name

Plugin manifests (openclaw.plugin.json) define a canonical 'id' field that
is used as the authoritative plugin identifier by the manifest registry.
However, the install command was deriving the config entry key from the npm
package name (e.g. 'cognee-openclaw') rather than the manifest id (e.g.
'memory-cognee'), causing a latent mismatch.

On the next gateway reload the plugin could not be found under the config key
derived from the npm package name, causing 'plugin not found' errors and
potentially shutting the gateway down.

Fix: after extracting the package directory, read openclaw.plugin.json and
prefer its 'id' field over the npm package name when registering the config
entry. Falls back to the npm-derived id if the manifest file is absent or
has no valid id. A diagnostic info message is emitted when the two values
differ so the mismatch is visible in the install log.

The update path (src/plugins/update.ts) already correctly reads the manifest
id and is unaffected.

Fixes #24429

* fix: format plugin install manifest-id path (#24796)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-24 03:33:51 +00:00
Sid f5cab29ec7 fix(synology-chat): deregister stale webhook route before re-registering on restart (#24971)
When the Synology Chat plugin restarts (auto-restart or health monitor),
startAccount is called again without calling the previous stop(). The
HTTP route is still registered, so registerPluginHttpRoute returns a
no-op unregister function and logs "already registered". This triggers
another restart, creating an infinite loop.

Store the unregister function at module level keyed by account+path.
Before registering, check for and call any stale unregister from the
previous start cycle, ensuring a clean slate for route registration.

Fixes #24894

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 03:33:47 +00:00
Peter Machona 9ced64054f fix(auth): classify missing OAuth scopes as auth failures (#24761) 2026-02-24 03:33:44 +00:00
Sid 38da3f40cb fix(discord): suppress reasoning/thinking block payloads from delivery (#24969)
Block payloads (info.kind === "block") contain reasoning/thinking content
that should only be visible in the internal web UI. When streamMode is
"partial", these blocks were being delivered to Discord as visible
messages, leaking chain-of-thought to end users.

Add an early return for block payloads in the deliver callback,
consistent with the WhatsApp fix and Telegram's existing behavior.

Fixes #24532

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 03:33:40 +00:00
Sid c1fe688d40 fix(gateway): safely extract text from content arrays in prompt builder (#24946)
* fix(gateway): safely extract text from message content arrays in prompt builder

When HistoryEntry.body is a content array (e.g. [{type:"text",
text:"hello"}]) rather than a plain string, template literal
interpolation produces "[object Object]" instead of the actual message
text. This affects users whose session messages were stored with array
content format.

Add a safeBody helper that detects non-string body values and uses
extractTextFromChatContent to extract the text, preventing the
[object Object] serialization in both the current-message return path
and the history formatting path.

Fixes openclaw#24688

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: format gateway agent prompt helper (#24946)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-24 03:33:37 +00:00
banna-commits e3da57d956 fix: add exponential backoff to announce queue drain on failure (#24783)
When the gateway rejects connections (e.g. scope-upgrade 'pairing required'),
the announce queue drain loop would retry every ~1s indefinitely because
the only delay was the fixed debounceMs (default 1000ms).

This adds a consecutiveFailures counter with exponential backoff:
2s, 4s, 8s, 16s, 32s, 60s (capped). The counter resets on successful drain.

The backoff is applied by shifting lastEnqueuedAt forward so that
waitForQueueDebounce naturally delays the next attempt.

Fixes #24777

Co-authored-by: Knut <knut@Knut-sin-Mac-mini.local>
2026-02-24 03:33:34 +00:00
青雲 52ac7634db fix: persist reasoningLevel 'off' instead of deleting it (#24406) (#24559)
When a user runs /reasoning off, the session patch handler deleted
the reasoningLevel field from the session entry. This caused
get-reply-directives to treat reasoning as 'not explicitly set',
which triggered resolveDefaultReasoningLevel() to re-enable
reasoning for capable models (e.g. Claude Opus).

The fix persists 'off' explicitly, matching how directive-handling.persist.ts
already handles the inline /reasoning off command.

Fixes #24406
Fixes #24411

Co-authored-by: echoVic <AkiraVic@outlook.com>
2026-02-24 03:33:30 +00:00
junwon 04bcabcbae fix(infra): handle Windows dev=0 in sameFileIdentity TOCTOU check (#24939)
* fix(infra): handle Windows dev=0 in sameFileIdentity TOCTOU check

On Windows, `fs.lstatSync` (path-based) returns `dev: 0` while
`fs.fstatSync` (fd-based) returns the real NTFS volume serial number.
This mismatch caused `sameFileIdentity` to always fail, making
`openVerifiedFileSync` reject every file — silently breaking all
Control UI static file serving (HTTP 404).

Fall back to ino-only comparison when either dev is 0 on Windows.
ino remains unique within a single volume, so TOCTOU protection
is preserved.

Fixes #24692

* fix: format sameFileIdentity wrapping (#24939)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-24 03:33:27 +00:00
Glucksberg a3b82a563d fix: resolve symlinks in pnpm/bun global install detection (#24744)
Use tryRealpath() instead of path.resolve() when comparing expected
package paths in detectGlobalInstallManagerForRoot(). path.resolve()
only normalizes path strings without following symlinks, causing pnpm
global installs to go undetected since pnpm symlinks node_modules
entries into its .pnpm content-addressable store.

Fixes #22768

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:33:24 +00:00
Glucksberg 1e23d2ecea fix(whatsapp): respect selfChatMode config in access-control (#24738)
The selfChatMode config field was resolved by accounts.ts but never
consumed in the access-control logic. Use nullish coalescing so an
explicit true/false from config takes precedence over the allowFrom
heuristic, while undefined falls back to the existing behavior.

Fixes #23788

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-24 03:33:21 +00:00
不做了睡大觉 ae281a6f61 fix: suppress "Run doctor --fix" hint when already in fix mode with no changes (#24666)
When running `openclaw doctor --fix` and no config changes are needed,
the else branch unconditionally showed "Run doctor --fix to apply changes"
which is confusing since we just ran --fix.

Now the hint only appears when NOT in fix mode (i.e. when running plain
`openclaw doctor`). When in fix mode with nothing to change, the command
silently proceeds to the "Doctor complete." outro.

Fixes #24566

Co-authored-by: User <user@example.com>
2026-02-24 03:33:17 +00:00
Peter Steinberger 3af9d1f8e9 fix: scope Telegram RFC2544 SSRF exception to policy opt-in (#24982) (thanks @stakeswky) 2026-02-24 03:28:00 +00:00
User 9df80b73e2 fix: allow RFC2544 benchmark range (198.18.0.0/15) through SSRF filter
Telegram's API and file servers resolve to IPs in the 198.18.0.0/15
range (RFC 2544 benchmarking range). The SSRF filter was blocking these
addresses because ipaddr.js classifies them as 'reserved', and the
filter also had an explicit RFC2544_BENCHMARK_PREFIX check that blocked
them unconditionally.

Fix: exempt 198.18.0.0/15 from the 'reserved' range block in
isBlockedSpecialUseIpv4Address(). Other 'reserved' ranges (TEST-NET-2,
TEST-NET-3, documentation prefixes) remain blocked. The explicit
RFC2544_BENCHMARK_PREFIX check is repurposed as the exemption guard.

Closes #24973
2026-02-24 03:28:00 +00:00
Ali Al Jufairi 237b9be937 chore(docs) : remove the mention of Anthropic OAuth since it is not allowed according to there new guidlines (#24989) 2026-02-24 03:23:01 +00:00
Adam d07d24eebe fix: clamp poll sleep duration to non-negative in bash-tools process (#24889)
`Math.min(250, deadline - Date.now())` could return a negative value if
the deadline expired between the while-condition check and the setTimeout
call. Wrap with `Math.max(0, ...)` to ensure the sleep is never negative.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 03:22:58 +00:00
青雲 dc8423f2c0 fix: back up existing systemd unit before overwriting on update (#24350) (#24937)
When `openclaw update` regenerates the systemd service file, any user
customizations to ExecStart (e.g. proxychains4 wrapper) are silently
lost. Now the existing unit file is copied to `.bak` before writing
the new one, so users can restore their customizations.

The backup path is printed in the install output so users are aware.

Co-authored-by: echoVic <AkiraVic@outlook.com>
2026-02-24 03:22:55 +00:00
Soumik Bhatta 70cfb69a5f fix(doctor): skip false positive permission warnings for Nix store symlinks (#24901)
On NixOS/Nix-managed installs, config and state directories are symlinks
into /nix/store/. Symlinks on Linux always report 0o777 via lstatSync,
causing `openclaw doctor` to incorrectly warn about open permissions.

Use lstatSync to detect symlinks, resolve the target, and only suppress
the warning when the resolved path lives in /nix/store/ (an immutable
filesystem). Symlinks to insecure targets still trigger warnings.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 03:22:52 +00:00
Bill Cropper 588ad7fb38 fix: respect agent model config in slug generator (#24776)
The slug generator was using hardcoded DEFAULT_PROVIDER and DEFAULT_MODEL
instead of resolving from agent config. This caused it to fall back to
anthropic/claude-opus-4-6 even when a cloud model was configured.

Now uses resolveAgentModelPrimary() to get the configured model, with
fallback to defaults if not configured.

Fixes issue where session memory filenames would fail to generate
when using cloud models that require special backends.
2026-02-24 03:22:48 +00:00
David Murray e2e10b3da4 fix(slack): map threadId to replyToId for restart sentinel notifications (#24885)
The restart sentinel wake path passes threadId to deliverOutboundPayloads,
but Slack requires replyToId (mapped to thread_ts) for threading. The agent
reply path already does this conversion but the sentinel path did not,
causing post-restart notifications to land as top-level DMs.

Fixes #17716
2026-02-24 03:22:45 +00:00
Omair Afzal 19c43eade2 fix(memory): strip null bytes from workspace paths causing ENOTDIR (#24876)
Add stripNullBytes() helper and apply it to all return paths in
resolveAgentWorkspaceDir() including configured, default, and
state-dir-derived paths. Null bytes in paths cause ENOTDIR errors
when Node tries to resolve them as directories.
2026-02-24 03:22:42 +00:00
Omair Afzal 177f167eab fix: guard .trim() calls on potentially undefined workspaceDir (#24875)
Change workspaceDir param type from string to string | undefined in
resolvePluginSkillDirs and use nullish coalescing before .trim() to
prevent TypeError when workspaceDir is undefined.
2026-02-24 03:22:39 +00:00
Peter Steinberger 7b2b86c60a fix(exec): add approval race changelog and regressions 2026-02-24 03:22:05 +00:00
Peter Steinberger 6f0dd61795 fix(exec): restore two-phase approval registration flow 2026-02-24 03:16:36 +00:00
Peter Steinberger c6c1e3e7cf docs(changelog): correct exec approvals reporter credit 2026-02-24 03:13:48 +00:00
Peter Steinberger ffd63b7a2c fix(security): trust resolved skill-bin paths in allowlist auto-allow 2026-02-24 03:12:43 +00:00
Peter Steinberger 204d9fb404 refactor(security): dedupe shell env probe and add path regression test 2026-02-24 03:11:33 +00:00
Peter Steinberger 64aab80201 test(exec): add regressions for safe-bin metadata and chain semantics 2026-02-24 03:10:19 +00:00
Peter Steinberger a67689a7e3 fix: harden allow-always shell multiplexer wrapper handling 2026-02-24 03:06:51 +00:00
Peter Steinberger 4a3f8438e5 fix(gateway): bind node exec approvals to nodeId 2026-02-24 03:05:58 +00:00
Peter Steinberger 9530c01085 refactor(exec): split safe-bin policy modules and dedupe allowlist flow 2026-02-24 03:05:03 +00:00
Peter Steinberger c5ac90ab92 docs(changelog): add shell-env fallback hardening note 2026-02-24 03:04:49 +00:00
Peter Steinberger 60f1d1959a test: stabilize invoke-system-run env-wrapper assertion on Windows 2026-02-24 03:02:38 +00:00
Peter Steinberger d0ef4c75c7 docs(changelog): credit safeBins advisory reporters 2026-02-24 02:59:17 +00:00
Peter Steinberger ff10fe8b91 fix(security): require /etc/shells for shell env fallback 2026-02-24 02:58:24 +00:00
Shakker 71f4b93656 docs: refresh clawtributors list 2026-02-24 02:55:02 +00:00
Shakker ef1ffacfb2 scripts: exclude unresolved clawtributors from README 2026-02-24 02:55:02 +00:00
Peter Steinberger 90383e00e9 fix(security): harden autoAllowSkills exec matching 2026-02-24 02:53:47 +00:00
Peter Steinberger e578521ef4 fix(security): harden session export image data-url handling 2026-02-24 02:53:39 +00:00
Peter Steinberger fefc414576 fix(security): harden structural session path fallback 2026-02-24 02:52:48 +00:00
Peter Steinberger ff4e6ca0d9 fix(ios): gate agent deep links with local confirmation 2026-02-24 02:51:58 +00:00
Peter Steinberger f8524ec77a fix(security): harden exported session html rendering 2026-02-24 02:40:29 +00:00
Peter Steinberger f6afc8c5b6 docs(security): clarify host-side exec trust model defaults 2026-02-24 02:40:18 +00:00
Peter Steinberger 1d28da55a5 fix(voice-call): block Twilio webhook replay and stale transitions 2026-02-24 02:37:24 +00:00
Gustavo Madeira Santana 4663d68384 Tests: make model-catalog fixtures type-valid 2026-02-23 21:36:34 -05:00
Peter Steinberger ce02ad9643 refactor(agents): centralize sandbox media and fs policy helpers 2026-02-24 02:32:01 +00:00
Gustavo Madeira Santana 207ec7cfae chore(provider): remove unused pruning functions 2026-02-23 21:31:12 -05:00
Peter Steinberger 4032390572 docs(security): clarify trusted user-triggered local actions 2026-02-24 02:29:09 +00:00
Peter Steinberger 3f923e8313 test: add env -S allowlist bypass regressions 2026-02-24 02:28:00 +00:00
Peter Steinberger 6634030be3 fix: enforce apply_patch workspaceOnly in sandbox mounts 2026-02-24 02:23:56 +00:00
Peter Steinberger c070be1bc4 fix(sandbox): harden fs bridge path checks and bind mount policy 2026-02-24 02:21:43 +00:00
Peter Steinberger dd9d9c1c60 fix(security): enforce workspaceOnly for sandbox image tool 2026-02-24 02:17:55 +00:00
Peter Steinberger 0026255def refactor(security): harden system.run wrapper enforcement 2026-02-24 02:17:41 +00:00
Gustavo Madeira Santana 5239b55c0a Config: expand Kilo catalog and persist selected Kilo models (#24921)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f5a7e1a38574593838a7cd62ab9f1488f2da461e
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 21:17:37 -05:00
Peter Steinberger 6c441ea797 fix: support legacy and beta prerelease version formats 2026-02-24 02:05:37 +00:00
Peter Steinberger 08e2aa44e7 fix(commands): restrict commands.allowFrom to sender principals 2026-02-24 02:01:01 +00:00
Peter Steinberger 223d7dc23d feat(gateway)!: require explicit non-loopback control-ui origins 2026-02-24 01:57:11 +00:00
Peter Steinberger edfefdff7d docs(changelog): mark ACP hardening as next npm release 2026-02-24 01:56:22 +00:00
Peter Steinberger a1c4bf07c6 fix(security): harden exec wrapper allowlist execution parity 2026-02-24 01:52:17 +00:00
Peter Steinberger 5eb72ab769 fix(security): harden browser SSRF defaults and migrate legacy key 2026-02-24 01:52:01 +00:00
Peter Steinberger 8779b523dc test(sandbox): speed up agent-config coverage with pure resolvers 2026-02-24 01:46:12 +00:00
Peter Steinberger 467666adc7 test(sandbox): use focused modules in lightweight suites 2026-02-24 01:46:12 +00:00
Peter Steinberger f0f886ecc4 docs(security): clarify gateway-node trust boundary in docs 2026-02-24 01:35:44 +00:00
Peter Steinberger 1f81677093 docs(changelog): note dangerous name-matching audit unification 2026-02-24 01:33:08 +00:00
Peter Steinberger 161d9841dc refactor(security): unify dangerous name matching handling 2026-02-24 01:33:08 +00:00
Peter Steinberger 6a7c303dcc test(msteams): fix allowlist name-match expectations 2026-02-24 01:26:53 +00:00
Peter Steinberger 2e36bdda85 docs(changelog): credit ACP security reporter 2026-02-24 01:19:03 +00:00
Peter Steinberger 22467902ea fix(doctor): inherit dangerous name-matching flag in mutable allowlist scan 2026-02-24 01:18:38 +00:00
Peter Steinberger e5931554bf test: tighten slow test timeouts and cleanup 2026-02-24 01:16:53 +00:00
Peter Steinberger 6c43d0a08e test(gateway): move sessions_send error paths to unit tests 2026-02-24 01:16:53 +00:00
Peter Steinberger 63dcd28ae0 fix(acp): harden permission tool-name validation 2026-02-24 01:11:34 +00:00
Peter Steinberger f97c0922e1 fix(security): harden account-key handling against prototype pollution 2026-02-24 01:09:31 +00:00
Peter Steinberger 12cc754332 fix(acp): harden permission auto-approval policy 2026-02-24 01:03:30 +00:00
Peter Steinberger ddf93d9845 docs(security): add vps trust-boundary guidance 2026-02-24 01:02:11 +00:00
Peter Steinberger cfa44ea6b4 fix(security): make allowFrom id-only by default with dangerous name opt-in (#24907)
* fix(channels): default allowFrom to id-only; add dangerous name opt-in

* docs(security): align channel allowFrom docs with id-only default
2026-02-24 01:01:51 +00:00
Peter Steinberger 41b0568b35 docs(security): clarify shared-agent trust boundaries 2026-02-24 01:00:05 +00:00
Peter Steinberger 0cc327546b test(gateway): speed up slow e2e test setup 2026-02-24 00:59:52 +00:00
Peter Steinberger 13478cc79a refactor(config): harden catchall hint mapping and array fallback 2026-02-24 00:59:44 +00:00
Vincent Koc 30c622554f Providers: disable developer role for DashScope-compatible endpoints (#24675)
* Agents: disable developer role for DashScope-compatible endpoints

* Agents: test DashScope developer-role compatibility

* Gateway: test allowlisted sessions.patch model selection

* Changelog: add DashScope role-compat fix note
2026-02-23 19:51:16 -05:00
Peter Steinberger 83eae14ed6 docs: add security-advisory triage reminder to agents guide 2026-02-24 00:45:41 +00:00
Peter Steinberger 400220275c docs: clarify multi-instance recommendations for user isolation 2026-02-24 00:40:08 +00:00
Peter Steinberger a430e1722b test(channels): reduce media test runtime and polling 2026-02-24 00:31:58 +00:00
Peter Steinberger 663f784e4e test(core): trim redundant setup and tighten waits 2026-02-24 00:31:58 +00:00
Peter Steinberger f58c1ef34e test(gateway): speed up contract and polling suites 2026-02-24 00:31:58 +00:00
Peter Steinberger 7d55277d72 docs: clarify operator trust boundary for shared gateways 2026-02-24 00:25:01 +00:00
Peter Steinberger f0c3c8b6a3 fix(config): redact dynamic catchall secret keys 2026-02-24 00:21:29 +00:00
Peter Steinberger 8dfa33d373 test(sandbox): add root bind mount regression 2026-02-24 00:17:21 +00:00
Peter Steinberger d68380bb7f docs(security): clarify exposed-secret report scope 2026-02-24 00:17:21 +00:00
Peter Steinberger 25f6fcc63a docs(changelog): note safeBins exec hardening 2026-02-23 23:58:58 +00:00
Peter Steinberger 3b8e33037a fix(security): harden safeBins long-option validation 2026-02-23 23:58:58 +00:00
Peter Steinberger 7b4d2cb5cb docs(security): clarify trusted-config dos scope 2026-02-23 23:57:26 +00:00
Peter Steinberger a2dfe9879f fix(security): harden regex compilation for filters and redaction 2026-02-23 23:54:50 +00:00
Peter Steinberger e6484cb65f refactor: harden kilocode auth ordering and dedupe provider wiring 2026-02-23 23:37:13 +00:00
Peter Steinberger f52a0228ca test: optimize auth and audit test runtime 2026-02-23 23:31:52 +00:00
John Fawcett 13f32e2f7d feat: Add Kilo Gateway provider (#20212)
* feat: Add Kilo Gateway provider

Add support for Kilo Gateway as a model provider, similar to OpenRouter.
Kilo Gateway provides a unified API that routes requests to many models
behind a single endpoint and API key.

Changes:
- Add kilocode provider option to auth-choice and onboarding flows
- Add KILOCODE_API_KEY environment variable support
- Add kilocode/ model prefix handling in model-auth and extra-params
- Add provider documentation in docs/providers/kilocode.md
- Update model-providers.md with Kilo Gateway section
- Add design doc for the integration

* kilocode: add provider tests and normalize onboard auth-choice registration

* kilocode: register in resolveImplicitProviders so models appear in provider filter

* kilocode: update base URL from /api/openrouter/ to /api/gateway/

* docs: fix formatting in kilocode docs

* fix: address PR review — remove kilocode from cacheRetention, fix stale model refs and CLI name in docs, fix TS2742

* docs: fix stale refs in design doc — Moltbot to OpenClaw, MoltbotConfig to OpenClawConfig, remove extra-params section, fix doc path

* fix: use resolveAgentModelPrimaryValue for AgentModelConfig union type

---------

Co-authored-by: Mark IJbema <mark@kilocode.ai>
2026-02-23 23:29:27 +00:00
Peter Steinberger ddb7ec99a8 test: speed up cron test polling and waits 2026-02-23 22:42:23 +00:00
Peter Steinberger 0cc46d774c test: consolidate auth-choice tests for faster coverage 2026-02-23 22:42:23 +00:00
Gustavo Madeira Santana eff3c5c707 Session/Cron maintenance hardening and cleanup UX (#24753)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7533b85156186863609fee9379cd9aedf74435af
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: shakkernerd <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
2026-02-23 22:39:48 +00:00
Peter Steinberger 29b19455e3 test(commands): collapse provider and endpoint matrices 2026-02-23 22:16:45 +00:00
Peter Steinberger b922ecb8c1 test(security): reduce duplicate audit assertions 2026-02-23 22:16:39 +00:00
Peter Steinberger cd5f3fe0c1 test(config): consolidate env/include scenario coverage 2026-02-23 22:16:30 +00:00
Peter Steinberger c248c515a3 test: collapse sandbox agent config duplicate cases 2026-02-23 22:01:32 +00:00
Peter Steinberger 287586206c test: consolidate sandbox docker merge scenarios 2026-02-23 22:01:22 +00:00
Peter Steinberger 8b192beaaf test: combine web reconnect progression assertions 2026-02-23 21:57:30 +00:00
Peter Steinberger ecd278b67b test: merge redundant telegram media path scenarios 2026-02-23 21:57:23 +00:00
Peter Steinberger ca761d6225 test: consolidate gateway auth test scenarios 2026-02-23 21:57:17 +00:00
Peter Steinberger b9f01e8d3f test: consolidate directive behavior suites for faster runs 2026-02-23 21:48:12 +00:00
Peter Steinberger b8fc8e7e6d test: optimize directive behavior test scenarios 2026-02-23 21:35:42 +00:00
Peter Steinberger 0183610db3 refactor: de-duplicate channel runtime and payload helpers 2026-02-23 21:25:28 +00:00
Peter Steinberger 0ae7f470a2 test: normalize skill prompt path assertions on windows 2026-02-23 21:17:29 +00:00
Peter Steinberger 31ca7fb277 test: consolidate directive behavior test scenarios 2026-02-23 21:13:11 +00:00
Peter Steinberger 426f803b8a test: speed up sessions_spawn tool harness 2026-02-23 21:13:05 +00:00
Peter Steinberger 7e5f771d27 test: speed up skills test suites 2026-02-23 21:02:13 +00:00
Peter Steinberger 75423a00d6 refactor: deduplicate shared helpers and test setup 2026-02-23 20:40:44 +00:00
Peter Steinberger 1f5e6444ee test: remove redundant pi embedded runner cases 2026-02-23 20:15:56 +00:00
Peter Steinberger 3b5a276a48 test: speed up supervisor test timing 2026-02-23 20:15:56 +00:00
Peter Steinberger 5a475259bb fix(telegram): suppress reasoning-only leaks when reasoning is off
Co-authored-by: avirweb <avirweb@users.noreply.github.com>
2026-02-23 20:06:16 +00:00
Peter Steinberger 63e4dfaa9c test: consolidate pi-tools gating assertions 2026-02-23 20:00:11 +00:00
Peter Steinberger cba8037d90 test: prune redundant trigger handling integration coverage 2026-02-23 20:00:11 +00:00
Peter Steinberger 32e6ccb7b6 test(cron): cover announce failure when best-effort is off 2026-02-23 19:48:37 +00:00
Peter Steinberger 9af3ec92a5 fix(gateway): add HSTS header hardening and docs 2026-02-23 19:47:29 +00:00
Peter Steinberger c88915b721 test: consolidate trigger handling suites 2026-02-23 19:41:47 +00:00
Peter Steinberger 87603b5c45 fix: sync built-in channel enablement across config paths 2026-02-23 19:40:42 +00:00
Peter Steinberger 69b17a37e8 docs(reference): add cache trace diagnostics knobs to prompt-caching guide 2026-02-23 19:39:35 +00:00
Peter Steinberger 7a40d99b1d refactor(cron): extract delivery dispatch + harden reset notices 2026-02-23 19:25:22 +00:00
Peter Steinberger fe62711342 test(gate): stabilize env- and timing-sensitive process/web-search checks 2026-02-23 19:19:58 +00:00
Peter Steinberger 46dee26600 docs(reference): add prompt-caching guide and knobs
Co-authored-by: Axel Svensson <svenssonaxel@users.noreply.github.com>
2026-02-23 19:19:45 +00:00
Peter Steinberger 31e4c21b67 fix(auto-reply): move volatile inbound flags out of system metadata
Co-authored-by: aidiffuser <aidiffuser@users.noreply.github.com>
2026-02-23 19:19:45 +00:00
Peter Steinberger cf38339f25 fix(tools): improve session_status cache-aware usage reporting
Co-authored-by: Lucian Feraru <1ucian@users.noreply.github.com>
2026-02-23 19:19:45 +00:00
Peter Steinberger 40db3fef49 fix(agents): cache bootstrap snapshots per session key
Co-authored-by: Isis Anisoptera <github@lotuswind.net>
2026-02-23 19:19:45 +00:00
Nimrod Gutman 8b3eee71ec fix: tier local vitest worker defaults by host memory (#24719) (thanks @ngutman) 2026-02-23 21:19:21 +02:00
Nimrod Gutman 420c18364e fix(test): tier local vitest worker defaults by host memory 2026-02-23 21:19:21 +02:00
Peter Steinberger 2931e215ca docs: add GitHub comment formatting/linking guardrails 2026-02-23 19:17:33 +00:00
Peter Steinberger 47723b646d refactor(test): de-duplicate msteams and bash test helpers 2026-02-23 19:12:27 +00:00
Ruslan Kharitonov 8d69251475 fix(doctor): use gateway health status for memory search key check (#22327)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 2f02ec94030754a2e2dfeb7d5a80f14747373ab5
Co-authored-by: therk <901920+therk@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 14:07:16 -05:00
Peter Steinberger bf373eeb43 refactor: harden reset notice + cron delivery target flow 2026-02-23 19:01:02 +00:00
Peter Steinberger d266d12be1 refactor(exec): simplify env-prefixed wrapper modifier check 2026-02-23 18:56:14 +00:00
Peter Steinberger 42373b6742 fix(skills): support multiline frontmatter fallback without PyYAML 2026-02-23 18:56:14 +00:00
Brian Mendonca f18f087c3c fix(skills): make quick_validate work without PyYAML
(cherry picked from commit 485a55b4ecdd4c24c97e75ffd79e015814d7c31b)
2026-02-23 18:56:14 +00:00
Brian Mendonca bd8b9af9a7 fix(exec): bind env-prefixed shell wrappers to full approval text
(cherry picked from commit 1edf9579882d427322129dc434d0dadc0699102d)
2026-02-23 18:56:14 +00:00
oneaix 216d99e585 fix(browser): derive relay auth token from gateway token in Chrome extension
The extension relay server authenticates using an HMAC-SHA256 derived
token (`openclaw-extension-relay-v1:<port>`), but the Chrome extension
was sending the raw gateway token. This caused both the WebSocket
connection and the options page validation to fail with 401 Unauthorized.

Additionally, the options page validation request triggered a CORS
preflight (due to the custom `x-openclaw-relay-token` header) which the
relay rejects because OPTIONS requests lack auth headers. The options
page now delegates the check to the background service worker which has
host_permissions and bypasses CORS preflight.

Fixes #23842

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit bbc654b9f063ef24e7d511275e7d8c670414970b)
2026-02-23 18:56:14 +00:00
Mustafa Kemal bb8f538cd4 Browser relay: accept raw gateway token in extension auth
(cherry picked from commit e682a768d0ebe65f9818c6d47fd79e18c38d650f)
2026-02-23 18:56:14 +00:00
justinhuangcode d00d814ad1 fix(gateway): include platform and reason in node command rejection error
The generic "node command not allowed" error gives no indication of why the
command was rejected, making it hard to diagnose issues (e.g. running
`nodes notify` against a Linux node that does not declare `system.notify`).

Include the rejection reason and node platform in the error message so
callers can tell whether the command is not supported by the node, not in
the platform allowlist, or the node did not advertise its capabilities.

Fixes #24616

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit e3d74619bc9d2cf6d93bc5cb19e35d318784bf6a)
2026-02-23 18:56:14 +00:00
Gustavo Madeira Santana 5de1f540e7 CLI: fix gateway restart health ownership for child listener pids (#24696)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: d6d4b43f7e0a59856f40d259053cbf653fac3bc2
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 13:53:10 -05:00
Peter Steinberger 78e7f41d28 docs: detail per-agent prompt caching configuration 2026-02-23 18:46:40 +00:00
Peter Steinberger d637fd4801 fix(config): tighten bedrock cache-retention type narrowing 2026-02-23 18:46:40 +00:00
Peter Steinberger 160bd61fff feat(agents): add per-agent stream params overrides for cache tuning (#17470) (thanks @rrenamed) 2026-02-23 18:46:40 +00:00
Peter Steinberger be6f0b8c84 fix(providers): support Bedrock Anthropic cacheRetention defaults/pass-through (#22303) (thanks @snese) 2026-02-23 18:46:40 +00:00
Peter Steinberger ca5c0bc02b fix(providers): disable Bedrock prompt caching for non-Anthropic models (#20866) (thanks @pierreeurope) 2026-02-23 18:46:40 +00:00
Peter Steinberger e40ee3c2c7 docs(changelog): note /new and /reset auth-label removal (#24409) 2026-02-23 18:30:30 +00:00
Peter Steinberger b9b77cea4e fix(reply): omit auth labels in /new and /reset 2026-02-23 18:30:30 +00:00
Peter Steinberger 4c21ef9ce9 docs(changelog): correct kimi issue references 2026-02-23 18:28:56 +00:00
Peter Steinberger ff0c40d367 test(tools): fix kimi web_search mock typing 2026-02-23 18:27:37 +00:00
Peter Steinberger 7837d23103 feat(media): add moonshot video provider and wiring
Co-authored-by: xiaoyaner0201 <xiaoyaner0201@users.noreply.github.com>
2026-02-23 18:27:37 +00:00
Peter Steinberger e02c470d5e feat(tools): add kimi web_search provider
Co-authored-by: adshine <adshine@users.noreply.github.com>
2026-02-23 18:27:37 +00:00
Peter Steinberger f93ca93498 fix(agents): extend cache-ttl eligibility for moonshot and zai
Co-authored-by: lailoo <lailoo@users.noreply.github.com>
2026-02-23 18:27:36 +00:00
Peter Steinberger 2fa6aa6ea6 test(agents): add comprehensive kimi regressions 2026-02-23 18:27:36 +00:00
Doruk Ardahan daaad03593 fix(infra): treat nested network request errors as non-fatal 2026-02-23 18:27:23 +00:00
Peter Steinberger 445c7a65e6 test: simplify session reset and rawbody coverage 2026-02-23 18:19:23 +00:00
Peter Steinberger 783a9134d6 test: prune redundant trigger-handling scenarios 2026-02-23 18:19:23 +00:00
chilu18 3cadc3eed1 fix(plugins): honor channels.<id>.enabled for bundled channels 2026-02-23 18:16:58 +00:00
Peter Steinberger 65d57eac12 docs(changelog): reorder 2026.2.23 entries by user impact 2026-02-23 18:02:21 +00:00
Peter Steinberger 97787d73c2 docs(changelog): align 2026.2.22 release heading with tags 2026-02-23 18:00:39 +00:00
Peter Steinberger cc7a498ace refactor(tests): deduplicate repeated fixtures in msteams and bash tests 2026-02-23 17:59:56 +00:00
Peter Steinberger b81bce703c test: streamline trigger and session coverage 2026-02-23 17:52:23 +00:00
Peter Steinberger ddc67aa4ef test: collapse duplicate trigger command coverage 2026-02-23 17:37:13 +00:00
Vincent Koc 6a0fcf6518 Sessions: consolidate path hardening and fallback resilience (#24657)
* Changelog: credit session path fixes

* Sessions: harden path resolution for symlink and stale metadata

* Tests: cover fallback for invalid absolute sessionFile

* Tests: add symlink alias session path coverage

* Tests: guard symlink escape in sessionFile resolution
2026-02-23 12:36:01 -05:00
Matthew ce1f12ff33 fix(slack): prevent Zod default groupPolicy from breaking multi-account config (#17579)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7d2da57b5054ab20711bb4ef1a8f31914270459b
Co-authored-by: ZetiMente <76985631+ZetiMente@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 12:35:41 -05:00
Vincent Koc f03ff39754 Providers: skip context1m beta for Anthropic OAuth tokens (#24620)
* Providers: skip context1m beta for Anthropic OAuth tokens

* Tests: cover OAuth context1m beta skip behavior

* Docs: note context1m OAuth incompatibility

* Agents: add context1m-aware context token resolver

* Agents: cover context1m context-token resolver

* Commands: apply context1m-aware context tokens in session store

* Commands: apply context1m-aware context tokens in status summary

* Status: resolve context tokens with context1m model params

* Status: test context1m status context display
2026-02-23 12:29:09 -05:00
Gustavo Madeira Santana 28377e1b7a UI: add version status pill before Health in web header (#24648)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f240589d33fd891efd99558c412d927aa9323908
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 12:27:17 -05:00
Peter Steinberger fdd185cfaa test: merge inline trigger command and elevated coverage 2026-02-23 17:19:39 +00:00
Peter Steinberger f7e45ce947 test: consolidate trigger-handling status and heartbeat scenarios 2026-02-23 17:19:39 +00:00
Peter Steinberger a8a4fa5b88 test: de-duplicate attachment and bash tool tests 2026-02-23 17:19:34 +00:00
Vincent Koc ae66a4b5d2 Changelog: add PR #22855 entry 2026-02-23 12:15:50 -05:00
Vincent Koc 5e1dd5fe69 Changelog: add PR #24593 entry 2026-02-23 12:15:50 -05:00
Vincent Koc d601392904 Changelog: add PR #16176 entry 2026-02-23 12:15:50 -05:00
Shakker 271a149058 chore: add skills-lock.json to gitignore 2026-02-23 17:12:39 +00:00
Sally O'Malley eb4ff6df81 Allow Claude model requests to route through Google Vertex AI (#23985)
* feat: add anthropic-vertex provider for Claude via GCP Vertex AI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>

* docs: add anthropic-vertex provider guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: sallyom <somalley@redhat.com>

* Agents: validate Anthropic Vertex project env

* Changelog: format update for Vertex entry

* Providers: rename Anthropic Vertex to Google Vertex Claude

* Providers: remove Vertex Claude provider path

* Models: normalize Vercel Claude shorthand refs

* Onboarding: default Vercel model to Claude shorthand

* Changelog: add @vincentkoc credit for #23985

* Onboarding: keep canonical Vercel default model ref

* Tests: expand Vercel model normalization coverage

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 11:04:31 -05:00
Clawborn 544809b6f6 Add Chinese context overflow patterns to isContextOverflowError (#22855)
Proxy providers returning Chinese error messages (e.g. Chinese LLM
gateways) use patterns like '上下文过长' or '上下文超出' that are not
matched by the existing English-only patterns in isContextOverflowError.
This prevents auto-compaction from triggering, leaving the session stuck.

Add the most common Chinese proxy patterns:
- 上下文过长 (context too long)
- 上下文超出 (context exceeded)
- 上下文长度超 (context length exceeds)
- 超出最大上下文 (exceeds maximum context)
- 请压缩上下文 (please compress context)

Chinese characters are unaffected by toLowerCase() so check the
original message directly.

Closes #22849
2026-02-23 10:54:24 -05:00
Vincent Koc 4f340b8812 fix(agents): avoid classifying reasoning-required errors as context overflow (#24593)
* Agents: exclude reasoning-required errors from overflow detection

* Tests: cover reasoning-required overflow classification guard

* Tests: format reasoning-required endpoint errors
2026-02-23 10:38:49 -05:00
Alice Losasso 652099cd5c fix: correctly identify Groq TPM limits as rate limits instead of context overflow (#16176)
Co-authored-by: Howard <dddabtc@users.noreply.github.com>
2026-02-23 10:32:53 -05:00
LI SHANXIN c1b75ab8e2 fix(telegram): make reaction handling soft-fail and message-id resilient (#20236)
* Telegram: soft-fail reactions and fallback to inbound message id

* Telegram: soft-fail missing reaction message id

* Update CHANGELOG.md

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 10:25:14 -05:00
DukeDeSouth ea47ab29bd fix: cancel compaction instead of truncating history when summarization fails (#10711)
* fix: cancel compaction instead of truncating history when summarization fails

When the compaction safeguard cannot generate a summary (no model, no API
key, or LLM error), it previously returned a "Summary unavailable" fallback
string and still truncated history. This caused irreversible data loss -
older messages were discarded even though no meaningful summary was produced.

Now returns `{ cancel: true }` in all three failure paths so the framework
aborts compaction entirely and preserves the full conversation history.

Fixes #10332

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: use deterministic timestamps in compaction safeguard tests

Replace Date.now() with fixed timestamp (0) in test data to prevent
nondeterministic behavior in snapshot-based or order-dependent tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Changelog: note compaction cancellation safeguard fix

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 10:23:13 -05:00
Owen 01380f49f5 fix(compaction): pass model through runtime for safeguard summaries (#17864)
* fix(compaction): pass model through runtime to fix ctx.model undefined

Fixes #3479

Root cause: extensionRunner.initialize() is never called in compact.ts workflow,
leaving ctx.model undefined. Compaction safeguard checks ctx.model and returns
fallback summary immediately without attempting LLM summarization.

Changes:
1. Pass model through compaction safeguard runtime registry (same pattern as maxHistoryShare)
2. Fall back to runtime.model when ctx.model is undefined
3. Add once-per-session warning when both models are missing (prevents log spam)
4. Add regression test for runtime.model fallback

This follows the established runtime registry pattern rather than attempting to call
extensionRunner.initialize() (which is SDK-internal and not meant for direct access).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* test: add comprehensive tests for compaction-safeguard model fallback

Add integration tests to verify the model fallback behavior:
- Test runtime.model fallback when ctx.model is undefined (compact.ts workflow)
- Test fallback summary when both ctx.model and runtime.model are undefined
- Test contextWindowTokens runtime storage/retrieval
- Test combined runtime values (maxHistoryShare + contextWindowTokens + model)

These tests verify the fix for issue #3479 where compaction fails due to
ctx.model being undefined in the compact.ts workflow. The runtime registry
pattern allows model to be passed when extensionRunner.initialize() is not
called, ensuring summarization works in all code paths.

Related: PR #17864

* fix(test): adapt compaction-safeguard tests to upstream type changes

- Add baseUrl to Model mock objects (now required by Model<Api>)
- Add explicit Model<Api> annotation to prevent provider string widening
- Cast modelRegistry mock through unknown (ModelRegistry expanded)
- Use non-null assertion for compactionHandler (TypeScript strict)
- Type compaction result explicitly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Compaction: add changelog credit for model fallback fix

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 10:14:21 -05:00
青雲 69692d0d3a fix: detect additional context overflow error patterns to prevent leak to user (#20539)
* fix: detect additional context overflow error patterns to prevent leak to user

Fixes #9951

The error 'input length and max_tokens exceed context limit: 170636 +
34048 > 200000' was not caught by isContextOverflowError() and leaked
to users via formatAssistantErrorText()'s invalidRequest fallback.

Add three new patterns to isContextOverflowError():
- 'exceed context limit' (direct match)
- 'exceeds the model\'s maximum context'
- max_tokens/input length + exceed + context (compound match)

These are now rewritten to the friendly context overflow message.

* Overflow: add regression tests and changelog credits

* Update CHANGELOG.md

* Update pi-embedded-helpers.isbillingerrormessage.test.ts

---------

Co-authored-by: echoVic <AkiraVic@outlook.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 10:03:56 -05:00
Vincent Koc 7fb69b7cd2 Gateway: stop repeated unauthorized WS request floods per connection (#24294)
* Gateway WS: add unauthorized flood guard primitive

* Gateway WS: close repeated unauthorized post-handshake request floods

* Gateway WS: test unauthorized flood guard behavior

* Changelog: note gateway WS unauthorized flood guard hardening

* Update CHANGELOG.md
2026-02-23 09:58:47 -05:00
Vincent Koc 8e821a061c fix(telegram): scope polling offsets per bot and await shared runner stop (#24549)
* Telegram: scope polling offsets and await shared runner stop

* Changelog: remove unrelated session-fix entries from PR

* Update CHANGELOG.md
2026-02-23 09:43:47 -05:00
AkosCz 3a3c2da916 [Feature]: Add Gemini (Google Search grounding) as web_search provider (#13075)
* feat: add Gemini (Google Search grounding) as web_search provider

Add Gemini as a fourth web search provider alongside Brave, Perplexity,
and Grok. Uses Gemini's built-in Google Search grounding tool to return
search results with citations.

- Add runGeminiSearch() with Google Search grounding via tools API
- Resolve Gemini's grounding redirect URLs to direct URLs via parallel
  HEAD requests (5s timeout, graceful fallback)
- Add Gemini config block (apiKey, model) with env var fallback
- Default model: gemini-2.5-flash (fast, cheap, grounding-capable)
- Strip API key from error messages for security
- Add config validation tests for Gemini provider
- Update docs/tools/web.md with Gemini provider documentation

Closes #13074

* feat: auto-detect search provider from available API keys

When no explicit provider is configured, resolveSearchProvider now
checks for available API keys in priority order (Brave → Gemini →
Perplexity → Grok) and selects the first provider with a valid key.

- Add auto-detection logic using existing resolve*ApiKey functions
- Export resolveSearchProvider via __testing_provider for tests
- Add 8 tests covering auto-detection, priority order, and explicit override
- Update docs/tools/web.md with auto-detection documentation

* fix: merge __testing exports, downgrade auto-detect log to debug

* fix: use defaultRuntime.log instead of .debug (not in RuntimeEnv type)

* fix: mark gemini apiKey as sensitive in zod schema

* fix: address Greptile review — add externalContent to Gemini payload, add Gemini/Grok entries to schema labels/help, remove dead schema-fields.ts

* fix(web-search): add JSON parse guard for Gemini API responses

Addresses Greptile review comment: add try/catch to handle non-JSON
responses from Gemini API gracefully, preventing runtime errors on
malformed responses.

Note: FIELD_HELP entries for gemini.apiKey and gemini.model were
already present in schema.help.ts, and gemini.apiKey was already
marked as sensitive in zod-schema.agent-runtime.ts (both fixed in
earlier commits).

* fix: use structured readResponseText result in Gemini error path

readResponseText returns { text, truncated, bytesRead }, not a string.
The Gemini error handler was using the result object directly, which
would always be truthy and never fall through to res.statusText.
Align with Perplexity/xAI/Brave error patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix import order and formatting after rebase onto main

* Web search: send Gemini API key via header

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 09:30:51 -05:00
Peter Steinberger 3f03cdea56 test: optimize redundant suites for faster runtime 2026-02-23 13:57:34 +00:00
Peter Steinberger 5196565f19 test: reduce trigger test redundancy and speed up model coverage 2026-02-23 13:41:47 +00:00
Nimrod Gutman 9d37654a90 fix(agents): gate auto reasoning by effective thinking level (openclaw#24335) thanks @Kay-051 2026-02-23 15:38:08 +02:00
Kay-051 42795b87a3 fix(agents): don't auto-enable reasoning when thinking is active (#24290)
When thinking is set (e.g. thinking=low), the model produces internal
thinking blocks. The reasoning auto-default (based on model capability)
was formatting these blocks as "Reasoning:" text and delivering them to
WhatsApp/Telegram, leaking internal content to users.

Skip auto-enabling reasoning when thinkLevel is already set — the two
features serve the same purpose and enabling both causes the model's
internal thinking to be exposed as visible chat messages.

Users who explicitly set /reasoning on still get reasoning output.

Closes #24290

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 15:38:08 +02:00
Peter Steinberger 89a4695020 test: consolidate shard tests for faster trigger/directive suites 2026-02-23 13:30:47 +00:00
Peter Steinberger 67bccc1fa0 test: merge allow-from trigger shard and dedupe inline cases 2026-02-23 13:18:03 +00:00
Peter Steinberger f6ee1c99a7 test: merge thinking and queue directive shards 2026-02-23 13:11:39 +00:00
Peter Steinberger c9fbcf39ee test: merge fuzzy model directive shards 2026-02-23 13:08:30 +00:00
Peter Steinberger e048ed1efd test: merge elevated allowlist directive shard 2026-02-23 13:05:39 +00:00
Peter Steinberger 706c9ec729 test: consolidate directive behavior suites 2026-02-23 13:02:56 +00:00
Peter Steinberger fbdb1b3e73 test: merge elevated status directive shards 2026-02-23 12:57:39 +00:00
Peter Steinberger b11ff9f7dd test: collapse directive behavior shards 2026-02-23 12:54:52 +00:00
Peter Steinberger be422a9d18 test: merge model picker tests into native command suite 2026-02-23 12:50:08 +00:00
Peter Steinberger 9757d2bb64 fix(agents): normalize strict openai-compatible turn ordering
Co-authored-by: liuwenyong1985 <48443240+liuwenyong1985@users.noreply.github.com>
2026-02-23 12:44:23 +00:00
Peter Steinberger 15e32c7341 fix(models): refresh Moonshot Kimi vision capabilities
Co-authored-by: manikv12 <mac1317@live.missouristate.edu>
2026-02-23 12:44:23 +00:00
Peter Steinberger 9bd04849ed fix(agents): detect Kimi model-token-limit overflows
Co-authored-by: Danilo Falcão <danilo@falcao.org>
2026-02-23 12:44:23 +00:00
Peter Steinberger 3640484e28 fix(agents): map Moonshot developer role compatibility
Co-authored-by: Sheng-Fu Chuang <sedernet@gmail.com>

# Conflicts:
#	CHANGELOG.md
2026-02-23 12:44:23 +00:00
Julia HeySalad 8897c9d53a ci: install pyyaml in skills-python job 2026-02-23 10:44:18 +00:00
Ayaan Zaidi 2247b81219 fix(auto-reply): hide direct-chat metadata without sender-id sentinel (openclaw#24373) thanks @jd316
Co-authored-by: jd316 <138361777+jd316@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
2026-02-23 15:25:31 +05:30
Gustavo Madeira Santana c92c3ad224 Tests: isolate quick_validate stub and remove DS_Store 2026-02-23 03:25:37 -05:00
边黎安 a4c373935f fix(agents): fall back to agents.defaults.model when agent has no model config (#24210)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0f272b102763736001a82cfda23f35ff2ee9cac8
Co-authored-by: bianbiandashen <16240681+bianbiandashen@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 03:18:55 -05:00
Vincent Koc db32677f1d Update CHANGELOG.md 2026-02-23 03:03:06 -05:00
taw0002 3c57bf4c85 fix: treat HTTP 502/503/504 as failover-eligible (timeout reason) (#21017)
* fix: treat HTTP 502/503/504 as failover-eligible (timeout reason)

When a model API returns 502 Bad Gateway, 503 Service Unavailable, or
504 Gateway Timeout, the error object carries the status code directly.
resolveFailoverReasonFromError() only checked 402/429/401/403/408/400,
so 5xx server errors fell through to message-based classification which
requires the status code to appear at the start of the error message.

Many API SDKs (Google, Anthropic) set err.status = 503 without prefixing
the message with '503', so the message classifier never matched and
failover never triggered — the run retried the same broken model.

Add 502/503/504 to the status-code branch, returning 'timeout' (matching
the existing behavior of isTransientHttpError in the message classifier).

Fixes #20999

* Changelog: add failover 502/503/504 note with credits

* Failover: classify HTTP 504 as transient in message parser

* Changelog: credit taw0002 and vincentkoc for failover fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 03:01:57 -05:00
Vincent Koc 07edadfa8a skill-creator: reject unclosed YAML frontmatter (#24289) 2026-02-23 02:51:51 -05:00
Frank Yang f208518cb9 fix(config): keep write inputs immutable when using unsetPaths (#24134)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 951f8480c30b9b22bdb0e8047c74c9460080f326
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 02:51:13 -05:00
Vincent Koc 0e28e50b45 fix(security): detect obfuscated commands that bypass allowlist filters (#24287)
* security(exec): add obfuscated command detector

* test(exec): cover obfuscation detector patterns

* security(exec): enforce obfuscation approval on gateway host

* security(exec): enforce obfuscation approval on node host

* test(exec): prevent obfuscation timeout bypass

* chore(changelog): credit obfuscation security fix
2026-02-23 02:50:06 -05:00
Henry Loenwind 7568ae52ce Typo (#24288) 2026-02-23 02:47:06 -05:00
Henry Loenwind ad666c5f37 Fixed Discord channel name (#24281) 2026-02-23 02:37:45 -05:00
Vincent Koc c8a62e1cea Skills/Python: harden script edge cases and add regression tests (#24277)
* Skill creator: skip self-including .skill output

* Skill creator tests: cover output-dir-inside-skill case

* Skill validator: parse frontmatter robustly across newlines

* Skill validator tests: add CRLF and malformed frontmatter coverage

* Model usage: require positive --days value

* Model usage tests: cover --days validation and filtering

* Nano banana: close input image handles after loading

* Skill validator: keep type hints compatible with older python

* Changelog: credit @vincentkoc for Python skills hardening
2026-02-23 02:34:23 -05:00
Glucksberg 36400df086 fix: pass agentDir to /compact command for agent-specific auth (#24133)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 4bb10ca78ca064e05669ccb358cdff9efc0da6fc
Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 02:33:35 -05:00
Vignesh Natarajan 9ea740afb6 Sessions: canonicalize mixed-case session keys 2026-02-22 23:27:08 -08:00
Vincent Koc 1be8897339 Security: enforce pre-commit security checks in hooks and CI (#24265)
* chore(pre-commit): add security audit hooks

* ci(security): enforce security hooks in ci

* docs(changelog): add security hooks and ci attribution
2026-02-23 02:20:00 -05:00
Vincent Koc 4ab4754bdf chore(changelog): credit skill packager hardening follow-up 2026-02-23 02:09:05 -05:00
Vincent Koc 844924cf8d fix(skill-creator): harden skill packaging path handling (#24260)
* fix(skill-creator): skip symlinks during skill packaging

* test(skill-creator): cover symlink skipping and root-escape guard
2026-02-23 02:07:36 -05:00
Vignesh Natarajan 8d9d01447e chore: align plugin versions and harden outbound cross-provider test 2026-02-22 23:04:17 -08:00
Vignesh Natarajan 5a0eb695fa chore: format pre-commit config for CI 2026-02-22 22:54:44 -08:00
Vincent Koc 76dabd5214 CI/Skills: add Python lint and test harness for skills scripts (#24246)
* CI: add skills Python checks job

* Chore: add Python lint and test pre-commit hooks

* Tests: fix skill-creator package test import path

* Chore: add Python tooling config for skills scripts

* CI: run all skills Python tests

* Chore: run all skills Python tests in pre-commit

* Chore: enable pytest discovery for all skills tests

* Changelog: note skills Python quality harness
2026-02-23 01:52:00 -05:00
Vignesh Natarajan de96f5fed2 CLI/Sessions: honor default agent for implicit store path 2026-02-22 22:47:10 -08:00
Vincent Koc af4330ef75 Update CHANGELOG.md 2026-02-23 01:44:53 -05:00
Misha Kolesnik ec1bc41cf2 fix(openrouter): remove conflicting reasoning_effort from payload (#24120)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: cc8ef4bb05a71626152109ca0d70f3c17cb0100c
Co-authored-by: tenequm <22403766+tenequm@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 01:41:29 -05:00
CornBrother0x f3adf142c1 fix(security): escape user input in HTML gallery to prevent stored XSS (#16958)
* Security/openai-image-gen: escape HTML gallery user input

* Tests/openai-image-gen: add gallery XSS regression coverage

* Changelog: add openai-image-gen XSS hardening note

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 01:39:58 -05:00
Vignesh Natarajan 5ad5ea53cd Agent: resolve resumed session agent scope before run 2026-02-22 22:38:05 -08:00
brandonwise 7fab4d128a fix(security): redact sensitive data in OTEL log exports (CWE-532) (#18182)
* fix(security): redact sensitive data in OTEL log exports (CWE-532)

The diagnostics-otel plugin exports ALL application logs to external
OTLP collectors without filtering. This leaks API keys, tokens, and
other sensitive data to third-party observability platforms.

Changes:
- Export redactSensitiveText from plugin-sdk for extension use
- Apply redaction to log messages before OTEL export
- Apply redaction to string attribute values
- Add tests for API key and token redaction

The existing redactSensitiveText function handles common patterns:
- API keys (sk-*, ghp_*, gsk_*, AIza*, etc.)
- Bearer tokens
- PEM private keys
- ENV-style assignments (KEY=value)
- JSON credential fields

Fixes #12542

* fix: also redact error/reason in trace spans

Address Greptile feedback:
- Redact evt.error in webhook.error span attributes and status
- Redact evt.reason in message.processed span attributes
- Redact evt.error in message.processed span status

* fix: handle undefined evt.error in type guard

* fix: redact session.state reason in OTEL metrics

Addresses Greptile feedback - session.state reason field now goes
through redactSensitiveText() like message.processed reason.

* test(diagnostics-otel): update service context for stateDir API change

* OTEL diagnostics: redact sensitive values before export

* OTEL diagnostics tests: cover message, attribute, and session reason redaction

* Changelog: note OTEL sensitive-data redaction fix

* Changelog: move OTEL redaction entry to current unreleased

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-23 01:35:32 -05:00
Ayaan Zaidi 61db3d4a16 fix(protocol): regenerate swift gateway models 2026-02-23 11:52:42 +05:30
Ayaan Zaidi 86fcca2352 fix(gateway): annotate connection test mocks 2026-02-23 11:47:27 +05:30
Ayaan Zaidi fda98f5605 docs(changelog): add telegram topic target fix 2026-02-23 11:45:18 +05:30
Ayaan Zaidi d5105ca456 fix(telegram): unify topic target normalization path 2026-02-23 11:45:18 +05:30
Ayaan Zaidi fddc60d174 fix(telegram): preserve legacy prefixed messaging targets 2026-02-23 11:45:18 +05:30
Ayaan Zaidi bf732b88e7 test(cron): avoid delivery.mode type widening in isolated announce test 2026-02-23 11:45:18 +05:30
Ayaan Zaidi 118611465c test(gateway): make strict-delivery bestEffort case deterministic 2026-02-23 11:45:18 +05:30
Ayaan Zaidi d589b3a95c test(gateway): clear agentCommand mock before strict bestEffort assert 2026-02-23 11:45:18 +05:30
Ayaan Zaidi 03122e5933 fix(cron): preserve telegram announce target + delivery truth 2026-02-23 11:45:18 +05:30
Ayaan Zaidi dcc52850c3 fix: persist resolved telegram delivery targets at runtime 2026-02-23 11:45:18 +05:30
Tak Hoffman 35fbf26d24 Gateway: suppress tools.catalog plugin conflict diagnostics 2026-02-23 00:05:57 -06:00
Tak Hoffman 9e1a13bf4c Gateway/UI: data-driven agents tools catalog with provenance (openclaw#24199) thanks @Takhoffman
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- gh pr checks 24199 --watch --fail-fast

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-22 23:55:59 -06:00
Peter Steinberger 1c753ea786 test: dedupe fixtures and test harness setup 2026-02-23 05:45:54 +00:00
Peter Steinberger 8af19ddc5b refactor: extract shared dedupe helpers for runtime paths 2026-02-23 05:43:43 +00:00
Peter Steinberger 9f508056d3 test: collapse remaining trigger command shards 2026-02-23 05:22:24 +00:00
Peter Steinberger d90e9f561f test: merge overlapping trigger-handling suites 2026-02-23 05:19:23 +00:00
Peter Steinberger af547ec52c test: consolidate trigger-handling suites 2026-02-23 05:15:35 +00:00
Evgeny Zislis 78f801e243 Validate Telegram delivery targets to reject invalid formats (#21930)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 02c9b1c3dd4273988d571d513403e02e3b062e46
Co-authored-by: kesor <7056+kesor@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-23 10:44:46 +05:30
Peter Steinberger 23598e0e3a test: prune redundant abort case and speed stream cap test 2026-02-23 05:06:34 +00:00
Tak Hoffman 77c3b142a9 Web UI: add full cron edit parity, all-jobs run history, and compact filters (openclaw#24155) thanks @Takhoffman
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-22 23:05:42 -06:00
Peter Steinberger 610863e733 test: speed up long-running async suites 2026-02-23 05:03:15 +00:00
Peter Steinberger 48f327c206 test: consolidate redundant suites and speed attachment tests 2026-02-23 04:55:43 +00:00
Peter Steinberger 86a8b65e9d test: consolidate redundant suites and speed up timers 2026-02-23 04:44:42 +00:00
Peter Steinberger a6a2a9276e test: reduce exec timer test runtime 2026-02-23 04:25:00 +00:00
Peter Steinberger 384a161bbc test: consolidate media auto-detect coverage 2026-02-23 04:25:00 +00:00
Peter Steinberger a53062ae3b refactor(test): deduplicate isolated agent cron test helpers 2026-02-23 04:20:41 +00:00
Peter Steinberger 382fe8009a refactor!: remove google-antigravity provider support 2026-02-23 05:20:14 +01:00
Peter Steinberger 558a0137bb chore(release): bump versions to 2026.2.23 2026-02-23 05:13:46 +01:00
Tak Hoffman a54dc7fe80 Cron: suppress fallback main summary for delivery-target errors (openclaw#24074) thanks @Takhoffman
Docker Release / build-amd64 (push) Has been cancelled
Docker Release / build-arm64 (push) Has been cancelled
Docker Release / create-manifest (push) Has been cancelled
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-22 20:24:08 -06:00
Tak Hoffman 457835b104 Compaction: count only completed auto-compactions (#24056)
* Compaction: count only completed auto-compactions

* Compaction: count only non-retry completions

* Changelog: note completed-only compaction counting

* Agents/Compaction: guard optional compaction increment
2026-02-22 20:16:45 -06:00
Tak Hoffman 05691be511 Compaction: ignore tool result details in oversized checks (#24057)
* Compaction: ignore tool result details in oversized checks

* Tests/Compaction: type estimateTokens message callback
2026-02-22 20:13:59 -06:00
Tak Hoffman 5c9f9722af Agent runner: align compaction floor guidance (#24059) 2026-02-22 20:13:43 -06:00
Tak Hoffman 50c5f75904 Compaction: sanitize token split accounting (#24058)
* Compaction: sanitize token split accounting

* Tests/Compaction: type sanitize token estimate callback
2026-02-22 20:13:21 -06:00
Tak Hoffman 259d863353 Gateway: harden cron.runs jobId path handling (openclaw#24038) thanks @Takhoffman
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-22 19:35:26 -06:00
Peter Steinberger 45febecf2a fix(exec): keep implicit sandbox default and restore no-alert baseline 2026-02-23 02:17:43 +01:00
Tak Hoffman f6c2e99f5d Cron: preserve due jobs after manual runs (#23994) 2026-02-22 19:02:05 -06:00
Peter Steinberger bac26b4472 chore(release): bump version to 2026.2.22-1 2026-02-23 01:59:52 +01:00
Vignesh Natarajan a10ec2607f Gateway/Chat UI: sanitize untrusted wrapper markup in final payloads 2026-02-22 16:53:54 -08:00
Peter Steinberger b482da8c9a chore: update appcast for 2026.2.22 beta.1 2026-02-23 01:52:53 +01:00
Peter Steinberger 80f430c2be fix(daemon): extend restart health timeout and improve restart errors 2026-02-23 01:50:02 +01:00
Peter Steinberger 278331c49c fix(exec): restore sandbox as implicit host default 2026-02-23 01:48:24 +01:00
Tak Hoffman 211ab9e4f6 Cron: persist manual run marker before unlock (#23993)
* Cron: persist manual run marker before unlock

* Cron tests: relax wakeMode now microtask wait after run lock persist
2026-02-22 18:39:37 -06:00
SleuthCo.AI 9c87b53c8e security(cli): redact sensitive values in config get output (#23654)
* security(cli): redact sensitive values in config get output

`runConfigGet()` reads raw config values but never applies redaction
before printing. When a user runs `openclaw config get gateway.token`
the real credential is printed to the terminal, leaking it into shell
history, scrollback buffers, and screenshots.

Use the existing `redactConfigObject()` (from redact-snapshot.ts,
already used by the Web UI path) to scrub sensitive fields before
`getAtPath()` resolves the requested key.

Fixes #13683

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* CLI/Config: add redaction regression test and changelog

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 19:37:33 -05:00
Vignesh Natarajan f0542df9f0 Docker: precreate identity dir in docker setup 2026-02-22 16:33:53 -08:00
Peter Steinberger 70dd6a30e7 chore(synology-chat): allow npm publish for plugin package 2026-02-23 01:30:36 +01:00
Peter Steinberger b19a6ee62d docs(changelog): move mistral to top and add synology chat 2026-02-23 01:25:22 +01:00
Peter Steinberger cc8e6e9939 fix(synology-chat): align docs metadata and declare runtime deps 2026-02-23 01:24:51 +01:00
Peter Steinberger 0371646a61 test: fix msteams shared attachment fetch mock typing 2026-02-23 00:19:40 +00:00
Peter Steinberger 60c494c024 test: tighten mistral media and onboarding coverage 2026-02-23 00:19:05 +00:00
Phineas1500 8a8faf066e doctor: clean up legacy Linux gateway services (#21188)
* Doctor: clean up legacy Linux gateway services

* doctor: refactor legacy service cleanup flow

* doctor: fix legacy systemd cleanup map key typing

* doctor: add changelog entry for legacy Linux service cleanup

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 19:18:59 -05:00
Peter Steinberger 1c2c7843a8 docs: add synology channel docs and fix unreleased changelog 2026-02-23 01:16:05 +01:00
Phineas1500 320b62265d fix(models): synthesize antigravity Gemini 3.1 pro high/low models (#22899)
* Models: add antigravity Gemini 3.1 forward-compat

* models: propagate availability to Gemini 3.1 dot IDs

* test(models): format Gemini 3.1 forward-compat test

* test(models): type Gemini 3.1 forward-compat fixtures

* models: add changelog note for antigravity gemini 3.1 forward-compat

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 19:11:39 -05:00
Vignesh Natarajan 5c7c37a02a Agents: infer auth-profile unavailable failover reason 2026-02-22 16:10:32 -08:00
Phineas1500 331b728b8d fix(tui): add OSC 8 hyperlinks for wrapped URLs (#17814)
* feat(tui): add OSC 8 hyperlinks to make wrapped URLs clickable

Long URLs that exceed terminal width get broken across lines by pi-tui's
word wrapping, making them unclickable. Post-process rendered markdown
output to add OSC 8 terminal hyperlink sequences around URL fragments,
so each line fragment links to the full URL. Gracefully degrades on
terminals without OSC 8 support.

* tui: harden OSC8 URL extraction and prefix resolution

* tui: add changelog entry for OSC 8 markdown hyperlinks

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 19:09:07 -05:00
Vincent Koc d92ba4f8aa feat: Provider/Mistral full support for Mistral on OpenClaw 🇫🇷 (#23845)
* Onboard: add Mistral auth choice and CLI flags

* Onboard/Auth: add Mistral provider config defaults

* Auth choice: wire Mistral API-key flow

* Onboard non-interactive: support --mistral-api-key

* Media understanding: add Mistral Voxtral audio provider

* Changelog: note Mistral onboarding and media support

* Docs: add Mistral provider and onboarding/media references

* Tests: cover Mistral media registry/defaults and auth mapping

* Memory: add Mistral embeddings provider support

* Onboarding: refresh Mistral model metadata

* Docs: document Mistral embeddings and endpoints

* Memory: persist Mistral embedding client state in managers

* Memory: add regressions for mistral provider wiring

* Gateway: add live tool probe retry helper

* Gateway: cover live tool probe retry helper

* Gateway: retry malformed live tool-read probe responses

* Memory: support plain-text batch error bodies

* Tests: add Mistral Voxtral live transcription smoke

* Docs: add Mistral live audio test command

* Revert: remove Mistral live voice test and docs entry

* Onboard: re-export Mistral default model ref from models

* Changelog: credit joeVenner for Mistral work

* fix: include Mistral in auto audio key fallback

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Shakker <shakkerdroid@gmail.com>
2026-02-23 00:03:56 +00:00
yinghaosang a66b98a9da fix(plugins): hook systemPrompt gets collected then thrown away (#14583) (#14602)
* fix(plugins): apply before_agent_start hook systemPrompt to session (#14583)

* fix(plugins): apply legacy systemPrompt override and add changelog credit

---------

Co-authored-by: yinghaosang <yinghaosang@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 18:58:21 -05:00
Peter Steinberger 970062872f chore: remove deprecated npm allow-build-scripts config 2026-02-23 00:51:00 +01:00
Peter Steinberger 14c54e6501 fix(reasoning): persist off override for discord directives 2026-02-23 00:50:13 +01:00
Peter Steinberger f79e3d5f03 fix(agents): remove synthetic done fallback reply 2026-02-23 00:50:00 +01:00
Vignesh Natarajan 1000ff04ea fix(memory): hard-cap embedding inputs before batch 2026-02-22 15:40:18 -08:00
Aether AI d306fc8ef1 fix(security): OC-07 redact session history credentials and enforce webhook secret (#16928)
* Security: refresh sessions history redaction patch

* tests: align sessions_history redaction-only truncation expectation

* Changelog: credit sessions history security hardening

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 18:29:40 -05:00
Tak Hoffman 3efe63d1ad Cron: respect aborts in main wake-now retries (#23967)
* Cron: respect aborts in main wake-now retries

* Changelog: add main-session cron abort retry fix note

* Cron tests: format post-rebase conflict resolution
2026-02-22 17:19:27 -06:00
Tak Hoffman 9bc265f379 Cron: clean run-log write queue entries (#23968)
* Cron: clean run-log write queue entries

* Changelog: add cron run-log write-queue cleanup note
2026-02-22 17:16:42 -06:00
Johann Zahlmann 22c9018303 WhatsApp: enforce allowFrom for explicit outbound sends (#20921)
* whatsapp: enforce allowFrom in explicit outbound mode

* Update CHANGELOG.md

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 18:13:23 -05:00
Vignesh Natarajan d7747148d0 fix(memory): reindex when sources change 2026-02-22 15:12:07 -08:00
Robin Waslander 44727dc3a1 security(web_fetch): strip hidden content to prevent indirect prompt injection (#21074)
* security(web_fetch): strip hidden content to prevent indirect prompt injection

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* security(web_fetch): address review feedback and credit author

* chore(changelog): credit reporter for web_fetch security fix

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 18:10:26 -05:00
Tak Hoffman 73e5bb7635 Cron: apply timeout to startup catch-up runs (#23966)
* Cron: apply timeout to startup catch-up runs

* Changelog: add cron startup timeout catch-up note
2026-02-22 17:04:30 -06:00
Lewis 26644c4b89 fix(msteams): add SSRF protection to attachment downloads via redirect and DNS validation (#23598)
* fix(msteams): add SSRF protection to attachment downloads via redirect and DNS validation

The attachment download flow in fetchWithAuthFallback() followed
redirects automatically on the initial fetch without any allowlist
or IP validation. This allowed DNS rebinding attacks where an
allowlisted domain (e.g. evil.trafficmanager.net) could redirect
or resolve to a private IP like 169.254.169.254, bypassing the
hostname allowlist entirely (issue #11811).

This commit adds three layers of SSRF protection:

1. safeFetch() in shared.ts: a redirect-safe fetch wrapper that uses
   redirect: "manual" and validates every redirect hop against the
   hostname allowlist AND DNS-resolved IP before following it.

2. isPrivateOrReservedIP() + resolveAndValidateIP() in shared.ts:
   rejects RFC 1918, loopback, link-local, and IPv6 private ranges
   for both initial URLs and redirect targets.

3. graph.ts SharePoint redirect handling now also uses redirect:
   "manual" and validates resolved IPs, not just hostnames.

The initial fetch in fetchWithAuthFallback now goes through safeFetch
instead of a bare fetch(), ensuring redirects are never followed
without validation.

Includes 38 new tests covering IP validation, DNS resolution checks,
redirect following, DNS rebinding attacks, redirect loops, and
protocol downgrade blocking.

* fix: address review feedback on SSRF protection

- Replace hand-rolled isPrivateOrReservedIP with SDK's isPrivateIpAddress
  which handles IPv4-mapped IPv6, expanded notation, NAT64, 6to4, Teredo,
  octal IPv4, and fails closed on parse errors
- Add redirect: "manual" to auth retry redirect fetch in download.ts to
  prevent chained redirect attacks bypassing SSRF checks
- Add redirect: "manual" to SharePoint redirect fetch in graph.ts to
  prevent the same chained redirect bypass
- Update test expectations for SDK's fail-closed behavior on malformed IPs
- Add expanded IPv6 loopback (0:0:0:0:0:0:0:1) test case

* fix: type fetchMock as typeof fetch to fix TS tuple index error

* msteams: harden attachment auth and graph redirect fetch flow

* changelog(msteams): credit redirect-safeFetch hardening contributors

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 18:00:54 -05:00
Vignesh Natarajan a58b40e153 chore(test): stabilize mcporter assertions on Windows 2026-02-22 14:47:50 -08:00
Vignesh Natarajan 82d34b4b06 fix(memory): harden qmd collection recovery 2026-02-22 14:40:04 -08:00
Peter Steinberger 5858de6078 docs: reorder 2026.2.22 changelog by user impact 2026-02-22 23:37:44 +01:00
Peter Steinberger 84e5ab598a fix: make windows CI path handling deterministic 2026-02-22 22:34:49 +00:00
Peter Steinberger 3b0e62d5bf fix(doctor): warn that approvals.exec.enabled only disables forwarding
Co-authored-by: nomadonwheels196 <nomadonwheels196@users.noreply.github.com>
2026-02-22 23:33:15 +01:00
Peter Steinberger a30f9c8673 fix(sandbox): fallback docker user to workspace owner uid/gid
Co-authored-by: LucasAIBuilder <LucasAIBuilder@users.noreply.github.com>
2026-02-22 23:33:15 +01:00
Peter Steinberger 394a1af70f fix(exec): apply per-agent exec defaults for opaque session keys
Co-authored-by: brin-tapcart <brin-tapcart@users.noreply.github.com>
2026-02-22 23:33:14 +01:00
Peter Steinberger 427b4360b9 build: update deps and stabilize tests 2026-02-22 23:32:38 +01:00
Peter Steinberger a5917e4ad8 test(exec): resolve rebase artifact in bash-tools test 2026-02-22 22:25:47 +00:00
Peter Steinberger 1d8968c8a8 fix(voice-call): harden media stream pre-start websocket handling 2026-02-22 23:25:32 +01:00
Vignesh Natarajan 1ad9f9af5a fix(memory): resolve qmd Windows shim commands 2026-02-22 14:24:49 -08:00
Peter Steinberger cd919ebd2d refactor(exec): unify wrapper resolution and split approvals tests 2026-02-22 23:20:09 +01:00
mudrii 3645420a33 perf: skip cache-busting for bundled hooks, use mtime for workspace hooks (openclaw#16960) thanks @mudrii
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: mudrii <220262+mudrii@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-22 16:14:51 -06:00
Peter Steinberger 84303f6a78 test: make exec timeout coverage deterministic 2026-02-22 22:14:01 +00:00
Peter Steinberger 13db0b88f5 refactor(gateway): share safe avatar file open checks 2026-02-22 23:12:45 +01:00
Peter Steinberger 7b229decdd test(perf): dedupe fixtures and reduce flaky waits 2026-02-22 22:06:01 +00:00
Peter Steinberger b534dfa3e0 fix(slack,web): harden thread hints and monitor tuning 2026-02-22 22:06:01 +00:00
Peter Steinberger 57b75678d4 test(security): consolidate runtime guardrail scans 2026-02-22 22:06:01 +00:00
Peter Steinberger fe58839ed1 docs(changelog): thank ghsa reporter for exec fix 2026-02-22 23:04:29 +01:00
Peter Steinberger 498138e77e docs(changelog): record avatar security hardening 2026-02-22 23:04:23 +01:00
Peter Steinberger 9a6a4131ba docs(changelog): note shell-wrapper line-continuation exec hardening 2026-02-22 23:03:53 +01:00
Peter Steinberger c677be9d5f fix(exec): skip default timeout for background sessions 2026-02-22 23:03:44 +01:00
Peter Steinberger 4b0fddc075 fix(test): prevent env leak causing models.json CI flake 2026-02-22 22:00:44 +00:00
Peter Steinberger 862975507a refactor(exec): split command resolution and trusted-dir normalization 2026-02-22 23:00:33 +01:00
Peter Steinberger 70cac824b1 perf(test): optimize parallel vitest worker budget 2026-02-22 21:59:22 +00:00
Peter Steinberger 24c954d972 fix(security): harden allow-always wrapper persistence 2026-02-22 22:55:33 +01:00
Peter Steinberger 4adfe80027 fix(extensions): preserve mediaLocalRoots in telegram/discord sendMedia 2026-02-22 22:53:57 +01:00
Peter Steinberger 1e582dcc6f fix: harden windows path handling in CI tests 2026-02-22 21:52:10 +00:00
Tak Hoffman 556af3f08b fix(cron): cancel timed-out runs before side effects (openclaw#22411) thanks @Takhoffman
Verified:
- pnpm check
- pnpm vitest run src/memory/qmd-manager.test.ts src/cron/service.issue-regressions.test.ts src/cron/isolated-agent.delivers-response-has-heartbeat-ok-but-includes.test.ts --maxWorkers=1

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-22 15:45:27 -06:00
Peter Steinberger 64b273a71c fix(exec): harden safe-bin trust and add explicit trusted dirs 2026-02-22 22:43:18 +01:00
Vignesh Natarajan 08fb38f729 Fix: resolve pnpm check type regressions 2026-02-22 13:40:51 -08:00
Peter Steinberger 6970c2c2db fix(gateway): harden control-ui avatar reads 2026-02-22 22:40:22 +01:00
Peter Steinberger e16f93af0c fix: stabilize ci test typings and mocks 2026-02-22 21:38:47 +00:00
Peter Steinberger 3f0b9dbb36 fix(security): block shell-wrapper line-continuation allowlist bypass 2026-02-22 22:36:29 +01:00
Peter Steinberger 7c109f5737 fix: resolve ci type errors and reconnect test flake 2026-02-22 21:35:20 +00:00
Vignesh Natarajan d75b594e07 Agents/Replies: scope done fallback to direct sessions 2026-02-22 13:30:30 -08:00
Peter Steinberger e4d67137db fix(node): default mac headless system.run to local host
Co-authored-by: aethnova <262512133+aethnova@users.noreply.github.com>
2026-02-22 22:24:28 +01:00
Peter Steinberger d24f5c1e3a fix(gateway): fail fast exec approvals when no approvers are reachable
Co-authored-by: fanxian831-netizen <262880470+fanxian831-netizen@users.noreply.github.com>
2026-02-22 22:24:27 +01:00
Peter Steinberger 73fab7e445 fix(agents): map container workdir paths in workspace guard
Co-authored-by: Explorer1092 <32663226+Explorer1092@users.noreply.github.com>
2026-02-22 22:24:27 +01:00
Peter Steinberger 7bbd597383 fix(media): enforce agent media roots in plugin send actions
Co-authored-by: Oliver Drobnik <333270+odrobnik@users.noreply.github.com>
Co-authored-by: thisischappy <257418353+thisischappy@users.noreply.github.com>
2026-02-22 22:24:27 +01:00
Peter Steinberger 33a43a151d refactor(security): split elevated allowFrom matcher internals 2026-02-22 22:20:04 +01:00
Peter Steinberger 2081b3a3c4 refactor(channels): dedupe hook and monitor execution paths 2026-02-22 21:19:09 +00:00
Peter Steinberger 06b0a60bef refactor(daemon): share runtime and service probe helpers 2026-02-22 21:19:09 +00:00
Peter Steinberger e029f78447 refactor(config): dedupe install and typing schema definitions 2026-02-22 21:19:09 +00:00
Peter Steinberger 4bf67ab698 refactor(commands): centralize shared command formatting helpers 2026-02-22 21:19:09 +00:00
Peter Steinberger 06bdd53658 refactor(agents): dedupe workspace and session tool flows 2026-02-22 21:19:09 +00:00
Peter Steinberger 2f8c68ae4d refactor(test): dedupe run-loop signal harness setup 2026-02-22 21:19:09 +00:00
Peter Steinberger 52ee1f697e test(memory): cover shared batch output and error helpers 2026-02-22 21:19:09 +00:00
Peter Steinberger ad51372f78 refactor(memory): share batch provider scaffolding 2026-02-22 21:19:09 +00:00
Tak Hoffman f8171ffcdc Config UI: tag filters and complete schema help/labels coverage (#23796)
* Config UI: add tag filters and complete schema help/labels

* Config UI: finalize tags/help polish and unblock test suite

* Protocol: regenerate Swift gateway models
2026-02-22 15:17:07 -06:00
Peter Steinberger c539782c09 test(gateway-lock): stabilize port-probe liveness coverage 2026-02-22 22:11:53 +01:00
Peter Steinberger ffb12397a8 fix(cron): direct-deliver thread and topic announce targets
Co-authored-by: Andrei Aratmonov <247877121+AndrewArto@users.noreply.github.com>
2026-02-22 22:11:52 +01:00
Peter Steinberger 320cf8eb3e fix(subagents): restore configurable announce timeout
Co-authored-by: Valadon <20071960+Valadon@users.noreply.github.com>
2026-02-22 22:11:52 +01:00
Peter Steinberger 3820ad77ba fix(cron): pass agentDir into embedded follow-up runs
Co-authored-by: seilk <88271769+seilk@users.noreply.github.com>
2026-02-22 22:11:52 +01:00
Peter Steinberger 34fef3ae60 fix(delivery): quarantine permanent recovery failures
Co-authored-by: Aldo <17973757+aldoeliacim@users.noreply.github.com>
2026-02-22 22:11:51 +01:00
Peter Steinberger e6383a2c13 fix(gateway): probe port liveness for stale lock recovery
Co-authored-by: Operative-001 <261882263+Operative-001@users.noreply.github.com>
2026-02-22 22:11:51 +01:00
Peter Steinberger 9165bd7f37 fix(gateway): auto-approve loopback scope upgrades
Co-authored-by: Marcus Widing <245375637+widingmarcus-cyber@users.noreply.github.com>
2026-02-22 22:11:50 +01:00
Peter Steinberger 6817c0ec7b fix(security): tighten elevated allowFrom sender matching 2026-02-22 22:00:08 +01:00
Peter Steinberger 3a088c9f4f docs: prune completed experiment plan notes 2026-02-22 21:56:01 +01:00
Peter Steinberger 382785c6ce refactor(webchat): extract shared chat state helpers 2026-02-22 21:37:19 +01:00
Peter Steinberger d574056761 fix(control-ui): send stable websocket instance IDs (#23616)
Co-authored-by: zq58855371-ui <248869919+zq58855371-ui@users.noreply.github.com>
2026-02-22 21:37:19 +01:00
Peter Steinberger dc6afeb4f8 perf(webchat): skip unnecessary full history reloads on final events (#20588)
Co-authored-by: amzzzzzzz <154392693+amzzzzzzz@users.noreply.github.com>
2026-02-22 21:37:19 +01:00
Peter Steinberger f2e9986813 fix(webchat): append out-of-band final payloads in active chat (#11139)
Co-authored-by: AkshayNavle <110360+AkshayNavle@users.noreply.github.com>
2026-02-22 21:37:19 +01:00
Peter Steinberger 8264d4521b fix(webchat): render final assistant payloads without history wait (#14928)
Co-authored-by: BradGroux <3053586+BradGroux@users.noreply.github.com>
2026-02-22 21:37:19 +01:00
Peter Steinberger 02dc0c8752 fix(control-ui): stop websocket client on lifecycle teardown (#23422)
Co-authored-by: floatinggball-design <262259579+floatinggball-design@users.noreply.github.com>
2026-02-22 21:37:19 +01:00
Peter Steinberger 19046e0cfc fix(webchat): preserve session labels across /new resets (#23755)
Co-authored-by: ThunderStormer <16649514+ThunderStormer@users.noreply.github.com>
2026-02-22 21:37:19 +01:00
Peter Steinberger 8a83ca54a1 fix(webchat): preserve session channel routing on internal turns (#23258)
Co-authored-by: binary64 <1680627+binary64@users.noreply.github.com>
2026-02-22 21:37:18 +01:00
Peter Steinberger b0252ab90c docs: fix canonical session doc path hint 2026-02-22 21:35:14 +01:00
Peter Steinberger 13541864e5 refactor: extract telegram lane delivery and e2e harness 2026-02-22 21:33:20 +01:00
Peter Steinberger acfbe158c6 docs: point pi extension paths to real source files 2026-02-22 21:32:28 +01:00
Peter Steinberger 820d765553 docs: update outbound refactor test path 2026-02-22 21:28:08 +01:00
Peter Steinberger 6ed08ddc24 docs: fix stale test file paths in experiment plans 2026-02-22 21:24:48 +01:00
Sid 3bfe990c33 fix(skill-creator): exclude .git and VCS internals from .skill archives (#23180)
The packager included .git directory contents in .skill archives,
causing unnecessary bloat, metadata leakage, and poor artifact hygiene.

Hard-exclude .git, .svn, .hg, __pycache__, and node_modules from
packaged archives. These paths are never useful in distributable skills.

Fixes #23149

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 15:24:11 -05:00
Peter Steinberger 3f64d4ad7b refactor(config): compile toolsBySender policy and migrate legacy keys 2026-02-22 21:22:20 +01:00
Peter Steinberger c73837d269 docs: replace stale pi test file list with maintained patterns 2026-02-22 21:21:08 +01:00
Peter Steinberger 7eae1933fb refactor(test): extract shared fixture helpers in gateway and outbound tests 2026-02-22 20:18:20 +00:00
Peter Steinberger 5d90e31807 refactor(cron): share timed job-execution helper 2026-02-22 20:18:20 +00:00
Peter Steinberger dff9ead59a docs: refresh gateway test references in testing guide 2026-02-22 21:16:53 +01:00
Peter Steinberger 30e8f41cfc docs: fix stale release checklist source paths 2026-02-22 21:15:09 +01:00
Peter Steinberger 06b4baf67f docs: remove internal hook import paths from examples 2026-02-22 21:12:49 +01:00
Peter Steinberger 5dba7501c9 docs: update stale tsgo reference in pty plan 2026-02-22 21:10:14 +01:00
Peter Steinberger 9c480d4dea docs: replace removed pi test script with current commands 2026-02-22 21:07:34 +01:00
Peter Steinberger 8af6d1a186 refactor(test): dedupe repeated fixture setup helpers 2026-02-22 20:04:51 +00:00
Peter Steinberger 6ef4eda1f0 refactor(memory): share post-json helper across remote fetchers 2026-02-22 20:04:51 +00:00
Peter Steinberger 2dcb244985 refactor(test): dedupe gateway and web scaffolding 2026-02-22 20:04:51 +00:00
Peter Steinberger 5e8b1f5ac8 refactor(test): centralize trigger and cron test helpers 2026-02-22 20:04:51 +00:00
Peter Steinberger 3c75bc0e41 refactor(test): dedupe agent and discord test fixtures 2026-02-22 20:04:51 +00:00
Peter Steinberger 5547a2275c fix(security): harden toolsBySender sender-key matching 2026-02-22 21:04:37 +01:00
Peter Steinberger 3461dda880 docs: fix voicecall expose disable example 2026-02-22 20:58:28 +01:00
Peter Steinberger 0d4c806406 docs: fix devices approve command in exe.dev guide 2026-02-22 20:52:46 +01:00
Peter Steinberger e0d4194869 docs: add missing summary/read_when metadata 2026-02-22 20:45:09 +01:00
Vincent Koc 5e73f33448 fix(slack): keep thread session fork/history context after first turn (#23843)
* Slack thread sessions: keep forking and history context after first turn

* Update CHANGELOG.md
2026-02-22 14:39:00 -05:00
Peter Steinberger 02772b029d fix(security): require sender-only matching for elevated allowFrom
Co-authored-by: coygeek <coygeek@users.noreply.github.com>
2026-02-22 20:37:22 +01:00
Peter Steinberger 51b0772e14 fix(exec-approvals): harden forwarding target and resolve delivery paths
Co-authored-by: bubmiller <bubmiller@users.noreply.github.com>
2026-02-22 20:37:22 +01:00
Peter Steinberger 6f895eb831 fix(sandbox): honor explicit bind mounts over workspace defaults
Co-authored-by: tasaankaeris <tasaankaeris@users.noreply.github.com>
2026-02-22 20:37:22 +01:00
Peter Steinberger eefbf3dc5a fix(sandbox): normalize /workspace media paths to host sandbox root
Co-authored-by: echo931 <echo931@users.noreply.github.com>
2026-02-22 20:37:21 +01:00
Peter Steinberger 0932adf361 fix(config): fail closed allowlist-only group policy
Co-authored-by: etereo <etereo@users.noreply.github.com>
2026-02-22 20:37:21 +01:00
Peter Steinberger 371a7da9c8 docs: add missing summaries and read_when hints 2026-02-22 20:37:02 +01:00
Vincent Koc 71c2c59c6c fix(slack): enforce replyToMode for auto-thread_ts and inline reply tags (#23839)
* Slack: respect replyToMode for auto-thread_ts and inline reply tags

* Update CHANGELOG.md
2026-02-22 14:36:46 -05:00
Vincent Koc 9f7c1686b4 fix(slack extension): preserve thread IDs for read + outbound delivery (#23836)
* Slack Extension: preserve thread IDs in reads and outbound sends

* Slack extension: fix threadTs typing and action test context

* Update CHANGELOG.md
2026-02-22 14:34:32 -05:00
Peter Steinberger 078e1a7fc9 fix(ui): remove unused Lit import in overview view 2026-02-22 20:32:52 +01:00
Peter Steinberger f5814cc002 docs: add extension channels to Channels nav 2026-02-22 20:28:05 +01:00
Peter Steinberger 00bbecede7 test(gateway): add telegram-session chat.send final-event e2e coverage 2026-02-22 20:26:50 +01:00
Peter Steinberger 290f375aa1 docs: fix Together provider env path 2026-02-22 20:23:40 +01:00
Peter Steinberger bbdfba5694 fix: harden connect auth flow and exec policy diagnostics 2026-02-22 20:22:00 +01:00
Peter Steinberger 7e83e7b3a7 fix(cron): narrow manual run execution state 2026-02-22 20:19:23 +01:00
Peter Steinberger 9cf445e37c fix(cron): restore interval cadence after restart 2026-02-22 20:19:23 +01:00
Peter Steinberger aa4c250eb8 fix(cron): split run and delivery status tracking 2026-02-22 20:19:23 +01:00
Peter Steinberger c3bb723673 fix(cron): enforce timeout for manual cron runs 2026-02-22 20:19:23 +01:00
Peter Steinberger 8bf3c37c6c fix(cron): keep watchdog timer armed during ticks 2026-02-22 20:19:23 +01:00
Peter Steinberger 5db1ee4ec6 fix(cron): keep manual runs non-blocking 2026-02-22 20:19:22 +01:00
Peter Steinberger 91f75a2b33 fix(cron): force fresh isolated session IDs 2026-02-22 20:19:22 +01:00
Peter Steinberger 6fef318fda docs: replace legacy chat examples in Venice provider guide 2026-02-22 20:15:07 +01:00
Peter Steinberger 72446f419f docs: align CLI docs and help surface 2026-02-22 20:05:01 +01:00
Val Alexander 42b3c52350 fix(ui): ensure nonce is always a string in gateway connect 2026-02-22 13:04:28 -06:00
Val Alexander 6298698008 revert(ui): remove UI portions of mixed commits from main
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 13:01:22 -06:00
Val Alexander 26ab93f0eb revert(ui): remove recent UI dashboard/theme commits from main
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 13:01:22 -06:00
Peter Steinberger 8eb71cec26 test(agents): add malformed MEDIA prose integration coverage
Co-authored-by: Ho Lim <166576253+HOYALIM@users.noreply.github.com>
2026-02-22 19:59:03 +01:00
Peter Steinberger c3d11d56c3 fix(agents): validate tool-result MEDIA directives with shared parser
Co-authored-by: Ho Lim <166576253+HOYALIM@users.noreply.github.com>
2026-02-22 19:52:29 +01:00
Peter Steinberger 0c1f491a02 fix(gateway): clarify pairing and node auth guidance 2026-02-22 19:50:29 +01:00
Peter Steinberger 53ed7a0f5c test: dedupe repeated test fixtures and assertions 2026-02-22 18:37:25 +00:00
Peter Steinberger 0e4f3ccbdf refactor: dedupe media and request-body test scaffolding 2026-02-22 18:37:25 +00:00
Peter Steinberger 4a88c579ba refactor: dedupe shared config type definitions 2026-02-22 18:37:25 +00:00
Peter Steinberger 12635de1c7 test: cover shared installer flow helpers 2026-02-22 18:37:25 +00:00
Peter Steinberger 07888bee34 refactor: share install flows across hooks and plugins 2026-02-22 18:37:25 +00:00
Peter Steinberger 176973b882 test(gateway): align auto-enable channel assertion 2026-02-22 18:37:18 +00:00
青雲 3dfee78d72 fix: sanitize tool call IDs in agent loop for Mistral strict9 format (#23595) (#23698)
* fix: sanitize tool call IDs in agent loop for Mistral strict9 format (#23595)

Mistral requires tool call IDs to be exactly 9 alphanumeric characters
([a-zA-Z0-9]{9}). The existing sanitizeToolCallIdsForCloudCodeAssist
mechanism only ran on historical messages at attempt start via
sanitizeSessionHistory, but the pi-agent-core agent loop's internal
tool call → tool result cycles bypassed that path entirely.

Changes:
- Wrap streamFn (like dropThinkingBlocks) so every outbound request
  sees sanitized tool call IDs when the transcript policy requires it
- Replace call_${Date.now()} in pendingToolCalls with a 9-char hex ID
  generated from crypto.randomBytes
- Add Mistral tool call ID error pattern to ERROR_PATTERNS.format so
  the error is correctly classified for retry/rotation

* Changelog: document Mistral strict9 tool-call ID fix

---------

Co-authored-by: echoVic <AkiraVic@outlook.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 13:37:12 -05:00
Drake Thomsen 042947b944 fix: add mistral to MemorySearchSchema provider/fallback unions (#14934)
* fix: add mistral to MemorySearchSchema provider/fallback unions

The Mistral embedding provider was added to the runtime code but the
Zod config schema was not updated, causing config validation to reject
`provider: "mistral"` and `fallback: "mistral"` as invalid input.

* Changelog: add unreleased note for Mistral memory schema fix

---------

Co-authored-by: Drake (Moltbot Dev) <drake@clawd.bot>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 13:36:53 -05:00
Peter Steinberger 95d7b0bbe1 fix(replies): normalize media path variants for dedupe
Co-authored-by: Ho Lim <subhoya@gmail.com>
2026-02-22 19:34:30 +01:00
Peter Steinberger 0342bed289 fix(replies): keep finals for cross-target messaging sends
Co-authored-by: Ion Mudreac <mudreac@gmail.com>
2026-02-22 19:34:30 +01:00
Peter Steinberger 40680432b4 fix(config): allowlist auto-enabled built-in channels when restricted
Co-authored-by: 4rev <4rev@users.noreply.github.com>
2026-02-22 19:31:18 +01:00
Peter Steinberger 772cf7df33 test: load chrome extension background utils across module modes 2026-02-22 18:29:20 +00:00
Vincent Koc 89a1e99815 fix(slack): finalize replyToMode off threading behavior (#23799)
* fix: make replyToMode 'off' actually prevent threading in Slack

Three independent bugs caused Slack replies to always create threads
even when replyToMode was set to 'off':

1. Typing indicator created threads via statusThreadTs fallback (#16868)
   - resolveSlackThreadTargets fell back to messageTs for statusThreadTs
   - 'is typing...' was posted as thread reply, creating a thread
   - Fix: remove messageTs fallback, let statusThreadTs be undefined

2. [[reply_to_current]] tags bypassed replyToMode entirely (#16080)
   - Slack dock had allowExplicitReplyTagsWhenOff: true
   - Reply tags from system prompt always threaded regardless of config
   - Fix: set allowExplicitReplyTagsWhenOff to false for Slack

3. Contradictory replyToMode defaults in codebase (#20827)
   - monitor/provider.ts defaulted to 'all'
   - accounts.ts defaulted to 'off' (matching docs)
   - Fix: align provider.ts default to 'off' per documentation

Fixes: openclaw/openclaw#16868, openclaw/openclaw#16080, openclaw/openclaw#20827

* fix(slack): respect replyToMode in DMs even with typing indicator thread

When replyToMode is 'off' in DMs, replies should stay in the main
conversation even when the typing indicator creates a thread context.

Previously, when incomingThreadTs was set (from the typing indicator's
thread), replyToMode was forced to 'all', causing all replies to go
into the thread.

Now, for direct messages, the user's configured replyToMode is always
respected. For channels/groups, the existing behavior is preserved
(stay in thread if already in one).

This fix:
- Keeps the typing indicator working (statusThreadTs fallback preserved)
- Prevents DM replies from being forced into threads
- Maintains channel thread continuity

Fixes #16868

* refactor(slack): eliminate redundant resolveSlackThreadContext call

- Add isThreadReply to resolveSlackThreadTargets return value
- Remove duplicate call in dispatch.ts
- Addresses greptile review feedback with cleaner DRY approach

* docs(slack): add JSDoc to resolveSlackThreadTargets

Document return values including isThreadReply distinction between
genuine user thread replies vs bot status message thread context.

* docs(changelog): record Slack replyToMode off threading fixes

---------

Co-authored-by: James <jamesrp13@gmail.com>
Co-authored-by: theoseo <suhong.seo@gmail.com>
2026-02-22 13:27:50 -05:00
Vincent Koc cd7b2814af fix(slack): preserve string thread context in queue + DM route (#23804)
* fix(slack): preserve thread_ts in queue drain and deliveryContext

Two related fixes for Slack thread reply routing:

1. Queue drain drops string thread_ts (#11195)
   - `typeof threadId === "number"` in drain.ts only matches Telegram numeric
     topic IDs. Slack thread_ts is a string like "1770474140.187459" which
     fails the check, causing threadKey to become empty.
   - Changed to `threadId != null && threadId !== ""` to accept both number
     and string thread IDs.
   - Applies to all 3 occurrences in drain.ts: cross-channel detection,
     thread key building, and collected originatingThreadId extraction.

2. DM deliveryContext missing thread_ts (#10837)
   - updateLastRoute calls for Slack DMs in both prepare.ts and dispatch.ts
     built deliveryContext without threadId, so the session's delivery context
     never included thread_ts for DM threads.
   - Added threadId from threadContext.messageThreadId / ctxPayload.MessageThreadId
     to both updateLastRoute call sites.

Tests: 3 new cases in queue.collect-routing.test.ts
- Collects messages with matching string thread_ts (same Slack thread)
- Separates messages with different string thread_ts (different threads)
- Treats empty string threadId same as absent

Closes #10837, closes #11195

* fix(slack): preserve string thread context in queue + DM route updates

---------

Co-authored-by: RobClawd <clawd@RobClawds-Mac-mini.local>
2026-02-22 13:26:31 -05:00
Peter Steinberger b13bba9c35 fix(gateway): skip operator pairing on valid shared auth 2026-02-22 19:25:50 +01:00
Peter Steinberger 9da5f9819b fix(plugins): ignore archived extension dirs during discovery
Co-authored-by: chenzhuoms <chenzhuoms@users.noreply.github.com>
2026-02-22 19:23:34 +01:00
Peter Steinberger 8839162b97 fix(config): persist built-in channel enable state in channels
Co-authored-by: HirokiKobayashi-R <HirokiKobayashi-R@users.noreply.github.com>
2026-02-22 19:23:34 +01:00
Peter Steinberger 1bd79add8f fix(plugins): sanitize workspace deps before plugin install
Co-authored-by: guanyu-zhang <guanyu-zhang@users.noreply.github.com>
2026-02-22 19:23:34 +01:00
Peter Steinberger e55ab6fd91 test(ci): harden background abort timing on windows 2026-02-22 18:23:04 +00:00
Peter Steinberger 1bc5ba6e29 fix(feishu): prefer video file_key for inbound media 2026-02-22 19:21:42 +01:00
Peter Steinberger 0efe2cab7d fix(telegram): set provider on native command context
Co-authored-by: Serhii Panchyshyn <panchyshyn.serhii@gmail.com>
2026-02-22 19:21:26 +01:00
Vincent Koc fbdae49988 Changelog: fix unreleased thanks attribution placement 2026-02-22 13:18:23 -05:00
Vincent Koc 35a7f6e7f6 Dev tooling: prevent CLAUDE symlink newline regressions 2026-02-22 13:18:04 -05:00
Peter Steinberger 95e85e627e fix(feishu): restore group command fallback and plugin deps 2026-02-22 19:13:19 +01:00
Peter Steinberger 8801130c5d fix(ci): annotate shared skill-install test mocks 2026-02-22 18:10:56 +00:00
Peter Steinberger 2858901441 test(flaky): harden slow vmFork unit suites
Co-authored-by: Ho Lim <166576253+HOYALIM@users.noreply.github.com>
2026-02-22 19:08:59 +01:00
Peter Steinberger 9ea5228f42 fix(browser): recover stale remote target ids
Co-authored-by: Ilya Strelov <10761735+strelov1@users.noreply.github.com>
2026-02-22 19:08:38 +01:00
Peter Steinberger 1fe2043742 fix(browser): harden extension relay worker recovery
Co-authored-by: codexGW <9350182+codexGW@users.noreply.github.com>
2026-02-22 19:08:38 +01:00
Peter Steinberger 40494d67f2 fix(browser): harden extension relay reconnect race
Co-authored-by: Ho Lim <166576253+HOYALIM@users.noreply.github.com>
2026-02-22 19:08:38 +01:00
Peter Steinberger b79c89fc90 fix: stabilize CI type and test harness coverage 2026-02-22 18:06:34 +00:00
Luis Conde af9881b9c5 fix(slack): resolve user IDs to DM channels before files.uploadV2 (#23773)
When a bare Slack user ID (U-prefix) is passed as the send target
without an explicit `user:` prefix, `parseSlackTarget` classifies it as
kind="channel".  `resolveChannelId` then passes it through to callers
without calling `conversations.open`.

This works for `chat.postMessage` (which tolerates user IDs), but
`files.uploadV2` delegates to `completeUploadExternal` which validates
`channel_id` against `^[CGDZ][A-Z0-9]{8,}$` — rejecting U-prefixed
IDs with `invalid_arguments`.

Fix: detect U-prefixed IDs in `resolveChannelId` regardless of the
parsed `kind`, and always resolve them via `conversations.open` to
obtain the DM channel ID (D-prefix).

Includes test coverage for bare, prefixed, and mention-style user ID
targets with file uploads, plus a channel-target negative case.
2026-02-22 13:04:53 -05:00
Peter Steinberger 568973e5ac perf(test): trim embedded/bash runtime fixture overhead 2026-02-22 17:56:05 +00:00
Peter Steinberger 08431da5d5 refactor(gateway): unify credential precedence across entrypoints 2026-02-22 18:55:44 +01:00
Peter Steinberger 98427453ba fix(network): normalize SSRF IP parsing and monitor typing 2026-02-22 18:55:34 +01:00
Peter Steinberger 4ed87a6672 fix(feishu): enforce id-only allowlist matching 2026-02-22 18:55:06 +01:00
Peter Steinberger 3286791316 refactor(agents): dedupe config and truncation guards 2026-02-22 17:54:51 +00:00
Peter Steinberger 409a02691f refactor(discord): dedupe directory and media send paths 2026-02-22 17:54:51 +00:00
Peter Steinberger b3c78e5e05 refactor(outbound): reuse signal uuid detection and payload types 2026-02-22 17:54:51 +00:00
Peter Steinberger dacb3d1aa2 refactor(queue): share drain helpers across announce and reply 2026-02-22 17:54:51 +00:00
Peter Steinberger 78220db2be refactor(browser): dedupe control-server test harness 2026-02-22 17:54:51 +00:00
Peter Steinberger 79ec29b150 test: consolidate embedded prompt error scenarios 2026-02-22 17:53:33 +00:00
Peter Steinberger 239f72c582 perf(test): consolidate archive safety cases and cache session manager 2026-02-22 17:53:33 +00:00
Peter Steinberger b17f677439 test: merge no-op notifyOnExit scenario coverage 2026-02-22 17:53:33 +00:00
Peter Steinberger 5b078c8305 test: consolidate sudo fallback edge-case scenarios 2026-02-22 17:53:12 +00:00
Peter Steinberger 03285465ff perf(test): lazy-load weak-random fallback scanner 2026-02-22 17:52:12 +00:00
Peter Steinberger 90a8ddc3c6 perf(test): replace temp-path guard AST parse with fast scanner 2026-02-22 17:52:12 +00:00
Peter Steinberger 2ed94a08c0 test: merge duplicate bash background session-name coverage 2026-02-22 17:52:12 +00:00
Peter Steinberger 60f3a2a244 perf(test): shorten bash tool timing fixtures 2026-02-22 17:52:12 +00:00
Peter Steinberger 61d0c55a80 perf(test): share workspace fixture in skills download safety suite 2026-02-22 17:52:12 +00:00
Peter Steinberger 1437f371fc test: trim duplicate embedded runner setup cases 2026-02-22 17:52:12 +00:00
Peter Steinberger 924455edb8 perf(test): reuse tar.bz2 workspace in download safety tests 2026-02-22 17:52:12 +00:00
Peter Steinberger a28464ec59 test: combine duplicate process log tail-window coverage 2026-02-22 17:52:12 +00:00
Peter Steinberger 64ecd3e81c test: merge duplicate targetDir escape cases 2026-02-22 17:51:38 +00:00
Peter Steinberger dd8c0b694d perf(test): speed async memory sync close coverage 2026-02-22 17:51:38 +00:00
Peter Steinberger 4493f7325d perf(test): run nodes program tests on focused nodes-cli harness 2026-02-22 17:51:38 +00:00
Peter Steinberger 0e38505d3d test: collapse duplicate sandbox skill mirroring cases 2026-02-22 17:51:38 +00:00
Peter Steinberger c964d21d74 perf(test): prebuild download archives and cache apply module 2026-02-22 17:51:38 +00:00
Peter Steinberger 2962e5a383 perf(test): tighten temp-path dynamic prefilter 2026-02-22 17:51:38 +00:00
Peter Steinberger d1836df714 test: trim duplicate plain nodes list smoke 2026-02-22 17:51:38 +00:00
Peter Steinberger 3e819f0af5 test: drop duplicate nodes media parser coverage 2026-02-22 17:51:38 +00:00
Peter Steinberger cc5cd51b13 docs(changelog): note installer gum auto-path smoke coverage 2026-02-22 18:48:36 +01:00
Jonathan Works 8c089bbe32 fix(hooks): suppress main session events for silent/delivered hook turns (#20678)
* fix(hooks): suppress main session events for silent/delivered hook turns

When a hook agent turn returns NO_REPLY (SILENT_REPLY_TOKEN), mark the
result as delivered so the hooks handler skips enqueueSystemEvent and
requestHeartbeatNow. Without this, every Gmail notification classified
as NO_REPLY still injects a system event into the main agent session,
causing context window growth proportional to email volume.

Two-part fix:
- cron/isolated-agent/run.ts: set delivered:true when synthesizedText
  matches SILENT_REPLY_TOKEN so callers know no notification is needed
- gateway/server/hooks.ts: guard enqueueSystemEvent + requestHeartbeatNow
  with !result.delivered (addresses duplicate delivery, refs #20196)

Refs: https://github.com/openclaw/openclaw/issues/20196

* Changelog: document hook silent-delivery suppression fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:47:42 -05:00
Peter Steinberger 3c6a15ce98 fix(discord): make opus optional and log fallback 2026-02-22 18:47:09 +01:00
Vincent Koc 24fd8cbdc8 fix(auto-reply): preserve OpenRouter @preset model directives (#23769)
* Auto-reply: preserve OpenRouter @preset model directives

* Changelog: move OpenRouter preset fix into 2026.2.22 unreleased
2026-02-22 12:46:04 -05:00
大猫子 91944ede4c fix(cron): propagate auth-profile resolution to isolated sessions (#20624) (#20689) 2026-02-22 12:45:03 -05:00
Peter Steinberger 3a19b0201c test(installer): drop legacy gum env from docker smoke 2026-02-22 18:44:21 +01:00
Alex Zaytsev 1685a0dd12 fix: remove trailing newline from CLAUDE.md symlink target (#21160)
* fix: remove trailing newline from CLAUDE.md symlink target

* Dev tooling: prevent CLAUDE symlink newline regressions

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:40:06 -05:00
Aleksandrs Tihenko c52b2ad5c3 fix(cache): inject cache_control into system prompt for OpenRouter Anthropic (#15151) (#17473)
* fix(cache): inject cache_control into system prompt for OpenRouter Anthropic

Add onPayload wrapper that injects cache_control: { type: "ephemeral" }
into the system/developer message content for OpenRouter requests routed
to Anthropic models. The system prompt is typically ~18k tokens and was
being re-processed on every request without caching.

Fixes #15151

* Changelog: add OpenRouter note for #17473

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:27:01 -05:00
Peter Steinberger 66529c7aa5 refactor(gateway): unify auth credential resolution 2026-02-22 18:23:13 +01:00
Joly0 ded9a59f78 OpenRouter: allow any model ID instead of restricting to static catalog (#14312)
* OpenRouter: allow any model ID instead of restricting to static catalog

OpenRouter models were restricted to a hardcoded prefix list in the internal model catalog, preventing use of newly added or less common models. This change makes OpenRouter work as the pass-through proxy it is -- any valid OpenRouter model ID now resolves dynamically.

Fixes https://github.com/openclaw/openclaw/issues/5241

Changes:
- Add OpenRouter as an implicit provider in resolveImplicitProviders so models.json is populated when an API key is detected (models-config.providers.ts)
- Add a pass-through fallback in resolveModel that creates OpenRouter models on-the-fly when they aren't pre-registered in the local catalog (
model.ts
)
- Remove the static prefix filter for OpenRouter/opencode in isModernModelRef (live-model-filter.ts)

* Apply requested change for maxTokens

* Agents: remove dead helper in live model filter

* Changelog: note Joly0/main OpenRouter fix

* Changelog: fix OpenRouter entry text

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:21:20 -05:00
zwffff c543994e90 Default reasoning to on when model has reasoning: true (fix #22456) (#22513)
* Default reasoning to on when model has reasoning: true (fix #22456)

What: When a model is configured with reasoning: true in openclaw.json (e.g. OpenRouter x-ai/grok-4.1-fast), the session now defaults reasoningLevel to on if the user has not set it via /reasoning or session store.

Why: Users expected setting reasoning: true on the model to enable reasoning; previously only session/directive reasoningLevel was used and it always defaulted to off, so Think stayed off despite the model config.

* Chore: sync formatted files from main for CI

* Changelog: note zwffff/main OpenRouter fix

* Changelog: fix OpenRouter entry text

* Update msteams.md

* Update msteams.md

* Update msteams.md

---------

Co-authored-by: 曾文锋0668000834 <zeng.wenfeng@xydigit.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:19:36 -05:00
Vincent Koc 9ae08ce205 Memory: add Arabic query expansion stop words (#23717) 2026-02-22 12:17:47 -05:00
Peter Steinberger 8c71bbe1e1 docs(changelog): add memory remote-guard hardening notes 2026-02-22 18:15:35 +01:00
Peter Steinberger eb041daee2 fix(memory): route batch APIs through guarded remote HTTP 2026-02-22 18:15:15 +01:00
Peter Steinberger f87db7c627 fix(memory): enforce guarded remote policy for embeddings 2026-02-22 18:15:15 +01:00
Peter Steinberger f6feb4144c refactor(memory): add guarded remote HTTP helper 2026-02-22 18:15:15 +01:00
Robby 99cfb3dab2 fix(openrouter): pass reasoning.effort based on thinking level (#14664) (#17236)
* fix(openrouter): pass reasoning.effort to OpenRouter API (#14664)

* Agents: pass thinkLevel to extra-params wrapper

* Changelog: note fix/openrouter-reasoning-effort-14664 OpenRouter fix

* Changelog: fix OpenRouter entry text

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:14:12 -05:00
Vincent Koc ecf2cff9cd Update CHANGELOG.md 2026-02-22 12:12:28 -05:00
Peter Steinberger 296b19e413 test: dedupe gateway browser discord and channel coverage 2026-02-22 17:11:54 +00:00
Peter Steinberger 34ea33f057 refactor: dedupe core config and runtime helpers 2026-02-22 17:11:54 +00:00
Peter Steinberger 24ea941e28 test: dedupe auto-reply web and signal flows 2026-02-22 17:11:54 +00:00
Peter Steinberger ad1072842e test: dedupe agent tests and session helpers 2026-02-22 17:11:54 +00:00
Mitsuyuki Osabe 415686244a feat: pass through OpenRouter provider routing params (#17148)
extraParams.provider was silently dropped by createStreamFnWithExtraParams().
This change injects it into model.compat.openRouterRouting so pi-ai's
buildParams includes params.provider in the API request body.

Enables OpenRouter provider routing options (only, order, allow_fallbacks,
data_collection, ignore, sort, quantizations) via model config:

```jsonc
"openrouter/model-name": {
  "params": {
    "provider": {
      "only": ["deepinfra", "fireworks"],
      "allow_fallbacks": false
    }
  }
}
```

Closes #10869

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
2026-02-22 12:11:04 -05:00
Vincent Koc 3254c72d4b Update CHANGELOG.md 2026-02-22 12:09:19 -05:00
Omair Afzal 3891ba4bb5 fix(providers): preserve openrouter/ prefix for native models (#12942)
* fix(providers): preserve openrouter/ prefix for native models (#12924)

OpenRouter-native models like 'openrouter/aurora-alpha' need the full
'openrouter/<name>' as the model ID in API requests. The existing
parseModelRef() stripped the prefix, sending just 'aurora-alpha'
which OpenRouter rejects with 400.

Fix: normalizeProviderModelId() now re-adds the 'openrouter/' prefix
for models without a slash (native models), while passing through
external provider models (e.g. 'anthropic/claude-sonnet-4-5') as-is.

Closes #12924

* Changelog: add OpenRouter note for #12942

---------

Co-authored-by: Luna AI <luna@coredirection.ai>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:08:46 -05:00
Dan Dodson 4cad674387 fix: preserve stored provider in resolveSessionModelRef for vendor-prefixed models (#22753)
* fix: preserve stored provider in resolveSessionModelRef for vendor-prefixed models

When an OpenRouter model with a vendor prefix (e.g. "anthropic/claude-haiku-4.5")
was successfully used and persisted to the session entry, the next call to
resolveSessionModelRef would re-parse the model string through parseModelRef,
which splits on the first slash and incorrectly extracts "anthropic" as the
provider — discarding the stored "openrouter" provider entirely. This caused
subsequent requests to attempt direct Anthropic API calls with an OpenRouter
API key, producing "credit balance too low" billing errors.

The fix trusts the explicitly stored modelProvider on the session entry and
skips parseModelRef re-parsing when a provider is already recorded. parseModelRef
is still used as a fallback when no provider is stored on the entry.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Changelog: add OpenRouter note for #22753

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-22 12:07:33 -05:00
Peter Steinberger 91cb28ecef perf(test): speed temp-path AST scan 2026-02-22 17:06:35 +00:00
Peter Steinberger 572daed456 test: trim duplicate async-search status reopen check 2026-02-22 17:06:35 +00:00
Peter Steinberger 35fecc4bee test: remove redundant runner ordering checks 2026-02-22 17:06:35 +00:00
Peter Steinberger e38196d42c test: trim duplicate program smoke onboarding coverage 2026-02-22 17:06:35 +00:00
Peter Steinberger c6b94f2652 test: speed up skills download tar traversal fixture 2026-02-22 17:06:35 +00:00
Peter Steinberger bd6be417e4 test: trim duplicate smoke and embedded runner cases 2026-02-22 17:06:35 +00:00
Peter Steinberger ee7a43b895 test: replace slow gateway SIGTERM integration coverage 2026-02-22 17:06:35 +00:00
Peter Steinberger d01cc69ef0 test: tighten process timeout fixtures 2026-02-22 17:06:35 +00:00
Peter Steinberger b1a97e77ca test: tighten bash timeout poll upper bound 2026-02-22 17:06:35 +00:00
Peter Steinberger c5904da85a test: trim bash tool timing constants 2026-02-22 17:06:35 +00:00
Peter Steinberger c23cdf67d7 test: speed up qmd boot retry lock test 2026-02-22 17:06:35 +00:00
Peter Steinberger 68b9b44498 test: reduce bash background abort wait constants 2026-02-22 17:06:35 +00:00
Peter Steinberger dd4495e23a test: optimize temp path guard scan prefilter 2026-02-22 17:06:35 +00:00
Peter Steinberger 7bf719fe85 test: narrow weak-random rg scan globs 2026-02-22 17:06:35 +00:00
Peter Steinberger 7626503965 test: reduce web auto-reply watchdog timer churn 2026-02-22 17:06:35 +00:00
Peter Steinberger 089ee242bc test: precompute skills download tar fixture and dedupe setup 2026-02-22 17:06:35 +00:00
Peter Steinberger 2b74e5f66d test: reduce bash tool suite sleep durations 2026-02-22 17:06:35 +00:00
Peter Steinberger 47514e35a2 test: dedupe pi embedded runner setup and orphan case 2026-02-22 17:06:35 +00:00
Peter Steinberger f3ba3fe8dc test: isolate skills-install temp home env 2026-02-22 17:06:35 +00:00
Peter Steinberger 992fc9cf4e test: trim cli program test bootstrap overhead 2026-02-22 17:06:35 +00:00
Peter Steinberger 3046fa31e8 test: isolate skills suite env and trim scan overhead 2026-02-22 17:06:35 +00:00
Peter Steinberger 6cdeb62a01 test: trim gateway sigterm bootstrap imports 2026-02-22 17:06:35 +00:00
Peter Steinberger 407f7017ec test: cache plugin install archive fixtures 2026-02-22 17:06:35 +00:00
Peter Steinberger 1d2f305117 style: format skills install download test 2026-02-22 17:06:35 +00:00
Peter Steinberger 6cd12ca1ce test: merge download archive safety suites 2026-02-22 17:06:35 +00:00
Peter Steinberger 07514361d7 test: speed up weak random guardrail scan 2026-02-22 17:06:35 +00:00
Peter Steinberger 13d3758efd test: preload doctor command in migration suites 2026-02-22 17:06:34 +00:00
Peter Steinberger c42b0b2dfc test: preload sandbox explain command module in suite 2026-02-22 17:06:34 +00:00
Peter Steinberger 0b13a0286e test: preload bash exec path tool module in suite 2026-02-22 17:06:34 +00:00
Peter Steinberger 6042075bdf test: preload safe-bins tool module in suite 2026-02-22 17:06:34 +00:00
Peter Steinberger 71747a7688 test: preload onboarding command modules in hot suites 2026-02-22 17:06:34 +00:00
Peter Steinberger b6ac0eef5d test: trim gateway fixture sizes and preload message command 2026-02-22 17:06:34 +00:00
Peter Steinberger 8cc744ef1f fix(logging): cap file logs with configurable maxFileBytes
Co-authored-by: Xinhua Gu <562450+xinhuagu@users.noreply.github.com>
2026-02-22 17:58:51 +01:00
Peter Steinberger 795db98f6a fix(telegram): notify users on media download failures
Co-authored-by: Artale <117890364+arosstale@users.noreply.github.com>
2026-02-22 17:54:16 +01:00
Peter Steinberger d0e6763263 fix(telegram): wire webhookPort through config and startup
Co-authored-by: xrf9268-hue <244283935+xrf9268-hue@users.noreply.github.com>
2026-02-22 17:54:16 +01:00
Peter Steinberger 5069250faf fix(telegram): clear webhook state before polling startup
Co-authored-by: Peter Machona <7957943+chilu18@users.noreply.github.com>
2026-02-22 17:54:16 +01:00
Peter Steinberger 81384daeb4 fix(telegram): harden polling retry setup and teardown order
Co-authored-by: Cklee <99405438+liebertar@users.noreply.github.com>
Co-authored-by: Ho Lim <166576253+HOYALIM@users.noreply.github.com>
2026-02-22 17:54:16 +01:00
Peter Steinberger 1a9b5840d2 fix(telegram): keep webhook monitor alive until abort
Co-authored-by: Evgeny Zislis <7056+kesor@users.noreply.github.com>
2026-02-22 17:54:16 +01:00
Peter Steinberger e58054b85c docs(telegram): align Node22 network defaults and setup guidance 2026-02-22 17:54:16 +01:00
Peter Steinberger e9ed688c2c fix(net): enable family fallback for pinned SSRF dispatcher 2026-02-22 17:54:15 +01:00
Peter Steinberger 4d0ca7c315 fix(telegram): restart stalled polling after unhandled network errors 2026-02-22 17:54:15 +01:00
Peter Steinberger 824d1e095b fix(infra): treat undici fetch failed as transient unhandled rejection 2026-02-22 17:54:15 +01:00
Peter Steinberger dbc1ed8933 fix(update): run auto-update via runtime argv and keep it independent of checkOnStart 2026-02-22 17:41:05 +01:00
Vincent Koc 35b162af76 Memory: add Spanish and Portuguese query expansion stop words (#23710) 2026-02-22 11:26:12 -05:00
Peter Steinberger f14ebd743c refactor(security): unify local-host and tailnet CIDR checks 2026-02-22 17:20:27 +01:00
Vincent Koc 21cbf59509 feat(memory): add Japanese query expansion support for FTS (#23156)
* Memory: add Japanese query expansion support

* Docs/Changelog: credit Japanese FTS update
2026-02-22 11:19:20 -05:00
Peter Steinberger f442a3539f feat(update): add core auto-updater and dry-run preview 2026-02-22 17:11:36 +01:00
Nikolay Petrov 13690d406a Telegram: coalesce forwarded text+media bursts into one inbound turn (#19476)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 09e0b4e9bd1a799c30249f21b87782429a8f8e32
Co-authored-by: napetrov <18015221+napetrov@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 21:41:09 +05:30
Peter Steinberger 333fbb8634 refactor(net): consolidate IP checks with ipaddr.js 2026-02-22 17:02:44 +01:00
Marcus Castro 337eef55d7 fix(telegram): link forwarded messages with comments (#9720)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5f81061b5f613903422a624d95aab8b0fc04027a
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 21:23:56 +05:30
Peter Steinberger 40a68a8936 docs: add concise gh search playbook to AGENTS 2026-02-22 16:41:17 +01:00
Ayaan Zaidi 6268ed57ea fix(agents): stop param shadowing in auth failure marker 2026-02-22 21:00:17 +05:30
Marcus Castro ace8357149 fix(telegram): skip failed photo downloads in media group instead of dropping entire group (#20598)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 4a9c5f7af7c136952bf5dd396acee7f9f4e3c5d1
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 20:57:06 +05:30
Peter Steinberger 9363c320d8 fix(security): harden shell env fallback startup env handling 2026-02-22 16:06:27 +01:00
Peter Steinberger ab1840b881 docs(changelog): credit SSRF report in unreleased notes 2026-02-22 16:02:49 +01:00
Peter Steinberger a0d0104a86 test: speed up signal reconnect and temp path guard scans 2026-02-22 14:44:19 +00:00
Peter Steinberger 142c0a7f7d refactor: extract gateway transcript append helper 2026-02-22 14:44:19 +00:00
Peter Steinberger 8e6b465fa8 test: speed up agent command suite with lightweight runtime mocks 2026-02-22 14:44:19 +00:00
Peter Steinberger 1cf8f41134 test: dedupe expensive web auto-reply compression coverage 2026-02-22 14:44:19 +00:00
Peter Steinberger d0b59270a7 refactor: dedupe auth-profile failure marking and rotation test setup 2026-02-22 15:44:10 +01:00
Peter Steinberger 44dfbd23df fix(ssrf): centralize host/ip block checks 2026-02-22 15:41:41 +01:00
Peter Steinberger 39be5e44df refactor: split config reload flow and test harness 2026-02-22 15:38:23 +01:00
Glucksberg 53adae9cec fix(telegram): add dnsResultOrder=ipv4first default on Node 22+ to fix fetch failures (#5405)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 71366e9532b6c67f0413b65a9ac8623eae000e9b
Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 20:07:51 +05:30
Peter Steinberger 4e65e61612 fix: retry missing config snapshots before skip (#23343) (thanks @lbo728) 2026-02-22 15:34:46 +01:00
lbo728 aaa9bd0f1c fix(config-reload): skip reload when config file is not found
When a config file is written atomically (tmp → rename), chokidar can
fire an 'unlink' event for the temporary removal of the destination file
before the rename completes. runReload() would then call readSnapshot(),
which returns { exists: false, valid: true, config: {} } — an empty
config that looks valid — causing diffConfigPaths() to find many changes
and triggering an unnecessary SIGUSR1 restart.

The restarted gateway process then fails to find the config file (still
in the middle of the write) and enters a crash loop with:
  'Missing config. Run openclaw setup...'

Fix: guard against exists=false before the existing valid=false check,
so mid-write snapshots are silently skipped rather than treated as a
config wipe.

Fixes #23321
2026-02-22 15:34:46 +01:00
Peter Steinberger 3e2849c578 fix: align timeout cooldown behavior docs/tests (#22622) (thanks @vageeshkumar) 2026-02-22 15:34:20 +01:00
Vageesh Kumar 71d0b86352 fix(agents): skip auth profile cooldown for timeout failures
A timeout is model/network-specific, not an auth issue. Marking the
auth profile as failed on timeout poisons fallback models on the same
provider (e.g. gpt-5.3 timeout would block gpt-5.2 via shared profile
cooldown). The prompt-phase path already guards against this; this
aligns the post-response timeout path to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:34:20 +01:00
Val Alexander c5be45dfd2 test: skip CLI auto-detect e2e tests on Windows (#23626) 2026-02-22 08:31:40 -06:00
Peter Steinberger 4c355a28a3 refactor: centralize tool-error visibility policy 2026-02-22 15:30:53 +01:00
Peter Steinberger ac3ac6a83a refactor(signal): extract rpc parse helper and validate response envelope 2026-02-22 15:29:56 +01:00
Peter Steinberger a5e2bd4eaa docs: document verbose-gated tool error details 2026-02-22 15:26:48 +01:00
Peter Steinberger 835be4392e fix: gate tool error details behind verbose 2026-02-22 15:26:47 +01:00
Peter Steinberger 184844e50c fix: add signal rpc malformed-json regression test (#22995) (thanks @adhitShet) 2026-02-22 15:23:37 +01:00
adhitShet 4b78e91acd fix(signal): guard JSON.parse of Signal RPC response with try-catch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 15:23:37 +01:00
Peter Steinberger 7d7297f57f fix: downgrade telegram autoSelectFamily log to debug 2026-02-22 15:21:23 +01:00
Peter Steinberger f5ede0f2bd test: stabilize acp cwd prefix assertions across env leakage 2026-02-22 14:18:44 +00:00
Peter Steinberger b0a8b3bebb test: share fast-path mocks for targeted doctor suites 2026-02-22 14:18:44 +00:00
Peter Steinberger 2c0b72acb8 test: speed up slow media and synology suites 2026-02-22 14:18:44 +00:00
Peter Steinberger 32c33f4faa test: isolate doctor allowFrom migration assertions from unrelated checks 2026-02-22 14:18:44 +00:00
tyler 9b23e5ce1f test: fix flaky auth tests when OPENCLAW_GATEWAY_TOKEN is present 2026-02-22 15:17:37 +01:00
Peter Steinberger 9f2b25426b test(core): increase coverage for sessions, auth choice, and model listing 2026-02-22 14:08:51 +00:00
Peter Steinberger d116bcfb14 refactor(runtime): consolidate followup, gateway, and provider dedupe paths 2026-02-22 14:08:51 +00:00
Peter Steinberger 38752338dc refactor(tui): dedupe handlers and formatter test setup 2026-02-22 14:08:51 +00:00
Peter Steinberger 66f814a0af refactor(channels): dedupe plugin routing and channel helpers 2026-02-22 14:08:51 +00:00
Peter Steinberger 7abae052f9 chore(skills): remove bundled food-order skill 2026-02-22 15:06:27 +01:00
Val Alexander e697ec273a UI: polish dashboard — agents overview, chat toolbar, debug & login UX (#23553)
* UI: polish dashboard — agents overview, chat toolbar, debug simplification, login UX

* fix(ui): restore chat draft ordering, remove extra toolbar buttons

* UI: replace agent avatar fallback with lobster emoji

* style(ui): update layout styles for sidebar and shell, adjusting navigation widths for improved responsiveness

* feat(ui): implement sidebar resizing functionality and enhance navigation with new search and sorting features for sessions

* fix(ui): update references from ClawDash to OpenClaw in checklist and dashboard header

* style(ui): adjust sidebar minimum width and add responsive behavior for narrow states

* UI: minimal chat agent bar — remove sessions panel, strip chrome

* style(ui): update light theme colors and add ambient gradient for Luxe Cream & Coral

* UI: replace sparkle with OpenClaw lobster logo in chat

* style(ui): rename theme toggle to theme select and update related styles; adjust layout and spacing for agents and chat components

* style(ui): enhance agents panel layout with grid system, update toolbar styles, and refine usage chart presentation

* style(ui): adjust sessions table column width and refine agent model fields layout for better responsiveness

* style(ui): refine component styles for improved layout and responsiveness; adjust gradients, spacing, and element alignment across chat and agent interfaces

* ui: align chat-controls session container

* ui: enlarge agent controls for better touch targets

* ui: pass basePath to avatar renderer in grouped chat

* ui: formatting fixups from pre-commit hooks

* style(ui): update layout and spacing for chat controls; enhance select component styles and improve responsiveness

* UI: tighten chat header spacing and icon sizes

* UI: widen chat attachment gap

* style(ui): refine chat header layout and adjust icon sizes for improved visual consistency

* style(ui): enhance component styles and layout; introduce new inline field styles, update overview card design, and improve session filters for better usability

* style(ui): improve CSS formatting and consistency across components; adjust gradients, spacing, and layout for better readability and visual appeal

* fix(ui): correct rendering of empty state in overview cards by replacing 'nothing' with an empty string
2026-02-22 07:56:17 -06:00
Peter Steinberger e578e8379c fix: align agent panel UI props after merge 2026-02-22 13:47:31 +00:00
Peter Steinberger 013299b001 perf: lazy-load non-interactive onboarding heavy paths 2026-02-22 13:47:25 +00:00
Peter Steinberger adfbbcf1f6 chore: merge origin/main into main 2026-02-22 13:42:52 +00:00
Peter Steinberger 06d93cc12c test: dedupe doctor routing allowFrom migration coverage 2026-02-22 13:41:00 +00:00
Peter Steinberger 1becebe188 fix: harden session lock contention and cleanup 2026-02-22 13:40:55 +00:00
Val Alexander 3ea3184efe refactor(ui): implement agent avatar resolution and logo fallback in agent rendering 2026-02-22 07:39:54 -06:00
Val Alexander 284961108a style(ui): update component styles with spacing, padding, and typography adjustments for improved layout 2026-02-22 07:39:54 -06:00
Val Alexander fb577d2482 style(ui): refine layout styles with adjustments to spacing, padding, and typography 2026-02-22 07:39:53 -06:00
Onur Solmaz f39a66de27 docs: make subagents thread guidance channel-first (#23589) (thanks @osolmaz) 2026-02-22 14:39:40 +01:00
Onur 3308c86002 docs: keep channel names only in thread-support list 2026-02-22 14:39:40 +01:00
Onur 418e4e32c9 docs: clarify thread-bound subagents are Discord-only 2026-02-22 14:39:40 +01:00
Onur c952334808 docs: list thread supporting channels in subagents guide 2026-02-22 14:39:40 +01:00
Onur 0b9b9d4301 docs: make subagents thread guidance channel-first 2026-02-22 14:39:40 +01:00
Peter Steinberger 648d2daf67 test: drop duplicate timeout-fallback e2e and trim onboarding auth overlap 2026-02-22 13:33:40 +00:00
Peter Steinberger 7a2b05314a test: speed up onboarding provider auth and temp-path guard scans 2026-02-22 13:24:59 +00:00
Peter Steinberger 494bb685f8 test: merge signal typing-read-receipt coverage into inbound contract suite 2026-02-22 13:24:53 +00:00
Peter Steinberger a395479d8b test: merge signal sender-prefix coverage into typing suite 2026-02-22 13:12:57 +00:00
Peter Steinberger 83597572df test: speed up thread-bindings shared-state loader test 2026-02-22 13:09:59 +00:00
Peter Steinberger d236ded43f test: speed up non-interactive gateway onboarding suite 2026-02-22 13:08:34 +00:00
Peter Steinberger 9e868dcf5a test: remove redundant channels smoke parse case 2026-02-22 12:56:18 +00:00
Peter Steinberger 5e62d0105b test: trim smoke duplicates and reuse telegram bot setup 2026-02-22 12:55:27 +00:00
Peter Steinberger 27053826e5 test: close bootstrap ws in approval bypass suite 2026-02-22 12:55:22 +00:00
Peter Steinberger 83a2926328 test: align remaining trigger configs with fast harness defaults 2026-02-22 12:43:10 +00:00
Peter Steinberger 829236afa7 test: reuse trigger harness defaults in custom configs 2026-02-22 12:41:37 +00:00
Peter Steinberger 2c40a20737 test: trim background hold duration in abort coverage 2026-02-22 12:38:57 +00:00
Peter Steinberger 00eb2541dc test: shorten idle child timers in timeout assertions 2026-02-22 12:37:49 +00:00
Peter Steinberger 5b23159c4c test: create homedir before sandbox image mkdtemp 2026-02-22 12:35:38 +00:00
Peter Steinberger 96515a5729 test: merge duplicate read-tool content coverage cases 2026-02-22 12:32:05 +00:00
Peter Steinberger 22ff83c3cf test: remove fixed delay from cron concurrency assertion 2026-02-22 12:30:43 +00:00
Peter Steinberger c8a4977378 test: replace mtime sleep with explicit utimes bump 2026-02-22 12:29:53 +00:00
Peter Steinberger 8e29160eaa test: remove fixed waits from tool-result ordering tests 2026-02-22 12:29:08 +00:00
Peter Steinberger dc356ae1c2 test: remove duplicate workspace path-resolution case 2026-02-22 12:27:55 +00:00
Peter Steinberger c7a4346e4d test: remove sharp dependency from read-tool metadata test 2026-02-22 12:27:10 +00:00
Peter Steinberger 60a0291bf8 test: dedupe workspace path-resolution scenarios 2026-02-22 12:25:57 +00:00
Peter Steinberger 07527e22ce refactor(auth-profiles): centralize active-window logic + strengthen regression coverage 2026-02-22 13:23:19 +01:00
Peter Steinberger c61c9e121a test: relax node connect challenge timeout in approval suite 2026-02-22 12:22:53 +00:00
Peter Steinberger 1152b25866 fix(gateway): guard trim crashes in subagent flow 2026-02-22 13:21:26 +01:00
Val Alexander 52d1ece262 style(ui): enhance agent model layout with margin adjustments and flexbox for actions 2026-02-22 06:21:12 -06:00
Val Alexander 1c86a1b337 refactor(ui): simplify agent overview component by removing unused identity fields and enhancing fallback display 2026-02-22 06:21:12 -06:00
Peter Steinberger eec3182cbb fix(utils): guard resolveUserPath for missing workspace input 2026-02-22 13:19:25 +01:00
Peter Steinberger 0d0f4c6992 refactor(exec): centralize safe-bin policy checks 2026-02-22 13:18:25 +01:00
Peter Steinberger 3a6e0e70f6 test: make gateway connectReq timeout configurable 2026-02-22 12:18:21 +00:00
Peter Steinberger 89e2928204 test: speed up trigger harness queue defaults 2026-02-22 12:18:15 +00:00
Peter Steinberger dc6440b9f3 test: harden claude usage fallback assertions 2026-02-22 12:18:10 +00:00
Peter Steinberger bcad4f67a2 fix(gateway): unify listen startup log across bind hosts 2026-02-22 13:17:25 +01:00
Artale 51e9c54f09 fix(agents): skip bootstrap files with undefined path (#22698)
* fix(agents): skip bootstrap files with undefined path

buildBootstrapContextFiles() called file.path.replace() without checking
that path was defined. If a hook pushed a bootstrap file using 'filePath'
instead of 'path', the function threw TypeError and crashed every agent
session — not just the misconfigured hook.

Fix: add a null-guard before the path.replace() call. Files with undefined
path are skipped with a warning so one bad hook can't take down all agents.

Also adds a test covering the undefined-path case.

Fixes #22693

* fix: harden bootstrap path validation and report guards (#22698) (thanks @arosstale)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-22 13:17:07 +01:00
Val Alexander 45d7776697 fix(ui): update topbar styles for improved layout and active state 2026-02-22 06:14:53 -06:00
Val Alexander 7ba970938e fix(ui): add label for stream mode in app render 2026-02-22 06:14:53 -06:00
Peter Steinberger d2542d9d37 chore(gateway): cover denied notifyOnExit path and clarify help 2026-02-22 13:14:08 +01:00
zerone0x 6fde581a25 test(node): add coverage for notifyOnExit=false suppressing exec events 2026-02-22 13:14:07 +01:00
zerone0x 0f7b259cca fix(node): respect tools.exec.notifyOnExit for node exec events
Node exec events (exec.started, exec.finished, exec.denied) now check
the tools.exec.notifyOnExit config setting before generating system
event notifications. When notifyOnExit is false, all node exec event
notifications are suppressed.

This makes node exec behavior consistent with gateway exec, which
already respects this setting.

Fixes #20193

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-22 13:14:07 +01:00
Peter Steinberger 7c3c406a35 fix: keep auth-profile cooldown windows immutable in-window (#23536) (thanks @arosstale) 2026-02-22 13:14:02 +01:00
artale dc69610d51 fix(auth-profiles): never shorten cooldown deadline on retry
When the backoff saturates at 60 min and retries fire every 30 min
(e.g. cron jobs), each failed request was resetting cooldownUntil to
now+60m.  Because now+60m < existing deadline, the window kept getting
renewed and the profile never recovered without manually clearing
usageStats in auth-profiles.json.

Fix: only write a new cooldownUntil (or disabledUntil for billing) when
the new deadline is strictly later than the existing one.  This lets the
original window expire naturally while still allowing genuine backoff
extension when error counts climb further.

Fixes #23516

[AI-assisted]
2026-02-22 13:14:02 +01:00
Val Alexander bec059f7b2 fix(ui): ensure correct draft value in chat input handling 2026-02-22 06:11:24 -06:00
Peter Steinberger 376eb6e99b docs(changelog): note safe-bin profile hardening 2026-02-22 13:03:05 +01:00
Peter Steinberger 47c3f742b6 fix(exec): require explicit safe-bin profiles 2026-02-22 12:58:55 +01:00
Peter Steinberger c7ff12ef29 fix: use effective home for legacy zai auth fallback 2026-02-22 11:58:14 +00:00
Peter Steinberger 09017b77a2 test: tighten e2e runner defaults 2026-02-22 11:58:07 +00:00
Peter Steinberger 760ad5dfb3 test: move local integration suites out of e2e 2026-02-22 11:54:01 +00:00
Peter Steinberger 99f05ba258 test: move gateway sigterm suite out of e2e 2026-02-22 11:53:03 +00:00
Peter Steinberger 5ffcc4b735 test: fix logger stub typing in directive-tags test 2026-02-22 11:52:18 +00:00
Peter Steinberger 5636e6257c test: make gateway sigterm e2e node25-compatible 2026-02-22 11:51:43 +00:00
Val Alexander d055b948fb fix(ui): stop auth failure reconnect loop, surface login gate 2026-02-22 05:51:15 -06:00
Val Alexander 79ae8148f7 fix(ui): stop reconnect loop on auth failure, surface login gate 2026-02-22 05:51:06 -06:00
Peter Steinberger 9f80ac47ee test: move sessions_send suite out of e2e 2026-02-22 11:50:22 +00:00
Peter Steinberger 3f0ab76422 test: stabilize remaining e2e gateway suites 2026-02-22 11:48:53 +00:00
Peter Steinberger aa14835607 test: reclassify gateway local suites from e2e 2026-02-22 11:48:46 +00:00
Peter Steinberger e80c803fa8 fix(security): block shell env allowlist bypass in system.run 2026-02-22 12:47:05 +01:00
Peter Steinberger d5bb9f026e fix: add changelog entry for remote ws onboarding hardening (#23476) (thanks @bmendonca3) 2026-02-22 12:46:20 +01:00
Brian Mendonca 8a3d04c19c Gateway UX: harden remote ws guidance and onboarding defaults 2026-02-22 12:46:20 +01:00
Peter Steinberger 6fda04e938 refactor: tighten onboarding dmScope typing and docs links 2026-02-22 12:46:09 +01:00
Peter Steinberger 29cc7f431f test: share runtime scan filters and cached test scans 2026-02-22 12:44:44 +01:00
Peter Steinberger 6dd36a6b77 refactor(channels): reuse runtime group policy helpers 2026-02-22 12:44:23 +01:00
Val Alexander 13944f773f UI: use gateway token for login gate auth 2026-02-22 05:40:35 -06:00
Peter Steinberger 3a65e4b523 test: make snapshot env override assertion independent of host env 2026-02-22 12:40:30 +01:00
Peter Steinberger 65dccbdb4b fix: document onboarding dmScope default as breaking change (#23468) (thanks @bmendonca3) 2026-02-22 12:36:49 +01:00
Brian Mendonca 8f0b2b84e7 Onboarding: default dmScope to per-channel-peer 2026-02-22 12:36:49 +01:00
Peter Steinberger 85e5ed3f78 refactor(channels): centralize runtime group policy handling 2026-02-22 12:35:41 +01:00
Peter Steinberger a4607277a9 test: consolidate sessions_spawn and guardrail helpers 2026-02-22 12:34:55 +01:00
Peter Steinberger 62ddc1ef7a test: move gateway client watchdog suite out of e2e 2026-02-22 11:34:50 +00:00
Val Alexander 59191474eb docs(ui): update checklist for 5-theme setup 2026-02-22 05:34:42 -06:00
Val Alexander 1e4e24852a UI: remove OpenAI/Ember theme, reduce to 5 themes 2026-02-22 05:34:42 -06:00
Peter Steinberger 38cd30836d test: reclassify openresponses parity suite 2026-02-22 11:34:15 +00:00
Peter Steinberger 868c0e4c56 test: move gateway server integration suites out of e2e 2026-02-22 11:33:27 +00:00
Peter Steinberger 6c61616d51 test: move gateway rpc/local suites out of e2e 2026-02-22 11:31:42 +00:00
Peter Steinberger 7fdf54f078 test: move cli local suites out of e2e 2026-02-22 11:30:29 +00:00
Peter Steinberger 0a758dc710 test(cron): improve fire-and-forget harness coverage 2026-02-22 11:29:31 +00:00
Peter Steinberger c343132dbb fix(agents): harden bash tool and reply directive handling 2026-02-22 11:29:31 +00:00
Peter Steinberger a4981efae3 fix(discord): improve outbound send consistency 2026-02-22 11:29:31 +00:00
Peter Steinberger 0f989d3109 fix(gateway): tighten openai-http edge handling 2026-02-22 11:29:31 +00:00
Peter Steinberger 05358173da fix(line): harden outbound send behavior 2026-02-22 11:29:31 +00:00
Peter Steinberger 32a1273d82 refactor(onboarding): dedupe channel allowlist flows 2026-02-22 11:29:31 +00:00
Peter Steinberger 49648daec0 fix(zalouser): normalize send and onboarding flows 2026-02-22 11:29:31 +00:00
Peter Steinberger 5c7ab8eae3 test(zalo): broaden webhook monitor coverage 2026-02-22 11:29:31 +00:00
Peter Steinberger 081ab9c99d fix(voice-call): tighten manager outbound behavior 2026-02-22 11:29:31 +00:00
Peter Steinberger 8c1afc4b63 fix(msteams): improve graph user and token parsing 2026-02-22 11:29:31 +00:00
Peter Steinberger e80c66a571 fix(mattermost): refine probe and onboarding flows 2026-02-22 11:29:31 +00:00
Peter Steinberger 0a421d7409 test(line): improve logout scenario coverage 2026-02-22 11:29:31 +00:00
Peter Steinberger 5574eb6b35 fix(feishu): harden onboarding and webhook validation 2026-02-22 11:29:31 +00:00
Peter Steinberger 9e6125ea2f test(discord): stabilize subagent hook coverage 2026-02-22 11:29:31 +00:00
Peter Steinberger 5056f4e142 fix(bluebubbles): tighten chat target handling 2026-02-22 11:29:31 +00:00
Peter Steinberger b36e7da07d test: move non-interactive onboarding suites out of e2e 2026-02-22 11:29:13 +00:00
Peter Steinberger 4c6e7c4fe0 test: reclassify agent command suite out of e2e 2026-02-22 11:28:45 +00:00
Peter Steinberger 50c7aef22f test: stabilize session lock tests and move out of e2e 2026-02-22 11:28:20 +00:00
Peter Steinberger ad404c9626 fix: align markdown code renderer with marked token typing 2026-02-22 12:27:48 +01:00
Val Alexander 944d2b826c docs(ui): add dashboard verification checklist 2026-02-22 05:26:57 -06:00
Peter Steinberger 9f2444314d test: stabilize agent embedded-run mocks 2026-02-22 11:25:59 +00:00
Peter Steinberger 26763d1910 fix: resolve extension type errors and harden probe mocks 2026-02-22 12:25:58 +01:00
Val Alexander 3bbbe33a1b UI: gateway dashboard with glassmorphism theme system
Add a full-featured gateway dashboard UI built on Lit web components.

Shell & plumbing:
- App shell with router, controllers, and dependency wiring
- Login gate, i18n keys, and base layout scaffolding

Styles & theming:
- Base styles, chat styles, and responsive layout CSS
- 6-theme glassmorphism system (Obsidian, Aurora, Solar, etc.)
- Glass card, glass panel, and glass input components
- Favicon logo in expanded sidebar header

Views & features:
- Overview with attention cards, event log, quick actions, and log tail
- Chat view with markdown rendering, tool-call collapse, and delete support
- Command palette with fuzzy search
- Agent overview with config display, slash commands, and sidebar filtering
- Session list navigation and agent selector

Privacy & polish:
- Redact toggle with stream-mode default
- Blur host/IP in Connected Instances with reveal toggle
- Sensitive config value masking with count badge
- Card accent borders, hover lift effects, and responsive grid
2026-02-22 05:24:54 -06:00
Peter Steinberger 401106b963 fix: harden flaky tests and cover native google thought signatures (#23457) (thanks @echoVic) 2026-02-22 12:24:53 +01:00
echoVic 9176571ec1 fix(gemini): sanitize thoughtSignatures for native Google provider
Native Google Gemini provider was accumulating 2K-8K tokens of Base64
thoughtSignature blobs per turn, causing premature context overflow.

The sanitizer was only enabled for OpenRouter Gemini, not native Google.

Fixes #23392
2026-02-22 12:24:53 +01:00
Peter Steinberger bf52273a58 test: harden flaky timeout-sensitive tests 2026-02-22 12:21:19 +01:00
Peter Steinberger 42f62821db fix: include shared runtime group-policy helper and coverage (#23367) (thanks @bmendonca3) 2026-02-22 12:21:04 +01:00
Peter Steinberger 777817392d fix: fail closed missing provider group policy across message channels (#23367) (thanks @bmendonca3) 2026-02-22 12:21:04 +01:00
Peter Steinberger 78c3c2a542 fix: stabilize flaky tests and sanitize directive-only chat tags 2026-02-22 12:19:33 +01:00
Peter Steinberger 7d09a9e74d test: update agent tool assertions and reclassify suites 2026-02-22 11:18:50 +00:00
Brian Mendonca 3700151ec0 Channels: fail closed when Slack/Discord config is missing 2026-02-22 12:18:43 +01:00
Peter Steinberger fcb86408fd test: move embedded and tool agent suites out of e2e 2026-02-22 11:17:47 +00:00
Peter Steinberger 11546b1177 test(auth-choice): expand api provider dedupe coverage 2026-02-22 11:16:59 +00:00
Peter Steinberger e441390fd1 test: reclassify agent local suites out of e2e 2026-02-22 11:16:37 +00:00
Peter Steinberger fc60f4923a refactor(auth-choice): unify api-key resolution flows 2026-02-22 11:16:30 +00:00
Peter Steinberger e2c7cf2f1a test: reclassify doctor command suites out of e2e 2026-02-22 11:12:48 +00:00
Peter Steinberger 895e6c4b9c test: move onboarding and sandbox command suites out of e2e 2026-02-22 11:10:05 +00:00
Peter Steinberger 08a5cba8af test: reclassify command config and channels suites 2026-02-22 11:09:43 +00:00
Ayaan Zaidi 8e00965618 test: use real SubsystemLogger in directive-tags test 2026-02-22 16:39:11 +05:30
Peter Steinberger 296b3f49ef refactor(bluebubbles): centralize private-api status handling 2026-02-22 12:08:41 +01:00
Peter Steinberger 817ca75cba test: move command status and health suites out of e2e 2026-02-22 11:07:46 +00:00
Peter Steinberger ec36dd81a9 test: reclassify command helper suites out of e2e 2026-02-22 11:07:07 +00:00
Yuzuru Suzuki 6f7e5f92c3 fix: add operator.read and operator.write to default CLI scopes (#22582)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 8569fc88c970e75934617c200ebfe117e9d5ae88
Co-authored-by: YuzuruS <1485195+YuzuruS@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 16:36:18 +05:30
Peter Steinberger ec0081ce9a test: move hooks and plugin local suites out of e2e 2026-02-22 11:05:53 +00:00
Peter Steinberger 4a2492496e test: move browser and web auto-reply local suites out of e2e 2026-02-22 11:05:26 +00:00
Peter Steinberger 585a143f21 test: reclassify config and channel monitor behavior suites 2026-02-22 11:04:58 +00:00
Peter Steinberger 2d133d3ec2 test: reclassify auto-reply behavior suites out of e2e 2026-02-22 11:04:10 +00:00
Peter Steinberger b77e53da67 refactor(session): centralize transcript path option resolution 2026-02-22 12:02:38 +01:00
Peter Steinberger 1ad284a85f test: move local cli and config scenario suites out of e2e 2026-02-22 10:58:04 +00:00
Peter Steinberger 713e2928b2 test: move duplicate local scenario suites out of agents e2e 2026-02-22 10:56:58 +00:00
Peter Steinberger bfada9e425 test: move more local agents helper suites out of e2e 2026-02-22 10:55:22 +00:00
Peter Steinberger 4267fc8593 test: reclassify pi embedded helper suites out of agents e2e 2026-02-22 10:53:50 +00:00
Peter Steinberger adace58505 test: reclassify local helper suites out of agents e2e 2026-02-22 10:53:40 +00:00
Peter Steinberger b98d3330f6 docs: update pty supervision test command paths 2026-02-22 10:48:37 +00:00
Peter Steinberger 1d4e9ad8d1 test: reclassify remaining bash suites as unit tests 2026-02-22 10:48:32 +00:00
Peter Steinberger 37f12eb7ee fix: align BlueBubbles private-api null fallback + warning (#23459) (thanks @echoVic) 2026-02-22 11:47:57 +01:00
echoVic 888b6bc948 fix(bluebubbles): treat null privateApiStatus as disabled, not enabled
Bug: privateApiStatus cache expires after 10 minutes, returning null.
The check '!== false' treats null as truthy, causing 500 errors when
trying to use Private API features that aren't actually available.

Root cause: In JavaScript, null !== false evaluates to true.

Fix: Changed all checks from '!== false' to '=== true', so null (cache
expired/unknown) is treated as disabled (safe default).

Files changed:
- extensions/bluebubbles/src/send.ts (line 376)
- extensions/bluebubbles/src/monitor-processing.ts (line 423)
- extensions/bluebubbles/src/attachments.ts (lines 210, 220)

Fixes #23393
2026-02-22 11:47:57 +01:00
Peter Steinberger ab38e1e6b2 test: reclassify image tool suite as unit test 2026-02-22 10:47:16 +00:00
Peter Steinberger 812bf7c8e1 fix: add bindings comment regression test (#23458) (thanks @echoVic) 2026-02-22 11:47:11 +01:00
echoVic 56f01bc493 fix(config): add missing comment field to BindingsSchema
Strict validation (added in d1e9490f9) rejects the legitimate 'comment'
field on bindings. This field is used for annotations in config files.

Changes:
- BindingsSchema: added comment: z.string().optional()
- AgentBinding type: added comment?: string

Fixes #23385
2026-02-22 11:47:11 +01:00
Peter Steinberger aa487bd4f3 test: reclassify bash pty suites as unit tests 2026-02-22 10:47:10 +00:00
Glucksberg 2739328508 fix(telegram): classify undici fetch errors as recoverable for retry (#16699)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 67b5bce44f7014c8cbefc00eed0731e61d6300b9
Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 16:16:11 +05:30
Peter Steinberger 3c9f98452e test: reclassify tool-result persist hook suite as unit test 2026-02-22 10:46:02 +00:00
Peter Steinberger 047e18693e test: reclassify exec approval-id suite as unit test 2026-02-22 10:45:23 +00:00
Peter Steinberger 17a65a6f4c test: split pure docker exec arg checks from bash e2e suite 2026-02-22 10:44:40 +00:00
Peter Steinberger 239963ac44 perf(test): shrink bash command fixtures and polling windows 2026-02-22 10:43:22 +00:00
Peter Steinberger 1d7dbd8cd9 test: reclassify web fetch/readability suites as unit tests 2026-02-22 10:41:29 +00:00
Peter Steinberger 304eef575b test: reclassify sandbox and web/image tool suites as unit tests 2026-02-22 10:40:40 +00:00
Peter Steinberger 3b09a0d2d0 perf(test): trim bash e2e log fixtures and abort wait bounds 2026-02-22 10:39:18 +00:00
Peter Steinberger c68bb8d6d5 test: stabilize bash e2e suites with explicit exec approvals mode 2026-02-22 10:37:44 +00:00
Peter Steinberger 38f02c7a32 fix(session): resolve agent session path with configured sessions dir
Co-authored-by: David Rudduck <david@rudduck.org.au>
2026-02-22 11:35:55 +01:00
Peter Steinberger c283f87ab0 refactor: clarify strict loopback proxy audit rules 2026-02-22 11:35:08 +01:00
Peter Steinberger 97eb4af01e test: harden models-config env isolation list 2026-02-22 10:34:23 +00:00
Peter Steinberger 744df0fbe7 test: reclassify models-config suites from e2e to unit lane 2026-02-22 10:34:23 +00:00
Peter Steinberger 740fd7ae35 test: reclassify skills suites from e2e to unit lane 2026-02-22 10:34:23 +00:00
Peter Steinberger 5c57a45a59 fix: add non-streaming directive-tag regression tests (#23298) (thanks @SidQin-cyber) 2026-02-22 11:31:23 +01:00
SidQin-cyber e6490732cd fix(gateway): strip directive tags from non-streaming webchat broadcasts
Closes #23053

The streaming path already strips [[reply_to_current]] and other
directive tags via stripInlineDirectiveTagsForDisplay, but the
non-streaming broadcastChatFinal path and the chat.inject path
sent raw message content to webchat clients, causing tags to
appear in rendered messages after streaming completes.
2026-02-22 11:31:23 +01:00
Peter Steinberger c56ab39da5 perf(test): reduce bash e2e wait windows 2026-02-22 10:28:43 +00:00
Peter Steinberger abff3f0f61 test: reclassify sessions_spawn lifecycle suite as unit test 2026-02-22 10:28:43 +00:00
Peter Steinberger 0b7c7ee1aa perf(test): speed up sessions_spawn lifecycle suite setup 2026-02-22 10:28:43 +00:00
Peter Steinberger c962bcba37 test: reclassify sandbox merge and exec path suites as unit tests 2026-02-22 10:28:43 +00:00
Peter Steinberger 9ab7b85a66 perf(test): tighten background abort timing windows 2026-02-22 10:28:43 +00:00
Peter Steinberger c995f9be07 test: reclassify mocked announce and sandbox suites as unit tests 2026-02-22 10:28:43 +00:00
Peter Steinberger 27f0d7ebcc test: reclassify auth-profile-rotation suite as unit test 2026-02-22 10:28:43 +00:00
Peter Steinberger c0b1c10a08 test: reclassify mocked runner/safe-bins suites as unit tests 2026-02-22 10:28:43 +00:00
Peter Steinberger a9b26d83de perf(test): narrow pi-embedded runner e2e import path 2026-02-22 10:28:42 +00:00
Peter Steinberger 2b0ca9447c perf(test): trim bash e2e sleep and poll windows 2026-02-22 10:28:42 +00:00
Peter Steinberger c348a13640 perf(test): lower subagent fast-mode wait floors 2026-02-22 10:28:42 +00:00
Peter Steinberger 54e0786ba6 perf(test): reduce subagent announce fast-mode polling waits 2026-02-22 10:28:42 +00:00
Peter Steinberger a96139e18c perf(test): mock compact module in auth rotation e2e 2026-02-22 10:28:42 +00:00
Peter Steinberger eda941f395 perf(test): remove flaky transport timeout and dedupe safeBins checks 2026-02-22 10:28:42 +00:00
Peter Steinberger d72b4ead18 perf(test): lower fast-mode nested output wait floor to 70ms 2026-02-22 10:28:42 +00:00
Peter Steinberger 7ccf62fb4c test(agents): remove dead shell-timeout override in safeBins suite 2026-02-22 10:28:42 +00:00
Peter Steinberger 60773c124e perf(test): lower fast-mode nested output wait floor to 80ms 2026-02-22 10:28:42 +00:00
Peter Steinberger 36375f121f perf(test): trim nested subagent output wait floor in fast mode 2026-02-22 10:28:42 +00:00
Peter Steinberger 2900eb5456 perf(test): trim background abort settle waits and dedupe cmd fixture 2026-02-22 10:28:42 +00:00
Peter Steinberger 7d13227d41 test(agents): dedupe auth profile rotation fixture setup 2026-02-22 10:28:42 +00:00
Peter Steinberger 6b5c20055b perf(test): speed subagent announce retry polling in fast mode 2026-02-22 10:28:42 +00:00
Peter Steinberger 8ad85de800 test(reply): align native trigger suite with fast-test fixture patterns 2026-02-22 10:28:42 +00:00
Peter Steinberger 9882bfe186 perf(test): compact remaining heartbeat fixture writes 2026-02-22 10:28:42 +00:00
Peter Steinberger c8d473c8e8 test(heartbeat): use shared sandbox in sender target suite 2026-02-22 10:28:42 +00:00
Peter Steinberger 29e41d4c0a fix: land security audit severity + temp-path guard fixes (#23428) (thanks @bmendonca3) 2026-02-22 11:26:17 +01:00
Brian Mendonca bc78b343ba Security: expand audit checks for mDNS and real-IP fallback 2026-02-22 11:26:17 +01:00
Peter Steinberger b13fc7eccd docs(security): clarify workspace memory trust boundary 2026-02-22 11:22:29 +01:00
Peter Steinberger 1cd3b30907 fix: stop hardcoded channel fallback and auto-pick sole configured channel (#23357) (thanks @lbo728)
Co-authored-by: lbo728 <extreme0728@gmail.com>
2026-02-22 11:21:43 +01:00
Frank Yang e33d7fcd13 fix(telegram): prevent update offset skipping queued updates (#23284)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 92efaf956bf906a176d1e6c5488ddcb02d89b4e1
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 15:50:33 +05:30
maweibin 98a03c490b Feat/logger support log level validation0222 (#23436)
* 1、环境变量**:新增 `OPENCLAW_LOG_LEVEL`,可取值 `silent|fatal|error|warn|info|debug|trace`。设置后同时覆盖**文件日志**与**控制台**的级别,优先级高于配置文件。
2、启动参数**:在 `openclaw gateway run` 上新增 `--log-level <level>`,对该次进程同时生效于文件与控制台;未传时仍使用环境变量或配置文件。

* fix(logging): make log-level override global and precedence-safe

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-22 11:15:13 +01:00
Peter Steinberger bf56196de3 fix: tighten feishu dedupe boundary (#23377) (thanks @SidQin-cyber) 2026-02-22 11:13:40 +01:00
SidQin-cyber 9e5e555ba3 fix(feishu): address dedup race condition, namespace isolation, and cache staleness
- Prefix memoryCache keys with namespace to prevent cross-account false
  positives when different accounts receive the same message_id
- Add inflight tracking map to prevent TOCTOU race where concurrent
  async calls for the same message both pass the check and both proceed
- Remove expired-entry deletion from has() to avoid silent cache/disk
  divergence; actual cleanup happens probabilistically inside record()
- Add time-based cache invalidation (30s) to DedupStore.load() so
  external writes are eventually picked up
- Refresh cacheLoadedAt after flush() so we don't immediately re-read
  data we just wrote

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 11:13:40 +01:00
SidQin-cyber 9a8179fd59 feat(feishu): persistent message deduplication to prevent duplicate replies
Closes #23369

Feishu may redeliver the same message during WebSocket reconnects or process
restarts.  The existing in-memory dedup map is lost on restart, so duplicates
slip through.

This adds a dual-layer dedup strategy:
- Memory cache (fast synchronous path, unchanged capacity)
- Filesystem store (~/.openclaw/feishu/dedup/) that survives restarts

TTL is extended from 30 min to 24 h.  Disk writes use atomic rename and
probabilistic cleanup to keep each per-account file under 10 k entries.
Disk errors are caught and logged — message handling falls back to
memory-only behaviour so it is never blocked.
2026-02-22 11:13:40 +01:00
Peter Steinberger 73804abcec fix(feishu): avoid template tmpdir join in dedup state path (#23398) 2026-02-22 11:12:01 +01:00
Peter Steinberger bfc9ecf32e test: harden temp path guard detection (#23398) 2026-02-22 11:12:01 +01:00
Peter Steinberger 57ce7214d2 test: stabilize temp-path guard across runtimes (#23398) 2026-02-22 11:12:01 +01:00
Peter Steinberger 1b327da6e3 fix: harden exec sandbox fallback semantics (#23398) (thanks @bmendonca3) 2026-02-22 11:12:01 +01:00
Brian Mendonca c76a47cce2 Exec: fail closed when sandbox host is unavailable 2026-02-22 11:12:01 +01:00
Peter Steinberger 5a0032de3e refactor(signal): extract daemon lifecycle and typed exit handling 2026-02-22 11:09:10 +01:00
Peter Steinberger 602a1ebd55 fix: handle intentional signal daemon shutdown on abort (#23379) (thanks @frankekn) 2026-02-22 10:59:34 +01:00
Frank Yang 1051f42f96 fix(stability): patch regex retries and timeout abort handling 2026-02-22 10:59:34 +01:00
Vignesh Natarajan 99a2f5379e Memory/QMD: normalize Han-script BM25 search queries 2026-02-22 01:53:00 -08:00
Peter Steinberger 9f0b6a8c92 fix: harden ACP gateway startup sequencing (#23390) (thanks @janckerchen) 2026-02-22 10:47:38 +01:00
janckerchen 7499e0f619 fix(acp): wait for gateway connection before processing ACP messages
- Move gateway.start() before AgentSideConnection creation
- Wait for hello message to confirm connection is established
- This fixes issues where messages were processed before gateway was ready

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:47:38 +01:00
Peter Steinberger 59807efa31 refactor(plugin-sdk): unify channel dedupe primitives 2026-02-22 10:46:34 +01:00
Peter Steinberger edaa5ef7a5 refactor(gateway): simplify restart flow and expand lock tests 2026-02-22 10:44:47 +01:00
Peter Steinberger bd4f670544 refactor: simplify windows ACL parsing and expand coverage 2026-02-22 10:43:03 +01:00
Peter Steinberger 9b9cc44a4e fix: finalize modelByChannel validator landing (#23412) (thanks @ProspectOre) 2026-02-22 10:41:40 +01:00
Peter Steinberger 6dad6a8cd0 fix: cover channels.modelByChannel validation/auto-enable 2026-02-22 10:41:40 +01:00
pickaxe d79f10297f also skip modelByChannel in plugin-auto-enable channel iteration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:41:40 +01:00
pickaxe 0d93c9f759 fix: include modelByChannel in config validator allowedChannels
The hand-written config validator rejects `channels.modelByChannel` as
"unknown channel id: modelByChannel" even though the Zod schema, TypeScript
types, runtime code, and CLI docs all treat it as valid. The `defaults`
meta-key was already whitelisted but `modelByChannel` was missed when
the feature was added in 2026.2.21.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:41:40 +01:00
Vignesh Natarajan 9325418098 chore: fix temp-path guard skip for *.test-helpers.ts 2026-02-22 01:41:06 -08:00
Peter Steinberger dd07c06d00 fix: tighten gateway restart loop handling (#23416) (thanks @jeffwnli) 2026-02-22 10:38:32 +01:00
jeffr 26acb77450 fix: guard entry.ts top-level code with isMainModule to prevent duplicate gateway start
The bundler exports shared symbols from dist/entry.js, so other chunks
import it as a dependency. When dist/index.js is the actual entry point
(e.g. systemd service), lazy module loading eventually imports entry.js,
triggering its unguarded top-level code which calls runCli(process.argv)
a second time. This starts a duplicate gateway that fails on lock/port
contention and crashes the process with exit(1), causing a restart loop.

Wrap all top-level executable code in an isMainModule() check so it only
runs when entry.ts is the actual main module, not when imported as a
shared dependency by the bundler.
2026-02-22 10:38:32 +01:00
jeffr 9c30243c8f fix: release gateway lock before spawning restart child
Move lock.release() before restartGatewayProcessWithFreshPid() so the
spawned child can immediately acquire the lock without racing against
a zombie parent. This eliminates the root cause of the restart loop
where the child times out waiting for a lock held by its now-dead parent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:38:32 +01:00
jeffr 01bd83d644 fix: release gateway lock before process.exit in run-loop
process.exit() called from inside an async IIFE bypasses the outer
try/finally block that releases the gateway lock. This leaves a stale
lock file pointing to a zombie PID, preventing the spawned child or
systemctl restart from acquiring the lock. Release the lock explicitly
before calling exit in both the restart-spawned and stop code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:38:32 +01:00
jeffr 6eaf2baa57 fix: detect zombie processes in isPidAlive on Linux
kill(pid, 0) succeeds for zombie processes, causing the gateway lock
to treat a zombie lock owner as alive. Read /proc/<pid>/status on
Linux to check for 'Z' (zombie) state before reporting the process
as alive. This prevents the lock from being held indefinitely by a
zombie process during gateway restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:38:32 +01:00
SK Akram 85a3c0c818 fix: use SID-based ACL classification for non-English Windows 2026-02-22 10:37:34 +01:00
Peter Steinberger 35d5bd4e07 perf(test): shrink subagent announce fast-mode settle waits 2026-02-22 09:29:04 +00:00
Peter Steinberger 267d2193bf perf(test): compact heartbeat session fixture writes 2026-02-22 09:29:04 +00:00
Peter Steinberger 694a9eb6d3 test(heartbeat): reuse shared sandbox for ghost reminder scenarios 2026-02-22 09:29:04 +00:00
Peter Steinberger c0995103a5 test(heartbeat): reuse shared temp sandbox in model override suite 2026-02-22 09:29:04 +00:00
Peter Steinberger 703f7213b6 test(agents): simplify subagent announce suite imports and call assertions 2026-02-22 09:29:04 +00:00
Peter Steinberger 4520fdda69 test(heartbeat): dedupe sandbox/session helpers and collapse ack cases 2026-02-22 09:29:04 +00:00
Vignesh Natarajan b4cdffc7a4 TUI: make Ctrl+C exit behavior reliably responsive 2026-02-22 01:28:55 -08:00
Peter Steinberger a96d89f343 refactor: unify exec wrapper resolution and parity fixtures 2026-02-22 10:26:44 +01:00
Peter Steinberger f4dd0577b0 fix(security): block hook transform symlink escapes 2026-02-22 10:18:05 +01:00
Peter Steinberger 2c6dd84718 fix(gateway): remove hello-ok host and commit fields 2026-02-22 10:17:36 +01:00
Peter Steinberger 6c2e999776 refactor(security): unify secure id paths and guard weak patterns 2026-02-22 10:16:19 +01:00
Peter Steinberger ae8d4a8eec fix(security): harden channel token and id generation 2026-02-22 10:16:02 +01:00
Peter Steinberger c3e13175d2 perf(test): bypass queue debounce in fast mode and tighten announce defaults 2026-02-22 09:13:01 +00:00
Peter Steinberger f101d59d57 feat(security): warn on dangerous config flags at startup 2026-02-22 10:11:46 +01:00
Peter Steinberger de2e5c7b74 docs(security): clarify dangerous control-ui bypass policy 2026-02-22 10:11:46 +01:00
Vignesh Natarajan b9e9fbc97c TUI: preserve RTL text order in terminal output 2026-02-22 01:10:03 -08:00
Peter Steinberger aa2b16abe8 test(commands): replace subagent gateway reset with lightweight clear 2026-02-22 09:06:54 +00:00
Peter Steinberger 833d7574e7 test(agents): consolidate repeated announce deferral and fallback matrices 2026-02-22 09:05:56 +00:00
Peter Steinberger 27bd6f4c54 test(reply): use lightweight clears for runner-level mocks 2026-02-22 09:02:53 +00:00
Peter Steinberger 4985fb7f05 test(agents): remove overflow compaction mock reset dependency 2026-02-22 09:02:24 +00:00
Peter Steinberger d9a7b447f5 test(agents): use lightweight clear for active-run announce mock 2026-02-22 09:01:55 +00:00
Peter Steinberger ee3abb2278 test(reply): merge duplicate runReplyAgent streaming and fallback cases 2026-02-22 08:59:46 +00:00
Peter Steinberger 15657dd48d test(agents): collapse repeated announce direct-send scenarios 2026-02-22 08:57:39 +00:00
Peter Steinberger 53a7afe238 test(agents): unify hook thread-target announce assertions 2026-02-22 08:55:11 +00:00
Peter Steinberger d625f888a9 test(core): dedupe command gating and trim announce reset overhead 2026-02-22 08:54:11 +00:00
Vignesh Natarajan a4c107ee11 chore(test): harden models status mock restoration 2026-02-22 00:53:23 -08:00
Peter Steinberger cf570d3b44 test(agents): avoid full mock resets in cli credential specs 2026-02-22 08:52:21 +00:00
Peter Steinberger 2b63592be5 fix: harden exec allowlist wrapper resolution 2026-02-22 09:52:02 +01:00
Peter Steinberger 48c0acc26f test(commands): dedupe subagent status assertions 2026-02-22 08:51:43 +00:00
Vignesh Natarajan 409b6a3321 chore(test): make shell-env trusted-shell assertion platform-aware 2026-02-22 00:51:13 -08:00
Peter Steinberger 8e7d8c3d8e docs(changelog): add shell startup env override fix note 2026-02-22 09:50:21 +01:00
Peter Steinberger a1c8525766 test(agents): dedupe subagent announce direct-send variants 2026-02-22 08:49:33 +00:00
Peter Steinberger cfb3cee7aa test(core): dedupe auth rotation and credential injection specs 2026-02-22 08:44:40 +00:00
Peter Steinberger c2c7114ed3 fix(security): block HOME and ZDOTDIR env override injection 2026-02-22 09:42:55 +01:00
Peter Steinberger ccc00d874c test(core): reduce mock reset overhead in targeted suites 2026-02-22 08:40:29 +00:00
Vignesh Natarajan 2a66c8d676 Agents/Subagents: honor subagent alsoAllow grants 2026-02-22 00:39:27 -08:00
Peter Steinberger 2d2e1c2403 test(core): use lightweight clear in cron, claude runner, and telegram delivery specs 2026-02-22 08:35:38 +00:00
Peter Steinberger 902544cf2d chore: remove dead macos relay and daemon code 2026-02-22 09:35:27 +01:00
Peter Steinberger c99e7696e6 fix: decouple owner display secret from gateway auth token 2026-02-22 09:35:07 +01:00
Peter Steinberger 1e76ca593e test(core): tighten reset usage in auth, registry restart, and memory search 2026-02-22 08:34:20 +00:00
Peter Steinberger 1ba1c3f306 test(core): reduce reset overhead in messaging and agent e2e mocks 2026-02-22 08:33:06 +00:00
Peter Steinberger ce09fe2bb7 test(config): use lightweight clear in session pruning e2e setup 2026-02-22 08:30:47 +00:00
Peter Steinberger e67f813b0e test(core): continue reset-to-clear cleanup in subagent focus and web fetch 2026-02-22 08:30:05 +00:00
Peter Steinberger 7cac6bd85d test(core): continue mock reset reductions in auth, gateway, npm install 2026-02-22 08:28:50 +00:00
Peter Steinberger c7606e7064 test(subagents): use lightweight clears in sessions spawn suites 2026-02-22 08:27:36 +00:00
Peter Steinberger 8887f41d7d refactor(gateway)!: remove legacy v1 device-auth handshake 2026-02-22 09:27:03 +01:00
Peter Steinberger ed38b50fa5 test(commands): use lightweight clears in config snapshot specs 2026-02-22 08:26:11 +00:00
Peter Steinberger b014c70292 test(core): trim reset usage in gateway and install source specs 2026-02-22 08:25:09 +00:00
Vignesh Natarajan 6ceadaa41f Agents: add fallback reply for tool-only completions 2026-02-22 00:23:31 -08:00
Peter Steinberger 8a0a28763e test(core): reduce mock reset overhead across unit and e2e specs 2026-02-22 08:22:58 +00:00
Peter Steinberger d06ad6bc55 chore: remove verified dead code paths 2026-02-22 09:21:09 +01:00
Peter Steinberger b967687e55 test(agents): keep targeted resets minimal in overflow retry spec 2026-02-22 08:19:00 +00:00
Peter Steinberger 45d1096951 test(memory): prefer clear over reset in qmd spawn setup 2026-02-22 08:18:28 +00:00
Peter Steinberger 5e9cbdc1a1 test(subagents): lighten session delete mock reset in announce spec 2026-02-22 08:17:26 +00:00
Peter Steinberger b10b8dc8f8 test(agents): reduce reset overhead in session visibility and hooks specs 2026-02-22 08:16:45 +00:00
Peter Steinberger 991e3184b7 test(reply): replace heavy resets in media and runner helper specs 2026-02-22 08:15:28 +00:00
Peter Steinberger 089a78c061 test(slack): avoid redundant reset in slash metadata wait case 2026-02-22 08:14:16 +00:00
Peter Steinberger 6f3fed0470 test(slack): use lightweight clear in interactions modal-close case 2026-02-22 08:13:42 +00:00
Peter Steinberger d6d73d0ed9 test(core): trim redundant test resets and use mockClear 2026-02-22 08:12:55 +00:00
Peter Steinberger e893157600 test(core): use lightweight clears in runtime and telegram setup 2026-02-22 08:09:14 +00:00
Peter Steinberger 2557945a8d test(core): use lightweight clears in subagent and browser setup 2026-02-22 08:07:41 +00:00
Peter Steinberger dd5774a300 test(agents): use lightweight clears in skills/sandbox setup 2026-02-22 08:06:06 +00:00
Peter Steinberger 6e253096ed test(core): use lightweight clears in command and dispatch setup 2026-02-22 08:06:06 +00:00
Peter Steinberger 96674ca301 fix(ci): add explicit mock types in pw-session mock setup 2026-02-22 08:05:12 +00:00
Peter Steinberger 008a8c9dc6 chore(docs): normalize security finding table formatting 2026-02-22 08:03:29 +00:00
Peter Steinberger 0194d50339 test: stabilize pw-session cdp mocking in parallel runs 2026-02-22 08:03:29 +00:00
Peter Steinberger 0c1a52307c fix: align draft/outbound typings and tests 2026-02-22 08:03:29 +00:00
Peter Steinberger 0ae7f962f9 test(commands): use lightweight clears in agents/channels setup 2026-02-22 08:02:03 +00:00
Peter Steinberger d559f226b3 test(telegram): use lightweight clears in media handler setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 9a0830bc7c test(infra): use lightweight clears in message action threading setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 88c564f050 test(gateway): use lightweight clears in agent handler tests 2026-02-22 08:01:16 +00:00
Peter Steinberger 24f477625a test(infra): use lightweight clears in update startup mocks 2026-02-22 08:01:16 +00:00
Peter Steinberger 50c0616278 test(daemon): use lightweight clears in systemd mocks 2026-02-22 08:01:16 +00:00
Peter Steinberger e16e7be85b test(core): trim redundant mock resets in heartbeat suites 2026-02-22 08:01:16 +00:00
Peter Steinberger ccd96873b5 test(agents): drop redundant subagent registry cleanups 2026-02-22 08:01:16 +00:00
Peter Steinberger f144a39bb7 test(agents): dedupe sessions_spawn allowlist reset setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 089270e769 test(core): use lightweight clears in stable mock setup 2026-02-22 08:01:16 +00:00
Peter Steinberger ad400afb24 test(agents): dedupe sessions_spawn e2e reset setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 1f0695ba47 test(core): use lightweight clears in update, child adapter, and copilot token setup 2026-02-22 08:01:16 +00:00
Peter Steinberger be5921e8fe test(gateway): use lightweight clears for openresponses agent fences 2026-02-22 08:01:16 +00:00
Peter Steinberger 682e42b0a1 test(gateway): use lightweight clears for openai http agent fences 2026-02-22 08:01:16 +00:00
Peter Steinberger d624aa5ab2 test(gateway): use lightweight clears for chat-b reply spy fences 2026-02-22 08:01:16 +00:00
Peter Steinberger b601f474f0 test(agents): use lightweight clears in skills install e2e setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 0511e28a27 test(ui): use lightweight clears in theme and telegram media retry setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 9daab2abb3 test(gateway): use lightweight clears in client close setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 4ddaafee68 test(plugins): use lightweight clears in wired hooks setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 9df896e5b9 test(auto-reply): use lightweight clears in agent runner setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 751ca08728 test(agents): use lightweight clears in sandbox browser create setup 2026-02-22 08:01:16 +00:00
Peter Steinberger b25b1812e8 test(auto-reply): use lightweight clears in dispatch setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 56c57048cb test(gateway): use lightweight clears for hook cron run fences 2026-02-22 08:01:16 +00:00
Peter Steinberger 4cc975fec1 test(gateway): use lightweight clears in node event setup 2026-02-22 08:01:16 +00:00
Peter Steinberger d9085a7704 test(gateway): use lightweight clears in node invoke wake setup 2026-02-22 08:01:16 +00:00
Peter Steinberger c358ada510 test(gateway): use lightweight clears in push handler setup 2026-02-22 08:01:16 +00:00
Peter Steinberger 7adcf5a49e test(outbound): dedupe shared setup hooks in message e2e 2026-02-22 08:01:16 +00:00
Peter Steinberger 0889ea221d test(commands): use lightweight clears in doctor memory search setup 2026-02-22 08:01:15 +00:00
Peter Steinberger 2b24a44cd9 test(gateway): use lightweight clears in cron service setup 2026-02-22 08:01:15 +00:00
Peter Steinberger d7f01c2c55 test(browser): use lightweight clears in server lifecycle setup 2026-02-22 08:01:15 +00:00
Peter Steinberger 6d74704d7a test(telegram): centralize native command session-meta mock setup 2026-02-22 08:01:15 +00:00
Peter Steinberger babe1b0f26 test(agents): centralize sessions tool gateway mock reset 2026-02-22 08:00:41 +00:00
Peter Steinberger 8acf5ffca7 test(auto-reply): centralize subagent command test reset setup 2026-02-22 08:00:41 +00:00
Peter Steinberger b56c07e991 test(agents): use lightweight clears in supervisor and session-status setup 2026-02-22 08:00:41 +00:00
Peter Steinberger ba2790222d test(gateway): dedupe loopback cases and trim setup resets 2026-02-22 08:00:41 +00:00
Peter Steinberger 9f97555b5e refactor(security): unify hook rate-limit and hook module loading 2026-02-22 08:57:01 +01:00
Peter Steinberger 7cf280805c test: dedupe cron and slack monitor test harness setup 2026-02-22 07:52:12 +00:00
Peter Steinberger 3d03375043 fix(gateway): block avatar symlink escapes 2026-02-22 08:51:17 +01:00
Peter Steinberger 94e5a46187 test(telegram): dedupe native-command test setup 2026-02-22 07:48:43 +00:00
Peter Steinberger cd7faea93b docs(changelog): note next npm release for hook auth fix 2026-02-22 08:48:13 +01:00
Vignesh Natarajan 6bf5e76be6 Agents: drop stale pre-compaction usage snapshots 2026-02-21 23:47:15 -08:00
Peter Steinberger bdbbcbcc11 test: dedupe telegram draft stream setup and extend state-dir env coverage 2026-02-22 07:46:17 +00:00
Peter Steinberger 265da4dd2a fix(security): harden gateway command/audit guardrails 2026-02-22 08:45:48 +01:00
Peter Steinberger 121d027229 chore: remove dead plugin hook loader 2026-02-22 08:45:24 +01:00
Peter Steinberger 185fba1d22 refactor(agents): dedupe plugin hooks and test helpers 2026-02-22 07:44:57 +00:00
Peter Steinberger 75c1bfbae8 refactor(channels): dedupe message routing and telegram helpers 2026-02-22 07:44:57 +00:00
Peter Steinberger b109fa53ea refactor(core): dedupe gateway runtime and config tests 2026-02-22 07:44:57 +00:00
Peter Steinberger ad1c07e7c0 refactor: eliminate remaining duplicate blocks across draft streams and tests 2026-02-22 07:44:57 +00:00
Peter Steinberger abf3dfc375 refactor(agents): reuse shared tool-policy base helpers 2026-02-22 07:44:57 +00:00
Peter Steinberger 794c902e50 refactor(agents): share volc model catalog helpers 2026-02-22 07:44:57 +00:00
Peter Steinberger 86907aa500 test: dedupe lifecycle oauth and prompt-limit fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger 4a1b6e42fd test(agents): dedupe sanitize-session-history copilot fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger ea91933e2c test(agents): dedupe spawn-hook wait mocks and add readiness error coverage 2026-02-22 07:44:57 +00:00
Peter Steinberger 639b2f5f5b test(browser): dedupe pw-session playwright mock wiring 2026-02-22 07:44:57 +00:00
Peter Steinberger 6bc753624f test(browser): dedupe generated-token persistence assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger 4f7032fbd9 test(utils): share temp-dir helper across cli and web tests 2026-02-22 07:44:57 +00:00
Peter Steinberger 23e07bc49c test(agent): reuse isolated agent mock setup 2026-02-22 07:44:57 +00:00
Peter Steinberger 9ec440d1f4 test(hooks): dedupe unsupported npm spec assertion 2026-02-22 07:44:57 +00:00
Peter Steinberger d325c01503 test(gateway): dedupe canvas ws connect assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger 6471ff02dc test(gateway): dedupe chat history transcript helpers 2026-02-22 07:44:57 +00:00
Peter Steinberger 64b9ae8fb1 test(gateway): reuse shared openai timeout e2e helpers 2026-02-22 07:44:57 +00:00
Peter Steinberger 271999d42a test(config): dedupe nested redaction round-trip assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger 71c17da2ba test(config): dedupe traversal include assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger c4aac407dc test(gateway): dedupe openai context assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger b0f6f18569 test(gateway): dedupe control-ui not-found fixture assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger 7778eee5e3 test(cron): dedupe delivered-status run scaffolding 2026-02-22 07:44:57 +00:00
Peter Steinberger 4c8545ad53 test(browser): dedupe relay probe server scaffolding 2026-02-22 07:44:57 +00:00
Peter Steinberger 16f6b55cd4 test(gateway): dedupe tailscale header auth fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger 44a272ef67 refactor(config): dedupe legacy stream-mode migration paths 2026-02-22 07:44:57 +00:00
Peter Steinberger 0e68789ebf test(discord): dedupe guild permission route mocks 2026-02-22 07:44:57 +00:00
Peter Steinberger f41be7159c test(pi): share overflow-compaction test setup 2026-02-22 07:44:57 +00:00
Peter Steinberger 2cf9c3abe4 test(models): dedupe auth-sync command assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger b791ac2167 refactor(logging): share node createRequire resolution 2026-02-22 07:44:57 +00:00
Peter Steinberger b25fd03b8c refactor(node-host): share invoke type definitions 2026-02-22 07:44:57 +00:00
Peter Steinberger a32edf423b refactor(text): share code-region parsing for reasoning tags 2026-02-22 07:44:57 +00:00
Peter Steinberger a2a19cdad2 test(gateway): dedupe transcript seed fixtures in fs session tests 2026-02-22 07:44:57 +00:00
Peter Steinberger b03656a771 test(auth-profiles): dedupe oauth mode resolution setup 2026-02-22 07:44:57 +00:00
Peter Steinberger fd8b7b5c4a test(outbound): share resolveOutboundTarget test suite 2026-02-22 07:44:57 +00:00
Peter Steinberger b6ce5e06cd test(memory): share short-timeout test helper 2026-02-22 07:44:57 +00:00
Peter Steinberger b257ba9e30 test(auth-profiles): dedupe cleared-state assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger d069f8b23a test(subagents): dedupe focus thread setup fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger d476994fb9 test(memory): share memory-tool manager mock fixture 2026-02-22 07:44:57 +00:00
Peter Steinberger 07d09c881d test(wizard): share onboarding prompter scaffold 2026-02-22 07:44:57 +00:00
Peter Steinberger 3d718b5c37 test(security): dedupe external marker sanitization assertions 2026-02-22 07:44:57 +00:00
Peter Steinberger df35829810 test(inbound): share dispatch capture mock across channels 2026-02-22 07:44:57 +00:00
Peter Steinberger be0e0ebf89 test(discord): share resolve-users guild probe fixture 2026-02-22 07:44:57 +00:00
Peter Steinberger 8613b6c6ee test(discord): share message handler draft fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger cca4dba53b test(discord): share model picker fallback fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger 77a8a253a9 refactor(discord): dedupe voice command runtime checks 2026-02-22 07:44:57 +00:00
Peter Steinberger 6fe4bbc24f test(infra): dedupe shell env fallback test setup 2026-02-22 07:44:57 +00:00
Peter Steinberger 3664d51b6f test(discord): share thread binding sweep fixtures 2026-02-22 07:44:57 +00:00
Peter Steinberger a9fa434191 test(discord): share provider lifecycle test harness 2026-02-22 07:44:56 +00:00
Peter Steinberger a4b3aeeefa test(gateway): reuse last agent command assertion helper 2026-02-22 07:44:56 +00:00
Peter Steinberger 244ccc801e refactor(commands): share preview streaming migration logic 2026-02-22 07:44:56 +00:00
Peter Steinberger 474ba45a2f refactor(slack): dedupe modal lifecycle interaction handlers 2026-02-22 07:44:56 +00:00
Peter Steinberger 9d17a30643 refactor(cli): share pinned npm install record helper 2026-02-22 07:44:56 +00:00
Peter Steinberger 2d4e4e2288 refactor(cli): share npm install metadata helpers 2026-02-22 07:44:56 +00:00
Peter Steinberger d6ad647f56 test(cli): share nodes ios fixture helpers 2026-02-22 07:44:56 +00:00
Peter Steinberger fb73c0034e refactor(cli): extract fish completion line builders 2026-02-22 07:44:56 +00:00
Peter Steinberger fc54e3eabd test(cli): dedupe cron shared test fixtures 2026-02-22 07:44:56 +00:00
Peter Steinberger ae07d3fa0f test(cli): dedupe update restart fallback scenario setup 2026-02-22 07:44:56 +00:00
Peter Steinberger 266b3a356d refactor(cli): dedupe allowlist command wiring 2026-02-22 07:44:56 +00:00
Peter Steinberger 7c9e1bada0 refactor(cli): dedupe channel auth resolution flow 2026-02-22 07:44:56 +00:00
Peter Steinberger c21792f5a0 refactor(cli): dedupe skills command report loading 2026-02-22 07:44:56 +00:00
Peter Steinberger 3284d2eb22 fix(security): normalize hook auth rate-limit client keys 2026-02-22 08:40:49 +01:00
Vignesh Natarajan aab20e58d7 Sessions: persist prompt-token totals without usage 2026-02-21 23:37:42 -08:00
Peter Steinberger 76828e8dc8 test(agents): use lightweight clears for stable subagent announce defaults 2026-02-22 07:35:55 +00:00
Peter Steinberger 649e910465 test(models): use lightweight clears in shared config setup 2026-02-22 07:35:55 +00:00
Peter Steinberger e729c992a7 test(cli): use lightweight clears in daemon lifecycle setup 2026-02-22 07:35:55 +00:00
Peter Steinberger 2fd57cec0b test(commands): trim dashboard setup resets and dedupe bind cases 2026-02-22 07:35:55 +00:00
Peter Steinberger 076c5ebaef test(hooks): use lightweight clears for gmail watcher log spies 2026-02-22 07:35:55 +00:00
Peter Steinberger 856b5aca2c test(outbound): use lightweight clears in send service setup 2026-02-22 07:35:55 +00:00
Peter Steinberger d4b0397378 test(outbound): use lightweight clears in sendMessage setup 2026-02-22 07:35:55 +00:00
Peter Steinberger b55979844b test(tui): dedupe local bind loopback assertions 2026-02-22 07:35:55 +00:00
Peter Steinberger fad2c0c8a1 test(auto-reply): trim setup resets in block streaming and subagent focus 2026-02-22 07:35:55 +00:00
Peter Steinberger f37a09a9e6 test(discord): use lightweight clears in outbound plugin setup 2026-02-22 07:35:55 +00:00
Peter Steinberger a9b14df1e3 test(signal): use lightweight clears in sender-prefix and receipts setup 2026-02-22 07:35:55 +00:00
Peter Steinberger 14d6b3741c test(channels): use lightweight clears in probe and reaction setup 2026-02-22 07:35:55 +00:00
Peter Steinberger f28fcf243a test(cli): use lightweight clears in message helper and gateway chat setup 2026-02-22 07:35:54 +00:00
Peter Steinberger 735fc23faf test(discord): use lightweight clears in tool-result setup 2026-02-22 07:35:54 +00:00
Peter Steinberger c2600c5d75 test(cli): use lightweight clear for gateway discover beacon mock 2026-02-22 07:35:54 +00:00
Peter Steinberger 856b8e28a6 test(discord): use lightweight clear for thread binding rest mock 2026-02-22 07:35:54 +00:00
Peter Steinberger 42f27ca39d test(cli): seed stable defaults while replacing setup resets 2026-02-22 07:35:54 +00:00
Peter Steinberger 391d32d461 test(cli): use lightweight clear for cron gateway mock 2026-02-22 07:35:54 +00:00
Peter Steinberger cea5bcc4ac test(cli): use lightweight clear for memory manager mock 2026-02-22 07:35:54 +00:00
Peter Steinberger 0858512abd test(cli): use lightweight clear for logs gateway mock 2026-02-22 07:35:54 +00:00
Peter Steinberger ab159a68c9 test(cli): use lightweight clears for browser extension runtime spies 2026-02-22 07:35:54 +00:00
Peter Steinberger a038ad29f9 test(cli): keep pairing notify mock on clear with default resolve 2026-02-22 07:35:54 +00:00
Peter Steinberger f4afa12054 test(discord): seed exec-approval rest mocks with lightweight clears 2026-02-22 07:35:54 +00:00
Peter Steinberger 7ed3ee0a26 test(discord): use lightweight clears in message-handler setup 2026-02-22 07:35:54 +00:00
Peter Steinberger e36f857e46 test(cli): seed restart and doctor defaults with lightweight clears 2026-02-22 07:35:54 +00:00
Peter Steinberger 706837f6a3 test(discord): trim proxy and reply-delivery setup resets 2026-02-22 07:35:54 +00:00
Peter Steinberger 1e1851a991 test(discord): use lightweight clears for media utility mocks 2026-02-22 07:35:54 +00:00
Peter Steinberger e2603aecf5 test(discord): use lightweight clears in provider setup 2026-02-22 07:35:54 +00:00
Peter Steinberger 10328892fa test(discord): use mock clears in monitor setup defaults 2026-02-22 07:35:54 +00:00
Peter Steinberger a3936264ea test(slack): use lightweight clears for interaction event mock 2026-02-22 07:35:54 +00:00
Peter Steinberger 142e8cb383 test(cli): use lightweight clears for devices runtime/detail mocks 2026-02-22 07:35:54 +00:00
Peter Steinberger 67aef31187 test(cli): replace setup mock resets with clears in update suite 2026-02-22 07:35:54 +00:00
Peter Steinberger 3a80934aaa test(telegram): drop redundant plugin auth mock resets 2026-02-22 07:35:54 +00:00
Peter Steinberger 342cd19e91 test(telegram): keep session-store mocks on clear in dispatch setup 2026-02-22 07:35:54 +00:00
Peter Steinberger 4a42bc64af test(telegram): scope fake timers in probe retry tests 2026-02-22 07:35:54 +00:00
Peter Steinberger b3c5b532ad test(outbound): replace setup mock resets with clears 2026-02-22 07:35:54 +00:00
Peter Steinberger 91dd21b6b6 test(telegram): table-drive proxy client assertions and trim resets 2026-02-22 07:35:54 +00:00
Peter Steinberger 397d48c0a4 test(telegram): avoid heavy pairing-store mock reset in dm flow loop 2026-02-22 07:35:54 +00:00
Peter Steinberger fcb191c5cb test(telegram): dedupe bot message processor call setup 2026-02-22 07:35:54 +00:00
Peter Steinberger e14af1a346 test(telegram): use lightweight mock clears in native command setup 2026-02-22 07:35:54 +00:00
Peter Steinberger c42a7aff37 test(telegram): trim setup resets and table-drive edit fallback cases 2026-02-22 07:35:54 +00:00
Peter Steinberger e0db04a50d fix(security): harden avatar validation and size limits 2026-02-22 08:35:32 +01:00
Peter Steinberger 049b8b14bc fix(security): flag open-group runtime/fs exposure in audit 2026-02-22 08:22:51 +01:00
Peter Steinberger 17c9d550e9 docs: clarify sessionKey trust boundary in security policy 2026-02-22 08:21:53 +01:00
Peter Steinberger 4508b818a1 fix(acp): escape C0/C1 controls in resource link metadata 2026-02-22 08:16:38 +01:00
Peter Steinberger 55e38d3b44 refactor: extract tmp media resolver helper and dedupe sandbox-path tests 2026-02-22 08:11:46 +01:00
Vignesh Natarajan 8202582f4b chore: fix sanitizeSessionHistory test harness typing 2026-02-21 23:08:33 -08:00
Vignesh Natarajan cdfe45eeb8 Agents: validate persisted tool-call names 2026-02-21 23:06:44 -08:00
Vignesh Natarajan 29a782b9cd Models/Config: default missing Anthropic model api fields 2026-02-21 22:50:43 -08:00
Vignesh Natarajan 7f611f0e13 chore: widen hook-runner test mock signatures for tsgo 2026-02-21 22:35:55 -08:00
Vignesh Natarajan 542fc169d2 Plugins/Hooks: avoid duplicate before_agent_start executions 2026-02-21 22:31:51 -08:00
Vignesh Natarajan 96c985400d BlueBubbles: accept webhook payloads with missing handles 2026-02-21 22:10:30 -08:00
Pierre 4f700e96af Fix Telegram DM last-route metadata leakage (#19491)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 16b025b3aa13c91fe3aab8a0eaac4987dddc574e
Co-authored-by: guirguispierre <22091706+guirguispierre@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 11:29:59 +05:30
Vignesh Natarajan 54e5f80424 Browser: accept canonical upload paths for symlinked roots 2026-02-21 21:54:57 -08:00
Vignesh Natarajan 98b2b16ac3 Security/Exec: persist inner commands for shell-wrapper approvals 2026-02-21 21:26:20 -08:00
miz-cha 2f023a4775 fix(telegram): disable autoSelectFamily by default on WSL2 (#21916)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 431fd966706e300a378b177b25b00af952eddc8b
Co-authored-by: MizukiMachine <185313792+MizukiMachine@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 10:54:49 +05:30
Vignesh Natarajan 73b4330d4c CLI/Config: keep explicitly unset keys removed 2026-02-21 21:08:04 -08:00
Robin Waslander daf036a4f6 fix(slash): persist channel metadata from slash command sessions (#23065)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 29fa20c7d773b2aac62dea912e00e438ce8ba9f6
Co-authored-by: hydro13 <6640526+hydro13@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 10:29:06 +05:30
Vignesh Natarajan 6d11b46994 Media: preserve PDF MIME classification in file extraction 2026-02-21 20:50:25 -08:00
Ayaan Zaidi 63b4c500d9 fix: prevent Telegram preview stream cross-edit race (#23202)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 529abf209d56d9f991a7d308f4ecce78ac992e94
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-22 10:04:33 +05:30
Vignesh Natarajan 413f81b856 Memory/QMD: migrate legacy unscoped collections 2026-02-21 20:31:12 -08:00
Vignesh Natarajan 961bde27fe Cron: guard missing expr in schedule parsing 2026-02-21 20:18:11 -08:00
Vignesh Natarajan eea0a68199 chore: make tui callback invocation tsgo-safe 2026-02-21 20:05:25 -08:00
Vignesh Natarajan 2b5952f8c3 chore: fix tui test callback narrowing for CI 2026-02-21 20:03:32 -08:00
Vignesh Natarajan c51c2a2dca Slack: preserve slash options receiver binding 2026-02-21 20:01:39 -08:00
Tak Hoffman 2e9ee22a9c UI: fix light-mode chat toggle active state 2026-02-21 21:55:21 -06:00
Vignesh Natarajan 8920e281cc Plugins: allowlist plugins when enabling from CLI 2026-02-21 19:37:26 -08:00
Vignesh Natarajan 483c464b62 Gateway: preserve token scopes on scope-less repair approvals 2026-02-21 19:37:15 -08:00
Vignesh Natarajan 55d492b4cd Gateway: allow operator admin scope for pairing and approvals 2026-02-21 19:37:04 -08:00
Vignesh Natarajan 68cb4fc8a1 TUI: render sending and waiting indicators immediately 2026-02-21 19:28:42 -08:00
Vignesh Natarajan 68b92e80f7 Agents: log lifecycle error text for embedded run failures 2026-02-21 19:24:45 -08:00
Vignesh Natarajan 35fe33aa90 Agents: classify Anthropic api_error internal server failures for fallback 2026-02-21 19:22:16 -08:00
Vignesh Natarajan a10d689860 TUI: coalesce multiline paste submits on macOS terminals 2026-02-21 19:19:55 -08:00
Vignesh Natarajan f2d664e24f Gateway: deep-compare array config paths for reload diff 2026-02-21 19:17:46 -08:00
Vignesh Natarajan 2830dafbe9 Cron: keep list/status responsive during startup catch-up 2026-02-21 19:13:04 -08:00
Vignesh Natarajan c45a5c551f Agents: preserve unsafe integer tool args in Ollama stream 2026-02-21 19:08:31 -08:00
Vignesh Natarajan 4550a52007 TUI: filter model picker to allowlisted models 2026-02-21 19:03:15 -08:00
Andrew Jeon 853ae626fa feat: add Korean language support for memory search query expansion (#18899)
* feat: add Korean stop words and tokenization for memory search

* fix: address review comments on Korean query expansion

* fix: lint errors - curly brace and toSorted

* fix(memory): improve Korean stop words and deduplicate

* Memory: tighten Korean query expansion filtering

* Docs/Changelog: credit Korean memory query expansion

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-02-21 21:33:30 -05:00
Vignesh Natarajan 5b4409d5d0 fix: pairing admin satisfies write (#23125) (thanks @vignesh07) 2026-02-21 18:25:13 -08:00
vignesh07 426d97797d fix(pairing): treat operator.admin as satisfying operator.write 2026-02-21 18:25:13 -08:00
vignesh07 a37e12eabc docs(changelog): credit nicole-luxe for mcporter QMD work 2026-02-21 17:32:59 -08:00
Vincent Koc 7a6ff4c55a docs(changelog): credit BlueBubbles DM history fix (#23095) 2026-02-21 20:03:17 -05:00
Ryan Haines 75a9ea004b Fix BlueBubbles DM history backfill bug (#20302)
* feat: implement DM history backfill for BlueBubbles

- Add fetchBlueBubblesHistory function to fetch message history from API
- Modify processMessage to fetch history for both groups and DMs
- Use dmHistoryLimit for DMs and historyLimit for groups
- Add InboundHistory field to finalizeInboundContext call

Fixes #20296

* style: format with oxfmt

* address review: in-memory history cache, resolveAccount try/catch, include is_from_me

- Wrap resolveAccount in try/catch instead of unreachable guard (it throws)
- Include is_from_me messages with 'me' sender label for full conversation context
- Add in-memory rolling history map (chatHistories) matching other channel patterns
- API backfill only on first message per chat, not every incoming message
- Remove unused buildInboundHistoryFromEntries import

* chore: remove unused buildInboundHistoryFromEntries helper

Dead code flagged by Greptile — mapping is done inline in
monitor-processing.ts.

* BlueBubbles: harden DM history backfill state handling

* BlueBubbles: add bounded exponential backoff and history payload guards

* BlueBubbles: evict merged history keys

* Update extensions/bluebubbles/src/monitor-processing.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: Ryan Mac Mini <ryanmacmini@ryans-mac-mini.tailf78f8b.ts.net>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-21 20:00:09 -05:00
Vignesh 3317b49d3b feat(memory): allow QMD searches via mcporter keep-alive (openclaw#19617) thanks @vignesh07
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-21 18:54:33 -06:00
Peter Steinberger 2e8e357bf7 test(telegram): use mockClear in per-case bot setup loops 2026-02-21 23:59:08 +00:00
Peter Steinberger 057233953e test(retry): table-drive retryAfter timer cases 2026-02-21 23:58:33 +00:00
Peter Steinberger 1381c4c64a test(telegram): replace redundant bot setup mock resets with clears 2026-02-21 23:58:33 +00:00
Peter Steinberger 5af39b051d test(telegram): dedupe send fallback/media fixtures and trim reset overhead 2026-02-21 23:58:33 +00:00
Peter Steinberger dfe0483d80 test(browser): table-drive scroll and click error rewrites 2026-02-21 23:58:33 +00:00
Peter Steinberger 8083cb8e0b test(web-fetch): dedupe blocked-url SSRF assertions 2026-02-21 23:58:33 +00:00
Peter Steinberger a97992fcf2 test(pi-tools): share safeBins e2e setup and teardown 2026-02-21 23:58:33 +00:00
Peter Steinberger ba23d2b1fe test(onboard): table-drive custom api flag rejection cases 2026-02-21 23:58:33 +00:00
Peter Steinberger 8cc3a5e460 test(doctor): tighten legacy migration e2e timeout budgets 2026-02-21 23:58:33 +00:00
Peter Steinberger 012654c7c5 test(sandbox): table-drive dangerous docker config rejection cases 2026-02-21 23:58:33 +00:00
Peter Steinberger a353dae14f test(image-tool): share temp agent dirs and table-drive validation cases 2026-02-21 23:58:33 +00:00
Peter Steinberger 150c048b0a refactor: unify discord listener slow-log flow and test helpers 2026-02-22 00:44:56 +01:00
Peter Steinberger f589295a0a test(actions): table-drive discord presence mappings 2026-02-21 23:44:01 +00:00
Peter Steinberger 0afd5d38c5 test(actions): table-drive discord reaction and permission cases 2026-02-21 23:43:01 +00:00
Peter Steinberger 2595690a4d test(actions): table-drive slack and telegram action cases 2026-02-21 23:43:01 +00:00
Peter Steinberger 7707e3406c fix: await DiscordMessageListener handler for queued messages (#22396)
Co-authored-by: Irene <huangxiyan2311@gmail.com>
2026-02-22 00:41:46 +01:00
Peter Steinberger 8922cb4085 test(sandbox): share sandbox-root setup across path cases 2026-02-21 23:38:43 +00:00
Peter Steinberger 548c227411 test: fix nodes camera case typing for CI 2026-02-22 00:38:36 +01:00
Peter Steinberger 6ea47c3f02 test(outbound): table-drive pre-aborted action cases 2026-02-21 23:37:12 +00:00
Peter Steinberger 8af676edb3 test: tighten web and cron cli timeout budgets 2026-02-21 23:36:24 +00:00
Peter Steinberger 204f379f6b test(archive): share zip/tar fixture generation 2026-02-21 23:35:21 +00:00
Peter Steinberger 9aa5b5d157 test(logging): dedupe stream and state-dir env assertions 2026-02-21 23:34:38 +00:00
Peter Steinberger ffd9b86ca4 test(ssrf): table-drive blocked hostname literal checks 2026-02-21 23:33:47 +00:00
Peter Steinberger e84d89ab06 test(gateway): extract shared parse warning helper 2026-02-21 23:32:32 +00:00
Peter Steinberger d3991d6aa9 fix: harden sandbox tmp media validation (#17892) (thanks @dashed) 2026-02-22 00:31:21 +01:00
Alberto Leal 2958a8414d test(media): narrow result kind before sendResult assertion 2026-02-22 00:31:21 +01:00
Alberto Leal 8934da785b test(media): verify tmpdir media paths allowed through message action runner
Add integration test confirming that runMessageAction with a sandbox
root now accepts media paths under os.tmpdir() through the full
normalization pipeline (normalizeSandboxMediaList → resolveSandboxedMediaSource).
2026-02-22 00:31:21 +01:00
Alberto Leal 0bb81f7294 fix(media): allow os.tmpdir() paths in sandbox media source validation
resolveSandboxedMediaSource() rejected all paths outside the sandbox
workspace root, including /tmp. This blocked sandboxed agents from
sending locally-generated temp files (e.g. images from Python scripts)
via messaging actions.

Add an os.tmpdir() prefix check before the strict sandbox containment
assertion, consistent with buildMediaLocalRoots() which already
includes os.tmpdir() in its default allowlist. Path traversal through
/tmp (e.g. /tmp/../etc/passwd) is prevented by path.resolve()
normalization before the prefix check.

Relates-to: #16382, #14174
2026-02-22 00:31:21 +01:00
Alberto Leal 4cf5c3e109 test: add unit tests for resolveSandboxedMediaSource
Add baseline test coverage for the previously untested
resolveSandboxedMediaSource() function, covering sandbox-relative
path resolution, rejection of paths outside the sandbox root,
path traversal prevention, file:// URL handling, HTTP URL
passthrough, and empty input edge cases.
2026-02-22 00:31:21 +01:00
Peter Steinberger 59563847e4 test(web): table-drive SSRF and voice input rejection cases 2026-02-21 23:30:13 +00:00
Peter Steinberger d748657265 test(gateway): table-drive runtime config validation matrix 2026-02-21 23:29:29 +00:00
Peter Steinberger 4ab85cee0b test(cli): table-drive repeated argv and byte-size checks 2026-02-21 23:28:07 +00:00
Peter Steinberger fc2ed0b843 test(cron): dedupe webhook patch validation cases 2026-02-21 23:28:07 +00:00
Peter Steinberger bcfae0434b test(fetch): table-drive sync throw cleanup coverage 2026-02-21 23:28:07 +00:00
Peter Steinberger 833144fd72 test(gateway): tighten e2e timeout budget 2026-02-21 23:28:07 +00:00
Peter Steinberger dd4e8f8098 test(cli): table-drive camera url failure cases 2026-02-21 23:28:07 +00:00
Peter Steinberger c9593c4c87 test(sandbox): table-drive bind and network validation cases 2026-02-21 23:28:07 +00:00
Peter Steinberger 7c248cca4a test(targets): table-drive slack and discord parse cases 2026-02-21 23:28:07 +00:00
Peter Steinberger 98790339ef test: dedupe repeated validation and throw assertions 2026-02-21 23:28:07 +00:00
Peter Steinberger 01ec832f78 test(actions): table-drive telegram and signal mappings 2026-02-21 23:28:06 +00:00
Peter Steinberger 884c6afc26 test(telegram): table-drive channel override and id helper cases 2026-02-21 23:28:06 +00:00
Peter Steinberger b97691f3a7 test(config): avoid duplicate include resolution in throw assertions 2026-02-21 23:28:06 +00:00
Peter Steinberger c78ea8ec3f test(gateway): tighten health e2e timeout ceilings 2026-02-21 23:28:06 +00:00
Peter Steinberger 8cdb184f10 test(actions): table-drive discord forwarding cases 2026-02-21 23:28:06 +00:00
Peter Steinberger 95dab6e019 fix: harden config prototype-key guards (#22968) (thanks @Clawborn) 2026-02-22 00:25:22 +01:00
Clawborn e23c08b5f4 Fix prototype pollution in applyMergePatch via blocked key filter
applyMergePatch in merge-patch.ts iterates Object.entries(patch) without
filtering dangerous keys. When a caller passes a JSON-parsed object with
a "__proto__" key, the loop assigns result["__proto__"] = value, which
replaces the prototype of result and pollutes Object.prototype for the
entire process.

Add a BLOCKED_KEYS set ({"__proto__", "constructor", "prototype"}) and
skip those keys during iteration, matching the guard already present in
deepMerge (includes.ts) via isBlockedObjectKey.

Adds four tests covering __proto__, constructor, prototype, and nested
__proto__ injection.

Co-authored-by: Clawborn <tianrun.yang103@gmail.com>
2026-02-22 00:25:22 +01:00
Peter Steinberger 780bbbd062 fix: restore CI checks after #23012 (thanks @druide67) 2026-02-22 00:16:15 +01:00
Peter Steinberger 1ef30b82b2 fix(test): guard optional forum topic options 2026-02-22 00:10:07 +01:00
Peter Steinberger 843a037532 fix(test): repair readonly case table typing 2026-02-22 00:10:07 +01:00
Peter Steinberger 8394f0e30e fix(test): resolve outbound envelope case typing 2026-02-22 00:10:07 +01:00
Peter Steinberger 8752203f59 refactor(test): stabilize case tables and readonly helper inputs 2026-02-22 00:10:07 +01:00
Jean-Marc 03586e3d00 feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel

Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.

- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(synology-chat): add pairing, warnings, messaging, agent hints

- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00
Peter Steinberger fbf0c99d7c test(security): simplify repeated audit finding assertions 2026-02-21 23:09:15 +00:00
Peter Steinberger d5cc357737 test(telegram): table-drive sticker and forum-topic cases 2026-02-21 23:07:58 +00:00
Peter Steinberger b1c50cc5c0 test(browser): tighten relay test watchdog timeouts 2026-02-21 23:07:58 +00:00
Peter Steinberger 1534248169 test(telegram): dedupe shared reply/chat-not-found cases 2026-02-21 23:07:58 +00:00
Marcus Widing fa4e4efd92 fix(gateway): restore localhost Control UI pairing when allowInsecureAuth is set (#22996)
* fix(gateway): allow localhost Control UI without device identity when allowInsecureAuth is set

* fix(gateway): pass isLocalClient to evaluateMissingDeviceIdentity

* test: add regression tests for localhost Control UI pairing

* fix(gateway): require pairing for legacy metadata upgrades

* test(gateway): fix legacy metadata e2e ws typing

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-22 00:04:52 +01:00
Peter Steinberger bfe016fa29 fix: clear stale remote discovery endpoints (#21618) (thanks @bmendonca3) 2026-02-22 00:04:36 +01:00
Peter Steinberger 37d5320f6b test: tighten canvas host websocket watchdog timeouts 2026-02-21 23:02:44 +00:00
Peter Steinberger 5164822cd5 test: table-drive status reactions and session key cases 2026-02-21 23:02:44 +00:00
Peter Steinberger 389630fc64 test: table-drive internal hook type-guard cases 2026-02-21 23:02:44 +00:00
Peter Steinberger 4a2ff03f49 test: dedupe channel/web cases and tighten gateway e2e waits 2026-02-21 23:02:44 +00:00
Peter Steinberger c708a18b0f test: table-drive utils and channel-match cases 2026-02-21 23:02:44 +00:00
Peter Steinberger 1b0e021e91 test(telegram): table-drive pairing DM scenarios 2026-02-21 23:02:44 +00:00
Peter Steinberger f3d4045c03 test: matrix owner and timezone system-prompt cases 2026-02-21 23:02:44 +00:00
Peter Steinberger 0e39371dc4 test: dedupe command gating coverage tables 2026-02-21 23:02:44 +00:00
Peter Steinberger b2de8719ad test(gateway): normalize canvas ws watchdog timeouts 2026-02-21 23:02:44 +00:00
Peter Steinberger 7731f28a24 test(ui): matrix chat indicator rendering cases 2026-02-21 23:02:44 +00:00
Peter Steinberger 5fd1d2cadc test(ui): collapse session key/display name fixtures 2026-02-21 23:02:44 +00:00
Peter Steinberger 81a85c19ff test(gateway): tighten e2e timeouts and dedupe invoke checks 2026-02-21 23:02:44 +00:00
Peter Steinberger 1baac3e31d test(ui): consolidate navigation/scroll/format matrices 2026-02-21 23:02:44 +00:00
Peter Steinberger 0bd9f0d4ac fix: enforce strict allowlist across pairing stores (#23017) 2026-02-22 00:00:23 +01:00
Brian Mendonca 617e38cec0 Security/macos: enforce wss for non-loopback direct gateway 2026-02-21 23:57:34 +01:00
Brian Mendonca 8942ac04a8 fix(security): fail closed on unauthenticated discovery routing 2026-02-21 23:57:34 +01:00
Brian Mendonca 21087c5c70 test: fix rebase-introduced tsgo regressions 2026-02-21 23:57:34 +01:00
Brian Mendonca 1357e02cff test: stabilize internal hook error assertions 2026-02-21 23:57:34 +01:00
Brian Mendonca 69cedc7a15 test: make brew fallback assertion windows-safe 2026-02-21 23:57:34 +01:00
Brian Mendonca 6c813bd32b test: avoid asserting auth.json absence for invalid profile creds 2026-02-21 23:57:34 +01:00
Brian Mendonca 4414af977a test: guard inline keyboard fixture against undefined input 2026-02-21 23:57:34 +01:00
Brian Mendonca a186036814 test: fix latest tsgo inference regressions in test suites 2026-02-21 23:57:34 +01:00
Brian Mendonca d12817994f test: stabilize model catalog and auth-sync assertions across runtimes 2026-02-21 23:57:34 +01:00
Brian Mendonca 60c735dd98 test: normalize outbound payload fixture typing 2026-02-21 23:57:34 +01:00
Brian Mendonca 828f4e18e0 test: finish readonly fixture compatibility for CI check 2026-02-21 23:57:34 +01:00
Brian Mendonca c7c047287e test: fix readonly typing regressions in check baseline 2026-02-21 23:57:34 +01:00
Gustavo Madeira Santana 0e1aa77928 chore(tsgo/format): fix CI errors 2026-02-21 17:51:56 -05:00
bmendonca3 6ac89757ba Security/Gateway: harden Control UI static path containment (#21203)
* Security/Gateway: harden Control UI static path containment

* gateway: block control-ui symlink escapes

* CI: retrigger flaky node test lane

---------

Co-authored-by: Brian Mendonca <brianmendonca@Brians-MacBook-Air.local>
2026-02-21 23:47:51 +01:00
Peter Steinberger 71bd15bb42 fix(ssrf): block special-use ipv4 ranges 2026-02-21 23:45:49 +01:00
Gustavo Madeira Santana 2f46308d5a refactor(logging): migrate non-agent internal console calls to subsystem logger (#22964)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b4a5b12422c7a90054dbb7473dd6c4b3e9ca8df5
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-21 17:44:00 -05:00
Peter Steinberger 4ef4aa3c10 refactor(gateway): streamline control-ui secure file serving 2026-02-21 23:36:55 +01:00
Peter Steinberger 0608587bc3 test: streamline config, audit, and qmd coverage 2026-02-21 22:23:43 +00:00
Peter Steinberger a9227f571b test: dedupe telegram formatting and send cases 2026-02-21 22:23:43 +00:00
Peter Steinberger 21b0eac917 test: consolidate infra approval and heartbeat test matrices 2026-02-21 22:23:43 +00:00
Gustavo Madeira Santana 738e2c21dd chore(tests): properly check logging in tests 2026-02-21 17:21:48 -05:00
Peter Steinberger dea154ccae docs(changelog): add control-ui symlink hardening entry 2026-02-21 23:19:35 +01:00
Peter Steinberger b34097f62d fix(security): enforce msteams redirect allowlist checks 2026-02-21 23:18:48 +01:00
Peter Steinberger 1bc5c2a7e9 refactor: unify exec shell parser parity and gateway websocket test helpers 2026-02-21 23:17:12 +01:00
Harry Cui Kepler ffa63173e0 refactor(agents): migrate console.warn/error/info to subsystem logger (#22906)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a806c4cb2700564096ce8980a8d7f839f8a0d388
Co-authored-by: Kepler2024 <166882517+Kepler2024@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-21 17:11:47 -05:00
Peter Steinberger 1257aee6e1 docs(agents): note ghsa severity cvss patch constraint 2026-02-21 23:10:55 +01:00
Peter Steinberger 7c500ff623 fix(security): harden control-ui static path resolution 2026-02-21 23:10:55 +01:00
Peter Steinberger 2028ca4428 fix(macos): unify exec allowlist validation pipeline 2026-02-21 23:09:07 +01:00
Peter Steinberger 61dc7ac679 refactor(msteams,bluebubbles): dedupe inbound media download helpers 2026-02-21 23:08:07 +01:00
Peter Steinberger 73d93dee64 fix: enforce inbound media max-bytes during remote fetch 2026-02-21 23:02:29 +01:00
Peter Steinberger dd41fadcaf fix(macos): enforce path-only exec allowlist patterns 2026-02-21 22:58:40 +01:00
Peter Steinberger 2712883d16 docs(changelog): clarify quoted substitution fix for macOS allowlist 2026-02-21 22:57:53 +01:00
Peter Steinberger 90a378ca3a fix(macos): block quoted shell substitution in allowlist checks 2026-02-21 22:57:53 +01:00
Peter Steinberger 861718e4dc test: group remaining suite cleanups 2026-02-21 21:44:57 +00:00
Peter Steinberger 5c8f0b5a77 test: tighten plugin e2e matrix coverage 2026-02-21 21:44:50 +00:00
Peter Steinberger cc2ff68947 test: optimize gateway infra memory and security coverage 2026-02-21 21:44:50 +00:00
Peter Steinberger 58254b3b57 test: dedupe channel and transport adapters 2026-02-21 21:44:01 +00:00
Peter Steinberger 52ddb6ae18 test: streamline auto-reply and tts suites 2026-02-21 21:44:01 +00:00
Peter Steinberger 5d9e7c942c test: consolidate agent command and config scenarios 2026-02-21 21:44:01 +00:00
Peter Steinberger a1ccd03da0 refactor(cli): share outbound send dependency mapping 2026-02-21 21:40:39 +00:00
Peter Steinberger 84686db850 refactor(cli): dedupe system gateway action handling 2026-02-21 21:40:39 +00:00
Peter Steinberger a04cdc0390 refactor(cli): share update global command runner adapter 2026-02-21 21:40:39 +00:00
Peter Steinberger 944913fc98 refactor(cli): extract shared command-removal and timeout action helpers 2026-02-21 21:40:39 +00:00
Peter Steinberger bb490a4b51 test(cli): expand agent registrar coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger b5a66e7b7e test(cli): add message registrar wiring coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger fecc29d2c8 test(cli): add onboard registrar coverage for daemon flag precedence 2026-02-21 21:40:39 +00:00
Peter Steinberger 3d2f4aea63 test(cli): add setup registrar coverage for wizard dispatch 2026-02-21 21:40:39 +00:00
Peter Steinberger bd8b3cd15e test(cli): add configure registrar coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger 580417685b test(cli): add build-program wiring coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger 1c78ade1a1 test(cli): add program help coverage for root output and version fast-path 2026-02-21 21:40:39 +00:00
Peter Steinberger ceaa43df7a test(cli): add preaction hook coverage for banner/config/plugin gating 2026-02-21 21:40:39 +00:00
Peter Steinberger d5bfbc36d8 test(cli): add program context unit coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger 0f36cbe677 test(cli): add program helper parser coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger ab3fa83f17 test(cli): add action-reparse coverage for fallback argv resolution 2026-02-21 21:40:39 +00:00
Peter Steinberger 5de9419748 test(cli): add status/health/sessions registrar coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger 938fb652b5 fix(cli): honor dashboard no-open and expand maintenance coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger 6de7f9d9b0 test(cli): dedupe config-guard harness and cover invalid-config gates 2026-02-21 21:40:39 +00:00
Peter Steinberger 4503bd0591 test(cli): expand command-registry grouped and subcommand coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger 037da5d8a8 test(cli): extend command option inheritance edge coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger cdb92494d1 test(cli): dedupe inspect runner and cover snapshot/screenshot mode defaults 2026-02-21 21:40:39 +00:00
Peter Steinberger 81ddc98e12 test(cli): dedupe browser state command runner and cover input validation 2026-02-21 21:40:39 +00:00
Peter Steinberger 8581e6b52d test(cli): dedupe route assertions and cover missing-flag guards 2026-02-21 21:40:39 +00:00
Peter Steinberger adedacbfe1 test(cron): dedupe delivery-target whatsapp stubs and cover sessionKey fallback 2026-02-21 21:40:39 +00:00
Peter Steinberger 04a23f45b7 test(channels): dedupe whatsapp heartbeat fixtures and cover recipient sources 2026-02-21 21:40:39 +00:00
Peter Steinberger 42e181dd4b test(web): dedupe inbound cfg fixtures and cover reply/from formatting 2026-02-21 21:40:39 +00:00
Peter Steinberger 2d62685ff0 test(cli): dedupe memory runtime spies and cover json/search fallback flows 2026-02-21 21:40:39 +00:00
Peter Steinberger e46634db9a test(media): dedupe server fixture helpers and cover 404/id validation 2026-02-21 21:40:39 +00:00
Peter Steinberger dc7ec65c8f test(web): dedupe mention assertions and cover diagnostics helpers 2026-02-21 21:40:39 +00:00
Peter Steinberger e2a50228a1 test(browser): dedupe chrome mocks and cover SIGKILL escalation 2026-02-21 21:40:39 +00:00
Peter Steinberger 00ab894feb test(cli): dedupe acp program setup and cover token-file errors 2026-02-21 21:40:39 +00:00
Peter Steinberger 7bfbbd6309 test(version): dedupe fixture setup and cover invalid URL/version metadata 2026-02-21 21:40:39 +00:00
Peter Steinberger bd74d49169 test(cli): dedupe camera temp fixtures and cover clip url error paths 2026-02-21 21:40:39 +00:00
Peter Steinberger 59189750e4 test(browser): dedupe path fixture calls and cover root resolvers 2026-02-21 21:40:39 +00:00
Peter Steinberger 0f9ea0229a test(infra): dedupe install-source fixtures and cover npm pack parsing 2026-02-21 21:40:39 +00:00
Peter Steinberger f9e21d5720 test(infra): dedupe gateway-lock setup and cover guard paths 2026-02-21 21:40:39 +00:00
Peter Steinberger b01335830d test(pairing): dedupe fixture writers and expand store coverage 2026-02-21 21:40:39 +00:00
Peter Steinberger c45ef5f8b5 test(line): dedupe event fixtures and cover room postback routing 2026-02-21 21:40:39 +00:00
Peter Steinberger 1794f42ac0 test(config): dedupe io fixture wiring and cover legacy config-path override 2026-02-21 21:40:39 +00:00
Peter Steinberger d35a8b48f5 test(infra): dedupe archive case setup and cover packed-root multi-dir failure 2026-02-21 21:40:39 +00:00
Peter Steinberger 544a1142b0 test(agents): dedupe skill helper fixtures and cover empty-body rendering 2026-02-21 21:40:39 +00:00
Peter Steinberger 822688dc13 test(infra): dedupe store temp fixtures and cover json5 voicewake sanitization 2026-02-21 21:40:39 +00:00
Peter Steinberger a418c6db06 test(agents): dedupe agent-path fixtures and cover env override precedence 2026-02-21 21:40:39 +00:00
Peter Steinberger 6fd31fc0b0 test(browser): dedupe invalid-path assertions and cover blank path rejection 2026-02-21 21:40:39 +00:00
Peter Steinberger 2000dcdcd0 test(memory): dedupe temp-dir lifecycle hooks and cover overlapping path dedupe 2026-02-21 21:40:39 +00:00
Peter Steinberger 6051dc10ff test(scripts): dedupe a2ui temp fixture and cover skip-missing env path 2026-02-21 21:40:39 +00:00
Peter Steinberger d6c2fd5453 test(web): dedupe logout fixture setup and cover non-legacy oauth removal 2026-02-21 21:40:39 +00:00
Peter Steinberger bdfb979940 test(cli): dedupe camera fetch stubs and cover empty-body download rejection 2026-02-21 21:40:39 +00:00
Peter Steinberger 31a0449f69 test(core): dedupe temp dirs in utils tests and cover lid lookup error fallback 2026-02-21 21:40:39 +00:00
Peter Steinberger c93fc3786c test(infra): dedupe brew fixtures and cover explicit brew file precedence 2026-02-21 21:40:39 +00:00
Peter Steinberger 2042a69211 test(infra): dedupe dotenv fixture setup and cover fallback-only load 2026-02-21 21:40:39 +00:00
Peter Steinberger c394c5fa99 test(daemon): dedupe schtasks install fixture and cover empty env omission 2026-02-21 21:40:39 +00:00
Peter Steinberger d015dc9216 test(cron): dedupe run-log temp fixtures and cover invalid line filtering 2026-02-21 21:40:39 +00:00
Peter Steinberger 7036352d94 test(config): dedupe temp roots and cover legacy state-dir fallback 2026-02-21 21:40:39 +00:00
Peter Steinberger 5d61afb362 test(commands): dedupe signal install extract fixture and cover zip extract 2026-02-21 21:40:39 +00:00
Peter Steinberger 3274a1b804 test(gateway): dedupe control-ui fixture setup and cover query asset 404 2026-02-21 21:40:39 +00:00
Peter Steinberger 8f1b467646 test(agents): dedupe exec preflight fixtures and cover quoted-path skip 2026-02-21 21:40:39 +00:00
Peter Steinberger 8f11868cc2 test(gateway): dedupe boot workspace setup and cover boot failures 2026-02-21 21:40:38 +00:00
Peter Steinberger 0e49eec056 test(commands): dedupe auth-sync fixture and cover invalid profile handling 2026-02-21 21:40:38 +00:00
Peter Steinberger e978297c28 test(agents): dedupe workspace template temp roots and cover fallback resolution 2026-02-21 21:40:38 +00:00
Peter Steinberger c481b22245 test(reply): reuse compaction fixture setup and cover numeric fallback defaults 2026-02-21 21:40:38 +00:00
Peter Steinberger 1bbeedfab2 test(infra): dedupe heartbeat ghost reminder temp/mocks setup 2026-02-21 21:40:38 +00:00
Peter Steinberger ac6c344d9b test(browser): dedupe fixture lifecycle and cover directory-path rejection 2026-02-21 21:40:38 +00:00
Peter Steinberger 626d8e9f62 test(web): dedupe temp dir setup in web auto-reply utils tests 2026-02-21 21:40:38 +00:00
Val Alexander b703ea3675 fix: prevent compaction "prompt too long" errors (#22921)
* includes: prompt overhead in compaction safeguard calculation.

Subtracts SUMMARIZATION_OVERHEAD_TOKENS from maxChunkTokens in both the main summarization path and the dropped-messages summarization path.

This ensures the chunk budget leaves room for the prompt overhead that generateSummary wraps around each chunk.

* adds: budget for overhead tokens to use an effectiveMax instead of maxTokens naïvely.

- Added `SUMMARIZATION_OVERHEAD_TOKENS = 4096` — a budget for the tokens that `generateSummary` adds on top of the serialized conversation (system prompt, `<conversation>` tags, summarization instructions, `<previous-summary>` block, and reasoning: "high" thinking budget).
- `chunkMessagesByMaxTokens` now divides `maxTokens` by `SAFETY_MARGIN` (1.2) before comparing against estimated token counts. Previously, the safety margin was only used in `computeAdaptiveChunkRatio` and `isOversizedForSummary` but not in the actual chunking loop — so chunks could be built that fit the estimated budget but exceeded the real budget once the API tokenized them properly.
2026-02-21 14:42:18 -06:00
Onur Solmaz ac633366ce docs: add Onur Solmaz to contributors (#22890) 2026-02-21 21:00:26 +01:00
Peter Steinberger 518dbbf4c6 test: avoid template-literal temp path in runner fixture 2026-02-21 20:49:38 +01:00
Peter Steinberger 302fa03f41 fix(test): skip test-utils files in temp path guard 2026-02-21 20:48:52 +01:00
Peter Steinberger 48ddb1cc81 fix(ci): stabilize install smoke in docker 2026-02-21 20:39:34 +01:00
Peter Steinberger 549549f6a0 fix(ci): sync plugin versions and harden install smoke 2026-02-21 20:18:37 +01:00
Peter Steinberger a20c773251 test(media): dedupe auto-e2e temp/env setup and cover no-binary path 2026-02-21 19:17:01 +00:00
Peter Steinberger b889a5d516 test(cli): dedupe temp dirs in camera tests and cover non-ok url responses 2026-02-21 19:17:01 +00:00
Peter Steinberger 0ecb07e6d1 test(cli): dedupe acp secret file setup and cover password flag collisions 2026-02-21 19:17:01 +00:00
Peter Steinberger 4f835c4c0d test(media): dedupe temp roots and cover directory attachment rejection 2026-02-21 19:17:01 +00:00
Peter Steinberger 9ebfc99c1b refactor(test): dedupe temp media fixture setup in apply e2e 2026-02-21 19:17:01 +00:00
Peter Steinberger 0a207b9860 refactor(test): share temp workspace helper in compact skill path tests 2026-02-21 19:16:22 +00:00
Peter Steinberger 324922f804 refactor(test): dedupe temp dir lifecycle in agents skills directory e2e 2026-02-21 19:16:22 +00:00
Peter Steinberger b3c7fd6c69 refactor(test): dedupe temp dirs and skill writer in snapshot e2e 2026-02-21 19:16:22 +00:00
Peter Steinberger 85c768d3d2 refactor(test): dedupe temp workspace setup in skills load entries e2e 2026-02-21 19:16:22 +00:00
Peter Steinberger 0401762144 refactor(test): dedupe temp root setup in identity avatar e2e 2026-02-21 19:16:22 +00:00
Peter Steinberger 9ead79937e refactor(test): dedupe temp session path setup in file repair e2e 2026-02-21 19:16:22 +00:00
Peter Steinberger 70fdab6e95 test(agents): add coverage for shared skill writer helper 2026-02-21 19:16:21 +00:00
Peter Steinberger 0876fbde19 refactor(test): reuse shared skill writer in skills e2e 2026-02-21 19:16:21 +00:00
Peter Steinberger f086245afe refactor(test): reuse shared skill writer in sandbox and bundled tests 2026-02-21 19:16:21 +00:00
Peter Steinberger 96ef00ec38 refactor(test): drop redundant env snapshots in skill download suites 2026-02-21 19:16:21 +00:00
Peter Steinberger 603e28648b refactor(test): centralize temp workspace env handling for skill install tests 2026-02-21 19:16:21 +00:00
Peter Steinberger 61817c90e7 refactor(test): share temp workspace helper for skill download suites 2026-02-21 19:16:21 +00:00
Peter Steinberger a814cce359 refactor(test): share temp command dir helper in shell utils e2e 2026-02-21 19:16:21 +00:00
Peter Steinberger c240104dc3 refactor(test): snapshot gateway auth env in security audit tests 2026-02-21 19:16:21 +00:00
Peter Steinberger e5aa04d432 refactor(test): snapshot daemon cli env in coverage e2e 2026-02-21 19:16:21 +00:00
Peter Steinberger 3fd7dc5046 refactor(test): snapshot shell/path env in bash tools e2e 2026-02-21 19:16:21 +00:00
Peter Steinberger 272bf2d8bc refactor(test): dedupe env override assertions in skills e2e 2026-02-21 19:16:21 +00:00
Peter Steinberger d982893490 refactor(test): use env helper for web auto-reply timezone test 2026-02-21 19:13:47 +00:00
Peter Steinberger 7ba09e414f refactor(test): snapshot env in shell utils e2e 2026-02-21 19:13:47 +00:00
Peter Steinberger c3e1c82871 refactor(test): snapshot bundled hooks env in loader tests 2026-02-21 19:13:47 +00:00
Peter Steinberger 5e607ae1eb refactor(test): snapshot deprecated auth profile env in e2e 2026-02-21 19:13:47 +00:00
Peter Steinberger 5dc1b5a8db refactor(test): reuse env helper in workspace skill sync gating 2026-02-21 19:13:47 +00:00
Peter Steinberger c0706b7799 refactor(test): reuse env helper in workspace skill status tests 2026-02-21 19:13:47 +00:00
Peter Steinberger cf371fde6d refactor(test): use env helper in workspace skills prompt gating 2026-02-21 19:13:47 +00:00
Peter Steinberger 8745964142 refactor(test): snapshot PATH env in bash tools exec path e2e 2026-02-21 19:13:47 +00:00
Peter Steinberger af66e3103a test(agents): cover bundled skills env override and dedupe setup 2026-02-21 19:13:47 +00:00
Peter Steinberger ae06dbb794 refactor(test): snapshot tar.bz2 skills install env 2026-02-21 19:13:47 +00:00
Peter Steinberger b44aa5b1f7 refactor(test): snapshot skills install state dir env 2026-02-21 19:13:47 +00:00
Peter Steinberger 884166c7af refactor(test): snapshot telegram action env in e2e suite 2026-02-21 19:13:47 +00:00
Peter Steinberger 1fd88af219 test(commands): stabilize message e2e env and gateway mock 2026-02-21 19:13:47 +00:00
Peter Steinberger 1b585b2959 refactor(test): snapshot tailscale test env per case 2026-02-21 19:13:47 +00:00
Peter Steinberger 2a0ea7cb97 test(tui): cover gateway auth fallbacks and dedupe env setup 2026-02-21 19:13:47 +00:00
Peter Steinberger ec8288e9b8 refactor(test): reuse env helper in gateway status e2e 2026-02-21 19:13:47 +00:00
Peter Steinberger 807968e4df refactor(test): replace manual PATH restore with env helpers 2026-02-21 19:13:47 +00:00
Peter Steinberger 01f42a0372 refactor(test): share media audio fixture across runner tests 2026-02-21 19:13:47 +00:00
Peter Steinberger 194ebd9e30 refactor(test): dedupe env setup in envelope and config tests 2026-02-21 19:13:47 +00:00
Peter Steinberger 50489fb2d4 refactor(test): use env helper for telegram TZ override 2026-02-21 19:13:47 +00:00
Peter Steinberger fc43a16d43 refactor(test): replace ad-hoc env restore blocks with helpers 2026-02-21 19:13:47 +00:00
Peter Steinberger 63488eb981 refactor(test): dedupe telegram token env handling in tests 2026-02-21 19:13:47 +00:00
Peter Steinberger bfa59bd22e refactor(test): collapse gateway e2e env snapshots 2026-02-21 19:13:47 +00:00
Peter Steinberger dda9e9f094 refactor(test): snapshot onboarding gateway env via helper 2026-02-21 19:13:47 +00:00
Peter Steinberger bd9d3e2f87 refactor(test): reuse env helper in update cli tests 2026-02-21 19:13:47 +00:00
Peter Steinberger b2ed54f600 refactor(test): reuse env helper in onboarding provider auth e2e 2026-02-21 19:13:47 +00:00
Peter Steinberger 2d7d00ef8e refactor(test): streamline env setup in auth and gateway e2e 2026-02-21 19:13:47 +00:00
Peter Steinberger a410dad602 refactor(test): simplify env setup in safe bins and skills status 2026-02-21 19:13:46 +00:00
Peter Steinberger 8fd8988ff7 refactor(test): reuse env helper in gateway tool e2e 2026-02-21 19:13:46 +00:00
Peter Steinberger bc037dfe01 refactor(test): dedupe provider env setup in model config tests 2026-02-21 19:13:46 +00:00
Peter Steinberger c41d1070b7 refactor(test): use env helper in agent paths e2e 2026-02-21 19:13:46 +00:00
Peter Steinberger e588e3cc20 refactor(test): standardize env helpers across suites 2026-02-21 19:13:46 +00:00
Peter Steinberger ae70bf4dca refactor(test): simplify env scoping in exec and usage tests 2026-02-21 19:13:46 +00:00
Peter Steinberger aff272ec35 refactor(test): reuse env helper in models auth sync 2026-02-21 19:13:46 +00:00
Peter Steinberger 992b7e5577 refactor(test): use env snapshots in setup hooks 2026-02-21 19:13:46 +00:00
Peter Steinberger 7724abeee0 refactor(test): dedupe env setup across suites 2026-02-21 19:13:46 +00:00
Peter Steinberger f903603722 docs(changelog): keep 2026.2.22 split from 2026.2.21 2026-02-21 20:10:51 +01:00
Sean McLellan 00b98a368a fix: flatten nested anyOf/oneOf in Gemini schema cleaning (openclaw#22825) thanks @Oceanswave
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Oceanswave <760674+Oceanswave@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-21 13:09:42 -06:00
Peter Steinberger f9108120c2 fix(gateway): strip inline directive tags from displayed text 2026-02-21 20:08:55 +01:00
Peter Steinberger 4540790cb6 refactor(bluebubbles): share dm/group access policy checks 2026-02-21 20:08:33 +01:00
Peter Steinberger c3af00bddb docs(changelog): split 2026.2.21 release entries 2026-02-21 20:06:57 +01:00
Peter Steinberger 22940b7b98 refactor(discord): split allowlist resolution flow 2026-02-21 20:01:21 +01:00
Peter Steinberger 25e89cc863 fix(security): harden shell env fallback 2026-02-21 20:01:08 +01:00
Peter Steinberger 817905f3a0 docs: document thread-bound subagent sessions and remove plan 2026-02-21 19:59:55 +01:00
Peter Steinberger 51c0893673 refactor(security): remove unused empty allowlist mode 2026-02-21 19:57:36 +01:00
Peter Steinberger 2ba6de7eaa refactor(security): make empty allowlist behavior explicit 2026-02-21 19:54:59 +01:00
Peter Steinberger ed960ba4eb refactor(security): centralize path guard helpers 2026-02-21 19:54:26 +01:00
Peter Steinberger 6ffca36284 fix(config): add shared streaming resolver module 2026-02-21 19:53:42 +01:00
Peter Steinberger 2c14b0cf4c refactor(config): unify streaming config across channels 2026-02-21 19:53:42 +01:00
Peter Steinberger 747bb581b3 fix(discord): canonicalize resolved allowlists to ids 2026-02-21 19:53:29 +01:00
Nimrod Gutman 3ed71d6f76 fix: update changelog for ios talk tts prefetch (#22833) (thanks @ngutman) 2026-02-21 20:52:05 +02:00
Nimrod Gutman d6353cc54b fix(ios): suppress expected speech cancellation errors 2026-02-21 20:52:05 +02:00
Nimrod Gutman 8a661e30c9 fix(ios): prefetch talk tts segments 2026-02-21 20:52:05 +02:00
Peter Steinberger 9632b9bcf0 fix(security): fail closed parsed chat allowlist 2026-02-21 19:51:36 +01:00
Simone Macario 09d5f508b1 fix(cron): persist delivered flag in job state to surface delivery failures (openclaw#19174) thanks @simonemacario
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: simonemacario <2116609+simonemacario@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-21 12:47:29 -06:00
Peter Steinberger 51149fcaf1 refactor(gateway): extract connect and role policy logic 2026-02-21 19:47:22 +01:00
Peter Steinberger f97c45c5b5 fix(security): warn on Discord name-based allowlists in audit 2026-02-21 19:45:17 +01:00
Peter Steinberger 4b226b74f5 fix(security): block zip symlink escape in archive extraction 2026-02-21 19:42:33 +01:00
Peter Steinberger ddcb2d79b1 fix(gateway): block node role when device identity is missing 2026-02-21 19:34:13 +01:00
Peter Steinberger 764b1f2932 refactor: simplify relay runtime state 2026-02-21 19:31:30 +01:00
Peter Steinberger e371da38aa fix(macos): consolidate exec approval evaluation 2026-02-21 19:30:35 +01:00
Peter Steinberger 9fc6c8b713 fix: hide synthetic untrusted metadata in chat history 2026-02-21 19:26:04 +01:00
Peter Steinberger afa22acc4a fix: harden extension relay auth token flow 2026-02-21 19:24:42 +01:00
Peter Steinberger 89aad7b922 refactor: tighten safe-bin policy model and docs parity 2026-02-21 19:24:23 +01:00
Peter Steinberger c730d4dd72 docs: clarify non-default scope for safeBins sort fix 2026-02-21 19:18:51 +01:00
Peter Steinberger 4c1dd9d068 fix(security): harden macos rawCommand allowlist resolution 2026-02-21 19:17:56 +01:00
niceysam 5e423b596c fix: remove false-positive billing error rewrite on normal assistant text (openclaw#17834) thanks @niceysam
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: niceysam <256747835+niceysam@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-21 12:17:39 -06:00
Peter Steinberger 57fbbaebca fix: block safeBins sort --compress-program bypass 2026-02-21 19:13:53 +01:00
Peter Steinberger bdfb97afad chore: prep 2026.2.22 unreleased and publish new npm plugins 2026-02-21 19:05:35 +01:00
Thorfinn efdec39254 fix: correct MiniMax M2.5 pricing (was ~50x too high) (openclaw#22755) thanks @miloudbelarebia
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: miloudbelarebia <136994453+miloudbelarebia@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-02-21 11:26:48 -06:00
Peter Steinberger 35a57bc940 fix: gate doctor oauth-dir repair by channel config 2026-02-21 18:08:15 +01:00
Peter Steinberger 905e355f65 fix: verify gateway restart health after daemon restart 2026-02-21 18:02:14 +01:00
2415 changed files with 135345 additions and 68362 deletions
+2
View File
@@ -1 +1,3 @@
* text=auto eol=lf
CLAUDE.md -text
src/gateway/server-methods/CLAUDE.md -text
+58 -3
View File
@@ -317,7 +317,9 @@ jobs:
- name: Check docs
run: pnpm check:docs
secrets:
skills-python:
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
@@ -330,10 +332,39 @@ jobs:
with:
python-version: "3.12"
- name: Install detect-secrets
- name: Install Python tooling
run: |
python -m pip install --upgrade pip
python -m pip install detect-secrets==1.5.0
python -m pip install pytest ruff pyyaml
- name: Lint Python skill scripts
run: python -m ruff check skills
- name: Test skill Python scripts
run: python -m pytest -q skills
secrets:
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install pre-commit
run: |
python -m pip install --upgrade pip
python -m pip install pre-commit detect-secrets==1.5.0
- name: Detect secrets
run: |
@@ -342,6 +373,30 @@ jobs:
exit 1
fi
- name: Detect committed private keys
run: pre-commit run --all-files detect-private-key
- name: Audit changed GitHub workflows with zizmor
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "push" ]; then
BASE="${{ github.event.before }}"
else
BASE="${{ github.event.pull_request.base.sha }}"
fi
mapfile -t workflow_files < <(git diff --name-only "$BASE" HEAD -- '.github/workflows/*.yml' '.github/workflows/*.yaml')
if [ "${#workflow_files[@]}" -eq 0 ]; then
echo "No workflow changes detected; skipping zizmor."
exit 0
fi
pre-commit run zizmor --files "${workflow_files[@]}"
- name: Audit production dependencies
run: pre-commit run --all-files pnpm-audit-prod
checks-windows:
needs: [docs-scope, changed-scope, build-artifacts, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
+15
View File
@@ -17,6 +17,8 @@ __pycache__/
ui/src/ui/__screenshots__/
ui/playwright-report/
ui/test-results/
packages/dashboard-next/.next/
packages/dashboard-next/out/
# Mise configuration files
mise.toml
@@ -96,6 +98,19 @@ package-lock.json
.agents/
.agents
.agent/
skills-lock.json
# Local iOS signing overrides
apps/ios/LocalSigning.xcconfig
# Generated protocol schema (produced via pnpm protocol:gen)
dist/protocol.schema.json
.ant-colony/
# Eclipse
**/.project
**/.classpath
**/.settings/
**/.gradle/
# Synthing
**/.stfolder/
+13
View File
@@ -0,0 +1,13 @@
# Canonical contributor identity mappings for cherry-picked commits.
bmendonca3 <208517100+bmendonca3@users.noreply.github.com> <brianmendonca@Brians-MacBook-Air.local>
hcl <7755017+hclsys@users.noreply.github.com> <chenglunhu@gmail.com>
Glucksberg <80581902+Glucksberg@users.noreply.github.com> <markuscontasul@gmail.com>
JackyWay <53031570+JackyWay@users.noreply.github.com> <jackybbc@gmail.com>
Marcus Castro <7562095+mcaxtr@users.noreply.github.com> <mcaxtr@gmail.com>
Marc Gratch <2238658+mgratch@users.noreply.github.com> <me@marcgratch.com>
Peter Machona <7957943+chilu18@users.noreply.github.com> <chilu.machona@icloud.com>
Ben Marvell <92585+easternbloc@users.noreply.github.com> <ben@marvell.consulting>
zerone0x <39543393+zerone0x@users.noreply.github.com> <hi@trine.dev>
Marco Di Dionisio <3519682+marcodd23@users.noreply.github.com> <m.didionisio23@gmail.com>
mujiannan <46643837+mujiannan@users.noreply.github.com> <shennan@mujiannan.com>
Santhanakrishnan <239082898+bitfoundry-ai@users.noreply.github.com> <noreply@anthropic.com>
+1 -1
View File
@@ -1 +1 @@
allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext,node-pty,@lydell/node-pty,@matrix-org/matrix-sdk-crypto-nodejs
# pnpm build-script allowlist lives in package.json -> pnpm.onlyBuiltDependencies.
+2
View File
@@ -11,12 +11,14 @@
"ignorePatterns": [
"apps/",
"assets/",
"CLAUDE.md",
"docker-compose.yml",
"dist/",
"docs/_layouts/",
"node_modules/",
"patches/",
"pnpm-lock.yaml/",
"src/gateway/server-methods/CLAUDE.md",
"src/auto-reply/reply/export-html/",
"Swabble/",
"vendor/",
+27 -1
View File
@@ -18,6 +18,8 @@ repos:
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
- id: detect-private-key
exclude: '(^|/)(\.secrets\.baseline$|\.detect-secrets\.cfg$|\.pre-commit-config\.yaml$|apps/ios/fastlane/Fastfile$|.*\.test\.ts$)'
# Secret detection (same as CI)
- repo: https://github.com/Yelp/detect-secrets
@@ -45,7 +47,6 @@ repos:
- '=== "string"'
- --exclude-lines
- 'typeof remote\?\.password === "string"'
# Shell script linting
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
@@ -69,9 +70,34 @@ repos:
args: [--persona=regular, --min-severity=medium, --min-confidence=medium]
exclude: "^(vendor/|Swabble/)"
# Python checks for skills scripts
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.1
hooks:
- id: ruff
files: "^skills/.*\\.py$"
args: [--config, pyproject.toml]
- repo: local
hooks:
- id: skills-python-tests
name: skills python tests
entry: pytest -q skills
language: python
additional_dependencies: [pytest>=8, <9]
pass_filenames: false
files: "^skills/.*\\.py$"
# Project checks (same commands as CI)
- repo: local
hooks:
# pnpm audit --prod --audit-level=high
- id: pnpm-audit-prod
name: pnpm-audit-prod
entry: pnpm audit --prod --audit-level=high
language: system
pass_filenames: false
# oxlint --type-aware src test
- id: oxlint
name: oxlint
+15
View File
@@ -2,6 +2,9 @@
- Repo: https://github.com/openclaw/openclaw
- GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n".
- GitHub comment footgun: never use `gh issue/pr comment -b "..."` when body contains backticks or shell chars. Always use single-quoted heredoc (`-F - <<'EOF'`) so no command substitution/escaping corruption.
- GitHub linking footgun: dont wrap issue/PR refs like `#24643` in backticks when you want auto-linking. Use plain `#24643` (optionally add full URL).
- Security advisory analysis: before triage/severity decisions, read `SECURITY.md` to align with OpenClaw's trust model and design boundaries.
## Project Structure & Module Organization
@@ -83,6 +86,7 @@
- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`.
- beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app).
- beta naming: prefer `-beta.N`; do not mint new `-1/-2` betas. Legacy `vYYYY.M.D-<patch>` and `vYYYY.M.D.beta.N` remain recognized.
- dev: moving head on `main` (no tag; git checkout main).
## Testing Guidelines
@@ -91,6 +95,7 @@
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
- Do not set test workers above 16; tried already.
- If local Vitest runs cause memory pressure (common on non-Mac-Studio hosts), use `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` for land/gate runs.
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- Full kit + whats covered: `docs/testing.md`.
- Changelog: user-facing changes only; no internal/meta notes (version alignment, appcast reminders, release process).
@@ -116,6 +121,15 @@
- If `git branch -d/-D <branch>` is policy-blocked, delete the local ref directly: `git update-ref -d refs/heads/<branch>`.
- Bulk PR close/reopen safety: if a close action would affect more than 5 PRs, first ask for explicit user confirmation with the exact PR count and target scope/query.
## GitHub Search (`gh`)
- Prefer targeted keyword search before proposing new work or duplicating fixes.
- Use `--repo openclaw/openclaw` + `--match title,body` first; add `--match comments` when triaging follow-up threads.
- PRs: `gh search prs --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"`
- Issues: `gh search issues --repo openclaw/openclaw --match title,body --limit 50 -- "auto-update"`
- Structured output example:
`gh search issues --repo openclaw/openclaw --match title,body --limit 50 --json number,title,state,url,updatedAt -- "auto update" --jq '.[] | "\(.number) | \(.state) | \(.title) | \(.url)"'`
## Security & Configuration Tips
- Web provider stores creds at `~/.openclaw/credentials/`; rerun `openclaw login` if logged out.
@@ -134,6 +148,7 @@
`gh pr list -R "$fork" --state open` (must be empty)
- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings)
- Build patch JSON via jq: `jq -n --rawfile desc /tmp/ghsa.desc.md '{summary,severity,description:$desc,vulnerabilities:[...]}' > /tmp/ghsa.patch.json`
- GHSA API footgun: cannot set `severity` and `cvss_vector_string` in the same PATCH; do separate calls.
- Patch + publish: `gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> --input /tmp/ghsa.patch.json` (publish = include `"state":"published"`; no `/publish` endpoint)
- If publish fails (HTTP 422): missing `severity`/`description`/`vulnerabilities[]`, or private fork has open PRs
- Verify: re-fetch; ensure `state=published`, `published_at` set; `jq -r .description | rg '\\\\n'` returns nothing
+393 -1
View File
@@ -2,7 +2,390 @@
Docs: https://docs.openclaw.ai
## 2026.2.21 (Unreleased)
## Unreleased
### Breaking
- **BREAKING:** non-loopback Control UI now requires explicit `gateway.controlUi.allowedOrigins` (full origins). Startup fails closed when missing unless `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` is set to use Host-header origin fallback mode.
- **BREAKING:** channel `allowFrom` matching is now ID-only by default across channels that previously allowed mutable name/tag/email principal matching. If you relied on direct mutable-name matching, migrate allowlists to stable IDs (recommended) or explicitly opt back in with `channels.<channel>.dangerouslyAllowNameMatching=true` (break-glass compatibility mode). (#24907)
### Changes
- Subagents/Sessions: add `agents.defaults.subagents.runTimeoutSeconds` so `sessions_spawn` can inherit a configurable default timeout when the tool call omits `runTimeoutSeconds` (unset remains `0`, meaning no timeout). (#24594) Thanks @mitchmcalister.
- Config/Kilo Gateway: Kilo provider flow now surfaces an updated list of models. (#24921) thanks @gumadeiras.
### Fixes
- Security/iOS deep links: require local confirmation (or trusted key) before forwarding `openclaw://agent` requests from iOS to gateway `agent.request`, and strip unkeyed delivery-routing fields to reduce exfiltration risk. This ships in the next npm release. Thanks @GCXWLP for reporting.
- Security/Export session HTML: escape raw HTML markdown tokens in the exported session viewer, harden tree/header metadata rendering against HTML injection, and sanitize image data-URL MIME types in export output to prevent stored XSS when opening exported HTML files. This ships in the next npm release. Thanks @allsmog for reporting.
- Security/Session export: harden exported HTML image rendering against data-URL attribute injection by validating image MIME/base64 fields, rejecting malformed base64 input in media ingestion paths, and dropping invalid tool-image payloads.
- Security/Image tool: enforce `tools.fs.workspaceOnly` for sandboxed `image` path resolution so mounted out-of-workspace paths are blocked before media bytes are loaded/sent to vision providers. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Sandbox: enforce `tools.exec.applyPatch.workspaceOnly` and `tools.fs.workspaceOnly` for `apply_patch` in sandbox-mounted paths so writes/deletes cannot escape the workspace boundary via mounts like `/agent` unless explicitly opted out (`tools.exec.applyPatch.workspaceOnly=false`). This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Commands: enforce sender-only matching for `commands.allowFrom` by blocking conversation-shaped `From` identities (`channel:`, `group:`, `thread:`, `@g.us`) while preserving direct-message fallback when sender fields are missing. Ships in the next npm release. Thanks @jiseoung.
- Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution.
- Security/Channels: unify dangerous name-matching policy checks (`dangerouslyAllowNameMatching`) across core and extension channels, share mutable-allowlist detectors between `openclaw doctor` and `openclaw security audit`, and scan all configured accounts (not only the default account) in channel security audit findings.
- Security/Exec approvals: bind `host=node` approvals to explicit `nodeId`, reject cross-node replay of approved `system.run` requests, and include the target node in approval prompts. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Exec approvals: restore two-phase approval registration + wait-decision handling for gateway/node exec paths, requiring approval IDs to be registered before returning `approval-pending` and honoring server-assigned approval IDs during wait resolution to prevent orphaned `/approve` flows and immediate-return races (`ask:on-miss`). This ships in the next npm release. Thanks @vitalyis for reporting.
- Security/Exec approvals: enforce canonical wrapper execution plans across allowlist analysis and runtime execution (node host + gateway host), fail closed on semantic `env` wrapper usage, and reject unknown short safe-bin flags to prevent `env -S/--split-string` interpretation-mismatch bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Exec approvals: recognize `busybox`/`toybox` shell applets in wrapper analysis and allow-always persistence, persist inner executables instead of multiplexer wrapper binaries, and fail closed when multiplexer unwrapping is unsafe to prevent allow-always bypasses. This ships in the next npm release. Thanks @jiseoung for reporting.
- Security/Exec approvals: for non-default setups that enable `autoAllowSkills`, require pathless invocations plus trusted resolved-path matches so `./<skill-bin>`/absolute-path basename collisions cannot satisfy skill auto-allow checks under allowlist mode. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. This ships in the next npm release. Thanks @tdjackey and @jiseoung for reporting.
- Security/Shell env fallback: remove trusted-prefix shell-path fallback and only trust login shells explicitly registered in `/etc/shells`, defaulting to `/bin/sh` when `SHELL` is not registered. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Voice Call: harden Twilio webhook replay handling by preserving provider event IDs through normalization, adding bounded replay dedupe, and enforcing per-call turn-token matching for call-state transitions. This ships in the next npm release. Thanks @jiseoung for reporting.
- Telegram/Media SSRF: keep RFC2544 benchmark range (`198.18.0.0/15`) blocked by default, add an explicit SSRF-policy opt-in for Telegram media downloads, and keep other channels/URL fetch paths blocked. (#24982) Thanks @stakeswky.
- WhatsApp/Auto-reply: send only final payloads to WhatsApp, suppress tool/block payload leakage (reasoning/thinking), and force block streaming off for WhatsApp dispatch so final-only delivery cannot cause silent turns. (#24962) Thanks @SidQin-cyber.
- Channels/Reasoning: suppress reasoning/thinking payload segments in the shared channel dispatch path so non-Telegram channels (including WhatsApp and Web) no longer emit internal reasoning blocks as user-visible replies. (#24991) Thanks @stakeswky.
- Discord/Reasoning: suppress reasoning/thinking-only payload blocks from Discord delivery output. (#24969)
- Auto-reply/Abort shortcuts: expand standalone stop phrases (`stop openclaw`, `stop action`, `stop run`, `stop agent`, `please stop`, and related variants) and accept trailing punctuation (for example `STOP OPENCLAW!!!`) so emergency stop messages are caught more reliably.
- WhatsApp/DM routing: only update main-session last-route state when DM traffic is bound to the main session, preserving isolated `dmScope` routing. (#24949) Thanks @kevinWangSheng.
- WhatsApp/Access control: honor `selfChatMode` in inbound access-control checks. (#24738)
- WhatsApp/Logging: redact outbound recipient identifiers in WhatsApp outbound + heartbeat logs and remove message/poll preview text from those log lines. (#24980) Thanks @coygeek.
- Discord/Threading: recover missing thread parent IDs by refetching thread metadata before resolving parent channel context. (#24897) Thanks @z-x-yang.
- Web UI/i18n: load and hydrate saved locale translations during startup so non-English sessions apply immediately without manual toggling. (#24795) Thanks @chilu18.
- Gateway/Browser control: load `src/browser/server.js` during browser-control startup so the control listener starts reliably when browser control is enabled. (#23974) Thanks @ieaves.
- Browser/Chrome relay: harden debugger detach handling during full-page navigation with bounded auto-reattach retries and better cancellation behavior for user/devtools detaches. (#19766) Thanks @nishantkabra77.
- Browser/Chrome extension options: validate relay `/json/version` payload shape and content type (not just HTTP status) to detect wrong-port gateway checks, and clarify relay port derivation for custom gateway ports (`gateway + 3`). (#22252) Thanks @krizpoon.
- Status/Pairing recovery: show explicit pairing-approval command hints (including requestId when safe) when gateway probe failures report pairing-required closures. (#24771) Thanks @markmusson.
- Onboarding/Custom providers: raise verification probe token budgets for OpenAI and Anthropic compatibility checks to avoid false negatives on strict provider defaults. (#24743) Thanks @Glucksberg.
- Auth/OAuth: classify missing OAuth scopes as auth failures for clearer remediation and retry behavior. (#24761)
- Providers/OpenRouter: when thinking is explicitly off, avoid injecting `reasoning.effort` so reasoning-required models can use provider defaults instead of failing request validation. (#24863) Thanks @DevSecTim.
- Sessions/Reasoning: persist `reasoningLevel: "off"` explicitly instead of deleting it so session overrides survive patch/update flows. (#24406, #24559)
- Cron/Isolated sessions: use full prompt mode for isolated cron runs so skills/extensions are available during cron execution. (#24944)
- Synology Chat/Webhooks: deregister stale webhook routes before re-registering on channel restart to prevent duplicate route handling. (#24971)
- Plugins/Config: use plugin manifest `id` (instead of npm package name) for config entry keys so plugin settings stay bound correctly. (#24796)
- Plugins/Config schema: support legacy plugin schemas without `toJSONSchema()` by falling back to permissive object schema generation. (#24933) Thanks @pandego.
- Gateway/Prompt builder: safely extract text from mixed content arrays when assembling prompts to avoid malformed prompt payloads. (#24946)
- Gateway/Slug generation: respect agent-level model config in slug generation flows. (#24776)
- Agents/Workspace paths: strip null bytes and guard undefined `.trim()` calls for workspace-path handling to avoid `ENOTDIR`/`TypeError` crashes. (#24876, #24875)
- Agents/Tool warnings: suppress `sessions_send` relay errors from chat-facing warning payloads to avoid leaking transient inter-session transport failures. (#24740) Thanks @Glucksberg.
- Sessions/Model overrides: keep stored sub-agent model overrides when `agents.defaults.models` is empty (allow-any mode) instead of resetting to defaults. (#21088) Thanks @Slats24.
- Subagents/Registry: prune orphaned restored runs (missing child session/sessionId) before retry/announce resume to prevent zombie entries and stale completion retries, and clarify status output to report bootstrap-file presence semantics. (#24244) Thanks @HeMuling.
- Subagents/Announce queue: add exponential backoff when queue-drain delivery fails to reduce retry storms. (#24783)
- Doctor/UX: suppress the redundant "Run doctor --fix" hint when already in fix mode with no changes. (#24666)
- Doctor/Nix: skip false-positive permission warnings for Nix store symlinks in state-integrity checks. (#24901)
- Update/Systemd: back up an existing systemd unit before overwriting it during update flows. (#24350, #24937)
- Install/Global detection: resolve symlinks when detecting pnpm/bun global install paths. (#24744)
- Infra/Windows TOCTOU: handle Windows `dev=0` edge cases in same-file identity checks. (#24939)
- Exec/Bash tools: clamp poll sleep duration to non-negative values in process polling loops. (#24889)
## 2026.2.23 (Unreleased)
### Changes
- Providers/Kilo Gateway: add first-class `kilocode` provider support (auth, onboarding, implicit provider detection, model defaults, transcript/cache-ttl handling, and docs), with default model `kilocode/anthropic/claude-opus-4.6`. (#20212) Thanks @jrf0110 and @markijbema.
- Providers/Vercel AI Gateway: accept Claude shorthand model refs (`vercel-ai-gateway/claude-*`) by normalizing to canonical Anthropic-routed model ids. (#23985) Thanks @sallyom, @markbooch, and @vincentkoc.
- Docs/Prompt caching: add a dedicated prompt-caching reference covering `cacheRetention`, per-agent `params` merge precedence, Bedrock/OpenRouter behavior, and cache-ttl + heartbeat tuning. Thanks @svenssonaxel.
- Gateway/HTTP security headers: add optional `gateway.http.securityHeaders.strictTransportSecurity` support to emit `Strict-Transport-Security` for direct HTTPS deployments, with runtime wiring, validation, tests, and hardening docs.
- Sessions/Cron: harden session maintenance with `openclaw sessions cleanup`, per-agent store targeting, disk-budget controls (`session.maintenance.maxDiskBytes` / `highWaterBytes`), and safer transcript/archive cleanup + run-log retention behavior. (#24753) thanks @gumadeiras.
- Tools/web_search: add `provider: "kimi"` (Moonshot) support with key/config schema wiring and a corrected two-step `$web_search` tool flow that echoes tool results before final synthesis, including citation extraction from search results. (#16616, #18822) Thanks @adshine.
- Media understanding/Video: add a native Moonshot video provider and include Moonshot in auto video key detection, plus refactor video execution to honor `entry/config/provider` baseUrl+header precedence (matching audio behavior). (#12063) Thanks @xiaoyaner0201.
- Agents/Config: support per-agent `params` overrides merged on top of model defaults (including `cacheRetention`) so mixed-traffic agents can tune cache behavior independently. (#17470, #17112) Thanks @rrenamed.
- Agents/Bootstrap: cache bootstrap file snapshots per session key and clear them on session reset/delete, reducing prompt-cache invalidations from in-session `AGENTS.md`/`MEMORY.md` writes. (#22220) Thanks @anisoptera.
### Breaking
- **BREAKING:** browser SSRF policy now defaults to trusted-network mode (`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork=true` when unset), and canonical config uses `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` instead of `browser.ssrfPolicy.allowPrivateNetwork`. `openclaw doctor --fix` migrates the legacy key automatically.
### Fixes
- Security/Config: redact sensitive-looking dynamic catchall keys in `config.get` snapshots (for example `env.*` and `skills.entries.*.env.*`) and preserve round-trip restore behavior for those redacted sentinels. Thanks @merc1305.
- Tests/Vitest: tier local parallel worker defaults by host memory, keep gateway serial by default on non-high-memory hosts, and document a low-profile fallback command for memory-constrained land/gate runs to prevent local OOMs. (#24719) Thanks @ngutman.
- WhatsApp/Group policy: fix `groupAllowFrom` sender filtering when `groupPolicy: "allowlist"` is set without explicit `groups` — previously all group messages were blocked even for allowlisted senders. (#24670)
- Agents/Context pruning: extend `cache-ttl` eligibility to Moonshot/Kimi and ZAI/GLM providers (including OpenRouter model refs), so `contextPruning.mode: "cache-ttl"` is no longer silently skipped for those sessions. (#24497) Thanks @lailoo.
- Doctor/Memory: query gateway-side default-agent memory embedding readiness during `openclaw doctor` (instead of inferring from generic gateway health), and warn when the gateway memory probe is unavailable or not ready while keeping `openclaw configure` remediation guidance. (#22327) thanks @therk.
- Sessions/Store: canonicalize inbound mixed-case session keys for metadata and route updates, and migrate legacy case-variant entries to a single lowercase key to prevent duplicate sessions and missing TUI/WebUI history. (#9561) Thanks @hillghost86.
- Telegram/Reactions: soft-fail reaction action errors (policy/token/emoji/API), accept snake_case `message_id`, and fallback to inbound message-id context when explicit `messageId` is omitted so DM reactions stay stable without regeneration loops. (#20236, #21001) Thanks @PeterShanxin and @vincentkoc.
- Telegram/Polling: scope persisted polling offsets to bot identity and reuse a single awaited runner-stop path on abort/retry, preventing cross-token offset bleed and overlapping pollers during restart/error recovery. (#10850, #11347) Thanks @talhaorak, @anooprdawar, and @vincentkoc.
- Telegram/Reasoning: when `/reasoning off` is active, suppress reasoning-only delivery segments and block raw fallback resend of suppressed `Reasoning:`/`<think>` text, preventing internal reasoning leakage in legacy sessions while preserving answer delivery. (#24626, #24518)
- Agents/Reasoning: when model-default thinking is active (for example `thinking=low`), keep auto-reasoning disabled unless explicitly enabled, preventing `Reasoning:` thinking-block leakage in channel replies. (#24335, #24290) thanks @Kay-051.
- Agents/Reasoning: avoid classifying provider reasoning-required errors as context overflows so these failures no longer trigger compaction-style overflow recovery. (#24593) Thanks @vincentkoc.
- Agents/Models: codify `agents.defaults.model` / `agents.defaults.imageModel` config-boundary input as `string | {primary,fallbacks}`, split explicit vs effective model resolution, and fix `models status --agent` source attribution so defaults-inherited agents are labeled as `defaults` while runtime selection still honors defaults fallback. (#24210) thanks @bianbiandashen.
- Agents/Compaction: pass `agentDir` into manual `/compact` command runs so compaction auth/profile resolution stays scoped to the active agent. (#24133) thanks @Glucksberg.
- Agents/Compaction: pass model metadata through the embedded runtime so safeguard summarization can run when `ctx.model` is unavailable, avoiding repeated `"Summary unavailable due to context limits"` fallback summaries. (#3479) Thanks @battman21, @hanxiao and @vincentkoc.
- Agents/Compaction: cancel safeguard compaction when summary generation cannot run (missing model/API key or summarization failure), preserving history instead of truncating to fallback `"Summary unavailable"` text. (#10711) Thanks @DukeDeSouth and @vincentkoc.
- Agents/Tools: make `session_status` read transcript-derived usage mid-turn and tail-read session logs for cache-aware context reporting without full-log scans. (#22387) Thanks @1ucian.
- Agents/Overflow: detect additional provider context-overflow error shapes (including `input length` + `max_tokens` exceed-context variants) so failures route through compaction/recovery paths instead of leaking raw provider errors to users. (#9951) Thanks @echoVic and @Glucksberg.
- Agents/Overflow: add Chinese context-overflow pattern detection in `isContextOverflowError` so localized provider errors route through overflow recovery paths. (#22855) Thanks @Clawborn.
- Agents/Failover: treat HTTP 502/503/504 errors as failover-eligible transient timeouts so fallback chains can switch providers/models during upstream outages instead of retrying the same failing target. (#20999) Thanks @taw0002 and @vincentkoc.
- Auto-reply/Inbound metadata: hide direct-chat `message_id`/`message_id_full` and sender metadata only from normalized chat type (not sender-id sentinels), preserving group metadata visibility and preventing sender-id spoofed direct-mode classification. (#24373) thanks @jd316.
- Auto-reply/Inbound metadata: move dynamic inbound `flags` (reply/forward/thread/history) from system metadata to user-context conversation info, preventing turn-by-turn prompt-cache invalidation from flag toggles. (#21785) Thanks @aidiffuser.
- Auto-reply/Sessions: remove auth-key labels from `/new` and `/reset` confirmation messages so session reset notices never expose API key prefixes or env-key labels in chat output. (#24384, #24409) Thanks @Clawborn.
- Slack/Group policy: move Slack account `groupPolicy` defaulting to provider-level schema defaults so multi-account configs inherit top-level `channels.slack.groupPolicy` instead of silently overriding inheritance with per-account `allowlist`. (#17579) Thanks @ZetiMente.
- Providers/Anthropic: skip `context-1m-*` beta injection for OAuth/subscription tokens (`sk-ant-oat-*`) while preserving OAuth-required betas, avoiding Anthropic 401 auth failures when `params.context1m` is enabled. (#10647, #20354) Thanks @ClumsyWizardHands and @dcruver.
- Providers/DashScope: mark DashScope-compatible `openai-completions` endpoints as `supportsDeveloperRole=false` so OpenClaw sends `system` instead of unsupported `developer` role on Qwen/DashScope APIs. (#19130) Thanks @Putzhuawa and @vincentkoc.
- Providers/Bedrock: disable prompt-cache retention for non-Anthropic Bedrock models so Nova/Mistral requests do not send unsupported cache metadata. (#20866) Thanks @pierreeurope.
- Providers/Bedrock: apply Anthropic-Claude cacheRetention defaults and runtime pass-through for `amazon-bedrock/*anthropic.claude*` model refs, while keeping non-Anthropic Bedrock models excluded. (#22303) Thanks @snese.
- Providers/OpenRouter: remove conflicting top-level `reasoning_effort` when injecting nested `reasoning.effort`, preventing OpenRouter 400 payload-validation failures for reasoning models. (#24120) thanks @tenequm.
- Providers/Groq: avoid classifying Groq TPM limit errors as context overflow so throttling paths no longer trigger overflow recovery logic. (#16176) Thanks @dddabtc.
- Gateway/WS: close repeated post-handshake `unauthorized role:*` request floods per connection and sample duplicate rejection logs, preventing a single misbehaving client from degrading gateway responsiveness. (#20168) Thanks @acy103, @vibecodooor, and @vincentkoc.
- Gateway/Restart: treat child listener PIDs as owned by the service runtime PID during restart health checks to avoid false stale-process kills and restart timeouts on launchd/systemd. (#24696) Thanks @gumadeiras.
- Config/Write: apply `unsetPaths` with immutable path-copy updates so config writes never mutate caller-provided objects, and harden `openclaw config get/set/unset` path traversal by rejecting prototype-key segments and inherited-property traversal. (#24134) thanks @frankekn.
- Channels/WhatsApp: accept `channels.whatsapp.enabled` in config validation to match built-in channel auto-enable behavior, preventing `Unrecognized key: "enabled"` failures during channel setup. (#24263)
- Security/Exec: detect obfuscated commands before exec allowlist decisions and require explicit approval for obfuscation patterns. (#8592) Thanks @CornBrother0x and @vincentkoc.
- Security/ACP: harden ACP client permission auto-approval to require trusted core tool IDs, ignore untrusted `toolCall.kind` hints, and scope `read` auto-approval to the active working directory so unknown tool names and out-of-scope file reads always prompt. This ships in the next npm release. Thanks @nedlir for reporting.
- Security/Skills: escape user-controlled prompt, filename, and output-path values in `openai-image-gen` HTML gallery generation to prevent stored XSS in generated `index.html` output. (#12538) Thanks @CornBrother0x.
- Security/Skills: harden `skill-creator` packaging by skipping symlink entries and rejecting files whose resolved paths escape the selected skill root. (#24260, #16959) Thanks @CornBrother0x and @vincentkoc.
- Security/OTEL: redact sensitive values (API keys, tokens, credential fields) from diagnostics-otel log bodies, log attributes, and error/reason span fields before OTLP export. (#12542) Thanks @brandonwise.
- Security/CI: add pre-commit security hook coverage for private-key detection and production dependency auditing, and enforce those checks in CI alongside baseline secret scanning. Thanks @vincentkoc.
- Skills/Python: harden skill script packaging and validation edge cases (self-including `.skill` outputs, CRLF frontmatter parsing, strict `--days` validation, and safer image file loading), with expanded Python regression coverage. Thanks @vincentkoc.
- Skills/Python: add CI + pre-commit linting (`ruff`) and pytest discovery coverage for Python scripts/tests under `skills/`, including package test execution from repo root. Thanks @vincentkoc.
## 2026.2.22
### Changes
- Control UI/Agents: make the Tools panel data-driven from runtime `tools.catalog`, add per-tool provenance labels (`core` / `plugin:<id>` + optional marker), and keep a static fallback list when the runtime catalog is unavailable.
- Web Search/Gemini: add grounded Gemini provider support with provider auto-detection and config/docs updates. (#13075, #13074) Thanks @akoscz.
- Control UI/Cron: add full web cron edit parity (including clone and richer validation/help text), plus all-jobs run history with pagination/search/sort/multi-filter controls and improved cron page layout for cleaner scheduling and failure triage workflows.
- Provider/Mistral: add support for the Mistral provider, including memory embeddings and voice support. (#23845) Thanks @vincentkoc.
- Update/Core: add an optional built-in auto-updater for package installs (`update.auto.*`), default-off, with stable rollout delay+jitter and beta hourly cadence.
- CLI/Update: add `openclaw update --dry-run` to preview channel/tag/target/restart actions without mutating config, installing, syncing plugins, or restarting.
- Config/UI: add tag-aware settings filtering and broaden config labels/help copy so fields are easier to discover and understand in the dashboard config screen.
- Channels/Synology Chat: add a native Synology Chat channel plugin with webhook ingress, direct-message routing, outbound send/media support, per-account config, and DM policy controls. (#23012)
- iOS/Talk: prefetch TTS segments and suppress expected speech-cancellation errors for smoother talk playback. (#22833) Thanks @ngutman.
- Memory/FTS: add Spanish and Portuguese stop-word filtering for query expansion in FTS-only search mode, improving conversational recall for both languages. Thanks @vincentkoc.
- Memory/FTS: add Japanese-aware query expansion tokenization and stop-word filtering (including mixed-script terms like ASCII + katakana) for FTS-only search mode. Thanks @vincentkoc.
- Memory/FTS: add Korean stop-word filtering and particle-aware keyword extraction (including mixed Korean/English stems) for query expansion in FTS-only search mode. (#18899) Thanks @ruypang.
- Memory/FTS: add Arabic stop-word filtering for query expansion in FTS-only search mode to reduce conversational filler in Arabic memory searches. Thanks @vincentkoc.
- Discord/Allowlist: canonicalize resolved Discord allowlist names to IDs and split resolution flow for clearer fail-closed behavior.
- Channels/Config: unify channel preview streaming config handling with a shared resolver and canonical migration path.
- Gateway/Auth: unify call/probe/status/auth credential-source precedence on shared resolver helpers, with table-driven parity coverage across gateway entrypoints.
- Gateway/Auth: refactor gateway credential resolution and websocket auth handshake paths to use shared typed auth contexts, including explicit `auth.deviceToken` support in connect frames and tests.
- Skills: remove bundled `food-order` skill from this repo; manage/install it from ClawHub instead.
- Docs/Subagents: make thread-bound session guidance channel-first instead of Discord-specific, and list thread-supporting channels explicitly. (#23589) Thanks @osolmaz.
### Breaking
- **BREAKING:** removed Google Antigravity provider support and the bundled `google-antigravity-auth` plugin. Existing `google-antigravity/*` model/profile configs no longer work; migrate to `google-gemini-cli` or other supported providers.
- **BREAKING:** tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require `/verbose on` or `/verbose full`.
- **BREAKING:** CLI local onboarding now sets `session.dmScope` to `per-channel-peer` by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set `session.dmScope` to `main`. (#23468) Thanks @bmendonca3.
- **BREAKING:** unify channel preview-streaming config to `channels.<channel>.streaming` with enum values `off | partial | block | progress`, and move Slack native stream toggle to `channels.slack.nativeStreaming`. Legacy keys (`streamMode`, Slack boolean `streaming`) are still read and migrated by `openclaw doctor --fix`, but canonical saved config/docs now use the unified names.
- **BREAKING:** remove legacy Gateway device-auth signature `v1`. Device-auth clients must now sign `v2` payloads with the per-connection `connect.challenge` nonce and send `device.nonce`; nonce-less connects are rejected.
### Fixes
- Sessions/Resilience: ignore invalid persisted `sessionFile` metadata and fall back to the derived safe transcript path instead of aborting session resolution for handlers and tooling. (#16061) Thanks @haoyifan and @vincentkoc.
- Sessions/Paths: resolve symlinked state-dir aliases during transcript-path validation while preserving safe cross-agent/state-root compatibility for valid `agents/<id>/sessions/**` paths. (#18593) Thanks @EpaL and @vincentkoc.
- Agents/Compaction: count auto-compactions only after a non-retry `auto_compaction_end`, keeping session `compactionCount` aligned to completed compactions.
- Security/CLI: redact sensitive values in `openclaw config get` output before printing config paths, preventing credential leakage to terminal output/history. (#13683) Thanks @SleuthCo.
- Agents/Moonshot: force `supportsDeveloperRole=false` for Moonshot-compatible `openai-completions` models (provider `moonshot` and Moonshot base URLs), so initial runs no longer send unsupported `developer` roles that trigger `ROLE_UNSPECIFIED` errors. (#21060, #22194) Thanks @ShengFuC.
- Agents/Kimi: classify Moonshot `Your request exceeded model token limit` failures as context overflows so auto-compaction and user-facing overflow recovery trigger correctly instead of surfacing raw invalid-request errors. (#9562) Thanks @danilofalcao.
- Providers/Moonshot: mark Kimi K2.5 as image-capable in implicit + onboarding model definitions, and refresh stale explicit provider capability fields (`input`/`reasoning`/context limits) from implicit catalogs so existing configs pick up Moonshot vision support without manual model rewrites. (#13135, #4459) Thanks @manikv12.
- Agents/Transcript: enable consecutive-user turn merging for strict non-OpenAI `openai-completions` providers (for example Moonshot/Kimi), reducing `roles must alternate` ordering failures on OpenAI-compatible endpoints while preserving current OpenRouter/Opencode behavior. (#7693)
- Install/Discord Voice: make `@discordjs/opus` an optional dependency so `openclaw` install/update no longer hard-fails when native Opus builds fail, while keeping `opusscript` as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) Thanks @jeadland, @Sheetaa, and @Breakyman.
- Docker/Setup: precreate `$OPENCLAW_CONFIG_DIR/identity` during `docker-setup.sh` so CLI commands that need device identity (for example `devices list`) avoid `EACCES ... /home/node/.openclaw/identity` failures on restrictive bind mounts. (#23948) Thanks @ackson-beep.
- Exec/Background: stop applying the default exec timeout to background sessions (`background: true` or explicit `yieldMs`) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303)
- Slack/Threading: sessions: keep parent-session forking and thread-history context active beyond first turn by removing first-turn-only gates in session init, thread-history fetch, and reply prompt context injection. (#23843, #23090) Thanks @vincentkoc and @Taskle.
- Slack/Threading: respect `replyToMode` when Slack auto-populates top-level `thread_ts`, and ignore inline `replyToId` directive tags when `replyToMode` is `off` so thread forcing stays disabled unless explicitly configured. (#23839, #23320, #23513) Thanks @vincentkoc and @dorukardahan.
- Slack/Extension: forward `message read` `threadId` to `readMessages` and use delivery-context `threadId` as outbound `thread_ts` fallback so extension replies/reads stay in the correct Slack thread. (#22216, #22485, #23836) Thanks @vincentkoc, @lan17 and @dorukardahan.
- Slack/Upload: resolve bare user IDs (U-prefix) to DM channel IDs via `conversations.open` before calling `files.uploadV2`, which rejects non-channel IDs. `chat.postMessage` tolerates user IDs directly, but `files.uploadV2``completeUploadExternal` validates `channel_id` against `^[CGDZ][A-Z0-9]{8,}$`, causing `invalid_arguments` when agents reply with media to DM conversations.
- Webchat/Chat: apply assistant `final` payload messages directly to chat state so sent turns render without waiting for a full history refresh cycle. (#14928) Thanks @BradGroux.
- Webchat/Chat: for out-of-band final events (for example tool-call side runs), append provided final assistant payloads directly instead of forcing a transient history reset. (#11139) Thanks @AkshayNavle.
- Webchat/Performance: reload `chat.history` after final events only when the final payload lacks a renderable assistant message, avoiding expensive full-history refreshes on normal turns. (#20588) Thanks @amzzzzzzz.
- Webchat/Sessions: preserve external session routing metadata when internal `chat.send` turns run under `webchat`, so explicit channel-keyed sessions (for example Telegram) no longer get rewritten to `webchat` and misroute follow-up delivery. (#23258) Thanks @binary64.
- Webchat/Sessions: preserve existing session `label` across `/new` and `/reset` rollovers so reset sessions remain discoverable in session history lists. (#23755) Thanks @ThunderStormer.
- Gateway/Chat UI: strip inline reply/audio directive tags from non-streaming final webchat broadcasts (including `chat.inject`) while preserving empty-string message content when tags are the entire reply. (#23298) Thanks @SidQin-cyber.
- Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:<id>]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces.
- Gateway/Chat UI: sanitize non-streaming final `chat.send`/`chat.inject` payload text with the same envelope/untrusted-context stripping used by `chat.history`, preventing `<<<EXTERNAL_UNTRUSTED_CONTENT...>>>` wrapper markup from rendering in Control UI chat. (#24012) Thanks @mittelaltergouda.
- Telegram/Media: send a user-facing Telegram reply when media download fails (non-size errors) instead of silently dropping the message.
- Telegram/Webhook: keep webhook monitors alive until gateway abort signals fire, preventing false channel exits and immediate webhook auto-restart loops.
- Telegram/Polling: retry recoverable setup-time network failures in monitor startup and await runner teardown before retry to avoid overlapping polling sessions.
- Telegram/Polling: clear Telegram webhooks (`deleteWebhook`) before starting long-poll `getUpdates`, including retry handling for transient cleanup failures.
- Telegram/Webhook: add `channels.telegram.webhookPort` config support and pass it through plugin startup wiring to the monitor listener.
- Browser/Extension Relay: refactor the MV3 worker to preserve debugger attachments across relay drops, auto-reconnect with bounded backoff+jitter, persist and rehydrate attached tab state via `chrome.storage.session`, recover from `target_closed` navigation detaches, guard stale socket handlers, enforce per-tab operation locks and per-request timeouts, and add lifecycle keepalive/badge refresh hooks (`alarms`, `webNavigation`). (#15099, #6175, #8468, #9807)
- Browser/Relay: treat extension websocket as connected only when `OPEN`, allow reconnect when a stale `CLOSING/CLOSED` extension socket lingers, and guard stale socket message/close handlers so late events cannot clear active relay state; includes regression coverage for live-duplicate `409` rejection and immediate reconnect-after-close races. (#15099, #18698, #20688)
- Browser/Remote CDP: extend stale-target recovery so `ensureTabAvailable()` now reuses the sole available tab for remote CDP profiles (same behavior as extension profiles) while preserving strict `tab not found` errors when multiple tabs exist; includes remote-profile regression tests. (#15989)
- Gateway/Pairing: treat `operator.admin` as satisfying other `operator.*` scope checks during device-auth verification so local CLI/TUI sessions stop entering pairing-required loops for pairing/approval-scoped commands. (#22062, #22193, #21191) Thanks @Botaccess, @jhartshorn, and @ctbritt.
- Gateway/Pairing: auto-approve loopback `scope-upgrade` pairing requests (including device-token reconnects) so local clients do not disconnect on pairing-required scope elevation. (#23708) Thanks @widingmarcus-cyber.
- Gateway/Scopes: include `operator.read` and `operator.write` in default operator connect scope bundles across CLI, Control UI, and macOS clients so write-scoped announce/sub-agent follow-up calls no longer hit `pairing required` disconnects on loopback gateways. (#22582) thanks @YuzuruS.
- Gateway/Pairing: treat operator.admin pairing tokens as satisfying operator.write requests so legacy devices stop looping through scope-upgrade prompts introduced in 2026.2.19. (#23125, #23006) Thanks @vignesh07.
- Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli.
- Gateway/Lock: use optional gateway-port reachability as a primary stale-lock liveness signal (and wire gateway run-loop lock acquisition to the resolved port), reducing false "already running" lockouts after unclean exits. (#23760) Thanks @Operative-001.
- Delivery/Queue: quarantine queue entries immediately on known permanent delivery errors (for example invalid recipients or missing conversation references) by moving them to `failed/` instead of retrying on every restart. (#23794) Thanks @aldoeliacim.
- Cron/Status: split execution outcome (`lastRunStatus`) from delivery outcome (`lastDeliveryStatus`) in persisted cron state, finished events, and run history so failed/unknown announcement delivery is visible without conflating it with run errors.
- Cron/Delivery: route text-only announce jobs with explicit thread/topic targets through direct outbound delivery so forum/thread destinations do not get dropped by intermediary announce turns. (#23841) Thanks @AndrewArto.
- Cron: honor `cron.maxConcurrentRuns` in the timer loop so due jobs can execute up to the configured parallelism instead of always running serially. (#11595) Thanks @Takhoffman.
- Cron/Run: enforce the same per-job timeout guard for manual `cron.run` executions as timer-driven runs, including abort propagation for isolated agent jobs, so forced runs cannot wedge indefinitely. (#23704) Thanks @tkuehnl.
- Cron/Run: persist the manual-run `runningAtMs` marker before releasing the cron lock so overlapping timer ticks cannot start the same job concurrently.
- Cron/Startup: enforce per-job timeout guards for startup catch-up replay runs so missed isolated jobs cannot hang indefinitely during gateway boot recovery.
- Cron/Main session: honor abort/timeout signals while retrying `wakeMode=now` heartbeat contention loops so main-target cron runs stop promptly instead of waiting through the full busy-retry window.
- Cron/Schedule: for `every` jobs, prefer `lastRunAtMs + everyMs` when still in the future after restarts, then fall back to anchor scheduling for catch-up windows, so NEXT timing matches the last successful cadence. (#22895) Thanks @SidQin-cyber.
- Cron/Service: execute manual `cron.run` jobs outside the cron lock (while still persisting started/finished state atomically) so `cron.list` and `cron.status` remain responsive during long forced runs. (#23628) Thanks @dsgraves.
- Cron/Timer: keep a watchdog recheck timer armed while `onTimer` is actively executing so the scheduler continues polling even if a due-run tick stalls for an extended period. (#23628) Thanks @dsgraves.
- Cron/Run log: clean up settled per-path run-log write queue entries so long-running cron uptime does not retain stale promise bookkeeping in memory.
- Cron/Run log: harden `cron.runs` run-log path resolution by rejecting path-separator `id`/`jobId` inputs and enforcing reads within the per-cron `runs/` directory.
- Cron/Announce: when announce delivery target resolution fails (for example multiple configured channels with no explicit target), skip injecting fallback `Cron (error): ...` into the main session so runs fail cleanly without accidental last-route sends. (#24074)
- Cron/Telegram: validate cron `delivery.to` with shared Telegram target parsing and resolve legacy `@username`/`t.me` targets to numeric IDs at send-time for deterministic delivery target writeback. (#21930) Thanks @kesor.
- Telegram/Targets: normalize unprefixed topic-qualified targets through the shared parse/normalize path so valid `@channel:topic:<id>` and `<chatId>:topic:<id>` routes are recognized again. (#24166) Thanks @obviyus.
- Cron/Isolation: force fresh session IDs for isolated cron runs so `sessionTarget="isolated"` executions never reuse prior run context. (#23470) Thanks @echoVic.
- Plugins/Install: strip `workspace:*` devDependency entries from copied plugin manifests before `npm install --omit=dev`, preventing `EUNSUPPORTEDPROTOCOL` install failures for npm-published channel plugins (including Feishu and MS Teams).
- Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603)
- Config/Channels: auto-enable built-in channels by writing `channels.<id>.enabled=true` (not `plugins.entries.<id>`), and stop adding built-ins to `plugins.allow`, preventing `plugins.entries.telegram: plugin not found` validation failures.
- Config/Channels: when `plugins.allow` is active, auto-enable/enable flows now also allowlist configured built-in channels so `channels.<id>.enabled=true` cannot remain blocked by restrictive plugin allowlists.
- Plugins/Discovery: ignore scanned extension backup/disabled directory patterns (for example `.backup-*`, `.bak`, `.disabled*`) and move updater backup directories under `.openclaw-install-backups`, preventing duplicate plugin-id collisions from archived copies.
- Plugins/CLI: make `openclaw plugins enable` and plugin install/link flows update allowlists via shared plugin-enable policy so enabled plugins are not left disabled by allowlist mismatch. (#23190) Thanks @downwind7clawd-ctrl.
- Security/Voice Call: harden media stream WebSocket handling against pre-auth idle-connection DoS by adding strict pre-start timeouts, pending/per-IP connection limits, and total connection caps for streaming endpoints. This ships in the next npm release. Thanks @jiseoung for reporting.
- Security/Sessions: redact sensitive token patterns from `sessions_history` tool output and surface `contentRedacted` metadata when masking occurs. (#16928) Thanks @aether-ai-agent.
- Security/Exec: stop trusting `PATH`-derived directories for safe-bin allowlist checks, add explicit `tools.exec.safeBinTrustedDirs`, and pin safe-bin shell execution to resolved absolute executable paths to prevent binary-shadowing approval bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Elevated: match `tools.elevated.allowFrom` against sender identities only (not recipient `ctx.To`), closing a recipient-token bypass for `/elevated` authorization. This ships in the next npm release. Thanks @jiseoung for reporting.
- Security/Feishu: enforce ID-only allowlist matching for DM/group sender authorization, normalize Feishu ID prefixes during checks, and ignore mutable display names so display-name collisions cannot satisfy allowlist entries. This ships in the next npm release. Thanks @jiseoung for reporting.
- Security/Group policy: harden `channels.*.groups.*.toolsBySender` matching by requiring explicit sender-key types (`id:`, `e164:`, `username:`, `name:`), preventing cross-identifier collisions across mutable/display-name fields while keeping legacy untyped keys on a deprecated ID-only path. This ships in the next npm release. Thanks @jiseoung for reporting.
- Channels/Group policy: fail closed when `groupPolicy: "allowlist"` is set without explicit `groups`, honor account-level `groupPolicy` overrides, and enforce `groupPolicy: "disabled"` as a hard group block. (#22215) Thanks @etereo.
- Telegram/Discord extensions: propagate trusted `mediaLocalRoots` through extension outbound `sendMedia` options so extension direct-send media paths honor agent-scoped local-media allowlists. (#20029, #21903, #23227)
- Agents/Exec: honor explicit agent context when resolving `tools.exec` defaults for runs with opaque/non-agent session keys, so per-agent `host/security/ask` policies are applied consistently. (#11832)
- CLI/Sessions: resolve implicit session-store path templates with the configured default agent ID so named-agent setups do not silently read/write stale `agent:main` session/auth stores. (#22685) Thanks @sene1337.
- Doctor/Security: add an explicit warning that `approvals.exec.enabled=false` disables forwarding only, while enforcement remains driven by host-local `exec-approvals.json` policy. (#15047)
- Sandbox/Docker: default sandbox container user to the workspace owner `uid:gid` when `agents.*.sandbox.docker.user` is unset, fixing non-root gateway file-tool permissions under capability-dropped containers. (#20979)
- Plugins/Media sandbox: propagate trusted `mediaLocalRoots` through plugin action dispatch (including Discord/Telegram action adapters) so plugin send paths enforce the same agent-scoped local-media sandbox roots as core outbound sends. (#20258, #22718)
- Agents/Workspace guard: map sandbox container-workdir file-tool paths (for example `/workspace/...` and `file:///workspace/...`) to host workspace roots before workspace-only validation, preventing false `Path escapes sandbox root` rejections for sandbox file tools. (#9560)
- Gateway/Exec approvals: expire approval requests immediately when no approval-capable gateway clients are connected and no forwarding targets are available, avoiding delayed approvals after restarts/offline approver windows. (#22144)
- Security/Exec approvals: when approving wrapper commands with allow-always in allowlist mode, persist inner executable paths for known dispatch wrappers (`env`, `nice`, `nohup`, `stdbuf`, `timeout`) and fail closed (no persisted entry) when wrapper unwrapping is not safe, preventing wrapper-path approval bypasses. Thanks @tdjackey for reporting.
- Node/macOS exec host: default headless macOS node `system.run` to local execution and only route through the companion app when `OPENCLAW_NODE_EXEC_HOST=app` is explicitly set, avoiding companion-app filesystem namespace mismatches during exec. (#23547)
- Sandbox/Media: map container workspace paths (`/workspace/...` and `file:///workspace/...`) back to the host sandbox root for outbound media validation, preventing false deny errors for sandbox-generated local media. (#23083) Thanks @echo931.
- Sandbox/Docker: apply custom bind mounts after workspace mounts and prioritize bind-source resolution on overlapping paths, so explicit workspace binds are no longer ignored. (#22669) Thanks @tasaankaeris.
- Exec approvals/Forwarding: restore Discord text forwarding when component approvals are not configured, and carry request snapshots through resolve events so resolved notices still forward after cache misses/restarts. (#22988) Thanks @bubmiller.
- Control UI/WebSocket: stop and clear the browser gateway client on UI teardown so remounts cannot leave orphan websocket clients that create duplicate active connections. (#23422) Thanks @floatinggball-design.
- Control UI/WebSocket: send a stable per-tab `instanceId` in websocket connect frames so reconnect cycles keep a consistent client identity for diagnostics and presence tracking. (#23616) Thanks @zq58855371-ui.
- Config/Memory: allow `"mistral"` in `agents.defaults.memorySearch.provider` and `agents.defaults.memorySearch.fallback` schema validation. (#14934) Thanks @ThomsenDrake.
- Feishu/Commands: in group chats, command authorization now falls back to top-level `channels.feishu.allowFrom` when per-group `allowFrom` is not set, so `/command` no longer gets blocked by an unintended empty allowlist. (#23756)
- Dev tooling: prevent `CLAUDE.md` symlink target regressions by excluding CLAUDE symlink sentinels from `oxfmt` and marking them `-text` in `.gitattributes`, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc.
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633)
- Hooks/Loader: avoid redundant hook-module recompilation on gateway restart by skipping cache-busting for bundled hooks and using stable file metadata keys (`mtime+size`) for mutable workspace/managed/plugin hook imports. (#16953) Thanks @mudrii.
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
- Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed.
- Installer/Smoke tests: remove legacy `OPENCLAW_USE_GUM` overrides from docker install-smoke runs so tests exercise installer auto TTY detection behavior directly.
- Providers/OpenRouter: allow pass-through OpenRouter and Opencode model IDs in live model filtering so custom routed model IDs are treated as modern refs. (#14312) Thanks @Joly0.
- Providers/OpenRouter: default reasoning to enabled when the selected model advertises `reasoning: true` and no session/directive override is set. (#22513) Thanks @zwffff.
- Providers/OpenRouter: map `/think` levels to `reasoning.effort` in embedded runs while preserving explicit `reasoning.max_tokens` payloads. (#17236) Thanks @robbyczgw-cla.
- Providers/OpenRouter: preserve stored session provider when model IDs are vendor-prefixed (for example, `anthropic/...`) so follow-up turns do not incorrectly route to direct provider APIs. (#22753) Thanks @dndodson.
- Providers/OpenRouter: preserve the required `openrouter/` prefix for OpenRouter-native model IDs during model-ref normalization. (#12942) Thanks @omair445.
- Providers/OpenRouter: pass through provider routing parameters from model params.provider to OpenRouter request payloads for provider selection controls. (#17148) Thanks @carrotRakko.
- Providers/OpenRouter: preserve model allowlist entries containing OpenRouter preset paths (for example `openrouter/@preset/...`) by treating `/model ...@profile` auth-profile parsing as a suffix-only override. (#14120) Thanks @NotMainstream.
- Cron/Auth: propagate auth-profile resolution to isolated cron sessions so provider API keys are resolved the same way as main sessions, fixing 401 errors when using providers configured via auth-profiles. (#20689) Thanks @lailoo.
- Cron/Follow-up: pass resolved `agentDir` through isolated cron and queued follow-up embedded runs so auth/profile lookups stay scoped to the correct agent directory. (#22845) Thanks @seilk.
- Agents/Media: route tool-result `MEDIA:` extraction through shared parser validation so malformed prose like `MEDIA:-prefixed ...` is no longer treated as a local file path (prevents Telegram ENOENT tool-error overrides). (#18780) Thanks @HOYALIM.
- Logging: cap single log-file size with `logging.maxFileBytes` (default 500 MB) and suppress additional writes after cap hit to prevent disk exhaustion from repeated error storms.
- Memory/Remote HTTP: centralize remote memory HTTP calls behind a shared guarded helper (`withRemoteHttpResponse`) so embeddings and batch flows use one request/release path.
- Memory/Embeddings: apply configured remote-base host pinning (`allowedHostnames`) across OpenAI/Voyage/Gemini embedding requests to keep private/self-hosted endpoints working without cross-host drift. (#18198) Thanks @ianpcook.
- Memory/Batch: route OpenAI/Voyage/Gemini batch upload/create/status/download requests through the same guarded HTTP path for consistent SSRF policy enforcement.
- Memory/Index: detect memory source-set changes (for example enabling `sessions` after an existing memory-only index) and trigger a full reindex so existing session transcripts are indexed without requiring `--force`. (#17576) Thanks @TarsAI-Agent.
- Memory/Embeddings: enforce a per-input 8k safety cap before embedding batching and apply a conservative 2k fallback limit for local providers without declared input limits, preventing oversized session/memory chunks from triggering provider context-size failures during sync/indexing. (#6016) Thanks @batumilove.
- Memory/QMD: on Windows, resolve bare `qmd`/`mcporter` command names to npm shim executables (`.cmd`) before spawning, so qmd boot updates and mcporter-backed searches no longer fail with `spawn ... ENOENT` on default npm installs. (#23899) Thanks @arcbuilder-ai.
- Memory/QMD: parse plain-text `qmd collection list --json` output when older qmd builds ignore JSON mode, and retry memory searches once after re-ensuring managed collections when qmd returns `Collection not found ...`. (#23613) Thanks @leozhucn.
- Signal/RPC: guard malformed Signal RPC JSON responses with a clear status-scoped error and add regression coverage for invalid JSON responses. (#22995) Thanks @adhitShet.
- Gateway/Subagents: guard gateway and subagent session-key/message trim paths against undefined inputs to prevent early `Cannot read properties of undefined (reading 'trim')` crashes during subagent spawn and wait flows.
- Agents/Workspace: guard `resolveUserPath` against undefined/null input to prevent `Cannot read properties of undefined (reading 'trim')` crashes when workspace paths are missing in embedded runner flows.
- Auth/Profiles: keep active `cooldownUntil`/`disabledUntil` windows immutable across retries so mid-window failures cannot extend recovery indefinitely; only recompute a backoff window after the previous deadline has expired. This resolves cron/inbound retry loops that could trap gateways until manual `usageStats` cleanup. (#23516, #23536) Thanks @arosstale.
- Channels/Security: fail closed on missing provider group policy config by defaulting runtime group policy to `allowlist` (instead of inheriting `channels.defaults.groupPolicy`) when `channels.<provider>` is absent across message channels, and align runtime + security warnings/docs to the same fallback behavior (Slack, Discord, iMessage, Telegram, WhatsApp, Signal, LINE, Matrix, Mattermost, Google Chat, IRC, Nextcloud Talk, Feishu, and Zalo user flows; plus Discord message/native-command paths). (#23367) Thanks @bmendonca3.
- Gateway/Onboarding: harden remote gateway onboarding defaults and guidance by defaulting discovered direct URLs to `wss://`, rejecting insecure non-loopback `ws://` targets in onboarding validation, and expanding remote-security remediation messaging across gateway client/call/doctor flows. (#23476) Thanks @bmendonca3.
- CLI/Sessions: pass the configured sessions directory when resolving transcript paths in `agentCommand`, so custom `session.store` locations resume sessions reliably. Thanks @davidrudduck.
- Signal/Monitor: treat user-initiated abort shutdowns as clean exits when auto-started `signal-cli` is terminated, while still surfacing unexpected daemon exits as startup/runtime failures. (#23379) Thanks @frankekn.
- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths. (#23377) Thanks @SidQin-cyber.
- Channels/Delivery: remove hardcoded WhatsApp delivery fallbacks; require explicit/session channel context or auto-pick the sole configured channel when unambiguous. (#23357) Thanks @lbo728.
- ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early `gateway not connected` request races. (#23390) Thanks @janckerchen.
- Gateway/Auth: preserve `OPENCLAW_GATEWAY_PASSWORD` env override precedence for remote gateway call credentials after shared resolver refactors, preventing stale configured remote passwords from overriding runtime secret rotation.
- Gateway/Auth: preserve shared-token `gateway token mismatch` auth errors when `auth.token` fallback device-token checks fail, and reserve `device token mismatch` guidance for explicit `auth.deviceToken` failures.
- Gateway/Tools: when agent tools pass an allowlisted `gatewayUrl` override, resolve local override tokens from env/config fallback but keep remote overrides strict to `gateway.remote.token`, preventing local token leakage to remote targets.
- Gateway/Client: keep cached device-auth tokens on `device token mismatch` closes when the client used explicit shared token/password credentials, avoiding accidental pairing-token churn during explicit-auth failures.
- Node host/Exec: keep strict Windows allowlist behavior for `cmd.exe /c` shell-wrapper runs, and return explicit approval guidance when blocked (`SYSTEM_RUN_DENIED: allowlist miss`).
- Control UI: show pairing-required guidance (commands + mobile tokenized URL reminder) when the dashboard disconnects with `1008 pairing required`.
- Security/Audit: add `openclaw security audit` detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (`security.exposure.open_groups_with_runtime_or_fs`).
- Security/Audit: make `gateway.real_ip_fallback_enabled` severity conditional for loopback trusted-proxy setups (warn for loopback-only `trustedProxies`, critical when non-loopback proxies are trusted). (#23428) Thanks @bmendonca3.
- Security/Exec env: block request-scoped `HOME` and `ZDOTDIR` overrides in host exec env sanitizers (Node + macOS), preventing shell startup-file execution before allowlist-evaluated command bodies. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Exec env: block `SHELLOPTS`/`PS4` in host exec env sanitizers and restrict shell-wrapper (`bash|sh|zsh ... -c/-lc`) request env overrides to a small explicit allowlist (`TERM`, `LANG`, `LC_*`, `COLORTERM`, `NO_COLOR`, `FORCE_COLOR`) on both node host and macOS companion paths, preventing xtrace prompt command-substitution allowlist bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
- WhatsApp/Security: enforce `allowFrom` for direct-message outbound targets in all send modes (including `mode: "explicit"`), preventing sends to non-allowlisted numbers. (#20108) Thanks @zahlmann.
- Security/Exec approvals: fail closed on shell line continuations (`\\\n`/`\\\r\n`) and treat shell-wrapper execution as approval-required in allowlist mode, preventing `$\\` newline command-substitution bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Gateway: emit a startup security warning when insecure/dangerous config flags are enabled (including `gateway.controlUi.dangerouslyDisableDeviceAuth=true`) and point operators to `openclaw security audit`.
- Security/Hooks auth: normalize hook auth rate-limit client IP keys so IPv4 and IPv4-mapped IPv6 addresses share one throttle bucket, preventing dual-form auth-attempt budget bypasses. This ships in the next npm release. Thanks @aether-ai-agent for reporting.
- Security/Exec approvals: treat `env` and shell-dispatch wrappers as transparent during allowlist analysis on node-host and macOS companion paths so policy checks match the effective executable/inline shell payload instead of the wrapper binary, blocking wrapper-smuggled allowlist bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Exec approvals: require explicit safe-bin profiles for `tools.exec.safeBins` entries in allowlist mode (remove generic safe-bin profile fallback), and add `tools.exec.safeBinProfiles` for safe custom binaries so unprofiled interpreter-style entries cannot be treated as stdin-safe. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Channels: harden Slack external menu token handling by switching to CSPRNG tokens, validating token shape, requiring user identity for external option lookups, and avoiding fabricated timestamp `trigger_id` fallbacks; also switch Tlon Urbit channel IDs to CSPRNG UUIDs, centralize secure ID/token generation via shared infra helpers, and add a guardrail test to block new runtime `Date.now()+Math.random()` token/id patterns.
- Security/Hooks transforms: enforce symlink-safe containment for webhook transform module paths (including `hooks.transformsDir` and `hooks.mappings[].transform.module`) by resolving existing-path ancestors via realpath before import, while preserving in-root symlink support; add regression coverage for both escape and allow cases. This ships in the next npm release. Thanks @aether-ai-agent for reporting.
- Telegram/WSL2: disable `autoSelectFamily` by default on WSL2 and memoize WSL2 detection in Telegram network decision logic to avoid repeated sync `/proc/version` probes on fetch/send paths. (#21916) Thanks @MizukiMachine.
- Telegram/Network: default Node 22+ DNS result ordering to `ipv4first` for Telegram fetch paths and add `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`/`channels.telegram.network.dnsResultOrder` overrides to reduce IPv6-path fetch failures. (#5405) Thanks @Glucksberg.
- Telegram/Forward bursts: coalesce forwarded text+media updates through a dedicated forward lane debounce window that works with default inbound debounce config, while keeping forwarded control commands immediate. (#19476) thanks @napetrov.
- Telegram/Streaming: preserve archived draft preview mapping after flush and clean superseded reasoning preview bubbles so multi-message preview finals no longer cross-edit or orphan stale messages under send/rotation races. (#23202) Thanks @obviyus.
- Telegram/Replies: scope messaging-tool text/media dedupe to same-target sends only, so cross-target tool sends can no longer silently suppress Telegram final replies.
- Telegram/Replies: normalize `file://` and local-path media variants during messaging dedupe so equivalent media paths do not produce duplicate Telegram replies.
- Telegram/Replies: extract forwarded-origin context from unified reply targets (`reply_to_message` and `external_reply`) so forward+comment metadata is preserved across partial reply shapes. (#9720) thanks @mcaxtr.
- Telegram/Polling: persist a safe update-offset watermark bounded by pending updates so crash/restart cannot skip queued lower `update_id` updates after out-of-order completion. (#23284) thanks @frankekn.
- Telegram/Polling: force-restart stuck runner instances when recoverable unhandled network rejections escape the polling task path, so polling resumes instead of silently stalling. (#19721) Thanks @jg-noncelogic.
- Slack/Slash commands: preserve the Bolt app receiver when registering external select options handlers so monitor startup does not crash on runtimes that require bound `app.options` calls. (#23209) Thanks @0xgaia.
- Slack/Telegram slash sessions: await session metadata persistence before dispatch so first-turn native slash runs do not race session-origin metadata updates. (#23065) thanks @hydro13.
- Slack/Queue routing: preserve string `thread_ts` values through collect-mode queue drain and DM `deliveryContext` updates so threaded follow-ups do not leak to the main channel when Slack thread IDs are strings. (#11934) Thanks @sandieman2 and @vincentkoc.
- Telegram/Native commands: set `ctx.Provider="telegram"` for native slash-command context so elevated gate checks resolve provider correctly (fixes `provider (ctx.Provider)` failures in `/elevated` flows). (#23748) Thanks @serhii12.
- Agents/Ollama: preserve unsafe integer tool-call arguments as exact strings during NDJSON parsing, preventing large numeric IDs from being rounded before tool execution. (#23170) Thanks @BestJoester.
- Cron/Gateway: keep `cron.list` and `cron.status` responsive during startup catch-up by avoiding a long-held cron lock while missed jobs execute. (#23106) Thanks @jayleekr.
- Gateway/Config reload: compare array-valued config paths structurally during diffing so unchanged `memory.qmd.paths` and `memory.qmd.scope.rules` no longer trigger false restart-required reloads. (#23185) Thanks @rex05ai.
- Gateway/Config reload: retry short-lived missing config snapshots during reload before skipping, preventing atomic-write unlink windows from triggering restart loops. (#23343) Thanks @lbo728.
- Cron/Scheduling: validate runtime cron expressions before schedule/stagger evaluation so malformed persisted jobs report a clear `invalid cron schedule: expr is required` error instead of crashing with `undefined.trim` failures and auto-disable churn. (#23223) Thanks @asimons81.
- Memory/QMD: migrate legacy unscoped collection bindings (for example `memory-root`) to per-agent scoped names (for example `memory-root-main`) during startup when safe, so QMD-backed `memory_search` no longer fails with `Collection not found` after upgrades. (#23228, #20727) Thanks @JLDynamics and @AaronFaby.
- Memory/QMD: normalize Han-script BM25 search queries before invoking `qmd search` so mixed CJK+Latin prompts no longer return empty results due to tokenizer mismatch. (#23426) Thanks @LunaLee0130.
- TUI/Input: enable multiline-paste burst coalescing on macOS Terminal.app and iTerm so pasted blocks no longer submit line-by-line as separate messages. (#18809) Thanks @fwends.
- TUI/RTL: isolate right-to-left script lines (Arabic/Hebrew ranges) with Unicode bidi isolation marks in TUI text sanitization so RTL assistant output no longer renders in reversed visual order in terminal chat panes. (#21936) Thanks @Asm3r96.
- TUI/Status: request immediate renders after setting `sending`/`waiting` activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.
- TUI/Input: arm Ctrl+C exit timing when clearing non-empty composer text and add a SIGINT fallback path so double Ctrl+C exits remain responsive during active runs instead of requiring an extra press or appearing stuck. (#23407) Thanks @tinybluedev.
- Agents/Fallbacks: treat JSON payloads with `type: "api_error"` + `"Internal server error"` as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.
- Agents/Google: sanitize non-base64 `thought_signature`/`thoughtSignature` values from assistant replay transcripts for native Google Gemini requests while preserving valid signatures and tool-call order. (#23457) Thanks @echoVic.
- Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.
- Agents/Mistral: sanitize tool-call IDs in the embedded agent loop and generate strict provider-safe pending tool-call IDs, preventing Mistral strict9 `HTTP 400` failures on tool continuations. (#23698) Thanks @echoVic.
- Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson.
- Agents/Replies: emit a default completion acknowledgement (`✅ Done.`) only for direct/private tool-only completions with no final assistant text, while suppressing synthetic acknowledgements for channel/group sessions and runs that already delivered output via messaging tools. (#22834) Thanks @Oldshue.
- Agents/Subagents: honor `tools.subagents.tools.alsoAllow` and explicit subagent `allow` entries when resolving built-in subagent deny defaults, so explicitly granted tools (for example `sessions_send`) are no longer blocked unless re-denied in `tools.subagents.tools.deny`. (#23359) Thanks @goren-beehero.
- Agents/Subagents: make announce call timeouts configurable via `agents.defaults.subagents.announceTimeoutMs` and restore a 60s default to prevent false timeout failures on slower announce paths. (#22719) Thanks @Valadon.
- Agents/Diagnostics: include resolved lifecycle error text in `embedded run agent end` warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.
- Agents/Auth profiles: resolve `agentCommand` session scope before choosing `agentDir`/workspace so resumed runs no longer read auth from `agents/main/agent` when the resolved session belongs to a different/default agent (for example `agent:exec:*` sessions). (#24016) Thanks @abersonFAC.
- Agents/Auth profiles: skip auth-profile cooldown writes for timeout failures in embedded runner rotation so model/network timeouts do not poison same-provider fallback model selection while still allowing in-turn account rotation. (#22622) Thanks @vageeshkumar.
- Plugins/Hooks: run legacy `before_agent_start` once per agent turn and reuse that result across model-resolve and prompt-build compatibility paths, preventing duplicate hook side effects (for example duplicate external API calls). (#23289) Thanks @ksato8710.
- Models/Config: default missing Anthropic provider/model `api` fields to `anthropic-messages` during config validation so custom relay model entries are preserved instead of being dropped by runtime model registry validation. (#23332) Thanks @bigbigmonkey123.
- Gateway/Pairing: preserve existing approved token scopes when processing repair pairings that omit `scopes`, preventing empty-scope token regressions on reconnecting clients. (#21906) Thanks @paki81.
- Memory/QMD: add optional `memory.qmd.mcporter` search routing so QMD `query/search/vsearch` can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07.
- Infra/Network: classify undici `TypeError: fetch failed` as transient in unhandled-rejection detection even when nested causes are unclassified, preventing avoidable gateway crash loops on flaky networks. (#14345) Thanks @Unayung.
- Telegram/Retry: classify undici `TypeError: fetch failed` as recoverable in both polling and send retry paths so transient fetch failures no longer fail fast. (#16699) thanks @Glucksberg.
- Docs/Telegram: correct Node 22+ network defaults (`autoSelectFamily`, `dnsResultOrder`) and clarify Telegram setup does not use positional `openclaw channels login telegram`. (#23609) Thanks @ryanbastic.
- BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines.
- BlueBubbles/Private API cache: treat unknown (`null`) private-API cache status as disabled for send/attachment/reply flows to avoid stale-cache 500s, and log a warning when reply/effect features are requested while capability is unknown. (#23459) Thanks @echoVic.
- BlueBubbles/Webhooks: accept inbound/reaction webhook payloads when BlueBubbles omits `handle` but provides DM `chatGuid`, and harden payload extraction for array/string-wrapped message bodies so valid webhook events no longer get rejected as unparseable. (#23275) Thanks @toph31.
- Security/Audit: add `openclaw security audit` finding `gateway.nodes.allow_commands_dangerous` for risky `gateway.nodes.allowCommands` overrides, with severity upgraded to critical on remote gateway exposure.
- Gateway/Control plane: reduce cross-client write limiter contention by adding `connId` fallback keying when device ID and client IP are both unavailable.
- Security/Config: block prototype-key traversal during config merge patch and legacy migration merge helpers (`__proto__`, `constructor`, `prototype`) to prevent prototype pollution during config mutation flows. (#22968) Thanks @Clawborn.
- Security/Shell env: validate login-shell executable paths for shell-env fallback (`/etc/shells` + trusted prefixes), block `SHELL`/`HOME`/`ZDOTDIR` in config env ingestion before fallback execution, and sanitize fallback shell exec env to pin `HOME` to the real user home while dropping `ZDOTDIR` and other dangerous startup vars. This ships in the next npm release. Thanks @tdjackey for reporting.
- Network/SSRF: enable `autoSelectFamily` on pinned undici dispatchers (with attempt timeout) so IPv6-unreachable environments can quickly fall back to IPv4 for guarded fetch paths. (#19950) Thanks @ENAwareness.
- Security/Config: make parsed chat allowlist checks fail closed when `allowFrom` is empty, restoring expected DM/pairing gating.
- Security/Exec: in non-default setups that manually add `sort` to `tools.exec.safeBins`, block `sort --compress-program` so allowlist-mode safe-bin checks cannot bypass approval. Thanks @tdjackey for reporting.
- Security/Exec approvals: when users choose `allow-always` for shell-wrapper commands (for example `/bin/zsh -lc ...`), persist allowlist patterns for the inner executable(s) instead of the wrapper shell binary, preventing accidental broad shell allowlisting in moderate mode. (#23276) Thanks @xrom2863.
- Security/Exec: fail closed when `tools.exec.host=sandbox` is configured/requested but sandbox runtime is unavailable. (#23398) Thanks @bmendonca3.
- Security/macOS app beta: enforce path-only `system.run` allowlist matching (drop basename matches like `echo`), migrate legacy basename entries to last resolved paths when available, and harden shell-chain handling to fail closed on unsafe parse/control syntax (including quoted command substitution/backticks). This is an optional allowlist-mode feature; default installs remain deny-by-default. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Agents: auto-generate and persist a dedicated `commands.ownerDisplaySecret` when `commands.ownerDisplay=hash`, remove gateway token fallback from owner-ID prompt hashing across CLI and embedded agent runners, and centralize owner-display secret resolution in one shared helper. This ships in the next npm release. Thanks @aether-ai-agent for reporting.
- Security/SSRF: expand IPv4 fetch guard blocking to include RFC special-use/non-global ranges (including benchmarking, TEST-NET, multicast, and reserved/broadcast blocks), centralize range checks into a single CIDR policy table, and reuse one shared host/IP classifier across literal + DNS checks to reduce classifier drift. This ships in the next npm release. Thanks @princeeismond-dot for reporting.
- Security/SSRF: block RFC2544 benchmarking range (`198.18.0.0/15`) across direct and embedded-IP paths, and normalize IPv6 dotted-quad transition literals (for example `::127.0.0.1`, `64:ff9b::8.8.8.8`) in shared IP parsing/classification.
- Security/Archive: block zip symlink escapes during archive extraction.
- Security/Media sandbox: keep tmp media allowance for absolute tmp paths only and enforce symlink-escape checks before sandbox-validated reads, preventing tmp symlink exfiltration and relative `../` sandbox escapes when sandboxes live under tmp. (#17892) Thanks @dashed.
- Browser/Upload: accept canonical in-root upload paths when the configured uploads directory is a symlink alias (for example `/tmp` -> `/private/tmp` on macOS), so browser upload validation no longer rejects valid files during client->server revalidation. (#23300, #23222, #22848) Thanks @bgaither4, @parkerati, and @Nabsku.
- Security/Discord: add `openclaw security audit` warnings for name/tag-based Discord allowlist entries (DM allowlists, guild/channel `users`, and pairing-store entries), highlighting slug-collision risk while keeping name-based matching supported, and canonicalize resolved Discord allowlist names to IDs at runtime without rewriting config files. Thanks @tdjackey for reporting.
- Security/Gateway: block node-role connections when device identity metadata is missing.
- Security/Media: enforce inbound media byte limits during download/read across Discord, Telegram, Zalo, Microsoft Teams, and BlueBubbles to prevent oversized payload memory spikes before rejection. This ships in the next npm release. Thanks @tdjackey for reporting.
- Media/Understanding: preserve `application/pdf` MIME classification during text-like file heuristics so PDF uploads use PDF extraction paths instead of being inlined as raw text. (#23191) Thanks @claudeplay2026-byte.
- Security/Control UI: block symlink-based out-of-root static file reads by enforcing realpath containment and file-identity checks when serving Control UI assets and SPA fallback `index.html`. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Gateway avatars: block symlink traversal during local avatar `data:` URL resolution by enforcing realpath containment and file-identity checks before reads. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Control UI: centralize avatar URL/path validation across gateway/config helpers and enforce a 2 MB max size for local agent avatar files before `/avatar` resolution, reducing oversized-avatar memory risk without changing supported avatar formats.
- Security/Control UI avatars: harden `/avatar/:agentId` local avatar serving by rejecting symlink paths and requiring fd-level file identity + size checks before reads. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/MSTeams media: enforce allowlist checks for SharePoint reference attachment URLs and redirect targets during Graph-backed media fetches so redirect chains cannot escape configured media host boundaries. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/MSTeams media: route attachment auth-retry and Graph SharePoint download redirects through shared `safeFetch` so each hop is validated with allowlist + DNS/IP checks across the full redirect chain. (#23598) Thanks @Asm3r96 and @lewiswigmore.
- Security/macOS discovery: fail closed for unresolved discovery endpoints by clearing stale remote selection values, use resolved service host only for SSH target derivation, and keep remote URL config aligned with resolved endpoint availability. (#21618) Thanks @bmendonca3.
- Chat/Usage/TUI: strip synthetic inbound metadata blocks (including `Conversation info` and trailing `Untrusted context` channel metadata wrappers) from displayed conversation history so internal prompt context no longer leaks into user-visible logs.
- CI/Tests: fix TypeScript case-table typing and lint assertion regressions so `pnpm check` passes again after Synology Chat landing. (#23012) Thanks @druide67.
- Security/Browser relay: harden extension relay auth token handling for `/extension` and `/cdp` pathways.
- Cron: persist `delivered` state in cron job records so delivery failures remain visible in status and logs. (#19174) Thanks @simonemacario.
- Config/Doctor: only repair the OAuth credentials directory when affected channels are configured, avoiding fresh-install noise.
- Config/Channels: whitelist `channels.modelByChannel` in config validation and exclude it from plugin auto-enable channel detection so model overrides no longer trigger `unknown channel id` validation errors or bogus `modelByChannel` plugin enables. (#23412) Thanks @ProspectOre.
- Config/Bindings: allow optional `bindings[].comment` in strict config validation so annotated binding entries no longer fail load. (#23458) Thanks @echoVic.
- Usage/Pricing: correct MiniMax M2.5 pricing defaults to fix inflated cost reporting. (#22755) Thanks @miloudbelarebia.
- Gateway/Daemon: verify gateway health after daemon restart.
- Agents/UI text: stop rewriting normal assistant billing/payment language outside explicit error contexts. (#17834) Thanks @niceysam.
## 2026.2.21
### Changes
@@ -32,6 +415,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/Bootstrap: skip malformed bootstrap files with missing/invalid paths instead of crashing agent sessions; hooks using `filePath` (or non-string `path`) are skipped with a warning. (#22693, #22698) Thanks @arosstale.
- Security/Agents: cap embedded Pi runner outer retry loop with a higher profile-aware dynamic limit (32-160 attempts) and return an explicit `retry_limit` error payload when retries never converge, preventing unbounded internal retry cycles (`GHSA-76m6-pj3w-v7mf`).
- Telegram: detect duplicate bot-token ownership across Telegram accounts at startup/status time, mark secondary accounts as not configured with an explicit fix message, and block duplicate account startup before polling to avoid endless `getUpdates` conflict loops.
- Agents/Tool images: include source filenames in `agents/tool-images` resize logs so compression events can be traced back to specific files.
@@ -45,6 +429,7 @@ Docs: https://docs.openclaw.ai
- Providers/Copilot: add `claude-sonnet-4.6` and `claude-sonnet-4.5` to the default GitHub Copilot model catalog and add coverage for model-list/definition helpers. (#20270, fixes #20091) Thanks @Clawborn.
- Auto-reply/WebChat: avoid defaulting inbound runtime channel labels to unrelated providers (for example `whatsapp`) for webchat sessions so channel-specific formatting guidance stays accurate. (#21534) Thanks @lbo728.
- Status: include persisted `cacheRead`/`cacheWrite` in session summaries so compact `/status` output consistently shows cache hit percentages from real session data.
- Sessions/Usage: persist `totalTokens` from `promptTokens` snapshots even when providers omit structured usage payloads, so session history/status no longer regress to `unknown` token utilization for otherwise successful runs. (#21819) Thanks @zymclaw.
- Heartbeat/Cron: restore interval heartbeat behavior so missing `HEARTBEAT.md` no longer suppresses runs (only effectively empty files skip), preserving prompt-driven and tagged-cron execution paths.
- WhatsApp/Cron/Heartbeat: enforce allowlisted routing for implicit scheduled/system delivery by merging pairing-store + configured `allowFrom` recipients, selecting authorized recipients when last-route context points to a non-allowlisted chat, and preventing heartbeat fan-out to recent unauthorized chats.
- Heartbeat/Active hours: constrain active-hours `24` sentinel parsing to `24:00` in time validation so invalid values like `24:30` are rejected early. (#21410) thanks @adhitShet.
@@ -52,6 +437,7 @@ Docs: https://docs.openclaw.ai
- CLI/Pairing: default `pairing list` and `pairing approve` to the sole available pairing channel when omitted, so TUI-only setups can recover from `pairing required` without guessing channel arguments. (#21527) Thanks @losts1.
- TUI/Pairing: show explicit pairing-required recovery guidance after gateway disconnects that return `pairing required`, including approval steps to unblock quickstart TUI hatching on fresh installs. (#21841) Thanks @nicolinux.
- TUI/Input: suppress duplicate backspace events arriving in the same input burst window so SSH sessions no longer delete two characters per backspace press in the composer. (#19318) Thanks @eheimer.
- TUI/Models: scope `models.list` to the configured model allowlist (`agents.defaults.models`) so `/model` picker no longer floods with unrelated catalog entries by default. (#18816) Thanks @fwends.
- TUI/Heartbeat: suppress heartbeat ACK/prompt noise in chat streaming when `showOk` is disabled, while still preserving non-ACK heartbeat alerts in final output. (#20228) Thanks @bhalliburton.
- TUI/History: cap chat-log component growth and prune stale render nodes/references so large default history loads no longer overflow render recursion with `RangeError: Maximum call stack size exceeded`. (#18068) Thanks @JaniJegoroff.
- Memory/QMD: diversify mixed-source search ranking when both session and memory collections are present so session transcript hits no longer crowd out durable memory-file matches in top results. (#19913) Thanks @alextempr.
@@ -61,6 +447,7 @@ Docs: https://docs.openclaw.ai
- Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0.
- Slack: pass `recipient_team_id` / `recipient_user_id` through Slack native streaming calls so `chat.startStream`/`appendStream`/`stopStream` work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012.
- CLI/Config: add canonical `--strict-json` parsing for `config set` and keep `--json` as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet.
- CLI/Config: preserve explicitly unset config paths in persisted JSON after writes so `openclaw config unset <path>` no longer re-introduces defaulted keys (for example `commands.ownerDisplay`) through schema normalization. (#22984) Thanks @aronchick.
- CLI: keep `openclaw -v` as a root-only version alias so subcommand `-v, --verbose` flags (for example ACP/hooks/skills) are no longer intercepted globally. (#21303) thanks @adhitShet.
- Memory: return empty snippets when `memory_get`/QMD read files that have not been created yet, and harden memory indexing/session helpers against ENOENT races so missing Markdown no longer crashes tools. (#20680) Thanks @pahdo.
- Telegram/Streaming: always clean up draft previews even when dispatch throws before fallback handling, preventing orphaned preview messages during failed runs. (#19041) thanks @mudrii.
@@ -68,6 +455,7 @@ Docs: https://docs.openclaw.ai
- Telegram/Streaming: restore 30-char first-preview debounce and scope `NO_REPLY` prefix suppression to partial sentinel fragments so normal `No...` text is not filtered. (#22613) thanks @obviyus.
- Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow.
- Telegram/Status reactions: keep lifecycle reactions active when available-reactions lookup fails by falling back to unrestricted variant selection instead of suppressing reaction updates. (#22380) thanks @obviyus.
- Discord/Events: await `DiscordMessageListener` message handlers so regular `MESSAGE_CREATE` traffic is processed through queue ordering/timeout flow instead of fire-and-forget drops. (#22396) Thanks @sIlENtbuffER.
- Discord/Streaming: apply `replyToMode: first` only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report.
- Discord/Components: map DM channel targets back to user-scoped component sessions so button/select interactions stay in the main DM session. Thanks @thewilloftheshadow.
- Discord/Allowlist: lazy-load guild lists when resolving Discord user allowlists so ID-only entries resolve even if guild fetch fails. (#20208) Thanks @zhangjunmengyang.
@@ -95,6 +483,7 @@ Docs: https://docs.openclaw.ai
- Agents/Subagents: restore announce-chain delivery to agent injection, defer nested announce output until descendant follow-up content is ready, and prevent descendant deferrals from consuming announce retry budget so deep chains do not drop final completions. (#22223) Thanks @tyler6204.
- Agents/System Prompt: label allowlisted senders as authorized senders to avoid implying ownership. Thanks @thewilloftheshadow.
- Agents/Tool display: fix exec cwd suffix inference so `pushd ... && popd ... && <command>` does not keep stale `(in <dir>)` context in summaries. (#21925) Thanks @Lukavyi.
- Agents/Google: flatten residual nested `anyOf`/`oneOf` unions in Gemini tool-schema cleanup so Cloud Code Assist no longer rejects unsupported union keywords that survive earlier simplification. (#22825) Thanks @Oceanswave.
- Tools/web_search: handle xAI Responses API payloads that emit top-level `output_text` blocks (without a `message` wrapper) so Grok web_search no longer returns `No response` for those results. (#20508) Thanks @echoVic.
- Agents/Failover: treat non-default override runs as direct fallback-to-configured-primary (skip configured fallback chain), normalize default-model detection for provider casing/whitespace, and add regression coverage for override/auth error paths. (#18820) Thanks @Glucksberg.
- Docker/Build: include `ownerDisplay` in `CommandsSchema` object-level defaults so Docker `pnpm build` no longer fails with `TS2769` during plugin SDK d.ts generation. (#22558) Thanks @obviyus.
@@ -158,6 +547,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security: strip hidden text from `web_fetch` extracted content to prevent indirect prompt injection, covering CSS-hidden elements, class-based hiding (sr-only, d-none, etc.), invisible Unicode, color:transparent, offscreen transforms, and non-content tags. (#8027, #21074) Thanks @hydro13 for the fix and @LucasAIBuilder for reporting.
- Agents/Streaming: keep assistant partial streaming active during reasoning streams, handle native `thinking_*` stream events consistently, dedupe mixed reasoning-end signals, and clear stale mutating tool errors after same-target retry success. (#20635) Thanks @obviyus.
- iOS/Chat: use a dedicated iOS chat session key for ChatSheet routing to avoid cross-client session collisions with main-session traffic. (#21139) thanks @mbelinky.
- iOS/Chat: auto-resync chat history after reconnect sequence gaps, clear stale pending runs, and avoid dead-end manual refresh errors after transient disconnects. (#21135) thanks @mbelinky.
@@ -167,8 +557,10 @@ Docs: https://docs.openclaw.ai
- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky.
- iOS/Signing: restore local auto-selected signing-team overrides during iOS project generation by wiring `.local-signing.xcconfig` into the active signing config and emitting `OPENCLAW_DEVELOPMENT_TEAM` in local signing setup. (#19993) Thanks @ngutman.
- Telegram: unify message-like inbound handling so `message` and `channel_post` share the same dedupe/access/media pipeline and remain behaviorally consistent. (#20591) Thanks @obviyus.
- Telegram: keep media-group processing resilient by skipping recoverable per-item download failures while still failing loud on non-recoverable media errors. (#20598) thanks @mcaxtr.
- Telegram/Agents: gate exec/bash tool-failure warnings behind verbose mode so default Telegram replies stay clean while verbose sessions still surface diagnostics. (#20560) Thanks @obviyus.
- Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`<chatId>:topic:<threadId>`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi.
- Telegram/DM routing: prevent DM inbound origin metadata from leaking into main-session `lastRoute` updates and normalize DM `lastRoute.to` to provider-prefixed `telegram:<chatId>`. (#19491) thanks @guirguispierre.
- Gateway/Daemon: forward `TMPDIR` into installed service environments so macOS LaunchAgent gateway runs can open SQLite temp/journal files reliably instead of failing with `SQLITE_CANTOPEN`. (#20512) Thanks @Clawborn.
- Agents/Billing: include the active model that produced a billing error in user-facing billing messages (for example, `OpenAI (gpt-5.3)`) across payload, failover, and lifecycle error paths, so users can identify exactly which key needs credits. (#20510) Thanks @echoVic.
- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor.
+1 -1
View File
@@ -1 +1 @@
AGENTS.md
AGENTS.md
+4 -1
View File
@@ -44,11 +44,14 @@ Welcome to the lobster tank! 🦞
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
- **Onur Solmaz** - Agents, dev workflows, ACP integrations, MS Teams
- GitHub: [@onutc](https://github.com/onutc), [@osolmaz](https://github.com/osolmaz) · X: [@onusoz](https://x.com/onusoz)
## How to Contribute
1. **Bugs & small fixes** → Open a PR!
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/openclaw/openclaw/discussions) or ask in Discord first
3. **Questions** → Discord #setup-help
3. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
## Before You PR
+78
View File
@@ -0,0 +1,78 @@
# OpenClaw PR Submission Status
> Auto-maintained by agent team. Last updated: 2026-02-22
## PR Plan Overview
All PRs target upstream `openclaw/openclaw` via fork `kevinWangSheng/openclaw`.
Each PR follows [CONTRIBUTING.md](./CONTRIBUTING.md) and uses the [PR template](./.github/PULL_REQUEST_TEMPLATE.md).
## Duplicate Check
Before submission, each PR was cross-referenced against:
- 100+ open upstream PRs (as of 2026-02-22)
- 50 recently merged PRs
- 50+ open issues
No overlap found with existing PRs.
## PR Status Table
| # | Branch | Title | Type | Status | PR URL |
| --- | -------------------------------------- | --------------------------------------------------------------------------- | -------- | --------------- | --------------------------------------------------------- |
| 1 | `security/redos-safe-regex` | fix(security): add ReDoS protection for user-controlled regex patterns | Security | CI Pass | [#23670](https://github.com/openclaw/openclaw/pull/23670) |
| 2 | `security/session-slug-crypto-random` | fix(security): use crypto.randomInt for session slug generation | Security | CI Pass | [#23671](https://github.com/openclaw/openclaw/pull/23671) |
| 3 | `fix/json-parse-crash-guard` | fix(resilience): guard JSON.parse of external process output with try-catch | Bug fix | CI Pass | [#23672](https://github.com/openclaw/openclaw/pull/23672) |
| 4 | `refactor/console-to-subsystem-logger` | refactor(logging): migrate remaining console calls to subsystem logger | Refactor | CI Pass | [#23669](https://github.com/openclaw/openclaw/pull/23669) |
| 5 | `fix/sanitize-rpc-error-messages` | fix(security): sanitize RPC error messages in signal and imessage clients | Security | CI Pass | [#23724](https://github.com/openclaw/openclaw/pull/23724) |
| 6 | `fix/download-stream-cleanup` | fix(resilience): destroy write streams on download errors | Bug fix | CI Pass | [#23726](https://github.com/openclaw/openclaw/pull/23726) |
| 7 | `fix/telegram-status-reaction-cleanup` | fix(telegram): clear done reaction when removeAckAfterReply is true | Bug fix | CI Pass | [#23728](https://github.com/openclaw/openclaw/pull/23728) |
| 8 | `fix/session-cache-eviction` | fix(memory): add max size eviction to session manager cache | Bug fix | CI Pass (17/17) | [#23744](https://github.com/openclaw/openclaw/pull/23744) |
| 9 | `fix/fetch-missing-timeout` | fix(resilience): add timeout to unguarded fetch calls in browser subsystem | Bug fix | CI Pass (18/18) | [#23745](https://github.com/openclaw/openclaw/pull/23745) |
| 10 | `fix/skills-download-partial-cleanup` | fix(resilience): clean up partial file on skill download failure | Bug fix | CI Pass (19/19) | [#24141](https://github.com/openclaw/openclaw/pull/24141) |
| 11 | `fix/extension-relay-stop-cleanup` | fix(browser): flush pending extension timers on relay stop | Bug fix | CI Pass (20/20) | [#24142](https://github.com/openclaw/openclaw/pull/24142) |
## Isolation Rules
- Each agent works on a separate git worktree branch
- No two agents modify the same file
- File ownership:
- PR 1: `src/infra/exec-approval-forwarder.ts`, `src/discord/monitor/exec-approvals.ts`
- PR 2: `src/agents/session-slug.ts`
- PR 3: `src/infra/bonjour-discovery.ts`, `src/infra/outbound/delivery-queue.ts`
- PR 4: `src/infra/tailscale.ts`, `src/node-host/runner.ts`
- PR 5: `src/signal/client.ts`, `src/imessage/client.ts`
- PR 6: `src/media/store.ts`, `src/commands/signal-install.ts`
- PR 7: `src/telegram/bot-message-dispatch.ts`
- PR 8: `src/agents/pi-embedded-runner/session-manager-cache.ts`
- PR 9: `src/cli/nodes-camera.ts`, `src/browser/pw-session.ts`
- PR 10: `src/agents/skills-install-download.ts`
- PR 11: `src/browser/extension-relay.ts`
## Verification Results
### Batch 1 (PRs 1-4) — All CI Green
- PR 1: 17 tests pass, check/build/tests all green
- PR 2: 3 tests pass, check/build/tests all green
- PR 3: 45 tests pass (3 new), check/build/tests all green
- PR 4: 12 tests pass, check/build/tests all green
### Batch 2 (PRs 5-7) — CI Running
- PR 5: 3 signal tests pass, check pass, awaiting full test suite
- PR 6: 38 tests pass (20 media + 18 signal-install), check pass, awaiting full suite
- PR 7: 47 tests pass (3 new), check pass, awaiting full suite
### Batch 3 (PRs 8-9) — All CI Green
- PR 8 & 9: Initially failed due to pre-existing upstream TS errors + Windows flaky test. Fixed by rebasing onto latest upstream/main and removing `yieldMs: 10` from flaky sandbox test.
- PR 8: 17/17 pass, check/build/tests/windows all green
- PR 9: 18/18 pass, check/build/tests/windows all green
### Batch 4 (PRs 10-11) — All CI Green
- PR 10 & 11: Initially failed Windows flaky test (`yieldMs: 10` race). Fixed by removing `yieldMs: 10` from flaky sandbox test (same fix as PRs 8-9).
- PR 10: 19/19 pass, check/build/tests/windows all green
- PR 11: 20/20 pass, check/build/tests/windows all green
+50 -75
View File
@@ -38,7 +38,6 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
**Subscriptions (OAuth):**
- **[Anthropic](https://www.anthropic.com/)** (Claude Pro/Max)
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.6** for longcontext strength and better promptinjection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
@@ -503,78 +502,54 @@ Special thanks to Adam Doppelt for lobster.bot.
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/abdelsfane"><img src="https://avatars.githubusercontent.com/u/32418586?v=4&s=48" width="48" height="48" alt="abdelsfane" title="abdelsfane"/></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/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/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/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/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/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/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/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/ethanpalm"><img src="https://avatars.githubusercontent.com/u/56270045?v=4&s=48" width="48" height="48" alt="ethanpalm" title="ethanpalm"/></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/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/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/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/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/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/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/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/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/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/buerbaumer"><img src="https://avatars.githubusercontent.com/u/44548809?v=4&s=48" width="48" height="48" alt="buerbaumer" title="buerbaumer"/></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/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/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/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/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/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/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/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/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></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/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/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/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/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/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/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/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/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/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/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/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/divanoli"><img src="https://avatars.githubusercontent.com/u/12023205?v=4&s=48" width="48" height="48" alt="divanoli" title="divanoli"/></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/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/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/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/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/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/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/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></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/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/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/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/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/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/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/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/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/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/CharlieGreenman"><img src="https://avatars.githubusercontent.com/u/8540141?v=4&s=48" width="48" height="48" alt="CharlieGreenman" title="CharlieGreenman"/></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/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/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/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/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/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/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/jessy2027"><img src="https://avatars.githubusercontent.com/u/89694096?v=4&s=48" width="48" height="48" alt="jessy2027" title="jessy2027"/></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/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a>
<a href="https://github.com/M00N7682"><img src="https://avatars.githubusercontent.com/u/170746674?v=4&s=48" width="48" height="48" alt="M00N7682" title="M00N7682"/></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/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/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/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/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/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/jscaldwell55"><img src="https://avatars.githubusercontent.com/u/111952840?v=4&s=48" width="48" height="48" alt="jscaldwell55" title="jscaldwell55"/></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/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/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/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/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/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/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/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/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/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/0xRaini"><img src="https://avatars.githubusercontent.com/u/190923101?v=4&s=48" width="48" height="48" alt="0xRaini" title="0xRaini"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/Jackten"><img src="https://avatars.githubusercontent.com/u/2895479?v=4&s=48" width="48" height="48" alt="Jackten" title="Jackten"/></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/pycckuu"><img src="https://avatars.githubusercontent.com/u/1489583?v=4&s=48" width="48" height="48" alt="pycckuu" title="pycckuu"/></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/simonemacario"><img src="https://avatars.githubusercontent.com/u/2116609?v=4&s=48" width="48" height="48" alt="simonemacario" title="simonemacario"/></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/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/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/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/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/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/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></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/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/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/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/stakeswky"><img src="https://avatars.githubusercontent.com/u/64798754?v=4&s=48" width="48" height="48" alt="不做了睡大觉" title="不做了睡大觉"/></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/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/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/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/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/search?q=nicolasstanley"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="nicolasstanley" title="nicolasstanley"/></a> <a href="https://github.com/davidiach"><img src="https://avatars.githubusercontent.com/u/28102235?v=4&s=48" width="48" height="48" alt="davidiach" title="davidiach"/></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/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/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/search?q=hudson-rivera"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="hudson-rivera" title="hudson-rivera"/></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/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/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></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/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/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/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/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/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/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/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/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/cdorsey"><img src="https://avatars.githubusercontent.com/u/12650570?v=4&s=48" width="48" height="48" alt="cdorsey" title="cdorsey"/></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/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/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/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/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/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/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/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/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/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/evanotero"><img src="https://avatars.githubusercontent.com/u/13204105?v=4&s=48" width="48" height="48" alt="evanotero" title="evanotero"/></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/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/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/garnetlyx"><img src="https://avatars.githubusercontent.com/u/12513503?v=4&s=48" width="48" height="48" alt="garnetlyx" title="garnetlyx"/></a> <a href="https://github.com/jlowin"><img src="https://avatars.githubusercontent.com/u/153965?v=4&s=48" width="48" height="48" alt="jlowin" title="jlowin"/></a> <a href="https://github.com/liebertar"><img src="https://avatars.githubusercontent.com/u/99405438?v=4&s=48" width="48" height="48" alt="liebertar" title="liebertar"/></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/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/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/carrotRakko"><img src="https://avatars.githubusercontent.com/u/24588751?v=4&s=48" width="48" height="48" alt="Mitsuyuki Osabe" title="Mitsuyuki Osabe"/></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/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/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/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/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/HirokiKobayashi-R"><img src="https://avatars.githubusercontent.com/u/37167840?v=4&s=48" width="48" height="48" alt="HirokiKobayashi-R" title="HirokiKobayashi-R"/></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/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/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/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/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/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/miloudbelarebia"><img src="https://avatars.githubusercontent.com/u/136994453?v=4&s=48" width="48" height="48" alt="Thorfinn" title="Thorfinn"/></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/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/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/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/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/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/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/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/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/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/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/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/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/dinakars777"><img src="https://avatars.githubusercontent.com/u/250428393?v=4&s=48" width="48" height="48" alt="dinakars777" title="dinakars777"/></a> <a href="https://github.com/divisonofficer"><img src="https://avatars.githubusercontent.com/u/41609506?v=4&s=48" width="48" height="48" alt="divisonofficer" title="divisonofficer"/></a> <a href="https://github.com/Flash-LHR"><img src="https://avatars.githubusercontent.com/u/47357603?v=4&s=48" width="48" height="48" alt="Flash-LHR" title="Flash-LHR"/></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/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></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/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/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/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/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/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/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/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/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/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/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/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/Iron9521"><img src="https://avatars.githubusercontent.com/u/261863182?v=4&s=48" width="48" height="48" alt="Iron9521" title="Iron9521"/></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/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/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/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/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/jabezborja"><img src="https://avatars.githubusercontent.com/u/64759159?v=4&s=48" width="48" height="48" alt="jabezborja" title="jabezborja"/></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/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/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/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/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/search?q=%E5%BA%B7%E7%86%99"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="康熙" title="康熙"/></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/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/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/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/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/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></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/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/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a>
<a href="https://github.com/17jmumford"><img src="https://avatars.githubusercontent.com/u/36290330?v=4&s=48" width="48" height="48" alt="17jmumford" title="17jmumford"/></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/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/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/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/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/search?q=Operative-001"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Operative-001" title="Operative-001"/></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/DylanWoodAkers"><img src="https://avatars.githubusercontent.com/u/253595314?v=4&s=48" width="48" height="48" alt="DylanWoodAkers" title="DylanWoodAkers"/></a> <a href="https://github.com/Hisleren"><img src="https://avatars.githubusercontent.com/u/83217244?v=4&s=48" width="48" height="48" alt="Hisleren" title="Hisleren"/></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/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/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/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/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/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/GHesericsu"><img src="https://avatars.githubusercontent.com/u/60202455?v=4&s=48" width="48" height="48" alt="GHesericsu" title="GHesericsu"/></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/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/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/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/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/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/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></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/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/search?q=zisisp"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="zisisp" title="zisisp"/></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/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/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/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/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/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/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></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/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></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/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></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/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/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/search?q=Rain"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rain" title="Rain"/></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/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/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/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/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/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/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></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/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/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/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/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/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/search?q=Joshua%20Mitchell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Joshua Mitchell" title="Joshua Mitchell"/></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/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/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></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/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/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/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/search?q=Ralph"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ralph" title="Ralph"/></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/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/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/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/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/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/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/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/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/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/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/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/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></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/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/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/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/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/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/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/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/justinhuangcode"><img src="https://avatars.githubusercontent.com/u/252443740?v=4&s=48" width="48" height="48" alt="justinhuangcode" title="justinhuangcode"/></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/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/search?q=ludd50155"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ludd50155" title="ludd50155"/></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="Mark Liu" title="Mark Liu"/></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/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/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/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/search?q=Roopak%20Nijhara"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Roopak Nijhara" title="Roopak Nijhara"/></a> <a href="https://github.com/search?q=Sean%20McLellan"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sean McLellan" title="Sean McLellan"/></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/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/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></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/search?q=xiaose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="xiaose" title="xiaose"/></a>
<a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></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/search?q=Aditya%20Singh"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aditya Singh" title="Aditya Singh"/></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/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></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/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/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/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/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a>
<a href="https://github.com/search?q=Clawdbot"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawdbot" title="Clawdbot"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></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/danballance"><img src="https://avatars.githubusercontent.com/u/13839912?v=4&s=48" width="48" height="48" alt="danballance" title="danballance"/></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/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/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/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/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/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/hanxiao"><img src="https://avatars.githubusercontent.com/u/2041322?v=4&s=48" width="48" height="48" alt="hanxiao" title="hanxiao"/></a> <a href="https://github.com/search?q=Ignacio"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ignacio" title="Ignacio"/></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/ivancasco"><img src="https://avatars.githubusercontent.com/u/2452858?v=4&s=48" width="48" height="48" alt="ivancasco" title="ivancasco"/></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/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></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/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></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/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/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/search?q=Marco%20Marandiz"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marco Marandiz" title="Marco Marandiz"/></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/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/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/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/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/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/search?q=Pocket%20Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pocket Clawd" title="Pocket Clawd"/></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/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/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/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></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/search?q=Sriram%20Naidu%20Thota"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sriram Naidu Thota" title="Sriram Naidu Thota"/></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/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/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/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/search?q=Yao"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yao" title="Yao"/></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/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/search?q=%E5%B0%B9%E5%87%AF"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="尹凯" title="尹凯"/></a> <a href="https://github.com/search?q=%7BSuksham-sharma%7D"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="{Suksham-sharma}" title="{Suksham-sharma}"/></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/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/search?q=abhijeet117"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="abhijeet117" title="abhijeet117"/></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/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></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/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/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/AlexZhangji"><img src="https://avatars.githubusercontent.com/u/3280924?v=4&s=48" width="48" height="48" alt="AlexZhangji" title="AlexZhangji"/></a> <a href="https://github.com/search?q=amabito"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="amabito" title="amabito"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anisoptera"><img src="https://avatars.githubusercontent.com/u/768771?v=4&s=48" width="48" height="48" alt="anisoptera" title="anisoptera"/></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/arthyn"><img src="https://avatars.githubusercontent.com/u/5466421?v=4&s=48" width="48" height="48" alt="arthyn" title="arthyn"/></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/search?q=Ayush%20Ojha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ayush Ojha" title="Ayush Ojha"/></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/search?q=baccula"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="baccula" title="baccula"/></a> <a href="https://github.com/beefiker"><img src="https://avatars.githubusercontent.com/u/55247450?v=4&s=48" width="48" height="48" alt="beefiker" title="beefiker"/></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/search?q=blacksmith-sh%5Bbot%5D"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/search?q=bqcfjwhz85-arch"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="bqcfjwhz85-arch" title="bqcfjwhz85-arch"/></a> <a href="https://github.com/search?q=bravostation"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="bravostation" title="bravostation"/></a> <a href="https://github.com/search?q=Buddy%20(AI)"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Buddy (AI)" title="Buddy (AI)"/></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/search?q=calvin-hpnet"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="calvin-hpnet" title="calvin-hpnet"/></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/search?q=chenglun.hu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="chenglun.hu" title="chenglun.hu"/></a> <a href="https://github.com/Chloe-VP"><img src="https://avatars.githubusercontent.com/u/257371598?v=4&s=48" width="48" height="48" alt="Chloe-VP" title="Chloe-VP"/></a> <a href="https://github.com/search?q=Claw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Claw" title="Claw"/></a> <a href="https://github.com/search?q=Clawdbot%20Maintainers"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawdbot Maintainers" title="Clawdbot Maintainers"/></a> <a href="https://github.com/search?q=cristip73"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/search?q=danielcadenhead"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="danielcadenhead" title="danielcadenhead"/></a> <a href="https://github.com/dario-github"><img src="https://avatars.githubusercontent.com/u/40749119?v=4&s=48" width="48" height="48" alt="dario-github" title="dario-github"/></a> <a href="https://github.com/DarwinsBuddy"><img src="https://avatars.githubusercontent.com/u/490836?v=4&s=48" width="48" height="48" alt="DarwinsBuddy" title="DarwinsBuddy"/></a>
<a href="https://github.com/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/search?q=davidbors-snyk"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="davidbors-snyk" title="davidbors-snyk"/></a> <a href="https://github.com/dcantu96"><img src="https://avatars.githubusercontent.com/u/32658690?v=4&s=48" width="48" height="48" alt="dcantu96" title="dcantu96"/></a> <a href="https://github.com/search?q=dependabot%5Bbot%5D"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/search?q=Developer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Developer" title="Developer"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/dvrshil"><img src="https://avatars.githubusercontent.com/u/81693876?v=4&s=48" width="48" height="48" alt="dvrshil" title="dvrshil"/></a> <a href="https://github.com/dxd5001"><img src="https://avatars.githubusercontent.com/u/1886046?v=4&s=48" width="48" height="48" alt="dxd5001" title="dxd5001"/></a> <a href="https://github.com/dylanneve1"><img src="https://avatars.githubusercontent.com/u/31746704?v=4&s=48" width="48" height="48" alt="dylanneve1" title="dylanneve1"/></a>
<a href="https://github.com/search?q=elliotsecops"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="elliotsecops" title="elliotsecops"/></a> <a href="https://github.com/EmberCF"><img src="https://avatars.githubusercontent.com/u/258471336?v=4&s=48" width="48" height="48" alt="EmberCF" title="EmberCF"/></a> <a href="https://github.com/ereid7"><img src="https://avatars.githubusercontent.com/u/27597719?v=4&s=48" width="48" height="48" alt="ereid7" title="ereid7"/></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/search?q=f-trycua"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="f-trycua" title="f-trycua"/></a> <a href="https://github.com/search?q=fan"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="fan" title="fan"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></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/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/search?q=fujiwara-tofu-shop"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="fujiwara-tofu-shop" title="fujiwara-tofu-shop"/></a>
<a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/search?q=gaowanqi08141999"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="gaowanqi08141999" title="gaowanqi08141999"/></a> <a href="https://github.com/search?q=gerardward2007"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/search?q=gitpds"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="gitpds" title="gitpds"/></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/search?q=habakan"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="habakan" title="habakan"/></a> <a href="https://github.com/HassanFleyah"><img src="https://avatars.githubusercontent.com/u/228002017?v=4&s=48" width="48" height="48" alt="HassanFleyah" title="HassanFleyah"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/search?q=hcl"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="hcl" title="hcl"/></a> <a href="https://github.com/search?q=headswim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="headswim" title="headswim"/></a>
<a href="https://github.com/search?q=hlbbbbbbb"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="hlbbbbbbb" title="hlbbbbbbb"/></a> <a href="https://github.com/search?q=Hubert"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Hubert" title="Hubert"/></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/search?q=hyaxia"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="hyaxia" title="hyaxia"/></a> <a href="https://github.com/iamEvanYT"><img src="https://avatars.githubusercontent.com/u/47493765?v=4&s=48" width="48" height="48" alt="iamEvanYT" title="iamEvanYT"/></a> <a href="https://github.com/search?q=ikari"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ikari" title="ikari"/></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/search?q=Iron"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Iron" title="Iron"/></a> <a href="https://github.com/search?q=ironbyte-rgb"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ironbyte-rgb" title="ironbyte-rgb"/></a> <a href="https://github.com/search?q=%C3%8Dtalo%20Souza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ítalo Souza" title="Ítalo Souza"/></a>
<a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jane"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jane" title="Jane"/></a> <a href="https://github.com/search?q=Jarvis%20Deploy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis Deploy" title="Jarvis Deploy"/></a> <a href="https://github.com/search?q=jarvis89757"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jarvis89757" title="jarvis89757"/></a> <a href="https://github.com/search?q=jasonftl"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jasonftl" title="jasonftl"/></a> <a href="https://github.com/search?q=jasonsschin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jasonsschin" title="jasonsschin"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=jg-noncelogic"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jg-noncelogic" title="jg-noncelogic"/></a> <a href="https://github.com/search?q=jigar"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jigar" title="jigar"/></a> <a href="https://github.com/search?q=joeynyc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="joeynyc" title="joeynyc"/></a>
<a href="https://github.com/search?q=Jon%20Uleis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jon Uleis" title="Jon Uleis"/></a> <a href="https://github.com/search?q=Josh%20Long"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Josh Long" title="Josh Long"/></a> <a href="https://github.com/search?q=justyannicc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="justyannicc" title="justyannicc"/></a> <a href="https://github.com/search?q=Karim%20Naguib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Karim Naguib" title="Karim Naguib"/></a> <a href="https://github.com/search?q=Kasper%20Neist%20Christjansen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kasper Neist Christjansen" title="Kasper Neist Christjansen"/></a> <a href="https://github.com/search?q=Keshav%20Rao"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keshav Rao" title="Keshav Rao"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/search?q=Kira"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kira" title="Kira"/></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/search?q=Knox"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Knox" title="Knox"/></a>
<a href="https://github.com/search?q=Kristijan%20Jovanovski"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kristijan Jovanovski" title="Kristijan Jovanovski"/></a> <a href="https://github.com/search?q=Kyle%20Chen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kyle Chen" title="Kyle Chen"/></a> <a href="https://github.com/search?q=Latitude%20Bot"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Latitude Bot" title="Latitude Bot"/></a> <a href="https://github.com/search?q=Levi%20Figueira"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Levi Figueira" title="Levi Figueira"/></a> <a href="https://github.com/search?q=Liu%20Weizhan"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Liu Weizhan" title="Liu Weizhan"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/search?q=Loganaden%20Velvindron"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Loganaden Velvindron" title="Loganaden Velvindron"/></a> <a href="https://github.com/search?q=lsh411"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="lsh411" title="lsh411"/></a> <a href="https://github.com/search?q=Lucas%20Kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lucas Kim" title="Lucas Kim"/></a> <a href="https://github.com/search?q=Luka%20Zhang"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Luka Zhang" title="Luka Zhang"/></a>
<a href="https://github.com/search?q=Luk%C3%A1%C5%A1%20Loukota"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lukáš Loukota" title="Lukáš Loukota"/></a> <a href="https://github.com/search?q=Lukin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lukin" title="Lukin"/></a> <a href="https://github.com/search?q=mac%20mimi"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="mac mimi" title="mac mimi"/></a> <a href="https://github.com/search?q=mac26ai"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="mac26ai" title="mac26ai"/></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/search?q=Mahsum%20Aktas"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mahsum Aktas" title="Mahsum Aktas"/></a> <a href="https://github.com/search?q=Marc%20Beaupre"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc Beaupre" title="Marc Beaupre"/></a> <a href="https://github.com/search?q=Marcus%20Neves"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marcus Neves" title="Marcus Neves"/></a> <a href="https://github.com/search?q=Mario%20Zechner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mario Zechner" title="Mario Zechner"/></a> <a href="https://github.com/search?q=Markus%20Buhatem%20Koch"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Markus Buhatem Koch" title="Markus Buhatem Koch"/></a>
<a href="https://github.com/search?q=Martin%20P%C3%BA%C4%8Dik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Martin Púčik" title="Martin Púčik"/></a> <a href="https://github.com/search?q=Martin%20Sch%C3%BCrrer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Martin Schürrer" title="Martin Schürrer"/></a> <a href="https://github.com/search?q=MarvinDontPanic"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="MarvinDontPanic" title="MarvinDontPanic"/></a> <a href="https://github.com/search?q=Mateusz%20Michalik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mateusz Michalik" title="Mateusz Michalik"/></a> <a href="https://github.com/search?q=Matias%20Wainsten"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matias Wainsten" title="Matias Wainsten"/></a> <a href="https://github.com/search?q=Matt%20Ezell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt Ezell" title="Matt Ezell"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Matthew%20Dicembrino"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matthew Dicembrino" title="Matthew Dicembrino"/></a> <a href="https://github.com/search?q=Mauro%20Bolis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mauro Bolis" title="Mauro Bolis"/></a> <a href="https://github.com/search?q=mcwigglesmcgee"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="mcwigglesmcgee" title="mcwigglesmcgee"/></a>
<a href="https://github.com/search?q=meaadore1221-afk"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="meaadore1221-afk" title="meaadore1221-afk"/></a> <a href="https://github.com/search?q=Mert%20%C3%87i%C3%A7ek%C3%A7i"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mert Çiçekçi" title="Mert Çiçekçi"/></a> <a href="https://github.com/search?q=Michael%20Verrilli"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Michael Verrilli" title="Michael Verrilli"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/search?q=minghinmatthewlam"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/search?q=Mr.%20Guy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mr. Guy" title="Mr. Guy"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/search?q=myfunc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/search?q=Nate"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Nate" title="Nate"/></a>
<a href="https://github.com/search?q=Nathaniel%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Nathaniel Kelner" title="Nathaniel Kelner"/></a> <a href="https://github.com/search?q=Netanel%20Draiman"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Netanel Draiman" title="Netanel Draiman"/></a> <a href="https://github.com/search?q=niceysam"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="niceysam" title="niceysam"/></a> <a href="https://github.com/search?q=Nick%20Lamb"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Nick Lamb" title="Nick Lamb"/></a> <a href="https://github.com/search?q=Nick%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Nick Taylor" title="Nick Taylor"/></a> <a href="https://github.com/search?q=Nikolay%20Petrov"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Nikolay Petrov" title="Nikolay Petrov"/></a> <a href="https://github.com/search?q=NM"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="NM" title="NM"/></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/search?q=norunners"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="norunners" title="norunners"/></a>
<a href="https://github.com/search?q=Ocean%20Vael"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ocean Vael" title="Ocean Vael"/></a> <a href="https://github.com/search?q=Ogulcan%20Celik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ogulcan Celik" title="Ogulcan Celik"/></a> <a href="https://github.com/search?q=Oleg%20Kossoy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Oleg Kossoy" title="Oleg Kossoy"/></a> <a href="https://github.com/Olshansk"><img src="https://avatars.githubusercontent.com/u/1892194?v=4&s=48" width="48" height="48" alt="Olshansk" title="Olshansk"/></a> <a href="https://github.com/search?q=Omar%20Khaleel"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Omar Khaleel" title="Omar Khaleel"/></a> <a href="https://github.com/search?q=OpenClaw%20Agent"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="OpenClaw Agent" title="OpenClaw Agent"/></a> <a href="https://github.com/search?q=Ozgur%20Polat"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ozgur Polat" title="Ozgur Polat"/></a> <a href="https://github.com/search?q=Pablo%20Nunez"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pablo Nunez" title="Pablo Nunez"/></a> <a href="https://github.com/search?q=Palash%20Oswal"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Palash Oswal" title="Palash Oswal"/></a> <a href="https://github.com/search?q=pasogott"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pasogott" title="pasogott"/></a>
<a href="https://github.com/search?q=Patrick%20Shao"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Patrick Shao" title="Patrick Shao"/></a> <a href="https://github.com/search?q=Paul%20Pamment"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Paul Pamment" title="Paul Pamment"/></a> <a href="https://github.com/search?q=Paulo%20Portella"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Paulo Portella" title="Paulo Portella"/></a> <a href="https://github.com/search?q=Peter%20Lee"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Peter Lee" title="Peter Lee"/></a> <a href="https://github.com/search?q=Petra%20Donka"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Petra Donka" title="Petra Donka"/></a> <a href="https://github.com/search?q=Pham%20Nam"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Pham Nam" title="Pham Nam"/></a> <a href="https://github.com/search?q=pierreeurope"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pierreeurope" title="pierreeurope"/></a> <a href="https://github.com/search?q=pip-nomel"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pip-nomel" title="pip-nomel"/></a> <a href="https://github.com/search?q=plum-dawg"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="plum-dawg" title="plum-dawg"/></a> <a href="https://github.com/search?q=pookNast"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pookNast" title="pookNast"/></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/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=rafaelreis-r"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></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/search?q=Ramin%20Shirali%20Hossein%20Zade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ramin Shirali Hossein Zade" title="Ramin Shirali Hossein Zade"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/search?q=Raphael%20Borg%20Ellul%20Vincenti"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Raphael Borg Ellul Vincenti" title="Raphael Borg Ellul Vincenti"/></a> <a href="https://github.com/search?q=Ratul%20Sarna"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ratul Sarna" title="Ratul Sarna"/></a> <a href="https://github.com/search?q=Richard%20Pinedo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Richard Pinedo" title="Richard Pinedo"/></a> <a href="https://github.com/search?q=Rick%20Qian"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rick Qian" title="Rick Qian"/></a>
<a href="https://github.com/search?q=robhparker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="robhparker" title="robhparker"/></a> <a href="https://github.com/search?q=Rohan%20Nagpal"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rohan Nagpal" title="Rohan Nagpal"/></a> <a href="https://github.com/search?q=Rohan%20Patil"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rohan Patil" title="Rohan Patil"/></a> <a href="https://github.com/rohanpatriot"><img src="https://avatars.githubusercontent.com/u/59978389?v=4&s=48" width="48" height="48" alt="rohanpatriot" title="rohanpatriot"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Ryan%20Nelson"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Nelson" title="Ryan Nelson"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/search?q=Santosh"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Santosh" title="Santosh"/></a> <a href="https://github.com/search?q=Sascha%20Reuter"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sascha Reuter" title="Sascha Reuter"/></a>
<a href="https://github.com/search?q=Saurabh.Chopade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Saurabh.Chopade" title="Saurabh.Chopade"/></a> <a href="https://github.com/search?q=saurav470"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="saurav470" title="saurav470"/></a> <a href="https://github.com/search?q=seans-openclawbot"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="seans-openclawbot" title="seans-openclawbot"/></a> <a href="https://github.com/SecondThread"><img src="https://avatars.githubusercontent.com/u/18317476?v=4&s=48" width="48" height="48" alt="SecondThread" title="SecondThread"/></a> <a href="https://github.com/search?q=seewhy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="seewhy" title="seewhy"/></a> <a href="https://github.com/search?q=Senol%20Dogan"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Senol Dogan" title="Senol Dogan"/></a> <a href="https://github.com/search?q=Sergiy%20Dybskiy"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sergiy Dybskiy" title="Sergiy Dybskiy"/></a> <a href="https://github.com/search?q=Shadow"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Shadow" title="Shadow"/></a> <a href="https://github.com/search?q=shatner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="shatner" title="shatner"/></a> <a href="https://github.com/search?q=Shaun%20Loo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Shaun Loo" title="Shaun Loo"/></a>
<a href="https://github.com/search?q=Shaun%20Mason"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Shaun Mason" title="Shaun Mason"/></a> <a href="https://github.com/search?q=Shiva%20Prasad"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Shiva Prasad" title="Shiva Prasad"/></a> <a href="https://github.com/search?q=Shrinija%20Kummari"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Shrinija Kummari" title="Shrinija Kummari"/></a> <a href="https://github.com/search?q=Siddhant%20Jain"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Siddhant Jain" title="Siddhant Jain"/></a> <a href="https://github.com/search?q=Simon%20Kelly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Simon Kelly" title="Simon Kelly"/></a> <a href="https://github.com/search?q=SK%20Heavy%20Industries"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="SK Heavy Industries" title="SK Heavy Industries"/></a> <a href="https://github.com/sldkfoiweuaranwdlaiwyeoaw"><img src="https://avatars.githubusercontent.com/u/2593660?v=4&s=48" width="48" height="48" alt="sldkfoiweuaranwdlaiwyeoaw" title="sldkfoiweuaranwdlaiwyeoaw"/></a> <a href="https://github.com/search?q=Soumyadeep%20Ghosh"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Soumyadeep Ghosh" title="Soumyadeep Ghosh"/></a> <a href="https://github.com/search?q=Spacefish"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Spacefish" title="Spacefish"/></a> <a href="https://github.com/search?q=spiceoogway"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="spiceoogway" title="spiceoogway"/></a>
<a href="https://github.com/search?q=Stephen%20Chen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Stephen Chen" title="Stephen Chen"/></a> <a href="https://github.com/search?q=Steve"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Steve" title="Steve"/></a> <a href="https://github.com/search?q=succ985"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="succ985" title="succ985"/></a> <a href="https://github.com/search?q=Suksham"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Suksham" title="Suksham"/></a> <a href="https://github.com/search?q=Sunwoo%20Yu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sunwoo Yu" title="Sunwoo Yu"/></a> <a href="https://github.com/search?q=Suvin%20Nimnaka"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Suvin Nimnaka" title="Suvin Nimnaka"/></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/swizzmagik"><img src="https://avatars.githubusercontent.com/u/3955528?v=4&s=48" width="48" height="48" alt="swizzmagik" title="swizzmagik"/></a> <a href="https://github.com/search?q=Tag"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tag" title="Tag"/></a> <a href="https://github.com/search?q=techboss"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="techboss" title="techboss"/></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/search?q=tewatia"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="tewatia" title="tewatia"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/search?q=therealZpoint-bot"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="therealZpoint-bot" title="therealZpoint-bot"/></a> <a href="https://github.com/search?q=tian%20Xiao"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="tian Xiao" title="tian Xiao"/></a> <a href="https://github.com/search?q=Tim%20Krase"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tim Krase" title="Tim Krase"/></a> <a href="https://github.com/search?q=Timo%20Lins"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Timo Lins" title="Timo Lins"/></a> <a href="https://github.com/search?q=Tom%20McKenzie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tom McKenzie" title="Tom McKenzie"/></a> <a href="https://github.com/search?q=Tom%20Peri"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tom Peri" title="Tom Peri"/></a> <a href="https://github.com/search?q=Tomas%20Hajek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tomas Hajek" title="Tomas Hajek"/></a>
<a href="https://github.com/search?q=Tomsun28"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tomsun28" title="Tomsun28"/></a> <a href="https://github.com/search?q=Tonic"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tonic" title="Tonic"/></a> <a href="https://github.com/search?q=Travis%20Hinton"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Travis Hinton" title="Travis Hinton"/></a> <a href="https://github.com/search?q=Travis%20Irby"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Travis Irby" title="Travis Irby"/></a> <a href="https://github.com/search?q=Tulsi%20Prasad"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tulsi Prasad" title="Tulsi Prasad"/></a> <a href="https://github.com/search?q=Ty%20Sabs"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ty Sabs" title="Ty Sabs"/></a> <a href="https://github.com/search?q=Tyler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tyler" title="Tyler"/></a> <a href="https://github.com/search?q=uos-status"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="uos-status" title="uos-status"/></a> <a href="https://github.com/search?q=Vai"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vai" title="Vai"/></a> <a href="https://github.com/search?q=Varun%20Kruthiventi"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Varun Kruthiventi" title="Varun Kruthiventi"/></a>
<a href="https://github.com/search?q=Vibe%20Kanban"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vibe Kanban" title="Vibe Kanban"/></a> <a href="https://github.com/search?q=Victor%20Castell"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Victor Castell" title="Victor Castell"/></a> <a href="https://github.com/search?q=victor-wu.eth"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="victor-wu.eth" title="victor-wu.eth"/></a> <a href="https://github.com/search?q=vikpos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="vikpos" title="vikpos"/></a> <a href="https://github.com/search?q=Vincent"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vincent" title="Vincent"/></a> <a href="https://github.com/search?q=VintLin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VintLin" title="VintLin"/></a> <a href="https://github.com/search?q=Vladimir%20Peshekhonov"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vladimir Peshekhonov" title="Vladimir Peshekhonov"/></a> <a href="https://github.com/search?q=void"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="void" title="void"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
<a href="https://github.com/search?q=williamtwomey"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="williamtwomey" title="williamtwomey"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=Winry"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Winry" title="Winry"/></a> <a href="https://github.com/search?q=Winston"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Winston" title="Winston"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></a> <a href="https://github.com/search?q=Xin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Xin" title="Xin"/></a> <a href="https://github.com/search?q=Xinhe%20Hu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Xinhe Hu" title="Xinhe Hu"/></a> <a href="https://github.com/search?q=Xu%20Haoran"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Xu Haoran" title="Xu Haoran"/></a> <a href="https://github.com/search?q=Yash"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yash" title="Yash"/></a> <a href="https://github.com/search?q=Yaxuan42"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yaxuan42" title="Yaxuan42"/></a>
<a href="https://github.com/search?q=Yazin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yazin" title="Yazin"/></a> <a href="https://github.com/search?q=Yevhen%20Bobrov"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yevhen Bobrov" title="Yevhen Bobrov"/></a> <a href="https://github.com/search?q=Yi%20Wang"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yi Wang" title="Yi Wang"/></a> <a href="https://github.com/search?q=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></a> <a href="https://github.com/search?q=Yuan%20Chen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yuan Chen" title="Yuan Chen"/></a> <a href="https://github.com/search?q=Yuanhai"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yuanhai" title="Yuanhai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/search?q=Zaf%20(via%20OpenClaw)"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zaf (via OpenClaw)" title="Zaf (via OpenClaw)"/></a> <a href="https://github.com/search?q=zhixian"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="zhixian" title="zhixian"/></a> <a href="https://github.com/search?q=%E7%9F%B3%E5%B7%9D%20%E8%AB%92"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="石川 諒" title="石川 諒"/></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/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/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/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></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/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></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/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/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/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/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></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/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/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></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/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> <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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/abdelsfane"><img src="https://avatars.githubusercontent.com/u/32418586?v=4&s=48" width="48" height="48" alt="abdelsfane" title="abdelsfane"/></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/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/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/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/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/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/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/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/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/ethanpalm"><img src="https://avatars.githubusercontent.com/u/56270045?v=4&s=48" width="48" height="48" alt="ethanpalm" title="ethanpalm"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/divanoli"><img src="https://avatars.githubusercontent.com/u/12023205?v=4&s=48" width="48" height="48" alt="divanoli" title="divanoli"/></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/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/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/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/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/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/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/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></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/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/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/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/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/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/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/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/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/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/CharlieGreenman"><img src="https://avatars.githubusercontent.com/u/8540141?v=4&s=48" width="48" height="48" alt="CharlieGreenman" title="CharlieGreenman"/></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/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/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/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/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/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/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/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/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/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/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/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/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/hirefrank"><img src="https://avatars.githubusercontent.com/u/183158?v=4&s=48" width="48" height="48" alt="hirefrank" title="hirefrank"/></a> <a href="https://github.com/M00N7682"><img src="https://avatars.githubusercontent.com/u/170746674?v=4&s=48" width="48" height="48" alt="M00N7682" title="M00N7682"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/0xRaini"><img src="https://avatars.githubusercontent.com/u/190923101?v=4&s=48" width="48" height="48" alt="0xRaini" title="0xRaini"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/dorukardahan"><img src="https://avatars.githubusercontent.com/u/35905596?v=4&s=48" width="48" height="48" alt="dorukardahan" title="dorukardahan"/></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/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/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/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/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/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/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/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/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/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/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/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/denysvitali"><img src="https://avatars.githubusercontent.com/u/4939519?v=4&s=48" width="48" height="48" alt="denysvitali" title="denysvitali"/></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/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/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/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/stakeswky"><img src="https://avatars.githubusercontent.com/u/64798754?v=4&s=48" width="48" height="48" alt="不做了睡大觉" title="不做了睡大觉"/></a> <a href="https://github.com/luijoc"><img src="https://avatars.githubusercontent.com/u/96428056?v=4&s=48" width="48" height="48" alt="Luis Conde" title="Luis Conde"/></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/SidQin-cyber"><img src="https://avatars.githubusercontent.com/u/201593046?v=4&s=48" width="48" height="48" alt="SidQin-cyber" title="SidQin-cyber"/></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/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/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/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/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/davidiach"><img src="https://avatars.githubusercontent.com/u/28102235?v=4&s=48" width="48" height="48" alt="davidiach" title="davidiach"/></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/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/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/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/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/minupla"><img src="https://avatars.githubusercontent.com/u/42547246?v=4&s=48" width="48" height="48" alt="minupla" title="minupla"/></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/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/dominicnunez"><img src="https://avatars.githubusercontent.com/u/43616264?v=4&s=48" width="48" height="48" alt="dominicnunez" title="dominicnunez"/></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/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/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/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/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/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/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/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/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/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/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/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/cdorsey"><img src="https://avatars.githubusercontent.com/u/12650570?v=4&s=48" width="48" height="48" alt="cdorsey" title="cdorsey"/></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/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/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/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/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/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/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/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/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/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/evanotero"><img src="https://avatars.githubusercontent.com/u/13204105?v=4&s=48" width="48" height="48" alt="evanotero" title="evanotero"/></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/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/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/garnetlyx"><img src="https://avatars.githubusercontent.com/u/12513503?v=4&s=48" width="48" height="48" alt="garnetlyx" title="garnetlyx"/></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/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/liebertar"><img src="https://avatars.githubusercontent.com/u/99405438?v=4&s=48" width="48" height="48" alt="liebertar" title="liebertar"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/pahdo"><img src="https://avatars.githubusercontent.com/u/12799392?v=4&s=48" width="48" height="48" alt="pahdo" title="pahdo"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/justinhuangcode"><img src="https://avatars.githubusercontent.com/u/252443740?v=4&s=48" width="48" height="48" alt="justinhuangcode" title="justinhuangcode"/></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/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/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/dinakars777"><img src="https://avatars.githubusercontent.com/u/250428393?v=4&s=48" width="48" height="48" alt="dinakars777" title="dinakars777"/></a>
<a href="https://github.com/Flash-LHR"><img src="https://avatars.githubusercontent.com/u/47357603?v=4&s=48" width="48" height="48" alt="Flash-LHR" title="Flash-LHR"/></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/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/kyleok"><img src="https://avatars.githubusercontent.com/u/58307870?v=4&s=48" width="48" height="48" alt="kyleok" title="kyleok"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/ThomsenDrake"><img src="https://avatars.githubusercontent.com/u/120344051?v=4&s=48" width="48" height="48" alt="ThomsenDrake" title="ThomsenDrake"/></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/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/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/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/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/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/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/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></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/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/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/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/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/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/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/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/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/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/DylanWoodAkers"><img src="https://avatars.githubusercontent.com/u/253595314?v=4&s=48" width="48" height="48" alt="DylanWoodAkers" title="DylanWoodAkers"/></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/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/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/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/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/GHesericsu"><img src="https://avatars.githubusercontent.com/u/60202455?v=4&s=48" width="48" height="48" alt="GHesericsu" title="GHesericsu"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/liuxiaopai-ai"><img src="https://avatars.githubusercontent.com/u/73659136?v=4&s=48" width="48" height="48" alt="Mark Liu" title="Mark Liu"/></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/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/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/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/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/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/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/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/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/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/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/danballance"><img src="https://avatars.githubusercontent.com/u/13839912?v=4&s=48" width="48" height="48" alt="danballance" title="danballance"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></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/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/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/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/Chloe-VP"><img src="https://avatars.githubusercontent.com/u/257371598?v=4&s=48" width="48" height="48" alt="Chloe-VP" title="Chloe-VP"/></a> <a href="https://github.com/dario-github"><img src="https://avatars.githubusercontent.com/u/40749119?v=4&s=48" width="48" height="48" alt="dario-github" title="dario-github"/></a> <a href="https://github.com/DarwinsBuddy"><img src="https://avatars.githubusercontent.com/u/490836?v=4&s=48" width="48" height="48" alt="DarwinsBuddy" title="DarwinsBuddy"/></a> <a href="https://github.com/David-Marsh-Photo"><img src="https://avatars.githubusercontent.com/u/228404527?v=4&s=48" width="48" height="48" alt="David-Marsh-Photo" title="David-Marsh-Photo"/></a> <a href="https://github.com/dcantu96"><img src="https://avatars.githubusercontent.com/u/32658690?v=4&s=48" width="48" height="48" alt="dcantu96" title="dcantu96"/></a> <a href="https://github.com/dndodson"><img src="https://avatars.githubusercontent.com/u/5123985?v=4&s=48" width="48" height="48" alt="dndodson" title="dndodson"/></a> <a href="https://github.com/dvrshil"><img src="https://avatars.githubusercontent.com/u/81693876?v=4&s=48" width="48" height="48" alt="dvrshil" title="dvrshil"/></a> <a href="https://github.com/dxd5001"><img src="https://avatars.githubusercontent.com/u/1886046?v=4&s=48" width="48" height="48" alt="dxd5001" title="dxd5001"/></a>
<a href="https://github.com/dylanneve1"><img src="https://avatars.githubusercontent.com/u/31746704?v=4&s=48" width="48" height="48" alt="dylanneve1" title="dylanneve1"/></a> <a href="https://github.com/EmberCF"><img src="https://avatars.githubusercontent.com/u/258471336?v=4&s=48" width="48" height="48" alt="EmberCF" title="EmberCF"/></a> <a href="https://github.com/ephraimm"><img src="https://avatars.githubusercontent.com/u/2803669?v=4&s=48" width="48" height="48" alt="ephraimm" title="ephraimm"/></a> <a href="https://github.com/ereid7"><img src="https://avatars.githubusercontent.com/u/27597719?v=4&s=48" width="48" height="48" alt="ereid7" title="ereid7"/></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/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/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/iamEvanYT"><img src="https://avatars.githubusercontent.com/u/47493765?v=4&s=48" width="48" height="48" alt="iamEvanYT" title="iamEvanYT"/></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/kesor"><img src="https://avatars.githubusercontent.com/u/7056?v=4&s=48" width="48" height="48" alt="kesor" title="kesor"/></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/Olshansk"><img src="https://avatars.githubusercontent.com/u/1892194?v=4&s=48" width="48" height="48" alt="Olshansk" title="Olshansk"/></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/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/SecondThread"><img src="https://avatars.githubusercontent.com/u/18317476?v=4&s=48" width="48" height="48" alt="SecondThread" title="SecondThread"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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> <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/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a>
</p>
+100 -3
View File
@@ -13,7 +13,7 @@ Report vulnerabilities directly to the repository where the issue lives:
- **ClawHub** — [openclaw/clawhub](https://github.com/openclaw/clawhub)
- **Trust and threat model** — [openclaw/trust](https://github.com/openclaw/trust)
For issues that don't fit a specific repo, or if you're unsure, email **security@openclaw.ai** and we'll route it.
For issues that don't fit a specific repo, or if you're unsure, email **[security@openclaw.ai](mailto:security@openclaw.ai)** and we'll route it.
For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
@@ -30,6 +30,40 @@ For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
Reports without reproduction steps, demonstrated impact, and remediation advice will be deprioritized. Given the volume of AI-generated scanner findings, we must ensure we're receiving vetted reports from researchers who understand the issues.
### Report Acceptance Gate (Triage Fast Path)
For fastest triage, include all of the following:
- Exact vulnerable path (`file`, function, and line range) on a current revision.
- Tested version details (OpenClaw version and/or commit SHA).
- Reproducible PoC against latest `main` or latest released version.
- Demonstrated impact tied to OpenClaw's documented trust boundaries.
- For exposed-secret reports: proof the credential is OpenClaw-owned (or grants access to OpenClaw-operated infrastructure/services).
- Explicit statement that the report does not rely on adversarial operators sharing one gateway host/config.
- Scope check explaining why the report is **not** covered by the Out of Scope section below.
Reports that miss these requirements may be closed as `invalid` or `no-action`.
### Common False-Positive Patterns
These are frequently reported but are typically closed with no code change:
- Prompt-injection-only chains without a boundary bypass (prompt injection is out of scope).
- Operator-intended local features (for example TUI local `!` shell) presented as remote injection.
- Authorized user-triggered local actions presented as privilege escalation. Example: an allowlisted/owner sender running `/export-session /absolute/path.html` to write on the host. In this trust model, authorized user actions are trusted host actions unless you demonstrate an auth/sandbox/boundary bypass.
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Missing HSTS findings on default local/loopback deployments.
- Slack webhook signature findings when HTTP mode already uses signing-secret verification.
- Discord inbound webhook signature findings for paths not used by this repo's Discord integration.
- Scanner-only claims against stale/nonexistent paths, or claims without a working repro.
### Duplicate Report Handling
- Search existing advisories before filing.
- Include likely duplicate GHSA IDs in your report when applicable.
- Maintainers may close lower-quality/later duplicates in favor of the earliest high-quality canonical report.
## Security & Trust
**Jamieson O'Reilly** ([@theonejvo](https://twitter.com/theonejvo)) is Security & Trust at OpenClaw. Jamieson is the founder of [Dvuln](https://dvuln.com) and brings extensive experience in offensive security, penetration testing, and security program development.
@@ -43,12 +77,34 @@ The best way to help the project right now is by sending PRs.
When patching a GHSA via `gh api`, include `X-GitHub-Api-Version: 2022-11-28` (or newer). Without it, some fields (notably CVSS) may not persist even if the request returns 200.
## Operator Trust Model (Important)
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
- Authenticated Gateway callers are treated as trusted operators for that gateway instance.
- Session identifiers (`sessionKey`, session IDs, labels) are routing controls, not per-user authorization boundaries.
- If one operator can view data from another operator on the same gateway, that is expected in this trust model.
- OpenClaw can technically run multiple gateway instances on one machine, but recommended operations are clean separation by trust boundary.
- Recommended mode: one user per machine/host (or VPS), one gateway for that user, and one or more agents inside that gateway.
- If multiple users need OpenClaw, use one VPS (or host/OS user boundary) per user.
- For advanced setups, multiple gateways on one machine are possible, but only with strict isolation and are not the recommended default.
- Exec behavior is host-first by default: `agents.defaults.sandbox.mode` defaults to `off`.
- `tools.exec.host` defaults to `sandbox` as a routing preference, but if sandbox runtime is not active for the session, exec runs on the gateway host.
- Implicit exec calls (no explicit host in the tool call) follow the same behavior.
- This is expected in OpenClaw's one-user trusted-operator model. If you need isolation, enable sandbox mode (`non-main`/`all`) and keep strict tool policy.
## Out of Scope
- Public Internet Exposure
- Using OpenClaw in ways that the docs recommend not to
- Deployments where mutually untrusted/adversarial operators share one gateway host and config
- Prompt injection attacks
- Deployments where mutually untrusted/adversarial operators share one gateway host and config (for example, reports expecting per-operator isolation for `sessions.list`, `sessions.preview`, `chat.history`, or similar control-plane reads)
- Prompt-injection-only attacks (without a policy/auth/sandbox boundary bypass)
- Reports that require write access to trusted local state (`~/.openclaw`, workspace files like `MEMORY.md` / `memory/*.md`)
- Reports where the only demonstrated impact is an already-authorized sender intentionally invoking a local-action command (for example `/export-session` writing to an absolute host path) without bypassing auth, sandbox, or another documented boundary
- Any report whose only claim is that an operator-enabled `dangerous*`/`dangerously*` config option weakens defaults (these are explicit break-glass tradeoffs by design)
- Reports that depend on trusted operator-supplied configuration values to trigger availability impact (for example custom regex patterns). These may still be fixed as defense-in-depth hardening, but are not security-boundary bypasses.
- Exposed secrets that are third-party/user-controlled credentials (not OpenClaw-owned and not granting access to OpenClaw-operated infrastructure/services) without demonstrated OpenClaw impact
- Reports whose only claim is host-side exec when sandbox runtime is disabled/unavailable (documented default behavior in the trusted-operator model), without a boundary bypass.
## Deployment Assumptions
@@ -57,6 +113,43 @@ OpenClaw security guidance assumes:
- The host where OpenClaw runs is within a trusted OS/admin boundary.
- Anyone who can modify `~/.openclaw` state/config (including `openclaw.json`) is effectively a trusted operator.
- A single Gateway shared by mutually untrusted people is **not a recommended setup**. Use separate gateways (or at minimum separate OS users/hosts) per trust boundary.
- Authenticated Gateway callers are treated as trusted operators. Session identifiers (for example `sessionKey`) are routing controls, not per-user authorization boundaries.
- Multiple gateway instances can run on one machine, but the recommended model is clean per-user isolation (prefer one host/VPS per user).
## One-User Trust Model (Personal Assistant)
OpenClaw's security model is "personal assistant" (one trusted operator, potentially many agents), not "shared multi-tenant bus."
- If multiple people can message the same tool-enabled agent (for example a shared Slack workspace), they can all steer that agent within its granted permissions.
- Session or memory scoping reduces context bleed, but does **not** create per-user host authorization boundaries.
- For mixed-trust or adversarial users, isolate by OS user/host/gateway and use separate credentials per boundary.
- A company-shared agent can be a valid setup when users are in the same trust boundary and the agent is strictly business-only.
- For company-shared setups, use a dedicated machine/VM/container and dedicated accounts; avoid mixing personal data on that runtime.
- If that host/browser profile is logged into personal accounts (for example Apple/Google/personal password manager), you have collapsed the boundary and increased personal-data exposure risk.
## Agent and Model Assumptions
- The model/agent is **not** a trusted principal. Assume prompt/content injection can manipulate behavior.
- Security boundaries come from host/config trust, auth, tool policy, sandboxing, and exec approvals.
- Prompt injection by itself is not a vulnerability report unless it crosses one of those boundaries.
## Gateway and Node trust concept
OpenClaw separates routing from execution, but both remain inside the same operator trust boundary:
- **Gateway** is the control plane. If a caller passes Gateway auth, they are treated as a trusted operator for that Gateway.
- **Node** is an execution extension of the Gateway. Pairing a node grants operator-level remote capability on that node.
- **Exec approvals** (allowlist/ask UI) are operator guardrails to reduce accidental command execution, not a multi-tenant authorization boundary.
- For untrusted-user isolation, split by trust boundary: separate gateways and separate OS users/hosts per boundary.
## Workspace Memory Trust Boundary
`MEMORY.md` and `memory/*.md` are plain workspace files and are treated as trusted local operator state.
- If someone can edit workspace memory files, they already crossed the trusted operator boundary.
- Memory search indexing/recall over those files is expected behavior, not a sandbox/security boundary.
- Example report pattern considered out of scope: "attacker writes malicious content into `memory/*.md`, then `memory_search` returns it."
- If you need isolation between mutually untrusted users, split by OS user or host and run separate gateways.
## Plugin Trust Boundary
@@ -85,6 +178,10 @@ OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for *
- Recommended: keep the Gateway **loopback-only** (`127.0.0.1` / `::1`).
- Config: `gateway.bind="loopback"` (default).
- CLI: `openclaw gateway run --bind loopback`.
- `gateway.controlUi.dangerouslyDisableDeviceAuth` is intended for localhost-only break-glass use.
- OpenClaw keeps deployment flexibility by design and does not hard-forbid non-local setups.
- Non-local and other risky configurations are surfaced by `openclaw security audit` as dangerous findings.
- This operator-selected tradeoff is by design and not, by itself, a security vulnerability.
- Canvas host note: network-visible canvas is **intentional** for trusted node scenarios (LAN/tailnet).
- Expected setup: non-loopback bind + Gateway auth (token/password/trusted-proxy) + firewall/tailnet controls.
- Expected routes: `/__openclaw__/canvas/`, `/__openclaw__/a2ui/`.
+233 -137
View File
@@ -209,155 +209,251 @@
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.15/OpenClaw-2026.2.15.zip" length="22896513" type="application/octet-stream" sparkle:edSignature="MLGsd2NeHXFRH1Or0bFQnAjqfuuJDuhl1mvKFIqTQcRvwbeyvOyyLXrqSbmaOgJR3wBQBKLs6jYQ9dQ/3R8RCg=="/>
</item>
<item>
<title>2026.2.21</title>
<pubDate>Sat, 21 Feb 2026 17:55:48 +0100</pubDate>
<title>2026.2.22</title>
<pubDate>Mon, 23 Feb 2026 01:51:13 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>13056</sparkle:version>
<sparkle:shortVersionString>2026.2.21</sparkle:shortVersionString>
<sparkle:version>14126</sparkle:version>
<sparkle:shortVersionString>2026.2.22</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.21</h2>
<description><![CDATA[<h2>OpenClaw 2026.2.22</h2>
<h3>Changes</h3>
<ul>
<li>Models/Google: add Gemini 3.1 support (<code>google/gemini-3.1-pro-preview</code>).</li>
<li>Providers/Onboarding: add Volcano Engine (Doubao) and BytePlus providers/models (including coding variants), wire onboarding auth choices for interactive + non-interactive flows, and align docs to <code>volcengine-api-key</code>. (#7967) Thanks @funmore123.</li>
<li>Channels/CLI: add per-account/channel <code>defaultTo</code> outbound routing fallback so <code>openclaw agent --deliver</code> can send without explicit <code>--reply-to</code> when a default target is configured. (#16985) Thanks @KirillShchetinin.</li>
<li>Channels: allow per-channel model overrides via <code>channels.modelByChannel</code> and note them in /status. Thanks @thewilloftheshadow.</li>
<li>Telegram/Streaming: simplify preview streaming config to <code>channels.telegram.streaming</code> (boolean), auto-map legacy <code>streamMode</code> values, and remove block-vs-partial preview branching. (#22012) thanks @obviyus.</li>
<li>Discord/Streaming: add stream preview mode for live draft replies with partial/block options and configurable chunking. Thanks @thewilloftheshadow. Inspiration @neoagentic-ship-it.</li>
<li>Discord/Telegram: add configurable lifecycle status reactions for queued/thinking/tool/done/error phases with a shared controller and emoji/timing overrides. Thanks @wolly-tundracube and @thewilloftheshadow.</li>
<li>Discord/Voice: add voice channel join/leave/status via <code>/vc</code>, plus auto-join configuration for realtime voice conversations. Thanks @thewilloftheshadow.</li>
<li>Discord: add configurable ephemeral defaults for slash-command responses. (#16563) Thanks @wei.</li>
<li>Discord: support updating forum <code>available_tags</code> via channel edit actions for forum tag management. (#12070) Thanks @xiaoyaner0201.</li>
<li>Discord: include channel topics in trusted inbound metadata on new sessions. Thanks @thewilloftheshadow.</li>
<li>Discord/Subagents: add thread-bound subagent sessions on Discord with per-thread focus/list controls and thread-bound continuation routing for spawned helper agents. (#21805) Thanks @onutc.</li>
<li>iOS/Chat: clean chat UI noise by stripping inbound untrusted metadata/timestamp prefixes, formatting tool outputs into concise summaries/errors, compacting the composer while typing, and supporting tap-to-dismiss keyboard in chat view. (#22122) thanks @mbelinky.</li>
<li>iOS/Watch: bridge mirrored watch prompt notification actions into iOS quick-reply handling, including queued action handoff until app model initialization. (#22123) thanks @mbelinky.</li>
<li>iOS/Gateway: stabilize background wake and reconnect behavior with background reconnect suppression/lease windows, BGAppRefresh wake fallback, location wake hook throttling, and APNs wake retry+nudge instrumentation. (#21226) thanks @mbelinky.</li>
<li>Auto-reply/UI: add model fallback lifecycle visibility in verbose logs, /status active-model context with fallback reason, and cohesive WebUI fallback indicators. (#20704) Thanks @joshavant.</li>
<li>MSTeams: dedupe sent-message cache storage by removing duplicate per-message Set storage and using timestamps Map keys as the single membership source. (#22514) Thanks @TaKO8Ki.</li>
<li>Agents/Subagents: default subagent spawn depth now uses shared <code>maxSpawnDepth=2</code>, enabling depth-1 orchestrator spawning by default while keeping depth policy checks consistent across spawn and prompt paths. (#22223) Thanks @tyler6204.</li>
<li>Security/Agents: make owner-ID obfuscation use a dedicated HMAC secret from configuration (<code>ownerDisplaySecret</code>) and update hashing behavior so obfuscation is decoupled from gateway token handling for improved control. (#7343) Thanks @vincentkoc.</li>
<li>Security/Infra: switch gateway lock and tool-call synthetic IDs from SHA-1 to SHA-256 with unchanged truncation length to strengthen hash basis while keeping deterministic behavior and lock key format. (#7343) Thanks @vincentkoc.</li>
<li>Dependencies/Tooling: add non-blocking dead-code scans in CI via Knip/ts-prune/ts-unused-exports to surface unused dependencies and exports earlier. (#22468) Thanks @vincentkoc.</li>
<li>Dependencies/Unused Dependencies: remove or scope unused root and extension deps (<code>@larksuiteoapi/node-sdk</code>, <code>signal-utils</code>, <code>ollama</code>, <code>lit</code>, <code>@lit/context</code>, <code>@lit-labs/signals</code>, <code>@microsoft/agents-hosting-express</code>, <code>@microsoft/agents-hosting-extensions-teams</code>, and plugin-local <code>openclaw</code> devDeps in <code>extensions/open-prose</code>, <code>extensions/lobster</code>, and <code>extensions/llm-task</code>). (#22471, #22495) Thanks @vincentkoc.</li>
<li>Dependencies/A2UI: harden dependency resolution after root cleanup (resolve <code>lit</code>, <code>@lit/context</code>, <code>@lit-labs/signals</code>, and <code>signal-utils</code> from workspace/root) and simplify bundling fallback behavior, including <code>pnpm dlx rolldown</code> compatibility. (#22481, #22507) Thanks @vincentkoc.</li>
<li>Provider/Mistral: add support for the Mistral provider, including memory embeddings and voice support. (#23845) Thanks @vincentkoc.</li>
<li>Update/Core: add an optional built-in auto-updater for package installs (<code>update.auto.*</code>), default-off, with stable rollout delay+jitter and beta hourly cadence.</li>
<li>CLI/Update: add <code>openclaw update --dry-run</code> to preview channel/tag/target/restart actions without mutating config, installing, syncing plugins, or restarting.</li>
<li>Config/UI: add tag-aware settings filtering and broaden config labels/help copy so fields are easier to discover and understand in the dashboard config screen.</li>
<li>Channels/Synology Chat: add a native Synology Chat channel plugin with webhook ingress, direct-message routing, outbound send/media support, per-account config, and DM policy controls. (#23012)</li>
<li>iOS/Talk: prefetch TTS segments and suppress expected speech-cancellation errors for smoother talk playback. (#22833) Thanks @ngutman.</li>
<li>Memory/FTS: add Spanish and Portuguese stop-word filtering for query expansion in FTS-only search mode, improving conversational recall for both languages. Thanks @vincentkoc.</li>
<li>Memory/FTS: add Japanese-aware query expansion tokenization and stop-word filtering (including mixed-script terms like ASCII + katakana) for FTS-only search mode. Thanks @vincentkoc.</li>
<li>Memory/FTS: add Korean stop-word filtering and particle-aware keyword extraction (including mixed Korean/English stems) for query expansion in FTS-only search mode. (#18899) Thanks @ruypang.</li>
<li>Memory/FTS: add Arabic stop-word filtering for query expansion in FTS-only search mode to reduce conversational filler in Arabic memory searches. Thanks @vincentkoc.</li>
<li>Discord/Allowlist: canonicalize resolved Discord allowlist names to IDs and split resolution flow for clearer fail-closed behavior.</li>
<li>Channels/Config: unify channel preview streaming config handling with a shared resolver and canonical migration path.</li>
<li>Gateway/Auth: unify call/probe/status/auth credential-source precedence on shared resolver helpers, with table-driven parity coverage across gateway entrypoints.</li>
<li>Gateway/Auth: refactor gateway credential resolution and websocket auth handshake paths to use shared typed auth contexts, including explicit <code>auth.deviceToken</code> support in connect frames and tests.</li>
<li>Skills: remove bundled <code>food-order</code> skill from this repo; manage/install it from ClawHub instead.</li>
<li>Docs/Subagents: make thread-bound session guidance channel-first instead of Discord-specific, and list thread-supporting channels explicitly. (#23589) Thanks @osolmaz.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> tool-failure replies now hide raw error details by default. OpenClaw still sends a failure summary, but detailed error suffixes (for example provider/runtime messages and local path fragments) now require <code>/verbose on</code> or <code>/verbose full</code>.</li>
<li><strong>BREAKING:</strong> CLI local onboarding now sets <code>session.dmScope</code> to <code>per-channel-peer</code> by default for new/implicit DM scope configuration. If you depend on shared DM continuity across senders, explicitly set <code>session.dmScope</code> to <code>main</code>. (#23468) Thanks @bmendonca3.</li>
<li><strong>BREAKING:</strong> unify channel preview-streaming config to <code>channels.<channel>.streaming</code> with enum values <code>off | partial | block | progress</code>, and move Slack native stream toggle to <code>channels.slack.nativeStreaming</code>. Legacy keys (<code>streamMode</code>, Slack boolean <code>streaming</code>) are still read and migrated by <code>openclaw doctor --fix</code>, but canonical saved config/docs now use the unified names.</li>
<li><strong>BREAKING:</strong> remove legacy Gateway device-auth signature <code>v1</code>. Device-auth clients must now sign <code>v2</code> payloads with the per-connection <code>connect.challenge</code> nonce and send <code>device.nonce</code>; nonce-less connects are rejected.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security/Agents: cap embedded Pi runner outer retry loop with a higher profile-aware dynamic limit (32-160 attempts) and return an explicit <code>retry_limit</code> error payload when retries never converge, preventing unbounded internal retry cycles (<code>GHSA-76m6-pj3w-v7mf</code>).</li>
<li>Telegram: detect duplicate bot-token ownership across Telegram accounts at startup/status time, mark secondary accounts as not configured with an explicit fix message, and block duplicate account startup before polling to avoid endless <code>getUpdates</code> conflict loops.</li>
<li>Agents/Tool images: include source filenames in <code>agents/tool-images</code> resize logs so compression events can be traced back to specific files.</li>
<li>Providers/OAuth: harden Qwen and Chutes refresh handling by validating refresh response expiry values and preserving prior refresh tokens when providers return empty refresh token fields, with regression coverage for empty-token responses.</li>
<li>Models/Kimi-Coding: add missing implicit provider template for <code>kimi-coding</code> with correct <code>anthropic-messages</code> API type and base URL, fixing 403 errors when using Kimi for Coding. (#22409)</li>
<li>Auto-reply/Tools: forward <code>senderIsOwner</code> through embedded queued/followup runner params so owner-only tools remain available for authorized senders. (#22296) thanks @hcoj.</li>
<li>Discord: restore model picker back navigation when a provider is missing and document the Discord picker flow. (#21458) Thanks @pejmanjohn and @thewilloftheshadow.</li>
<li>Memory/QMD: respect per-agent <code>memorySearch.enabled=false</code> during gateway QMD startup initialization, split multi-collection QMD searches into per-collection queries (<code>search</code>/<code>vsearch</code>/<code>query</code>) to avoid sparse-term drops, prefer collection-hinted doc resolution to avoid stale-hash collisions, retry boot updates on transient lock/timeout failures, skip <code>qmd embed</code> in BM25-only <code>search</code> mode (including <code>memory index --force</code>), and serialize embed runs globally with failure backoff to prevent CPU storms on multi-agent hosts. (#20581, #21590, #20513, #20001, #21266, #21583, #20346, #19493) Thanks @danielrevivo, @zanderkrause, @sunyan034-cmd, @tilleulenspiegel, @dae-oss, @adamlongcreativellc, @jonathanadams96, and @kiliansitel.</li>
<li>Memory/Builtin: prevent automatic sync races with manager shutdown by skipping post-close sync starts and waiting for in-flight sync before closing SQLite, so <code>onSearch</code>/<code>onSessionStart</code> no longer fail with <code>database is not open</code> in ephemeral CLI flows. (#20556, #7464) Thanks @FuzzyTG and @henrybottter.</li>
<li>Providers/Copilot: drop persisted assistant <code>thinking</code> blocks for Claude models (while preserving turn structure/tool blocks) so follow-up requests no longer fail on invalid <code>thinkingSignature</code> payloads. (#19459) Thanks @jackheuberger.</li>
<li>Providers/Copilot: add <code>claude-sonnet-4.6</code> and <code>claude-sonnet-4.5</code> to the default GitHub Copilot model catalog and add coverage for model-list/definition helpers. (#20270, fixes #20091) Thanks @Clawborn.</li>
<li>Auto-reply/WebChat: avoid defaulting inbound runtime channel labels to unrelated providers (for example <code>whatsapp</code>) for webchat sessions so channel-specific formatting guidance stays accurate. (#21534) Thanks @lbo728.</li>
<li>Status: include persisted <code>cacheRead</code>/<code>cacheWrite</code> in session summaries so compact <code>/status</code> output consistently shows cache hit percentages from real session data.</li>
<li>Heartbeat/Cron: restore interval heartbeat behavior so missing <code>HEARTBEAT.md</code> no longer suppresses runs (only effectively empty files skip), preserving prompt-driven and tagged-cron execution paths.</li>
<li>WhatsApp/Cron/Heartbeat: enforce allowlisted routing for implicit scheduled/system delivery by merging pairing-store + configured <code>allowFrom</code> recipients, selecting authorized recipients when last-route context points to a non-allowlisted chat, and preventing heartbeat fan-out to recent unauthorized chats.</li>
<li>Heartbeat/Active hours: constrain active-hours <code>24</code> sentinel parsing to <code>24:00</code> in time validation so invalid values like <code>24:30</code> are rejected early. (#21410) thanks @adhitShet.</li>
<li>Heartbeat: treat <code>activeHours</code> windows with identical <code>start</code>/<code>end</code> times as zero-width (always outside the window) instead of always-active. (#21408) thanks @adhitShet.</li>
<li>CLI/Pairing: default <code>pairing list</code> and <code>pairing approve</code> to the sole available pairing channel when omitted, so TUI-only setups can recover from <code>pairing required</code> without guessing channel arguments. (#21527) Thanks @losts1.</li>
<li>TUI/Pairing: show explicit pairing-required recovery guidance after gateway disconnects that return <code>pairing required</code>, including approval steps to unblock quickstart TUI hatching on fresh installs. (#21841) Thanks @nicolinux.</li>
<li>TUI/Input: suppress duplicate backspace events arriving in the same input burst window so SSH sessions no longer delete two characters per backspace press in the composer. (#19318) Thanks @eheimer.</li>
<li>TUI/Heartbeat: suppress heartbeat ACK/prompt noise in chat streaming when <code>showOk</code> is disabled, while still preserving non-ACK heartbeat alerts in final output. (#20228) Thanks @bhalliburton.</li>
<li>TUI/History: cap chat-log component growth and prune stale render nodes/references so large default history loads no longer overflow render recursion with <code>RangeError: Maximum call stack size exceeded</code>. (#18068) Thanks @JaniJegoroff.</li>
<li>Memory/QMD: diversify mixed-source search ranking when both session and memory collections are present so session transcript hits no longer crowd out durable memory-file matches in top results. (#19913) Thanks @alextempr.</li>
<li>Memory/Tools: return explicit <code>unavailable</code> warnings/actions from <code>memory_search</code> when embedding/provider failures occur (including quota exhaustion), so disabled memory does not look like an empty recall result. (#21894) Thanks @XBS9.</li>
<li>Session/Startup: require the <code>/new</code> and <code>/reset</code> greeting path to run Session Startup file-reading instructions before responding, so daily memory startup context is not skipped on fresh-session greetings. (#22338) Thanks @armstrong-pv.</li>
<li>Auth/Onboarding: align OAuth profile-id config mapping with stored credential IDs for OpenAI Codex and Chutes flows, preventing <code>provider:default</code> mismatches when OAuth returns email-scoped credentials. (#12692) thanks @mudrii.</li>
<li>Provider/HTTP: treat HTTP 503 as failover-eligible for LLM provider errors. (#21086) Thanks @Protocol-zero-0.</li>
<li>Slack: pass <code>recipient_team_id</code> / <code>recipient_user_id</code> through Slack native streaming calls so <code>chat.startStream</code>/<code>appendStream</code>/<code>stopStream</code> work reliably across DMs and Slack Connect setups, and disable block streaming when native streaming is active. (#20988) Thanks @Dithilli. Earlier recipient-ID groundwork was contributed in #20377 by @AsserAl1012.</li>
<li>CLI/Config: add canonical <code>--strict-json</code> parsing for <code>config set</code> and keep <code>--json</code> as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet.</li>
<li>CLI: keep <code>openclaw -v</code> as a root-only version alias so subcommand <code>-v, --verbose</code> flags (for example ACP/hooks/skills) are no longer intercepted globally. (#21303) thanks @adhitShet.</li>
<li>Memory: return empty snippets when <code>memory_get</code>/QMD read files that have not been created yet, and harden memory indexing/session helpers against ENOENT races so missing Markdown no longer crashes tools. (#20680) Thanks @pahdo.</li>
<li>Telegram/Streaming: always clean up draft previews even when dispatch throws before fallback handling, preventing orphaned preview messages during failed runs. (#19041) thanks @mudrii.</li>
<li>Telegram/Streaming: split reasoning and answer draft preview lanes to prevent cross-lane overwrites, and ignore literal <code><think></code> tags inside inline/fenced code snippets so sample markup is not misrouted as reasoning. (#20774) Thanks @obviyus.</li>
<li>Telegram/Streaming: restore 30-char first-preview debounce and scope <code>NO_REPLY</code> prefix suppression to partial sentinel fragments so normal <code>No...</code> text is not filtered. (#22613) thanks @obviyus.</li>
<li>Telegram/Status reactions: refresh stall timers on repeated phase updates and honor ack-reaction scope when lifecycle reactions are enabled, preventing false stall emojis and unwanted group reactions. Thanks @wolly-tundracube and @thewilloftheshadow.</li>
<li>Telegram/Status reactions: keep lifecycle reactions active when available-reactions lookup fails by falling back to unrestricted variant selection instead of suppressing reaction updates. (#22380) thanks @obviyus.</li>
<li>Discord/Streaming: apply <code>replyToMode: first</code> only to the first Discord chunk so block-streamed replies do not spam mention pings. (#20726) Thanks @thewilloftheshadow for the report.</li>
<li>Discord/Components: map DM channel targets back to user-scoped component sessions so button/select interactions stay in the main DM session. Thanks @thewilloftheshadow.</li>
<li>Discord/Allowlist: lazy-load guild lists when resolving Discord user allowlists so ID-only entries resolve even if guild fetch fails. (#20208) Thanks @zhangjunmengyang.</li>
<li>Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow.</li>
<li>Discord: ingest inbound stickers as media so sticker-only messages and forwarded stickers are visible to agents. Thanks @thewilloftheshadow.</li>
<li>Auto-reply/Runner: emit <code>onAgentRunStart</code> only after agent lifecycle or tool activity begins (and only once per run), so fallback preflight errors no longer mark runs as started. (#21165) Thanks @shakkernerd.</li>
<li>Auto-reply/Tool results: serialize tool-result delivery and keep the delivery chain progressing after individual failures so concurrent tool outputs preserve user-visible ordering. (#21231) thanks @ahdernasr.</li>
<li>Auto-reply/Prompt caching: restore prefix-cache stability by keeping inbound system metadata session-stable and moving per-message IDs (<code>message_id</code>, <code>message_id_full</code>, <code>reply_to_id</code>, <code>sender_id</code>) into untrusted conversation context. (#20597) Thanks @anisoptera.</li>
<li>iOS/Watch: add actionable watch approval/reject controls and quick-reply actions so watch-originated approvals and responses can be sent directly from notification flows. (#21996) Thanks @mbelinky.</li>
<li>iOS/Watch: refresh iOS and watch app icon assets with the lobster icon set to keep phone/watch branding aligned. (#21997) Thanks @mbelinky.</li>
<li>CLI/Onboarding: fix Anthropic-compatible custom provider verification by normalizing base URLs to avoid duplicate <code>/v1</code> paths during setup checks. (#21336) Thanks @17jmumford.</li>
<li>iOS/Gateway/Tools: prefer uniquely connected node matches when duplicate display names exist, surface actionable <code>nodes invoke</code> pairing-required guidance with request IDs, and refresh active iOS gateway registration after location-capability setting changes so capability updates apply immediately. (#22120) thanks @mbelinky.</li>
<li>Gateway/Auth: require <code>gateway.trustedProxies</code> to include a loopback proxy address when <code>auth.mode="trusted-proxy"</code> and <code>bind="loopback"</code>, preventing same-host proxy misconfiguration from silently blocking auth. (#22082, follow-up to #20097) thanks @mbelinky.</li>
<li>Gateway/Auth: allow trusted-proxy mode with loopback bind for same-host reverse-proxy deployments, while still requiring configured <code>gateway.trustedProxies</code>. (#20097) thanks @xinhuagu.</li>
<li>Gateway/Auth: allow authenticated clients across roles/scopes to call <code>health</code> while preserving role and scope enforcement for non-health methods. (#19699) thanks @Nachx639.</li>
<li>Gateway/Hooks: include transform export name in hook-transform cache keys so distinct exports from the same module do not reuse the wrong cached transform function. (#13855) thanks @mcaxtr.</li>
<li>Gateway/Control UI: return 404 for missing static-asset paths instead of serving SPA fallback HTML, while preserving client-route fallback behavior for extensionless and non-asset dotted paths. (#12060) thanks @mcaxtr.</li>
<li>Gateway/Pairing: prevent device-token rotate scope escalation by enforcing an approved-scope baseline, preserving approved scopes across metadata updates, and rejecting rotate requests that exceed approved role scope implications. (#20703) thanks @coygeek.</li>
<li>Gateway/Pairing: clear persisted paired-device state when the gateway client closes with <code>device token mismatch</code> (<code>1008</code>) so reconnect flows can cleanly re-enter pairing. (#22071) Thanks @mbelinky.</li>
<li>Gateway/Config: allow <code>gateway.customBindHost</code> in strict config validation when <code>gateway.bind="custom"</code> so valid custom bind-host configurations no longer fail startup. (#20318, fixes #20289) Thanks @MisterGuy420.</li>
<li>Gateway/Pairing: tolerate legacy paired devices missing <code>roles</code>/<code>scopes</code> metadata in websocket upgrade checks and backfill metadata on reconnect. (#21447, fixes #21236) Thanks @joshavant.</li>
<li>Gateway/Pairing/CLI: align read-scope compatibility in pairing/device-token checks and add local <code>openclaw devices</code> fallback recovery for loopback <code>pairing required</code> deadlocks, with explicit fallback notice to unblock approval bootstrap flows. (#21616) Thanks @shakkernerd.</li>
<li>Security/CLI: redact sensitive values in <code>openclaw config get</code> output before printing config paths, preventing credential leakage to terminal output/history. (#13683) Thanks @SleuthCo.</li>
<li>Install/Discord Voice: make <code>@discordjs/opus</code> an optional dependency so <code>openclaw</code> install/update no longer hard-fails when native Opus builds fail, while keeping <code>opusscript</code> as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) Thanks @jeadland, @Sheetaa, and @Breakyman.</li>
<li>Docker/Setup: precreate <code>$OPENCLAW_CONFIG_DIR/identity</code> during <code>docker-setup.sh</code> so CLI commands that need device identity (for example <code>devices list</code>) avoid <code>EACCES ... /home/node/.openclaw/identity</code> failures on restrictive bind mounts. (#23948) Thanks @ackson-beep.</li>
<li>Exec/Background: stop applying the default exec timeout to background sessions (<code>background: true</code> or explicit <code>yieldMs</code>) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303)</li>
<li>Slack/Threading: sessions: keep parent-session forking and thread-history context active beyond first turn by removing first-turn-only gates in session init, thread-history fetch, and reply prompt context injection. (#23843, #23090) Thanks @vincentkoc and @Taskle.</li>
<li>Slack/Threading: respect <code>replyToMode</code> when Slack auto-populates top-level <code>thread_ts</code>, and ignore inline <code>replyToId</code> directive tags when <code>replyToMode</code> is <code>off</code> so thread forcing stays disabled unless explicitly configured. (#23839, #23320, #23513) Thanks @vincentkoc and @dorukardahan.</li>
<li>Slack/Extension: forward <code>message read</code> <code>threadId</code> to <code>readMessages</code> and use delivery-context <code>threadId</code> as outbound <code>thread_ts</code> fallback so extension replies/reads stay in the correct Slack thread. (#22216, #22485, #23836) Thanks @vincentkoc, @lan17 and @dorukardahan.</li>
<li>Slack/Upload: resolve bare user IDs (U-prefix) to DM channel IDs via <code>conversations.open</code> before calling <code>files.uploadV2</code>, which rejects non-channel IDs. <code>chat.postMessage</code> tolerates user IDs directly, but <code>files.uploadV2</code><code>completeUploadExternal</code> validates <code>channel_id</code> against <code>^[CGDZ][A-Z0-9]{8,}$</code>, causing <code>invalid_arguments</code> when agents reply with media to DM conversations.</li>
<li>Webchat/Chat: apply assistant <code>final</code> payload messages directly to chat state so sent turns render without waiting for a full history refresh cycle. (#14928) Thanks @BradGroux.</li>
<li>Webchat/Chat: for out-of-band final events (for example tool-call side runs), append provided final assistant payloads directly instead of forcing a transient history reset. (#11139) Thanks @AkshayNavle.</li>
<li>Webchat/Performance: reload <code>chat.history</code> after final events only when the final payload lacks a renderable assistant message, avoiding expensive full-history refreshes on normal turns. (#20588) Thanks @amzzzzzzz.</li>
<li>Webchat/Sessions: preserve external session routing metadata when internal <code>chat.send</code> turns run under <code>webchat</code>, so explicit channel-keyed sessions (for example Telegram) no longer get rewritten to <code>webchat</code> and misroute follow-up delivery. (#23258) Thanks @binary64.</li>
<li>Webchat/Sessions: preserve existing session <code>label</code> across <code>/new</code> and <code>/reset</code> rollovers so reset sessions remain discoverable in session history lists. (#23755) Thanks @ThunderStormer.</li>
<li>Gateway/Chat UI: strip inline reply/audio directive tags from non-streaming final webchat broadcasts (including <code>chat.inject</code>) while preserving empty-string message content when tags are the entire reply. (#23298) Thanks @SidQin-cyber.</li>
<li>Chat/UI: strip inline reply/audio directive tags (<code>[[reply_to_current]]</code>, <code>[[reply_to:<id>]]</code>, <code>[[audio_as_voice]]</code>) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces.</li>
<li>Telegram/Media: send a user-facing Telegram reply when media download fails (non-size errors) instead of silently dropping the message.</li>
<li>Telegram/Webhook: keep webhook monitors alive until gateway abort signals fire, preventing false channel exits and immediate webhook auto-restart loops.</li>
<li>Telegram/Polling: retry recoverable setup-time network failures in monitor startup and await runner teardown before retry to avoid overlapping polling sessions.</li>
<li>Telegram/Polling: clear Telegram webhooks (<code>deleteWebhook</code>) before starting long-poll <code>getUpdates</code>, including retry handling for transient cleanup failures.</li>
<li>Telegram/Webhook: add <code>channels.telegram.webhookPort</code> config support and pass it through plugin startup wiring to the monitor listener.</li>
<li>Browser/Extension Relay: refactor the MV3 worker to preserve debugger attachments across relay drops, auto-reconnect with bounded backoff+jitter, persist and rehydrate attached tab state via <code>chrome.storage.session</code>, recover from <code>target_closed</code> navigation detaches, guard stale socket handlers, enforce per-tab operation locks and per-request timeouts, and add lifecycle keepalive/badge refresh hooks (<code>alarms</code>, <code>webNavigation</code>). (#15099, #6175, #8468, #9807)</li>
<li>Browser/Relay: treat extension websocket as connected only when <code>OPEN</code>, allow reconnect when a stale <code>CLOSING/CLOSED</code> extension socket lingers, and guard stale socket message/close handlers so late events cannot clear active relay state; includes regression coverage for live-duplicate <code>409</code> rejection and immediate reconnect-after-close races. (#15099, #18698, #20688)</li>
<li>Browser/Remote CDP: extend stale-target recovery so <code>ensureTabAvailable()</code> now reuses the sole available tab for remote CDP profiles (same behavior as extension profiles) while preserving strict <code>tab not found</code> errors when multiple tabs exist; includes remote-profile regression tests. (#15989)</li>
<li>Gateway/Pairing: treat <code>operator.admin</code> as satisfying other <code>operator.*</code> scope checks during device-auth verification so local CLI/TUI sessions stop entering pairing-required loops for pairing/approval-scoped commands. (#22062, #22193, #21191) Thanks @Botaccess, @jhartshorn, and @ctbritt.</li>
<li>Gateway/Pairing: auto-approve loopback <code>scope-upgrade</code> pairing requests (including device-token reconnects) so local clients do not disconnect on pairing-required scope elevation. (#23708) Thanks @widingmarcus-cyber.</li>
<li>Gateway/Scopes: include <code>operator.read</code> and <code>operator.write</code> in default operator connect scope bundles across CLI, Control UI, and macOS clients so write-scoped announce/sub-agent follow-up calls no longer hit <code>pairing required</code> disconnects on loopback gateways. (#22582) thanks @YuzuruS.</li>
<li>Gateway/Pairing: treat operator.admin pairing tokens as satisfying operator.write requests so legacy devices stop looping through scope-upgrade prompts introduced in 2026.2.19. (#23125, #23006) Thanks @vignesh07.</li>
<li>Gateway/Restart: fix restart-loop edge cases by keeping <code>openclaw.mjs -> dist/entry.js</code> bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli.</li>
<li>Gateway/Lock: use optional gateway-port reachability as a primary stale-lock liveness signal (and wire gateway run-loop lock acquisition to the resolved port), reducing false "already running" lockouts after unclean exits. (#23760) Thanks @Operative-001.</li>
<li>Delivery/Queue: quarantine queue entries immediately on known permanent delivery errors (for example invalid recipients or missing conversation references) by moving them to <code>failed/</code> instead of retrying on every restart. (#23794) Thanks @aldoeliacim.</li>
<li>Cron/Status: split execution outcome (<code>lastRunStatus</code>) from delivery outcome (<code>lastDeliveryStatus</code>) in persisted cron state, finished events, and run history so failed/unknown announcement delivery is visible without conflating it with run errors.</li>
<li>Cron/Delivery: route text-only announce jobs with explicit thread/topic targets through direct outbound delivery so forum/thread destinations do not get dropped by intermediary announce turns. (#23841) Thanks @AndrewArto.</li>
<li>Cron: honor <code>cron.maxConcurrentRuns</code> in the timer loop so due jobs can execute up to the configured parallelism instead of always running serially. (#11595) Thanks @Takhoffman.</li>
<li>Cron/Run: enforce the same per-job timeout guard for manual <code>cron.run</code> executions as timer-driven runs, including abort propagation for isolated agent jobs, so forced runs cannot wedge indefinitely. (#23704) Thanks @tkuehnl.</li>
<li>Cron/Run: persist the manual-run <code>runningAtMs</code> marker before releasing the cron lock so overlapping timer ticks cannot start the same job concurrently.</li>
<li>Cron/Startup: enforce per-job timeout guards for startup catch-up replay runs so missed isolated jobs cannot hang indefinitely during gateway boot recovery.</li>
<li>Cron/Main session: honor abort/timeout signals while retrying <code>wakeMode=now</code> heartbeat contention loops so main-target cron runs stop promptly instead of waiting through the full busy-retry window.</li>
<li>Cron/Schedule: for <code>every</code> jobs, prefer <code>lastRunAtMs + everyMs</code> when still in the future after restarts, then fall back to anchor scheduling for catch-up windows, so NEXT timing matches the last successful cadence. (#22895) Thanks @SidQin-cyber.</li>
<li>Cron/Service: execute manual <code>cron.run</code> jobs outside the cron lock (while still persisting started/finished state atomically) so <code>cron.list</code> and <code>cron.status</code> remain responsive during long forced runs. (#23628) Thanks @dsgraves.</li>
<li>Cron/Timer: keep a watchdog recheck timer armed while <code>onTimer</code> is actively executing so the scheduler continues polling even if a due-run tick stalls for an extended period. (#23628) Thanks @dsgraves.</li>
<li>Cron/Run log: clean up settled per-path run-log write queue entries so long-running cron uptime does not retain stale promise bookkeeping in memory.</li>
<li>Cron/Isolation: force fresh session IDs for isolated cron runs so <code>sessionTarget="isolated"</code> executions never reuse prior run context. (#23470) Thanks @echoVic.</li>
<li>Plugins/Install: strip <code>workspace:*</code> devDependency entries from copied plugin manifests before <code>npm install --omit=dev</code>, preventing <code>EUNSUPPORTEDPROTOCOL</code> install failures for npm-published channel plugins (including Feishu and MS Teams).</li>
<li>Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip <code>openclaw: workspace:*</code> from plugin <code>devDependencies</code> during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603)</li>
<li>Config/Channels: auto-enable built-in channels by writing <code>channels.<id>.enabled=true</code> (not <code>plugins.entries.<id></code>), and stop adding built-ins to <code>plugins.allow</code>, preventing <code>plugins.entries.telegram: plugin not found</code> validation failures.</li>
<li>Config/Channels: when <code>plugins.allow</code> is active, auto-enable/enable flows now also allowlist configured built-in channels so <code>channels.<id>.enabled=true</code> cannot remain blocked by restrictive plugin allowlists.</li>
<li>Plugins/Discovery: ignore scanned extension backup/disabled directory patterns (for example <code>.backup-*</code>, <code>.bak</code>, <code>.disabled*</code>) and move updater backup directories under <code>.openclaw-install-backups</code>, preventing duplicate plugin-id collisions from archived copies.</li>
<li>Plugins/CLI: make <code>openclaw plugins enable</code> and plugin install/link flows update allowlists via shared plugin-enable policy so enabled plugins are not left disabled by allowlist mismatch. (#23190) Thanks @downwind7clawd-ctrl.</li>
<li>Security/Voice Call: harden media stream WebSocket handling against pre-auth idle-connection DoS by adding strict pre-start timeouts, pending/per-IP connection limits, and total connection caps for streaming endpoints. This ships in the next npm release. Thanks @jiseoung for reporting.</li>
<li>Security/Sessions: redact sensitive token patterns from <code>sessions_history</code> tool output and surface <code>contentRedacted</code> metadata when masking occurs. (#16928) Thanks @aether-ai-agent.</li>
<li>Security/Exec: stop trusting <code>PATH</code>-derived directories for safe-bin allowlist checks, add explicit <code>tools.exec.safeBinTrustedDirs</code>, and pin safe-bin shell execution to resolved absolute executable paths to prevent binary-shadowing approval bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Elevated: match <code>tools.elevated.allowFrom</code> against sender identities only (not recipient <code>ctx.To</code>), closing a recipient-token bypass for <code>/elevated</code> authorization. This ships in the next npm release. Thanks @jiseoung for reporting.</li>
<li>Security/Feishu: enforce ID-only allowlist matching for DM/group sender authorization, normalize Feishu ID prefixes during checks, and ignore mutable display names so display-name collisions cannot satisfy allowlist entries. This ships in the next npm release. Thanks @jiseoung for reporting.</li>
<li>Security/Group policy: harden <code>channels.*.groups.*.toolsBySender</code> matching by requiring explicit sender-key types (<code>id:</code>, <code>e164:</code>, <code>username:</code>, <code>name:</code>), preventing cross-identifier collisions across mutable/display-name fields while keeping legacy untyped keys on a deprecated ID-only path. This ships in the next npm release. Thanks @jiseoung for reporting.</li>
<li>Channels/Group policy: fail closed when <code>groupPolicy: "allowlist"</code> is set without explicit <code>groups</code>, honor account-level <code>groupPolicy</code> overrides, and enforce <code>groupPolicy: "disabled"</code> as a hard group block. (#22215) Thanks @etereo.</li>
<li>Telegram/Discord extensions: propagate trusted <code>mediaLocalRoots</code> through extension outbound <code>sendMedia</code> options so extension direct-send media paths honor agent-scoped local-media allowlists. (#20029, #21903, #23227)</li>
<li>Agents/Exec: honor explicit agent context when resolving <code>tools.exec</code> defaults for runs with opaque/non-agent session keys, so per-agent <code>host/security/ask</code> policies are applied consistently. (#11832)</li>
<li>Doctor/Security: add an explicit warning that <code>approvals.exec.enabled=false</code> disables forwarding only, while enforcement remains driven by host-local <code>exec-approvals.json</code> policy. (#15047)</li>
<li>Sandbox/Docker: default sandbox container user to the workspace owner <code>uid:gid</code> when <code>agents.*.sandbox.docker.user</code> is unset, fixing non-root gateway file-tool permissions under capability-dropped containers. (#20979)</li>
<li>Plugins/Media sandbox: propagate trusted <code>mediaLocalRoots</code> through plugin action dispatch (including Discord/Telegram action adapters) so plugin send paths enforce the same agent-scoped local-media sandbox roots as core outbound sends. (#20258, #22718)</li>
<li>Agents/Workspace guard: map sandbox container-workdir file-tool paths (for example <code>/workspace/...</code> and <code>file:///workspace/...</code>) to host workspace roots before workspace-only validation, preventing false <code>Path escapes sandbox root</code> rejections for sandbox file tools. (#9560)</li>
<li>Gateway/Exec approvals: expire approval requests immediately when no approval-capable gateway clients are connected and no forwarding targets are available, avoiding delayed approvals after restarts/offline approver windows. (#22144)</li>
<li>Security/Exec approvals: when approving wrapper commands with allow-always in allowlist mode, persist inner executable paths for known dispatch wrappers (<code>env</code>, <code>nice</code>, <code>nohup</code>, <code>stdbuf</code>, <code>timeout</code>) and fail closed (no persisted entry) when wrapper unwrapping is not safe, preventing wrapper-path approval bypasses. Thanks @tdjackey for reporting.</li>
<li>Node/macOS exec host: default headless macOS node <code>system.run</code> to local execution and only route through the companion app when <code>OPENCLAW_NODE_EXEC_HOST=app</code> is explicitly set, avoiding companion-app filesystem namespace mismatches during exec. (#23547)</li>
<li>Sandbox/Media: map container workspace paths (<code>/workspace/...</code> and <code>file:///workspace/...</code>) back to the host sandbox root for outbound media validation, preventing false deny errors for sandbox-generated local media. (#23083) Thanks @echo931.</li>
<li>Sandbox/Docker: apply custom bind mounts after workspace mounts and prioritize bind-source resolution on overlapping paths, so explicit workspace binds are no longer ignored. (#22669) Thanks @tasaankaeris.</li>
<li>Exec approvals/Forwarding: restore Discord text forwarding when component approvals are not configured, and carry request snapshots through resolve events so resolved notices still forward after cache misses/restarts. (#22988) Thanks @bubmiller.</li>
<li>Control UI/WebSocket: stop and clear the browser gateway client on UI teardown so remounts cannot leave orphan websocket clients that create duplicate active connections. (#23422) Thanks @floatinggball-design.</li>
<li>Control UI/WebSocket: send a stable per-tab <code>instanceId</code> in websocket connect frames so reconnect cycles keep a consistent client identity for diagnostics and presence tracking. (#23616) Thanks @zq58855371-ui.</li>
<li>Config/Memory: allow <code>"mistral"</code> in <code>agents.defaults.memorySearch.provider</code> and <code>agents.defaults.memorySearch.fallback</code> schema validation. (#14934) Thanks @ThomsenDrake.</li>
<li>Feishu/Commands: in group chats, command authorization now falls back to top-level <code>channels.feishu.allowFrom</code> when per-group <code>allowFrom</code> is not set, so <code>/command</code> no longer gets blocked by an unintended empty allowlist. (#23756)</li>
<li>Dev tooling: prevent <code>CLAUDE.md</code> symlink target regressions by excluding CLAUDE symlink sentinels from <code>oxfmt</code> and marking them <code>-text</code> in <code>.gitattributes</code>, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc.</li>
<li>Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.</li>
<li>Agents/Subagents: restore announce-chain delivery to agent injection, defer nested announce output until descendant follow-up content is ready, and prevent descendant deferrals from consuming announce retry budget so deep chains do not drop final completions. (#22223) Thanks @tyler6204.</li>
<li>Agents/System Prompt: label allowlisted senders as authorized senders to avoid implying ownership. Thanks @thewilloftheshadow.</li>
<li>Agents/Tool display: fix exec cwd suffix inference so <code>pushd ... && popd ... && <command></code> does not keep stale <code>(in <dir>)</code> context in summaries. (#21925) Thanks @Lukavyi.</li>
<li>Tools/web_search: handle xAI Responses API payloads that emit top-level <code>output_text</code> blocks (without a <code>message</code> wrapper) so Grok web_search no longer returns <code>No response</code> for those results. (#20508) Thanks @echoVic.</li>
<li>Agents/Failover: treat non-default override runs as direct fallback-to-configured-primary (skip configured fallback chain), normalize default-model detection for provider casing/whitespace, and add regression coverage for override/auth error paths. (#18820) Thanks @Glucksberg.</li>
<li>Docker/Build: include <code>ownerDisplay</code> in <code>CommandsSchema</code> object-level defaults so Docker <code>pnpm build</code> no longer fails with <code>TS2769</code> during plugin SDK d.ts generation. (#22558) Thanks @obviyus.</li>
<li>Docker/Browser: install Playwright Chromium into <code>/home/node/.cache/ms-playwright</code> and set <code>node:node</code> ownership so browser binaries are available to the runtime user in browser-enabled images. (#22585) thanks @obviyus.</li>
<li>Hooks/Session memory: trigger bundled <code>session-memory</code> persistence on both <code>/new</code> and <code>/reset</code> so reset flows no longer skip markdown transcript capture before archival. (#21382) Thanks @mofesolapaul.</li>
<li>Dependencies/Agents: bump embedded Pi SDK packages (<code>@mariozechner/pi-agent-core</code>, <code>@mariozechner/pi-ai</code>, <code>@mariozechner/pi-coding-agent</code>, <code>@mariozechner/pi-tui</code>) to <code>0.54.0</code>. (#21578) Thanks @Takhoffman.</li>
<li>Config/Agents: expose Pi compaction tuning values <code>agents.defaults.compaction.reserveTokens</code> and <code>agents.defaults.compaction.keepRecentTokens</code> in config schema/types and apply them in embedded Pi runner settings overrides with floor enforcement via <code>reserveTokensFloor</code>. (#21568) Thanks @Takhoffman.</li>
<li>Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.</li>
<li>Docker: run build steps as the <code>node</code> user and use <code>COPY --chown</code> to avoid recursive ownership changes, trimming image size and layer churn. Thanks @huntharo.</li>
<li>Config/Memory: restore schema help/label metadata for hybrid <code>mmr</code> and <code>temporalDecay</code> settings so configuration surfaces show correct names and guidance. (#18786) Thanks @rodrigouroz.</li>
<li>Skills/SonosCLI: add troubleshooting guidance for <code>sonos discover</code> failures on macOS direct mode (<code>sendto: no route to host</code>) and sandbox network restrictions (<code>bind: operation not permitted</code>). (#21316) Thanks @huntharo.</li>
<li>macOS/Build: default release packaging to <code>BUNDLE_ID=ai.openclaw.mac</code> in <code>scripts/package-mac-dist.sh</code>, so Sparkle feed URL is retained and auto-update no longer fails with an empty appcast feed. (#19750) thanks @loganprit.</li>
<li>Signal/Outbound: preserve case for Base64 group IDs during outbound target normalization so cross-context routing and policy checks no longer break when group IDs include uppercase characters. (#5578) Thanks @heyhudson.</li>
<li>Anthropic/Agents: preserve required pi-ai default OAuth beta headers when <code>context1m</code> injects <code>anthropic-beta</code>, preventing 401 auth failures for <code>sk-ant-oat-*</code> tokens. (#19789, fixes #19769) Thanks @minupla.</li>
<li>Security/Exec: block unquoted heredoc body expansion tokens in shell allowlist analysis, reject unterminated heredocs, and require explicit approval for allowlisted heredoc execution on gateway hosts to prevent heredoc substitution allowlist bypass. Thanks @torturado for reporting.</li>
<li>macOS/Security: evaluate <code>system.run</code> allowlists per shell segment in macOS node runtime and companion exec host (including chained shell operators), fail closed on shell/process substitution parsing, and require explicit approval on unsafe parse cases to prevent allowlist bypass via <code>rawCommand</code> chaining. Thanks @tdjackey for reporting.</li>
<li>WhatsApp/Security: enforce allowlist JID authorization for reaction actions so authenticated callers cannot target non-allowlisted chats by forging <code>chatJid</code> + valid <code>messageId</code> pairs. Thanks @aether-ai-agent for reporting.</li>
<li>ACP/Security: escape control and delimiter characters in ACP <code>resource_link</code> title/URI metadata before prompt interpolation to prevent metadata-driven prompt injection through resource links. Thanks @aether-ai-agent for reporting.</li>
<li>TTS/Security: make model-driven provider switching opt-in by default (<code>messages.tts.modelOverrides.allowProvider=false</code> unless explicitly enabled), while keeping voice/style overrides available, to reduce prompt-injection-driven provider hops and unexpected TTS cost escalation. Thanks @aether-ai-agent for reporting.</li>
<li>Security/Agents: keep overflow compaction retry budgeting global across tool-result truncation recovery so successful truncation cannot reset the overflow retry counter and amplify retry/cost cycles. Thanks @aether-ai-agent for reporting.</li>
<li>BlueBubbles/Security: require webhook token authentication for all BlueBubbles webhook requests (including loopback/proxied setups), removing passwordless webhook fallback behavior. Thanks @zpbrent.</li>
<li>iOS/Security: force <code>https://</code> for non-loopback manual gateway hosts during iOS onboarding to block insecure remote transport URLs. (#21969) Thanks @mbelinky.</li>
<li>Gateway/Security: remove shared-IP fallback for canvas endpoints and require token or session capability for canvas access. Thanks @thewilloftheshadow.</li>
<li>Gateway/Security: require secure context and paired-device checks for Control UI auth even when <code>gateway.controlUi.allowInsecureAuth</code> is set, and align audit messaging with the hardened behavior. (#20684) Thanks @coygeek and @Vasco0x4 for reporting.</li>
<li>Gateway/Security: scope tokenless Tailscale forwarded-header auth to Control UI websocket auth only, so HTTP gateway routes still require token/password even on trusted hosts. Thanks @zpbrent for reporting.</li>
<li>Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow.</li>
<li>Skills/Security: sanitize skill env overrides to block unsafe runtime injection variables and only allow sensitive keys when declared in skill metadata, with warnings for suspicious values. Thanks @thewilloftheshadow.</li>
<li>Security/Commands: block prototype-key injection in runtime <code>/debug</code> overrides and require own-property checks for gated command flags (<code>bash</code>, <code>config</code>, <code>debug</code>) so inherited prototype values cannot enable privileged commands. Thanks @tdjackey for reporting.</li>
<li>Security/Browser: block non-network browser navigation protocols (including <code>file:</code>, <code>data:</code>, and <code>javascript:</code>) while preserving <code>about:blank</code>, preventing local file reads via browser tool navigation. Thanks @q1uf3ng for reporting.</li>
<li>Security/Exec: block shell startup-file env injection (<code>BASH_ENV</code>, <code>ENV</code>, <code>BASH_FUNC_*</code>, <code>LD_*</code>, <code>DYLD_*</code>) across config env ingestion, node-host inherited environment sanitization, and macOS exec host runtime to prevent pre-command execution from attacker-controlled environment variables. Thanks @tdjackey.</li>
<li>Security/Exec (Windows): canonicalize <code>cmd.exe /c</code> command text across validation, approval binding, and audit/event rendering to prevent trailing-argument approval mismatches in <code>system.run</code>. Thanks @tdjackey for reporting.</li>
<li>Security/Gateway/Hooks: block <code>__proto__</code>, <code>constructor</code>, and <code>prototype</code> traversal in webhook template path resolution to prevent prototype-chain payload data leakage in <code>messageTemplate</code> rendering. (#22213) Thanks @SleuthCo.</li>
<li>Security/OpenClawKit/UI: prevent injected inbound user context metadata blocks from leaking into chat history in TUI, webchat, and macOS surfaces by stripping all untrusted metadata prefixes at display boundaries. (#22142) Thanks @Mellowambience, @vincentkoc.</li>
<li>Security/OpenClawKit/UI: strip inbound metadata blocks from user messages in TUI rendering while preserving user-authored content. (#22345) Thanks @kansodata, @vincentkoc.</li>
<li>Security/OpenClawKit/UI: prevent inbound metadata leaks and reply-tag streaming artifacts in TUI rendering by stripping untrusted metadata prefixes at display boundaries. (#22346) Thanks @akramcodez, @vincentkoc.</li>
<li>Security/Agents: restrict local MEDIA tool attachments to core tools and the OpenClaw temp root to prevent untrusted MCP tool file exfiltration. Thanks @NucleiAv and @thewilloftheshadow.</li>
<li>Security/Net: strip sensitive headers (<code>Authorization</code>, <code>Proxy-Authorization</code>, <code>Cookie</code>, <code>Cookie2</code>) on cross-origin redirects in <code>fetchWithSsrFGuard</code> to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm.</li>
<li>Security/Systemd: reject CR/LF in systemd unit environment values and fix argument escaping so generated units cannot be injected with extra directives. Thanks @thewilloftheshadow.</li>
<li>Security/Tools: add per-wrapper random IDs to untrusted-content markers from <code>wrapExternalContent</code>/<code>wrapWebContent</code>, preventing marker spoofing from escaping content boundaries. (#19009) Thanks @Whoaa512.</li>
<li>Shared/Security: reject insecure deep links that use <code>ws://</code> non-loopback gateway URLs to prevent plaintext remote websocket configuration. (#21970) Thanks @mbelinky.</li>
<li>macOS/Security: reject non-loopback <code>ws://</code> remote gateway URLs in macOS remote config to block insecure plaintext websocket endpoints. (#21971) Thanks @mbelinky.</li>
<li>Browser/Security: block upload path symlink escapes so browser upload sources cannot traverse outside the allowed workspace via symlinked paths. (#21972) Thanks @mbelinky.</li>
<li>Security/Dependencies: bump transitive <code>hono</code> usage to <code>4.11.10</code> to incorporate timing-safe authentication comparison hardening for <code>basicAuth</code>/<code>bearerAuth</code> (<code>GHSA-gq3j-xvxp-8hrf</code>). Thanks @vincentkoc.</li>
<li>Security/Gateway: parse <code>X-Forwarded-For</code> with trust-preserving semantics when requests come from configured trusted proxies, preventing proxy-chain spoofing from influencing client IP classification and rate-limit identity. Thanks @AnthonyDiSanti and @vincentkoc.</li>
<li>Security/Sandbox: remove default <code>--no-sandbox</code> for the browser container entrypoint, add explicit opt-in via <code>OPENCLAW_BROWSER_NO_SANDBOX</code> / <code>CLAWDBOT_BROWSER_NO_SANDBOX</code>, and add security-audit checks for stale/missing sandbox browser Docker hash labels. Thanks @TerminalsandCoffee and @vincentkoc.</li>
<li>Security/Sandbox Browser: require VNC password auth for noVNC observer sessions in the sandbox browser entrypoint, plumb per-container noVNC passwords from runtime, and emit short-lived noVNC observer token URLs while keeping loopback-only host port publishing. Thanks @TerminalsandCoffee for reporting.</li>
<li>Security/Sandbox Browser: default browser sandbox containers to a dedicated Docker network (<code>openclaw-sandbox-browser</code>), add optional CDP ingress source-range restrictions, auto-create missing dedicated networks, and warn in <code>openclaw security --audit</code> when browser sandboxing runs on bridge without source-range limits. Thanks @TerminalsandCoffee for reporting.</li>
<li>Feishu/Media: for inbound video messages that include both <code>file_key</code> (video) and <code>image_key</code> (thumbnail), prefer <code>file_key</code> when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633)</li>
<li>Hooks/Loader: avoid redundant hook-module recompilation on gateway restart by skipping cache-busting for bundled hooks and using stable file metadata keys (<code>mtime+size</code>) for mutable workspace/managed/plugin hook imports. (#16953) Thanks @mudrii.</li>
<li>Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark <code>SILENT_REPLY_TOKEN</code> (<code>NO_REPLY</code>) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.</li>
<li>Providers/OpenRouter: inject <code>cache_control</code> on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed.</li>
<li>Installer/Smoke tests: remove legacy <code>OPENCLAW_USE_GUM</code> overrides from docker install-smoke runs so tests exercise installer auto TTY detection behavior directly.</li>
<li>Providers/OpenRouter: allow pass-through OpenRouter and Opencode model IDs in live model filtering so custom routed model IDs are treated as modern refs. (#14312) Thanks @Joly0.</li>
<li>Providers/OpenRouter: default reasoning to enabled when the selected model advertises <code>reasoning: true</code> and no session/directive override is set. (#22513) Thanks @zwffff.</li>
<li>Providers/OpenRouter: map <code>/think</code> levels to <code>reasoning.effort</code> in embedded runs while preserving explicit <code>reasoning.max_tokens</code> payloads. (#17236) Thanks @robbyczgw-cla.</li>
<li>Providers/OpenRouter: preserve stored session provider when model IDs are vendor-prefixed (for example, <code>anthropic/...</code>) so follow-up turns do not incorrectly route to direct provider APIs. (#22753) Thanks @dndodson.</li>
<li>Providers/OpenRouter: preserve the required <code>openrouter/</code> prefix for OpenRouter-native model IDs during model-ref normalization. (#12942) Thanks @omair445.</li>
<li>Providers/OpenRouter: pass through provider routing parameters from model params.provider to OpenRouter request payloads for provider selection controls. (#17148) Thanks @carrotRakko.</li>
<li>Providers/OpenRouter: preserve model allowlist entries containing OpenRouter preset paths (for example <code>openrouter/@preset/...</code>) by treating <code>/model ...@profile</code> auth-profile parsing as a suffix-only override. (#14120) Thanks @NotMainstream.</li>
<li>Cron/Auth: propagate auth-profile resolution to isolated cron sessions so provider API keys are resolved the same way as main sessions, fixing 401 errors when using providers configured via auth-profiles. (#20689) Thanks @lailoo.</li>
<li>Cron/Follow-up: pass resolved <code>agentDir</code> through isolated cron and queued follow-up embedded runs so auth/profile lookups stay scoped to the correct agent directory. (#22845) Thanks @seilk.</li>
<li>Agents/Media: route tool-result <code>MEDIA:</code> extraction through shared parser validation so malformed prose like <code>MEDIA:-prefixed ...</code> is no longer treated as a local file path (prevents Telegram ENOENT tool-error overrides). (#18780) Thanks @HOYALIM.</li>
<li>Logging: cap single log-file size with <code>logging.maxFileBytes</code> (default 500 MB) and suppress additional writes after cap hit to prevent disk exhaustion from repeated error storms.</li>
<li>Memory/Remote HTTP: centralize remote memory HTTP calls behind a shared guarded helper (<code>withRemoteHttpResponse</code>) so embeddings and batch flows use one request/release path.</li>
<li>Memory/Embeddings: apply configured remote-base host pinning (<code>allowedHostnames</code>) across OpenAI/Voyage/Gemini embedding requests to keep private/self-hosted endpoints working without cross-host drift. (#18198) Thanks @ianpcook.</li>
<li>Memory/Batch: route OpenAI/Voyage/Gemini batch upload/create/status/download requests through the same guarded HTTP path for consistent SSRF policy enforcement.</li>
<li>Memory/Index: detect memory source-set changes (for example enabling <code>sessions</code> after an existing memory-only index) and trigger a full reindex so existing session transcripts are indexed without requiring <code>--force</code>. (#17576) Thanks @TarsAI-Agent.</li>
<li>Memory/Embeddings: enforce a per-input 8k safety cap before embedding batching and apply a conservative 2k fallback limit for local providers without declared input limits, preventing oversized session/memory chunks from triggering provider context-size failures during sync/indexing. (#6016) Thanks @batumilove.</li>
<li>Memory/QMD: on Windows, resolve bare <code>qmd</code>/<code>mcporter</code> command names to npm shim executables (<code>.cmd</code>) before spawning, so qmd boot updates and mcporter-backed searches no longer fail with <code>spawn ... ENOENT</code> on default npm installs. (#23899) Thanks @arcbuilder-ai.</li>
<li>Memory/QMD: parse plain-text <code>qmd collection list --json</code> output when older qmd builds ignore JSON mode, and retry memory searches once after re-ensuring managed collections when qmd returns <code>Collection not found ...</code>. (#23613) Thanks @leozhucn.</li>
<li>Signal/RPC: guard malformed Signal RPC JSON responses with a clear status-scoped error and add regression coverage for invalid JSON responses. (#22995) Thanks @adhitShet.</li>
<li>Gateway/Subagents: guard gateway and subagent session-key/message trim paths against undefined inputs to prevent early <code>Cannot read properties of undefined (reading 'trim')</code> crashes during subagent spawn and wait flows.</li>
<li>Agents/Workspace: guard <code>resolveUserPath</code> against undefined/null input to prevent <code>Cannot read properties of undefined (reading 'trim')</code> crashes when workspace paths are missing in embedded runner flows.</li>
<li>Auth/Profiles: keep active <code>cooldownUntil</code>/<code>disabledUntil</code> windows immutable across retries so mid-window failures cannot extend recovery indefinitely; only recompute a backoff window after the previous deadline has expired. This resolves cron/inbound retry loops that could trap gateways until manual <code>usageStats</code> cleanup. (#23516, #23536) Thanks @arosstale.</li>
<li>Channels/Security: fail closed on missing provider group policy config by defaulting runtime group policy to <code>allowlist</code> (instead of inheriting <code>channels.defaults.groupPolicy</code>) when <code>channels.<provider></code> is absent across message channels, and align runtime + security warnings/docs to the same fallback behavior (Slack, Discord, iMessage, Telegram, WhatsApp, Signal, LINE, Matrix, Mattermost, Google Chat, IRC, Nextcloud Talk, Feishu, and Zalo user flows; plus Discord message/native-command paths). (#23367) Thanks @bmendonca3.</li>
<li>Gateway/Onboarding: harden remote gateway onboarding defaults and guidance by defaulting discovered direct URLs to <code>wss://</code>, rejecting insecure non-loopback <code>ws://</code> targets in onboarding validation, and expanding remote-security remediation messaging across gateway client/call/doctor flows. (#23476) Thanks @bmendonca3.</li>
<li>CLI/Sessions: pass the configured sessions directory when resolving transcript paths in <code>agentCommand</code>, so custom <code>session.store</code> locations resume sessions reliably. Thanks @davidrudduck.</li>
<li>Signal/Monitor: treat user-initiated abort shutdowns as clean exits when auto-started <code>signal-cli</code> is terminated, while still surfacing unexpected daemon exits as startup/runtime failures. (#23379) Thanks @frankekn.</li>
<li>Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths. (#23377) Thanks @SidQin-cyber.</li>
<li>Channels/Delivery: remove hardcoded WhatsApp delivery fallbacks; require explicit/session channel context or auto-pick the sole configured channel when unambiguous. (#23357) Thanks @lbo728.</li>
<li>ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early <code>gateway not connected</code> request races. (#23390) Thanks @janckerchen.</li>
<li>Gateway/Auth: preserve <code>OPENCLAW_GATEWAY_PASSWORD</code> env override precedence for remote gateway call credentials after shared resolver refactors, preventing stale configured remote passwords from overriding runtime secret rotation.</li>
<li>Gateway/Auth: preserve shared-token <code>gateway token mismatch</code> auth errors when <code>auth.token</code> fallback device-token checks fail, and reserve <code>device token mismatch</code> guidance for explicit <code>auth.deviceToken</code> failures.</li>
<li>Gateway/Tools: when agent tools pass an allowlisted <code>gatewayUrl</code> override, resolve local override tokens from env/config fallback but keep remote overrides strict to <code>gateway.remote.token</code>, preventing local token leakage to remote targets.</li>
<li>Gateway/Client: keep cached device-auth tokens on <code>device token mismatch</code> closes when the client used explicit shared token/password credentials, avoiding accidental pairing-token churn during explicit-auth failures.</li>
<li>Node host/Exec: keep strict Windows allowlist behavior for <code>cmd.exe /c</code> shell-wrapper runs, and return explicit approval guidance when blocked (<code>SYSTEM_RUN_DENIED: allowlist miss</code>).</li>
<li>Control UI: show pairing-required guidance (commands + mobile tokenized URL reminder) when the dashboard disconnects with <code>1008 pairing required</code>.</li>
<li>Security/Audit: add <code>openclaw security audit</code> detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (<code>security.exposure.open_groups_with_runtime_or_fs</code>).</li>
<li>Security/Audit: make <code>gateway.real_ip_fallback_enabled</code> severity conditional for loopback trusted-proxy setups (warn for loopback-only <code>trustedProxies</code>, critical when non-loopback proxies are trusted). (#23428) Thanks @bmendonca3.</li>
<li>Security/Exec env: block request-scoped <code>HOME</code> and <code>ZDOTDIR</code> overrides in host exec env sanitizers (Node + macOS), preventing shell startup-file execution before allowlist-evaluated command bodies. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Exec env: block <code>SHELLOPTS</code>/<code>PS4</code> in host exec env sanitizers and restrict shell-wrapper (<code>bash|sh|zsh ... -c/-lc</code>) request env overrides to a small explicit allowlist (<code>TERM</code>, <code>LANG</code>, <code>LC_*</code>, <code>COLORTERM</code>, <code>NO_COLOR</code>, <code>FORCE_COLOR</code>) on both node host and macOS companion paths, preventing xtrace prompt command-substitution allowlist bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>WhatsApp/Security: enforce <code>allowFrom</code> for direct-message outbound targets in all send modes (including <code>mode: "explicit"</code>), preventing sends to non-allowlisted numbers. (#20108) Thanks @zahlmann.</li>
<li>Security/Exec approvals: fail closed on shell line continuations (<code>\\\n</code>/<code>\\\r\n</code>) and treat shell-wrapper execution as approval-required in allowlist mode, preventing <code>$\\</code> newline command-substitution bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Gateway: emit a startup security warning when insecure/dangerous config flags are enabled (including <code>gateway.controlUi.dangerouslyDisableDeviceAuth=true</code>) and point operators to <code>openclaw security audit</code>.</li>
<li>Security/Hooks auth: normalize hook auth rate-limit client IP keys so IPv4 and IPv4-mapped IPv6 addresses share one throttle bucket, preventing dual-form auth-attempt budget bypasses. This ships in the next npm release. Thanks @aether-ai-agent for reporting.</li>
<li>Security/Exec approvals: treat <code>env</code> and shell-dispatch wrappers as transparent during allowlist analysis on node-host and macOS companion paths so policy checks match the effective executable/inline shell payload instead of the wrapper binary, blocking wrapper-smuggled allowlist bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Exec approvals: require explicit safe-bin profiles for <code>tools.exec.safeBins</code> entries in allowlist mode (remove generic safe-bin profile fallback), and add <code>tools.exec.safeBinProfiles</code> for safe custom binaries so unprofiled interpreter-style entries cannot be treated as stdin-safe. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Channels: harden Slack external menu token handling by switching to CSPRNG tokens, validating token shape, requiring user identity for external option lookups, and avoiding fabricated timestamp <code>trigger_id</code> fallbacks; also switch Tlon Urbit channel IDs to CSPRNG UUIDs, centralize secure ID/token generation via shared infra helpers, and add a guardrail test to block new runtime <code>Date.now()+Math.random()</code> token/id patterns.</li>
<li>Security/Hooks transforms: enforce symlink-safe containment for webhook transform module paths (including <code>hooks.transformsDir</code> and <code>hooks.mappings[].transform.module</code>) by resolving existing-path ancestors via realpath before import, while preserving in-root symlink support; add regression coverage for both escape and allow cases. This ships in the next npm release. Thanks @aether-ai-agent for reporting.</li>
<li>Telegram/WSL2: disable <code>autoSelectFamily</code> by default on WSL2 and memoize WSL2 detection in Telegram network decision logic to avoid repeated sync <code>/proc/version</code> probes on fetch/send paths. (#21916) Thanks @MizukiMachine.</li>
<li>Telegram/Network: default Node 22+ DNS result ordering to <code>ipv4first</code> for Telegram fetch paths and add <code>OPENCLAW_TELEGRAM_DNS_RESULT_ORDER</code>/<code>channels.telegram.network.dnsResultOrder</code> overrides to reduce IPv6-path fetch failures. (#5405) Thanks @Glucksberg.</li>
<li>Telegram/Forward bursts: coalesce forwarded text+media updates through a dedicated forward lane debounce window that works with default inbound debounce config, while keeping forwarded control commands immediate. (#19476) thanks @napetrov.</li>
<li>Telegram/Streaming: preserve archived draft preview mapping after flush and clean superseded reasoning preview bubbles so multi-message preview finals no longer cross-edit or orphan stale messages under send/rotation races. (#23202) Thanks @obviyus.</li>
<li>Telegram/Replies: scope messaging-tool text/media dedupe to same-target sends only, so cross-target tool sends can no longer silently suppress Telegram final replies.</li>
<li>Telegram/Replies: normalize <code>file://</code> and local-path media variants during messaging dedupe so equivalent media paths do not produce duplicate Telegram replies.</li>
<li>Telegram/Replies: extract forwarded-origin context from unified reply targets (<code>reply_to_message</code> and <code>external_reply</code>) so forward+comment metadata is preserved across partial reply shapes. (#9720) thanks @mcaxtr.</li>
<li>Telegram/Polling: persist a safe update-offset watermark bounded by pending updates so crash/restart cannot skip queued lower <code>update_id</code> updates after out-of-order completion. (#23284) thanks @frankekn.</li>
<li>Telegram/Polling: force-restart stuck runner instances when recoverable unhandled network rejections escape the polling task path, so polling resumes instead of silently stalling. (#19721) Thanks @jg-noncelogic.</li>
<li>Slack/Slash commands: preserve the Bolt app receiver when registering external select options handlers so monitor startup does not crash on runtimes that require bound <code>app.options</code> calls. (#23209) Thanks @0xgaia.</li>
<li>Slack/Telegram slash sessions: await session metadata persistence before dispatch so first-turn native slash runs do not race session-origin metadata updates. (#23065) thanks @hydro13.</li>
<li>Slack/Queue routing: preserve string <code>thread_ts</code> values through collect-mode queue drain and DM <code>deliveryContext</code> updates so threaded follow-ups do not leak to the main channel when Slack thread IDs are strings. (#11934) Thanks @sandieman2 and @vincentkoc.</li>
<li>Telegram/Native commands: set <code>ctx.Provider="telegram"</code> for native slash-command context so elevated gate checks resolve provider correctly (fixes <code>provider (ctx.Provider)</code> failures in <code>/elevated</code> flows). (#23748) Thanks @serhii12.</li>
<li>Agents/Ollama: preserve unsafe integer tool-call arguments as exact strings during NDJSON parsing, preventing large numeric IDs from being rounded before tool execution. (#23170) Thanks @BestJoester.</li>
<li>Cron/Gateway: keep <code>cron.list</code> and <code>cron.status</code> responsive during startup catch-up by avoiding a long-held cron lock while missed jobs execute. (#23106) Thanks @jayleekr.</li>
<li>Gateway/Config reload: compare array-valued config paths structurally during diffing so unchanged <code>memory.qmd.paths</code> and <code>memory.qmd.scope.rules</code> no longer trigger false restart-required reloads. (#23185) Thanks @rex05ai.</li>
<li>Gateway/Config reload: retry short-lived missing config snapshots during reload before skipping, preventing atomic-write unlink windows from triggering restart loops. (#23343) Thanks @lbo728.</li>
<li>Cron/Scheduling: validate runtime cron expressions before schedule/stagger evaluation so malformed persisted jobs report a clear <code>invalid cron schedule: expr is required</code> error instead of crashing with <code>undefined.trim</code> failures and auto-disable churn. (#23223) Thanks @asimons81.</li>
<li>Memory/QMD: migrate legacy unscoped collection bindings (for example <code>memory-root</code>) to per-agent scoped names (for example <code>memory-root-main</code>) during startup when safe, so QMD-backed <code>memory_search</code> no longer fails with <code>Collection not found</code> after upgrades. (#23228, #20727) Thanks @JLDynamics and @AaronFaby.</li>
<li>Memory/QMD: normalize Han-script BM25 search queries before invoking <code>qmd search</code> so mixed CJK+Latin prompts no longer return empty results due to tokenizer mismatch. (#23426) Thanks @LunaLee0130.</li>
<li>TUI/Input: enable multiline-paste burst coalescing on macOS Terminal.app and iTerm so pasted blocks no longer submit line-by-line as separate messages. (#18809) Thanks @fwends.</li>
<li>TUI/RTL: isolate right-to-left script lines (Arabic/Hebrew ranges) with Unicode bidi isolation marks in TUI text sanitization so RTL assistant output no longer renders in reversed visual order in terminal chat panes. (#21936) Thanks @Asm3r96.</li>
<li>TUI/Status: request immediate renders after setting <code>sending</code>/<code>waiting</code> activity states so in-flight runs always show visible progress indicators instead of appearing idle until completion. (#21549) Thanks @13Guinness.</li>
<li>TUI/Input: arm Ctrl+C exit timing when clearing non-empty composer text and add a SIGINT fallback path so double Ctrl+C exits remain responsive during active runs instead of requiring an extra press or appearing stuck. (#23407) Thanks @tinybluedev.</li>
<li>Agents/Fallbacks: treat JSON payloads with <code>type: "api_error"</code> + <code>"Internal server error"</code> as transient failover errors so Anthropic 500-style failures trigger model fallback. (#23193) Thanks @jarvis-lane.</li>
<li>Agents/Google: sanitize non-base64 <code>thought_signature</code>/<code>thoughtSignature</code> values from assistant replay transcripts for native Google Gemini requests while preserving valid signatures and tool-call order. (#23457) Thanks @echoVic.</li>
<li>Agents/Transcripts: validate assistant tool-call names (syntax/length + registered tool allowlist) before transcript persistence and during replay sanitization so malformed failover tool names no longer poison sessions with repeated provider HTTP 400 errors. (#23324) Thanks @johnsantry.</li>
<li>Agents/Mistral: sanitize tool-call IDs in the embedded agent loop and generate strict provider-safe pending tool-call IDs, preventing Mistral strict9 <code>HTTP 400</code> failures on tool continuations. (#23698) Thanks @echoVic.</li>
<li>Agents/Compaction: strip stale assistant usage snapshots from pre-compaction turns when replaying history after a compaction summary so context-token estimation no longer reuses pre-compaction totals and immediately re-triggers destructive follow-up compactions. (#19127) Thanks @tedwatson.</li>
<li>Agents/Replies: emit a default completion acknowledgement (<code>✅ Done.</code>) only for direct/private tool-only completions with no final assistant text, while suppressing synthetic acknowledgements for channel/group sessions and runs that already delivered output via messaging tools. (#22834) Thanks @Oldshue.</li>
<li>Agents/Subagents: honor <code>tools.subagents.tools.alsoAllow</code> and explicit subagent <code>allow</code> entries when resolving built-in subagent deny defaults, so explicitly granted tools (for example <code>sessions_send</code>) are no longer blocked unless re-denied in <code>tools.subagents.tools.deny</code>. (#23359) Thanks @goren-beehero.</li>
<li>Agents/Subagents: make announce call timeouts configurable via <code>agents.defaults.subagents.announceTimeoutMs</code> and restore a 60s default to prevent false timeout failures on slower announce paths. (#22719) Thanks @Valadon.</li>
<li>Agents/Diagnostics: include resolved lifecycle error text in <code>embedded run agent end</code> warnings so UI/TUI “Connection error” runs expose actionable provider failure reasons in gateway logs. (#23054) Thanks @Raize.</li>
<li>Agents/Auth profiles: skip auth-profile cooldown writes for timeout failures in embedded runner rotation so model/network timeouts do not poison same-provider fallback model selection while still allowing in-turn account rotation. (#22622) Thanks @vageeshkumar.</li>
<li>Plugins/Hooks: run legacy <code>before_agent_start</code> once per agent turn and reuse that result across model-resolve and prompt-build compatibility paths, preventing duplicate hook side effects (for example duplicate external API calls). (#23289) Thanks @ksato8710.</li>
<li>Models/Config: default missing Anthropic provider/model <code>api</code> fields to <code>anthropic-messages</code> during config validation so custom relay model entries are preserved instead of being dropped by runtime model registry validation. (#23332) Thanks @bigbigmonkey123.</li>
<li>Gateway/Pairing: preserve existing approved token scopes when processing repair pairings that omit <code>scopes</code>, preventing empty-scope token regressions on reconnecting clients. (#21906) Thanks @paki81.</li>
<li>Memory/QMD: add optional <code>memory.qmd.mcporter</code> search routing so QMD <code>query/search/vsearch</code> can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07.</li>
<li>Infra/Network: classify undici <code>TypeError: fetch failed</code> as transient in unhandled-rejection detection even when nested causes are unclassified, preventing avoidable gateway crash loops on flaky networks. (#14345) Thanks @Unayung.</li>
<li>Telegram/Retry: classify undici <code>TypeError: fetch failed</code> as recoverable in both polling and send retry paths so transient fetch failures no longer fail fast. (#16699) thanks @Glucksberg.</li>
<li>Docs/Telegram: correct Node 22+ network defaults (<code>autoSelectFamily</code>, <code>dnsResultOrder</code>) and clarify Telegram setup does not use positional <code>openclaw channels login telegram</code>. (#23609) Thanks @ryanbastic.</li>
<li>BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines.</li>
<li>BlueBubbles/Private API cache: treat unknown (<code>null</code>) private-API cache status as disabled for send/attachment/reply flows to avoid stale-cache 500s, and log a warning when reply/effect features are requested while capability is unknown. (#23459) Thanks @echoVic.</li>
<li>BlueBubbles/Webhooks: accept inbound/reaction webhook payloads when BlueBubbles omits <code>handle</code> but provides DM <code>chatGuid</code>, and harden payload extraction for array/string-wrapped message bodies so valid webhook events no longer get rejected as unparseable. (#23275) Thanks @toph31.</li>
<li>Security/Audit: add <code>openclaw security audit</code> finding <code>gateway.nodes.allow_commands_dangerous</code> for risky <code>gateway.nodes.allowCommands</code> overrides, with severity upgraded to critical on remote gateway exposure.</li>
<li>Gateway/Control plane: reduce cross-client write limiter contention by adding <code>connId</code> fallback keying when device ID and client IP are both unavailable.</li>
<li>Security/Config: block prototype-key traversal during config merge patch and legacy migration merge helpers (<code>__proto__</code>, <code>constructor</code>, <code>prototype</code>) to prevent prototype pollution during config mutation flows. (#22968) Thanks @Clawborn.</li>
<li>Security/Shell env: validate login-shell executable paths for shell-env fallback (<code>/etc/shells</code> + trusted prefixes), block <code>SHELL</code>/<code>HOME</code>/<code>ZDOTDIR</code> in config env ingestion before fallback execution, and sanitize fallback shell exec env to pin <code>HOME</code> to the real user home while dropping <code>ZDOTDIR</code> and other dangerous startup vars. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Network/SSRF: enable <code>autoSelectFamily</code> on pinned undici dispatchers (with attempt timeout) so IPv6-unreachable environments can quickly fall back to IPv4 for guarded fetch paths. (#19950) Thanks @ENAwareness.</li>
<li>Security/Config: make parsed chat allowlist checks fail closed when <code>allowFrom</code> is empty, restoring expected DM/pairing gating.</li>
<li>Security/Exec: in non-default setups that manually add <code>sort</code> to <code>tools.exec.safeBins</code>, block <code>sort --compress-program</code> so allowlist-mode safe-bin checks cannot bypass approval. Thanks @tdjackey for reporting.</li>
<li>Security/Exec approvals: when users choose <code>allow-always</code> for shell-wrapper commands (for example <code>/bin/zsh -lc ...</code>), persist allowlist patterns for the inner executable(s) instead of the wrapper shell binary, preventing accidental broad shell allowlisting in moderate mode. (#23276) Thanks @xrom2863.</li>
<li>Security/Exec: fail closed when <code>tools.exec.host=sandbox</code> is configured/requested but sandbox runtime is unavailable. (#23398) Thanks @bmendonca3.</li>
<li>Security/macOS app beta: enforce path-only <code>system.run</code> allowlist matching (drop basename matches like <code>echo</code>), migrate legacy basename entries to last resolved paths when available, and harden shell-chain handling to fail closed on unsafe parse/control syntax (including quoted command substitution/backticks). This is an optional allowlist-mode feature; default installs remain deny-by-default. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Agents: auto-generate and persist a dedicated <code>commands.ownerDisplaySecret</code> when <code>commands.ownerDisplay=hash</code>, remove gateway token fallback from owner-ID prompt hashing across CLI and embedded agent runners, and centralize owner-display secret resolution in one shared helper. This ships in the next npm release. Thanks @aether-ai-agent for reporting.</li>
<li>Security/SSRF: expand IPv4 fetch guard blocking to include RFC special-use/non-global ranges (including benchmarking, TEST-NET, multicast, and reserved/broadcast blocks), centralize range checks into a single CIDR policy table, and reuse one shared host/IP classifier across literal + DNS checks to reduce classifier drift. This ships in the next npm release. Thanks @princeeismond-dot for reporting.</li>
<li>Security/SSRF: block RFC2544 benchmarking range (<code>198.18.0.0/15</code>) across direct and embedded-IP paths, and normalize IPv6 dotted-quad transition literals (for example <code>::127.0.0.1</code>, <code>64:ff9b::8.8.8.8</code>) in shared IP parsing/classification.</li>
<li>Security/Archive: block zip symlink escapes during archive extraction.</li>
<li>Security/Media sandbox: keep tmp media allowance for absolute tmp paths only and enforce symlink-escape checks before sandbox-validated reads, preventing tmp symlink exfiltration and relative <code>../</code> sandbox escapes when sandboxes live under tmp. (#17892) Thanks @dashed.</li>
<li>Browser/Upload: accept canonical in-root upload paths when the configured uploads directory is a symlink alias (for example <code>/tmp</code> -> <code>/private/tmp</code> on macOS), so browser upload validation no longer rejects valid files during client->server revalidation. (#23300, #23222, #22848) Thanks @bgaither4, @parkerati, and @Nabsku.</li>
<li>Security/Discord: add <code>openclaw security audit</code> warnings for name/tag-based Discord allowlist entries (DM allowlists, guild/channel <code>users</code>, and pairing-store entries), highlighting slug-collision risk while keeping name-based matching supported, and canonicalize resolved Discord allowlist names to IDs at runtime without rewriting config files. Thanks @tdjackey for reporting.</li>
<li>Security/Gateway: block node-role connections when device identity metadata is missing.</li>
<li>Security/Media: enforce inbound media byte limits during download/read across Discord, Telegram, Zalo, Microsoft Teams, and BlueBubbles to prevent oversized payload memory spikes before rejection. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Media/Understanding: preserve <code>application/pdf</code> MIME classification during text-like file heuristics so PDF uploads use PDF extraction paths instead of being inlined as raw text. (#23191) Thanks @claudeplay2026-byte.</li>
<li>Security/Control UI: block symlink-based out-of-root static file reads by enforcing realpath containment and file-identity checks when serving Control UI assets and SPA fallback <code>index.html</code>. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Gateway avatars: block symlink traversal during local avatar <code>data:</code> URL resolution by enforcing realpath containment and file-identity checks before reads. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/Control UI: centralize avatar URL/path validation across gateway/config helpers and enforce a 2 MB max size for local agent avatar files before <code>/avatar</code> resolution, reducing oversized-avatar memory risk without changing supported avatar formats.</li>
<li>Security/Control UI avatars: harden <code>/avatar/:agentId</code> local avatar serving by rejecting symlink paths and requiring fd-level file identity + size checks before reads. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/MSTeams media: enforce allowlist checks for SharePoint reference attachment URLs and redirect targets during Graph-backed media fetches so redirect chains cannot escape configured media host boundaries. This ships in the next npm release. Thanks @tdjackey for reporting.</li>
<li>Security/MSTeams media: route attachment auth-retry and Graph SharePoint download redirects through shared <code>safeFetch</code> so each hop is validated with allowlist + DNS/IP checks across the full redirect chain. (#23598) Thanks @Asm3r96 and @lewiswigmore.</li>
<li>Security/macOS discovery: fail closed for unresolved discovery endpoints by clearing stale remote selection values, use resolved service host only for SSH target derivation, and keep remote URL config aligned with resolved endpoint availability. (#21618) Thanks @bmendonca3.</li>
<li>Chat/Usage/TUI: strip synthetic inbound metadata blocks (including <code>Conversation info</code> and trailing <code>Untrusted context</code> channel metadata wrappers) from displayed conversation history so internal prompt context no longer leaks into user-visible logs.</li>
<li>CI/Tests: fix TypeScript case-table typing and lint assertion regressions so <code>pnpm check</code> passes again after Synology Chat landing. (#23012) Thanks @druide67.</li>
<li>Security/Browser relay: harden extension relay auth token handling for <code>/extension</code> and <code>/cdp</code> pathways.</li>
<li>Cron: persist <code>delivered</code> state in cron job records so delivery failures remain visible in status and logs. (#19174) Thanks @simonemacario.</li>
<li>Config/Doctor: only repair the OAuth credentials directory when affected channels are configured, avoiding fresh-install noise.</li>
<li>Config/Channels: whitelist <code>channels.modelByChannel</code> in config validation and exclude it from plugin auto-enable channel detection so model overrides no longer trigger <code>unknown channel id</code> validation errors or bogus <code>modelByChannel</code> plugin enables. (#23412) Thanks @ProspectOre.</li>
<li>Config/Bindings: allow optional <code>bindings[].comment</code> in strict config validation so annotated binding entries no longer fail load. (#23458) Thanks @echoVic.</li>
<li>Usage/Pricing: correct MiniMax M2.5 pricing defaults to fix inflated cost reporting. (#22755) Thanks @miloudbelarebia.</li>
<li>Gateway/Daemon: verify gateway health after daemon restart.</li>
<li>Agents/UI text: stop rewriting normal assistant billing/payment language outside explicit error contexts. (#17834) Thanks @niceysam.</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.2.21/OpenClaw-2026.2.21.zip" length="23065599" type="application/octet-stream" sparkle:edSignature="Wg3P8rMvYO3uWoVR7Izxjm5hC5W0C5jCG2dR4WFSe8ULpUUU79YDJc99NMBnl8ym7ZVbelS3kZ0QSg0Wq2GhCw=="/>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.22-beta.1/OpenClaw-2026.2.22.zip" length="23096856" type="application/octet-stream" sparkle:edSignature="aoVaCQPj9ajiSD+OjMZdUOyNzACFlMxU7m4ns+4LF1eWaizGLGHk4S0OPnHVQ+DAQY2DCHua+z4F0SMI6o01DA=="/>
</item>
</channel>
</rss>
+2 -2
View File
@@ -21,8 +21,8 @@ android {
applicationId = "ai.openclaw.android"
minSdk = 31
targetSdk = 36
versionCode = 202602210
versionName = "2026.2.21"
versionCode = 202602230
versionName = "2026.2.23"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@@ -178,7 +178,7 @@ class GatewaySession(
private val connectDeferred = CompletableDeferred<Unit>()
private val closedDeferred = CompletableDeferred<Unit>()
private val isClosed = AtomicBoolean(false)
private val connectNonceDeferred = CompletableDeferred<String?>()
private val connectNonceDeferred = CompletableDeferred<String>()
private val client: OkHttpClient = buildClient()
private var socket: WebSocket? = null
private val loggerTag = "OpenClawGateway"
@@ -296,7 +296,7 @@ class GatewaySession(
}
}
private suspend fun sendConnect(connectNonce: String?) {
private suspend fun sendConnect(connectNonce: String) {
val identity = identityStore.loadOrCreate()
val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role)
val trimmedToken = token?.trim().orEmpty()
@@ -332,7 +332,7 @@ class GatewaySession(
private fun buildConnectParams(
identity: DeviceIdentity,
connectNonce: String?,
connectNonce: String,
authToken: String,
authPassword: String?,
): JsonObject {
@@ -385,9 +385,7 @@ class GatewaySession(
put("publicKey", JsonPrimitive(publicKey))
put("signature", JsonPrimitive(signature))
put("signedAt", JsonPrimitive(signedAtMs))
if (!connectNonce.isNullOrBlank()) {
put("nonce", JsonPrimitive(connectNonce))
}
put("nonce", JsonPrimitive(connectNonce))
}
} else {
null
@@ -447,8 +445,8 @@ class GatewaySession(
frame["payload"]?.let { it.toString() } ?: frame["payloadJSON"].asStringOrNull()
if (event == "connect.challenge") {
val nonce = extractConnectNonce(payloadJson)
if (!connectNonceDeferred.isCompleted) {
connectNonceDeferred.complete(nonce)
if (!connectNonceDeferred.isCompleted && !nonce.isNullOrBlank()) {
connectNonceDeferred.complete(nonce.trim())
}
return
}
@@ -459,12 +457,11 @@ class GatewaySession(
onEvent(event, payloadJson)
}
private suspend fun awaitConnectNonce(): String? {
if (isLoopbackHost(endpoint.host)) return null
private suspend fun awaitConnectNonce(): String {
return try {
withTimeout(2_000) { connectNonceDeferred.await() }
} catch (_: Throwable) {
null
} catch (err: Throwable) {
throw IllegalStateException("connect challenge timeout", err)
}
}
@@ -595,14 +592,13 @@ class GatewaySession(
scopes: List<String>,
signedAtMs: Long,
token: String?,
nonce: String?,
nonce: String,
): String {
val scopeString = scopes.joinToString(",")
val authToken = token.orEmpty()
val version = if (nonce.isNullOrBlank()) "v1" else "v2"
val parts =
mutableListOf(
version,
"v2",
deviceId,
clientId,
clientMode,
@@ -610,10 +606,8 @@ class GatewaySession(
scopeString,
signedAtMs.toString(),
authToken,
nonce,
)
if (!nonce.isNullOrBlank()) {
parts.add(nonce)
}
return parts.joinToString("|")
}
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.21</string>
<string>2026.2.23</string>
<key>CFBundleVersion</key>
<string>20260220</string>
<string>20260223</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
@@ -0,0 +1,40 @@
import SwiftUI
struct DeepLinkAgentPromptAlert: ViewModifier {
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
private var promptBinding: Binding<NodeAppModel.AgentDeepLinkPrompt?> {
Binding(
get: { self.appModel.pendingAgentDeepLinkPrompt },
set: { _ in
// Keep prompt state until explicit user action.
})
}
func body(content: Content) -> some View {
content.alert(item: self.promptBinding) { prompt in
Alert(
title: Text("Run OpenClaw agent?"),
message: Text(
"""
Message:
\(prompt.messagePreview)
URL:
\(prompt.urlPreview)
"""),
primaryButton: .cancel(Text("Cancel")) {
self.appModel.declinePendingAgentDeepLinkPrompt()
},
secondaryButton: .default(Text("Run")) {
Task { await self.appModel.approvePendingAgentDeepLinkPrompt() }
})
}
}
}
extension View {
func deepLinkAgentPromptAlert() -> some View {
self.modifier(DeepLinkAgentPromptAlert())
}
}
@@ -704,7 +704,7 @@ final class GatewayConnectionController {
var addr = in_addr()
let parsed = host.withCString { inet_pton(AF_INET, $0, &addr) == 1 }
guard parsed else { return false }
let value = ntohl(addr.s_addr)
let value = UInt32(bigEndian: addr.s_addr)
let firstOctet = UInt8((value >> 24) & 0xFF)
return firstOctet == 127
}
+2 -2
View File
@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.21</string>
<string>2026.2.23</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -32,7 +32,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>20260220</string>
<string>20260223</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
+249 -131
View File
@@ -3,6 +3,7 @@ import OpenClawKit
import OpenClawProtocol
import Observation
import os
import Security
import SwiftUI
import UIKit
import UserNotifications
@@ -37,9 +38,22 @@ private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
cont?.resume(returning: response)
}
}
private enum IOSDeepLinkAgentPolicy {
static let maxMessageChars = 20000
static let maxUnkeyedConfirmChars = 240
}
@MainActor
@Observable
final class NodeAppModel {
struct AgentDeepLinkPrompt: Identifiable, Equatable {
let id: String
let messagePreview: String
let urlPreview: String
let request: AgentDeepLink
}
private let deepLinkLogger = Logger(subsystem: "ai.openclaw.ios", category: "DeepLink")
private let pushWakeLogger = Logger(subsystem: "ai.openclaw.ios", category: "PushWake")
private let locationWakeLogger = Logger(subsystem: "ai.openclaw.ios", category: "LocationWake")
@@ -74,6 +88,8 @@ final class NodeAppModel {
var gatewayAgents: [AgentSummary] = []
var lastShareEventText: String = "No share events yet."
var openChatRequestID: Int = 0
private(set) var pendingAgentDeepLinkPrompt: AgentDeepLinkPrompt?
private var lastAgentDeepLinkPromptAt: Date = .distantPast
// Primary "node" connection: used for device capabilities and node.invoke requests.
private let nodeGateway = GatewayNodeSession()
@@ -485,21 +501,14 @@ final class NodeAppModel {
}
}
private func applyMainSessionKey(_ key: String?) {
let trimmed = (key ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let current = self.mainSessionBaseKey.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed == current { return }
self.mainSessionBaseKey = trimmed
self.talkMode.updateMainSessionKey(self.mainSessionKey)
}
var seamColor: Color {
Self.color(fromHex: self.seamColorHex) ?? Self.defaultSeamColor
}
private static let defaultSeamColor = Color(red: 79 / 255.0, green: 122 / 255.0, blue: 154 / 255.0)
private static let apnsDeviceTokenUserDefaultsKey = "push.apns.deviceTokenHex"
private static let deepLinkKeyUserDefaultsKey = "deeplink.agent.key"
private static let canvasUnattendedDeepLinkKey: String = NodeAppModel.generateDeepLinkKey()
private static var apnsEnvironment: String {
#if DEBUG
"sandbox"
@@ -508,17 +517,6 @@ final class NodeAppModel {
#endif
}
private static func color(fromHex raw: String?) -> Color? {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let hex = trimmed.hasPrefix("#") ? String(trimmed.dropFirst()) : trimmed
guard hex.count == 6, let value = Int(hex, radix: 16) else { return nil }
let r = Double((value >> 16) & 0xFF) / 255.0
let g = Double((value >> 8) & 0xFF) / 255.0
let b = Double(value & 0xFF) / 255.0
return Color(red: r, green: g, blue: b)
}
private func refreshBrandingFromGateway() async {
do {
let res = try await self.operatorGateway.request(method: "config.get", paramsJSON: "{}", timeoutSeconds: 8)
@@ -699,117 +697,6 @@ final class NodeAppModel {
self.gatewayHealthMonitor.stop()
}
private func refreshWakeWordsFromGateway() async {
do {
let data = try await self.operatorGateway.request(method: "voicewake.get", paramsJSON: "{}", timeoutSeconds: 8)
guard let triggers = VoiceWakePreferences.decodeGatewayTriggers(from: data) else { return }
VoiceWakePreferences.saveTriggerWords(triggers)
} catch {
if let gatewayError = error as? GatewayResponseError {
let lower = gatewayError.message.lowercased()
if lower.contains("unauthorized role") || lower.contains("missing scope") {
await self.setGatewayHealthMonitorDisabled(true)
return
}
}
// Best-effort only.
}
}
private func isGatewayHealthMonitorDisabled() -> Bool {
self.gatewayHealthMonitorDisabled
}
private func setGatewayHealthMonitorDisabled(_ disabled: Bool) {
self.gatewayHealthMonitorDisabled = disabled
}
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
if await !self.isGatewayConnected() {
throw NSError(domain: "Gateway", code: 10, userInfo: [
NSLocalizedDescriptionKey: "Gateway not connected",
])
}
struct Payload: Codable {
var text: String
var sessionKey: String?
}
let payload = Payload(text: text, sessionKey: sessionKey)
let data = try JSONEncoder().encode(payload)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode voice transcript payload as UTF-8",
])
}
await self.nodeGateway.sendEvent(event: "voice.transcript", payloadJSON: json)
}
func handleDeepLink(url: URL) async {
guard let route = DeepLinkParser.parse(url) else { return }
switch route {
case let .agent(link):
await self.handleAgentDeepLink(link, originalURL: url)
case .gateway:
break
}
}
private func handleAgentDeepLink(_ link: AgentDeepLink, originalURL: URL) async {
let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines)
guard !message.isEmpty else { return }
self.deepLinkLogger.info(
"agent deep link received messageChars=\(message.count) url=\(originalURL.absoluteString, privacy: .public)"
)
if message.count > 20000 {
self.screen.errorText = "Deep link too large (message exceeds 20,000 characters)."
self.recordShareEvent("Rejected: message too large (\(message.count) chars).")
return
}
guard await self.isGatewayConnected() else {
self.screen.errorText = "Gateway not connected (cannot forward deep link)."
self.recordShareEvent("Failed: gateway not connected.")
self.deepLinkLogger.error("agent deep link rejected: gateway not connected")
return
}
do {
try await self.sendAgentRequest(link: link)
self.screen.errorText = nil
self.recordShareEvent("Sent to gateway (\(message.count) chars).")
self.deepLinkLogger.info("agent deep link forwarded to gateway")
self.openChatRequestID &+= 1
} catch {
self.screen.errorText = "Agent request failed: \(error.localizedDescription)"
self.recordShareEvent("Failed: \(error.localizedDescription)")
self.deepLinkLogger.error("agent deep link send failed: \(error.localizedDescription, privacy: .public)")
}
}
private func sendAgentRequest(link: AgentDeepLink) async throws {
if link.message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
throw NSError(domain: "DeepLink", code: 1, userInfo: [
NSLocalizedDescriptionKey: "invalid agent message",
])
}
// iOS gateway forwards to the gateway; no local auth prompts here.
// (Key-based unattended auth is handled on macOS for openclaw:// links.)
let data = try JSONEncoder().encode(link)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode agent request payload as UTF-8",
])
}
await self.nodeGateway.sendEvent(event: "agent.request", payloadJSON: json)
}
private func isGatewayConnected() async -> Bool {
self.gatewayConnected
}
private func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
let command = req.command
@@ -2560,6 +2447,229 @@ extension NodeAppModel {
}
}
extension NodeAppModel {
private func refreshWakeWordsFromGateway() async {
do {
let data = try await self.operatorGateway.request(method: "voicewake.get", paramsJSON: "{}", timeoutSeconds: 8)
guard let triggers = VoiceWakePreferences.decodeGatewayTriggers(from: data) else { return }
VoiceWakePreferences.saveTriggerWords(triggers)
} catch {
if let gatewayError = error as? GatewayResponseError {
let lower = gatewayError.message.lowercased()
if lower.contains("unauthorized role") || lower.contains("missing scope") {
await self.setGatewayHealthMonitorDisabled(true)
return
}
}
// Best-effort only.
}
}
private func isGatewayHealthMonitorDisabled() -> Bool {
self.gatewayHealthMonitorDisabled
}
private func setGatewayHealthMonitorDisabled(_ disabled: Bool) {
self.gatewayHealthMonitorDisabled = disabled
}
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
if await !self.isGatewayConnected() {
throw NSError(domain: "Gateway", code: 10, userInfo: [
NSLocalizedDescriptionKey: "Gateway not connected",
])
}
struct Payload: Codable {
var text: String
var sessionKey: String?
}
let payload = Payload(text: text, sessionKey: sessionKey)
let data = try JSONEncoder().encode(payload)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode voice transcript payload as UTF-8",
])
}
await self.nodeGateway.sendEvent(event: "voice.transcript", payloadJSON: json)
}
func handleDeepLink(url: URL) async {
guard let route = DeepLinkParser.parse(url) else { return }
switch route {
case let .agent(link):
await self.handleAgentDeepLink(link, originalURL: url)
case .gateway:
break
}
}
private func handleAgentDeepLink(_ link: AgentDeepLink, originalURL: URL) async {
let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines)
guard !message.isEmpty else { return }
self.deepLinkLogger.info(
"agent deep link received messageChars=\(message.count) url=\(originalURL.absoluteString, privacy: .public)"
)
if message.count > IOSDeepLinkAgentPolicy.maxMessageChars {
self.screen.errorText = "Deep link too large (message exceeds \(IOSDeepLinkAgentPolicy.maxMessageChars) characters)."
self.recordShareEvent("Rejected: message too large (\(message.count) chars).")
return
}
guard await self.isGatewayConnected() else {
self.screen.errorText = "Gateway not connected (cannot forward deep link)."
self.recordShareEvent("Failed: gateway not connected.")
self.deepLinkLogger.error("agent deep link rejected: gateway not connected")
return
}
let allowUnattended = self.isUnattendedDeepLinkAllowed(link.key)
if !allowUnattended {
if message.count > IOSDeepLinkAgentPolicy.maxUnkeyedConfirmChars {
self.screen.errorText = "Deep link blocked (message too long without key)."
self.recordShareEvent(
"Rejected: deep link over \(IOSDeepLinkAgentPolicy.maxUnkeyedConfirmChars) chars without key.")
self.deepLinkLogger.error(
"agent deep link rejected: unkeyed message too long chars=\(message.count, privacy: .public)")
return
}
if Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt) < 1.0 {
self.deepLinkLogger.debug("agent deep link prompt throttled")
return
}
self.lastAgentDeepLinkPromptAt = Date()
let urlText = originalURL.absoluteString
let prompt = AgentDeepLinkPrompt(
id: UUID().uuidString,
messagePreview: message,
urlPreview: urlText.count > 500 ? "\(urlText.prefix(500))" : urlText,
request: self.effectiveAgentDeepLinkForPrompt(link))
self.pendingAgentDeepLinkPrompt = prompt
self.recordShareEvent("Awaiting local confirmation (\(message.count) chars).")
self.deepLinkLogger.info("agent deep link requires local confirmation")
return
}
await self.submitAgentDeepLink(link, messageCharCount: message.count)
}
private func sendAgentRequest(link: AgentDeepLink) async throws {
if link.message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
throw NSError(domain: "DeepLink", code: 1, userInfo: [
NSLocalizedDescriptionKey: "invalid agent message",
])
}
let data = try JSONEncoder().encode(link)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode agent request payload as UTF-8",
])
}
await self.nodeGateway.sendEvent(event: "agent.request", payloadJSON: json)
}
private func isGatewayConnected() async -> Bool {
self.gatewayConnected
}
private func applyMainSessionKey(_ key: String?) {
let trimmed = (key ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let current = self.mainSessionBaseKey.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed == current { return }
self.mainSessionBaseKey = trimmed
self.talkMode.updateMainSessionKey(self.mainSessionKey)
}
private static func color(fromHex raw: String?) -> Color? {
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let hex = trimmed.hasPrefix("#") ? String(trimmed.dropFirst()) : trimmed
guard hex.count == 6, let value = Int(hex, radix: 16) else { return nil }
let r = Double((value >> 16) & 0xFF) / 255.0
let g = Double((value >> 8) & 0xFF) / 255.0
let b = Double(value & 0xFF) / 255.0
return Color(red: r, green: g, blue: b)
}
func approvePendingAgentDeepLinkPrompt() async {
guard let prompt = self.pendingAgentDeepLinkPrompt else { return }
self.pendingAgentDeepLinkPrompt = nil
guard await self.isGatewayConnected() else {
self.screen.errorText = "Gateway not connected (cannot forward deep link)."
self.recordShareEvent("Failed: gateway not connected.")
self.deepLinkLogger.error("agent deep link approval failed: gateway not connected")
return
}
await self.submitAgentDeepLink(prompt.request, messageCharCount: prompt.messagePreview.count)
}
func declinePendingAgentDeepLinkPrompt() {
guard self.pendingAgentDeepLinkPrompt != nil else { return }
self.pendingAgentDeepLinkPrompt = nil
self.screen.errorText = "Deep link cancelled."
self.recordShareEvent("Cancelled: deep link confirmation declined.")
self.deepLinkLogger.info("agent deep link cancelled by local user")
}
private func submitAgentDeepLink(_ link: AgentDeepLink, messageCharCount: Int) async {
do {
try await self.sendAgentRequest(link: link)
self.screen.errorText = nil
self.recordShareEvent("Sent to gateway (\(messageCharCount) chars).")
self.deepLinkLogger.info("agent deep link forwarded to gateway")
self.openChatRequestID &+= 1
} catch {
self.screen.errorText = "Agent request failed: \(error.localizedDescription)"
self.recordShareEvent("Failed: \(error.localizedDescription)")
self.deepLinkLogger.error("agent deep link send failed: \(error.localizedDescription, privacy: .public)")
}
}
private func effectiveAgentDeepLinkForPrompt(_ link: AgentDeepLink) -> AgentDeepLink {
// Without a trusted key, strip delivery/routing knobs to reduce exfiltration risk.
AgentDeepLink(
message: link.message,
sessionKey: link.sessionKey,
thinking: link.thinking,
deliver: false,
to: nil,
channel: nil,
timeoutSeconds: link.timeoutSeconds,
key: link.key)
}
private func isUnattendedDeepLinkAllowed(_ key: String?) -> Bool {
let normalizedKey = key?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !normalizedKey.isEmpty else { return false }
return normalizedKey == Self.canvasUnattendedDeepLinkKey || normalizedKey == Self.expectedDeepLinkKey()
}
private static func expectedDeepLinkKey() -> String {
let defaults = UserDefaults.standard
if let key = defaults.string(forKey: self.deepLinkKeyUserDefaultsKey), !key.isEmpty {
return key
}
let key = self.generateDeepLinkKey()
defaults.set(key, forKey: self.deepLinkKeyUserDefaultsKey)
return key
}
private static func generateDeepLinkKey() -> String {
var bytes = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
let data = Data(bytes)
return data
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
extension NodeAppModel {
func _bridgeConsumeMirroredWatchReply(_ event: WatchQuickReplyEvent) async {
await self.handleWatchQuickReply(event)
@@ -2607,5 +2717,13 @@ extension NodeAppModel {
func _test_queuedWatchReplyCount() -> Int {
self.queuedWatchReplies.count
}
func _test_setGatewayConnected(_ connected: Bool) {
self.gatewayConnected = connected
}
static func _test_currentDeepLinkKey() -> String {
self.expectedDeepLinkKey()
}
}
#endif
+1
View File
@@ -88,6 +88,7 @@ struct RootCanvas: View {
}
}
.gatewayTrustPromptAlert()
.deepLinkAgentPromptAlert()
.sheet(item: self.$presentedSheet) { sheet in
switch sheet {
case .settings:
+271 -69
View File
@@ -91,6 +91,8 @@ final class TalkModeManager: NSObject {
private var incrementalSpeechBuffer = IncrementalSpeechBuffer()
private var incrementalSpeechContext: IncrementalSpeechContext?
private var incrementalSpeechDirective: TalkDirective?
private var incrementalSpeechPrefetch: IncrementalSpeechPrefetchState?
private var incrementalSpeechPrefetchMonitorTask: Task<Void, Never>?
private let logger = Logger(subsystem: "bot.molt", category: "TalkMode")
@@ -551,6 +553,16 @@ final class TalkModeManager: NSObject {
guard let self else { return }
if let error {
let msg = error.localizedDescription
let lowered = msg.lowercased()
let isCancellation = lowered.contains("cancelled") || lowered.contains("canceled")
if isCancellation {
GatewayDiagnostics.log("talk speech: cancelled")
if self.captureMode == .continuous, self.isEnabled, !self.isSpeaking {
self.statusText = "Listening"
}
self.logger.debug("speech recognition cancelled")
return
}
GatewayDiagnostics.log("talk speech: error=\(msg)")
if !self.isSpeaking {
if msg.localizedCaseInsensitiveContains("no speech detected") {
@@ -1177,6 +1189,7 @@ final class TalkModeManager: NSObject {
self.incrementalSpeechQueue.removeAll()
self.incrementalSpeechTask?.cancel()
self.incrementalSpeechTask = nil
self.cancelIncrementalPrefetch()
self.incrementalSpeechActive = true
self.incrementalSpeechUsed = false
self.incrementalSpeechLanguage = nil
@@ -1189,6 +1202,7 @@ final class TalkModeManager: NSObject {
self.incrementalSpeechQueue.removeAll()
self.incrementalSpeechTask?.cancel()
self.incrementalSpeechTask = nil
self.cancelIncrementalPrefetch()
self.incrementalSpeechActive = false
self.incrementalSpeechContext = nil
self.incrementalSpeechDirective = nil
@@ -1216,20 +1230,168 @@ final class TalkModeManager: NSObject {
self.incrementalSpeechTask = Task { @MainActor [weak self] in
guard let self else { return }
defer {
self.cancelIncrementalPrefetch()
self.isSpeaking = false
self.stopRecognition()
self.incrementalSpeechTask = nil
}
while !Task.isCancelled {
guard !self.incrementalSpeechQueue.isEmpty else { break }
let segment = self.incrementalSpeechQueue.removeFirst()
self.statusText = "Speaking…"
self.isSpeaking = true
self.lastSpokenText = segment
await self.speakIncrementalSegment(segment)
await self.updateIncrementalContextIfNeeded()
let context = self.incrementalSpeechContext
let prefetchedAudio = await self.consumeIncrementalPrefetchedAudioIfAvailable(
for: segment,
context: context)
if let context {
self.startIncrementalPrefetchMonitor(context: context)
}
await self.speakIncrementalSegment(
segment,
context: context,
prefetchedAudio: prefetchedAudio)
self.cancelIncrementalPrefetchMonitor()
}
self.isSpeaking = false
self.stopRecognition()
self.incrementalSpeechTask = nil
}
}
private func cancelIncrementalPrefetch() {
self.cancelIncrementalPrefetchMonitor()
self.incrementalSpeechPrefetch?.task.cancel()
self.incrementalSpeechPrefetch = nil
}
private func cancelIncrementalPrefetchMonitor() {
self.incrementalSpeechPrefetchMonitorTask?.cancel()
self.incrementalSpeechPrefetchMonitorTask = nil
}
private func startIncrementalPrefetchMonitor(context: IncrementalSpeechContext) {
self.cancelIncrementalPrefetchMonitor()
self.incrementalSpeechPrefetchMonitorTask = Task { @MainActor [weak self] in
guard let self else { return }
while !Task.isCancelled {
if self.ensureIncrementalPrefetchForUpcomingSegment(context: context) {
return
}
try? await Task.sleep(nanoseconds: 40_000_000)
}
}
}
private func ensureIncrementalPrefetchForUpcomingSegment(context: IncrementalSpeechContext) -> Bool {
guard context.canUseElevenLabs else {
self.cancelIncrementalPrefetch()
return false
}
guard let nextSegment = self.incrementalSpeechQueue.first else { return false }
if let existing = self.incrementalSpeechPrefetch {
if existing.segment == nextSegment, existing.context == context {
return true
}
existing.task.cancel()
self.incrementalSpeechPrefetch = nil
}
self.startIncrementalPrefetch(segment: nextSegment, context: context)
return self.incrementalSpeechPrefetch != nil
}
private func startIncrementalPrefetch(segment: String, context: IncrementalSpeechContext) {
guard context.canUseElevenLabs, let apiKey = context.apiKey, let voiceId = context.voiceId else { return }
let prefetchOutputFormat = self.resolveIncrementalPrefetchOutputFormat(context: context)
let request = self.makeIncrementalTTSRequest(
text: segment,
context: context,
outputFormat: prefetchOutputFormat)
let id = UUID()
let task = Task { [weak self] in
let stream = ElevenLabsTTSClient(apiKey: apiKey).streamSynthesize(voiceId: voiceId, request: request)
var chunks: [Data] = []
do {
for try await chunk in stream {
try Task.checkCancellation()
chunks.append(chunk)
}
await self?.completeIncrementalPrefetch(id: id, chunks: chunks)
} catch is CancellationError {
await self?.clearIncrementalPrefetch(id: id)
} catch {
await self?.failIncrementalPrefetch(id: id, error: error)
}
}
self.incrementalSpeechPrefetch = IncrementalSpeechPrefetchState(
id: id,
segment: segment,
context: context,
outputFormat: prefetchOutputFormat,
chunks: nil,
task: task)
}
private func completeIncrementalPrefetch(id: UUID, chunks: [Data]) {
guard var prefetch = self.incrementalSpeechPrefetch, prefetch.id == id else { return }
prefetch.chunks = chunks
self.incrementalSpeechPrefetch = prefetch
}
private func clearIncrementalPrefetch(id: UUID) {
guard let prefetch = self.incrementalSpeechPrefetch, prefetch.id == id else { return }
prefetch.task.cancel()
self.incrementalSpeechPrefetch = nil
}
private func failIncrementalPrefetch(id: UUID, error: any Error) {
guard let prefetch = self.incrementalSpeechPrefetch, prefetch.id == id else { return }
self.logger.debug("incremental prefetch failed: \(error.localizedDescription, privacy: .public)")
prefetch.task.cancel()
self.incrementalSpeechPrefetch = nil
}
private func consumeIncrementalPrefetchedAudioIfAvailable(
for segment: String,
context: IncrementalSpeechContext?
) async -> IncrementalPrefetchedAudio?
{
guard let context else {
self.cancelIncrementalPrefetch()
return nil
}
guard let prefetch = self.incrementalSpeechPrefetch else {
return nil
}
guard prefetch.context == context else {
prefetch.task.cancel()
self.incrementalSpeechPrefetch = nil
return nil
}
guard prefetch.segment == segment else {
return nil
}
if let chunks = prefetch.chunks, !chunks.isEmpty {
let prefetched = IncrementalPrefetchedAudio(chunks: chunks, outputFormat: prefetch.outputFormat)
self.incrementalSpeechPrefetch = nil
return prefetched
}
await prefetch.task.value
guard let completed = self.incrementalSpeechPrefetch else { return nil }
guard completed.context == context, completed.segment == segment else { return nil }
guard let chunks = completed.chunks, !chunks.isEmpty else { return nil }
let prefetched = IncrementalPrefetchedAudio(chunks: chunks, outputFormat: completed.outputFormat)
self.incrementalSpeechPrefetch = nil
return prefetched
}
private func resolveIncrementalPrefetchOutputFormat(context: IncrementalSpeechContext) -> String? {
if TalkTTSValidation.pcmSampleRate(from: context.outputFormat) != nil {
return ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
}
return context.outputFormat
}
private func finishIncrementalSpeech() async {
guard self.incrementalSpeechActive else { return }
let leftover = self.incrementalSpeechBuffer.flush()
@@ -1337,77 +1499,103 @@ final class TalkModeManager: NSObject {
canUseElevenLabs: canUseElevenLabs)
}
private func speakIncrementalSegment(_ text: String) async {
await self.updateIncrementalContextIfNeeded()
guard let context = self.incrementalSpeechContext else {
private func makeIncrementalTTSRequest(
text: String,
context: IncrementalSpeechContext,
outputFormat: String?
) -> ElevenLabsTTSRequest
{
ElevenLabsTTSRequest(
text: text,
modelId: context.modelId,
outputFormat: outputFormat,
speed: TalkTTSValidation.resolveSpeed(
speed: context.directive?.speed,
rateWPM: context.directive?.rateWPM),
stability: TalkTTSValidation.validatedStability(
context.directive?.stability,
modelId: context.modelId),
similarity: TalkTTSValidation.validatedUnit(context.directive?.similarity),
style: TalkTTSValidation.validatedUnit(context.directive?.style),
speakerBoost: context.directive?.speakerBoost,
seed: TalkTTSValidation.validatedSeed(context.directive?.seed),
normalize: ElevenLabsTTSClient.validatedNormalize(context.directive?.normalize),
language: context.language,
latencyTier: TalkTTSValidation.validatedLatencyTier(context.directive?.latencyTier))
}
private static func makeBufferedAudioStream(chunks: [Data]) -> AsyncThrowingStream<Data, Error> {
AsyncThrowingStream { continuation in
for chunk in chunks {
continuation.yield(chunk)
}
continuation.finish()
}
}
private func speakIncrementalSegment(
_ text: String,
context preferredContext: IncrementalSpeechContext? = nil,
prefetchedAudio: IncrementalPrefetchedAudio? = nil
) async
{
let context: IncrementalSpeechContext
if let preferredContext {
context = preferredContext
} else {
await self.updateIncrementalContextIfNeeded()
guard let resolvedContext = self.incrementalSpeechContext else {
try? await TalkSystemSpeechSynthesizer.shared.speak(
text: text,
language: self.incrementalSpeechLanguage)
return
}
context = resolvedContext
}
guard context.canUseElevenLabs, let apiKey = context.apiKey, let voiceId = context.voiceId else {
try? await TalkSystemSpeechSynthesizer.shared.speak(
text: text,
language: self.incrementalSpeechLanguage)
return
}
if context.canUseElevenLabs, let apiKey = context.apiKey, let voiceId = context.voiceId {
let request = ElevenLabsTTSRequest(
text: text,
modelId: context.modelId,
outputFormat: context.outputFormat,
speed: TalkTTSValidation.resolveSpeed(
speed: context.directive?.speed,
rateWPM: context.directive?.rateWPM),
stability: TalkTTSValidation.validatedStability(
context.directive?.stability,
modelId: context.modelId),
similarity: TalkTTSValidation.validatedUnit(context.directive?.similarity),
style: TalkTTSValidation.validatedUnit(context.directive?.style),
speakerBoost: context.directive?.speakerBoost,
seed: TalkTTSValidation.validatedSeed(context.directive?.seed),
normalize: ElevenLabsTTSClient.validatedNormalize(context.directive?.normalize),
language: context.language,
latencyTier: TalkTTSValidation.validatedLatencyTier(context.directive?.latencyTier))
let client = ElevenLabsTTSClient(apiKey: apiKey)
let stream = client.streamSynthesize(voiceId: voiceId, request: request)
let sampleRate = TalkTTSValidation.pcmSampleRate(from: context.outputFormat)
let result: StreamingPlaybackResult
if let sampleRate {
self.lastPlaybackWasPCM = true
var playback = await self.pcmPlayer.play(stream: stream, sampleRate: sampleRate)
if !playback.finished, playback.interruptedAt == nil {
self.logger.warning("pcm playback failed; retrying mp3")
self.lastPlaybackWasPCM = false
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
request: ElevenLabsTTSRequest(
text: text,
modelId: context.modelId,
outputFormat: mp3Format,
speed: TalkTTSValidation.resolveSpeed(
speed: context.directive?.speed,
rateWPM: context.directive?.rateWPM),
stability: TalkTTSValidation.validatedStability(
context.directive?.stability,
modelId: context.modelId),
similarity: TalkTTSValidation.validatedUnit(context.directive?.similarity),
style: TalkTTSValidation.validatedUnit(context.directive?.style),
speakerBoost: context.directive?.speakerBoost,
seed: TalkTTSValidation.validatedSeed(context.directive?.seed),
normalize: ElevenLabsTTSClient.validatedNormalize(context.directive?.normalize),
language: context.language,
latencyTier: TalkTTSValidation.validatedLatencyTier(context.directive?.latencyTier)))
playback = await self.mp3Player.play(stream: mp3Stream)
}
result = playback
} else {
self.lastPlaybackWasPCM = false
result = await self.mp3Player.play(stream: stream)
}
if !result.finished, let interruptedAt = result.interruptedAt {
self.lastInterruptedAtSeconds = interruptedAt
}
let client = ElevenLabsTTSClient(apiKey: apiKey)
let request = self.makeIncrementalTTSRequest(
text: text,
context: context,
outputFormat: context.outputFormat)
let stream: AsyncThrowingStream<Data, Error>
if let prefetchedAudio, !prefetchedAudio.chunks.isEmpty {
stream = Self.makeBufferedAudioStream(chunks: prefetchedAudio.chunks)
} else {
try? await TalkSystemSpeechSynthesizer.shared.speak(
text: text,
language: self.incrementalSpeechLanguage)
stream = client.streamSynthesize(voiceId: voiceId, request: request)
}
let playbackFormat = prefetchedAudio?.outputFormat ?? context.outputFormat
let sampleRate = TalkTTSValidation.pcmSampleRate(from: playbackFormat)
let result: StreamingPlaybackResult
if let sampleRate {
self.lastPlaybackWasPCM = true
var playback = await self.pcmPlayer.play(stream: stream, sampleRate: sampleRate)
if !playback.finished, playback.interruptedAt == nil {
self.logger.warning("pcm playback failed; retrying mp3")
self.lastPlaybackWasPCM = false
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
request: self.makeIncrementalTTSRequest(
text: text,
context: context,
outputFormat: mp3Format))
playback = await self.mp3Player.play(stream: mp3Stream)
}
result = playback
} else {
self.lastPlaybackWasPCM = false
result = await self.mp3Player.play(stream: stream)
}
if !result.finished, let interruptedAt = result.interruptedAt {
self.lastInterruptedAtSeconds = interruptedAt
}
}
@@ -1874,7 +2062,7 @@ extension TalkModeManager {
}
#endif
private struct IncrementalSpeechContext {
private struct IncrementalSpeechContext: Equatable {
let apiKey: String?
let voiceId: String?
let modelId: String?
@@ -1884,4 +2072,18 @@ private struct IncrementalSpeechContext {
let canUseElevenLabs: Bool
}
private struct IncrementalSpeechPrefetchState {
let id: UUID
let segment: String
let context: IncrementalSpeechContext
let outputFormat: String?
var chunks: [Data]?
let task: Task<Void, Never>
}
private struct IncrementalPrefetchedAudio {
let chunks: [Data]
let outputFormat: String?
}
// swiftlint:enable type_body_length
+2 -2
View File
@@ -17,8 +17,8 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.21</string>
<string>2026.2.23</string>
<key>CFBundleVersion</key>
<string>20260220</string>
<string>20260223</string>
</dict>
</plist>
+80 -1
View File
@@ -29,8 +29,35 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
return try body()
}
private func makeAgentDeepLinkURL(
message: String,
deliver: Bool = false,
to: String? = nil,
channel: String? = nil,
key: String? = nil) -> URL
{
var components = URLComponents()
components.scheme = "openclaw"
components.host = "agent"
var queryItems: [URLQueryItem] = [URLQueryItem(name: "message", value: message)]
if deliver {
queryItems.append(URLQueryItem(name: "deliver", value: "1"))
}
if let to {
queryItems.append(URLQueryItem(name: "to", value: to))
}
if let channel {
queryItems.append(URLQueryItem(name: "channel", value: channel))
}
if let key {
queryItems.append(URLQueryItem(name: "key", value: key))
}
components.queryItems = queryItems
return components.url!
}
@MainActor
private final class MockWatchMessagingService: WatchMessagingServicing, @unchecked Sendable {
private final class MockWatchMessagingService: @preconcurrency WatchMessagingServicing, @unchecked Sendable {
var currentStatus = WatchMessagingStatus(
supported: true,
paired: true,
@@ -327,6 +354,58 @@ private final class MockWatchMessagingService: WatchMessagingServicing, @uncheck
#expect(appModel.screen.errorText?.contains("Deep link too large") == true)
}
@Test @MainActor func handleDeepLinkRequiresConfirmationWhenConnectedAndUnkeyed() async {
let appModel = NodeAppModel()
appModel._test_setGatewayConnected(true)
let url = makeAgentDeepLinkURL(message: "hello from deep link")
await appModel.handleDeepLink(url: url)
#expect(appModel.pendingAgentDeepLinkPrompt != nil)
#expect(appModel.openChatRequestID == 0)
await appModel.approvePendingAgentDeepLinkPrompt()
#expect(appModel.pendingAgentDeepLinkPrompt == nil)
#expect(appModel.openChatRequestID == 1)
}
@Test @MainActor func handleDeepLinkStripsDeliveryFieldsWhenUnkeyed() async throws {
let appModel = NodeAppModel()
appModel._test_setGatewayConnected(true)
let url = makeAgentDeepLinkURL(
message: "route this",
deliver: true,
to: "123456",
channel: "telegram")
await appModel.handleDeepLink(url: url)
let prompt = try #require(appModel.pendingAgentDeepLinkPrompt)
#expect(prompt.request.deliver == false)
#expect(prompt.request.to == nil)
#expect(prompt.request.channel == nil)
}
@Test @MainActor func handleDeepLinkRejectsLongUnkeyedMessageWhenConnected() async {
let appModel = NodeAppModel()
appModel._test_setGatewayConnected(true)
let message = String(repeating: "x", count: 241)
let url = makeAgentDeepLinkURL(message: message)
await appModel.handleDeepLink(url: url)
#expect(appModel.pendingAgentDeepLinkPrompt == nil)
#expect(appModel.screen.errorText?.contains("blocked") == true)
}
@Test @MainActor func handleDeepLinkBypassesPromptWithValidKey() async {
let appModel = NodeAppModel()
appModel._test_setGatewayConnected(true)
let key = NodeAppModel._test_currentDeepLinkKey()
let url = makeAgentDeepLinkURL(message: "trusted request", key: key)
await appModel.handleDeepLink(url: url)
#expect(appModel.pendingAgentDeepLinkPrompt == nil)
#expect(appModel.openChatRequestID == 1)
}
@Test @MainActor func sendVoiceTranscriptThrowsWhenGatewayOffline() async {
let appModel = NodeAppModel()
await #expect(throws: Error.self) {
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.21</string>
<string>2026.2.23</string>
<key>CFBundleVersion</key>
<string>20260220</string>
<string>20260223</string>
<key>WKCompanionAppBundleIdentifier</key>
<string>$(OPENCLAW_APP_BUNDLE_ID)</string>
<key>WKWatchKitApp</key>
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.21</string>
<string>2026.2.23</string>
<key>CFBundleVersion</key>
<string>20260220</string>
<string>20260223</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
+10 -10
View File
@@ -92,8 +92,8 @@ targets:
- CFBundleURLName: ai.openclaw.ios
CFBundleURLSchemes:
- openclaw
CFBundleShortVersionString: "2026.2.21"
CFBundleVersion: "20260220"
CFBundleShortVersionString: "2026.2.23"
CFBundleVersion: "20260223"
UILaunchScreen: {}
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
@@ -146,8 +146,8 @@ targets:
path: ShareExtension/Info.plist
properties:
CFBundleDisplayName: OpenClaw Share
CFBundleShortVersionString: "2026.2.21"
CFBundleVersion: "20260220"
CFBundleShortVersionString: "2026.2.23"
CFBundleVersion: "20260223"
NSExtension:
NSExtensionPointIdentifier: com.apple.share-services
NSExtensionPrincipalClass: "$(PRODUCT_MODULE_NAME).ShareViewController"
@@ -176,8 +176,8 @@ targets:
path: WatchApp/Info.plist
properties:
CFBundleDisplayName: OpenClaw
CFBundleShortVersionString: "2026.2.21"
CFBundleVersion: "20260220"
CFBundleShortVersionString: "2026.2.23"
CFBundleVersion: "20260223"
WKCompanionAppBundleIdentifier: "$(OPENCLAW_APP_BUNDLE_ID)"
WKWatchKitApp: true
@@ -200,8 +200,8 @@ targets:
path: WatchExtension/Info.plist
properties:
CFBundleDisplayName: OpenClaw
CFBundleShortVersionString: "2026.2.21"
CFBundleVersion: "20260220"
CFBundleShortVersionString: "2026.2.23"
CFBundleVersion: "20260223"
NSExtension:
NSExtensionAttributes:
WKAppBundleIdentifier: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)"
@@ -228,5 +228,5 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: OpenClawTests
CFBundleShortVersionString: "2026.2.21"
CFBundleVersion: "20260220"
CFBundleShortVersionString: "2026.2.23"
CFBundleVersion: "20260223"
+1 -2
View File
@@ -480,8 +480,7 @@ final class AppState {
remote.removeValue(forKey: "url")
remoteChanged = true
}
} else {
let normalizedUrl = GatewayRemoteConfig.normalizeGatewayUrlString(trimmedUrl) ?? trimmedUrl
} else if let normalizedUrl = GatewayRemoteConfig.normalizeGatewayUrlString(trimmedUrl) {
if (remote["url"] as? String) != normalizedUrl {
remote["url"] = normalizedUrl
remoteChanged = true
@@ -0,0 +1,79 @@
import Foundation
enum ExecAllowlistMatcher {
static func match(entries: [ExecAllowlistEntry], resolution: ExecCommandResolution?) -> ExecAllowlistEntry? {
guard let resolution, !entries.isEmpty else { return nil }
let rawExecutable = resolution.rawExecutable
let resolvedPath = resolution.resolvedPath
for entry in entries {
switch ExecApprovalHelpers.validateAllowlistPattern(entry.pattern) {
case .valid(let pattern):
let target = resolvedPath ?? rawExecutable
if self.matches(pattern: pattern, target: target) { return entry }
case .invalid:
continue
}
}
return nil
}
static func matchAll(
entries: [ExecAllowlistEntry],
resolutions: [ExecCommandResolution]) -> [ExecAllowlistEntry]
{
guard !entries.isEmpty, !resolutions.isEmpty else { return [] }
var matches: [ExecAllowlistEntry] = []
matches.reserveCapacity(resolutions.count)
for resolution in resolutions {
guard let match = self.match(entries: entries, resolution: resolution) else {
return []
}
matches.append(match)
}
return matches
}
private static func matches(pattern: String, target: String) -> Bool {
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
let expanded = trimmed.hasPrefix("~") ? (trimmed as NSString).expandingTildeInPath : trimmed
let normalizedPattern = self.normalizeMatchTarget(expanded)
let normalizedTarget = self.normalizeMatchTarget(target)
guard let regex = self.regex(for: normalizedPattern) else { return false }
let range = NSRange(location: 0, length: normalizedTarget.utf16.count)
return regex.firstMatch(in: normalizedTarget, options: [], range: range) != nil
}
private static func normalizeMatchTarget(_ value: String) -> String {
value.replacingOccurrences(of: "\\\\", with: "/").lowercased()
}
private static func regex(for pattern: String) -> NSRegularExpression? {
var regex = "^"
var idx = pattern.startIndex
while idx < pattern.endIndex {
let ch = pattern[idx]
if ch == "*" {
let next = pattern.index(after: idx)
if next < pattern.endIndex, pattern[next] == "*" {
regex += ".*"
idx = pattern.index(after: next)
} else {
regex += "[^/]*"
idx = next
}
continue
}
if ch == "?" {
regex += "."
idx = pattern.index(after: idx)
continue
}
regex += NSRegularExpression.escapedPattern(for: String(ch))
idx = pattern.index(after: idx)
}
regex += "$"
return try? NSRegularExpression(pattern: regex, options: [.caseInsensitive])
}
}
@@ -0,0 +1,68 @@
import Foundation
struct ExecApprovalEvaluation {
let command: [String]
let displayCommand: String
let agentId: String?
let security: ExecSecurity
let ask: ExecAsk
let env: [String: String]
let resolution: ExecCommandResolution?
let allowlistResolutions: [ExecCommandResolution]
let allowlistMatches: [ExecAllowlistEntry]
let allowlistSatisfied: Bool
let allowlistMatch: ExecAllowlistEntry?
let skillAllow: Bool
}
enum ExecApprovalEvaluator {
static func evaluate(
command: [String],
rawCommand: String?,
cwd: String?,
envOverrides: [String: String]?,
agentId: String?) async -> ExecApprovalEvaluation
{
let trimmedAgent = agentId?.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedAgentId = (trimmedAgent?.isEmpty == false) ? trimmedAgent : nil
let approvals = ExecApprovalsStore.resolve(agentId: normalizedAgentId)
let security = approvals.agent.security
let ask = approvals.agent.ask
let shellWrapper = ExecShellWrapperParser.extract(command: command, rawCommand: rawCommand).isWrapper
let env = HostEnvSanitizer.sanitize(overrides: envOverrides, shellWrapper: shellWrapper)
let displayCommand = ExecCommandFormatter.displayString(for: command, rawCommand: rawCommand)
let allowlistResolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: rawCommand,
cwd: cwd,
env: env)
let allowlistMatches = security == .allowlist
? ExecAllowlistMatcher.matchAll(entries: approvals.allowlist, resolutions: allowlistResolutions)
: []
let allowlistSatisfied = security == .allowlist &&
!allowlistResolutions.isEmpty &&
allowlistMatches.count == allowlistResolutions.count
let skillAllow: Bool
if approvals.agent.autoAllowSkills, !allowlistResolutions.isEmpty {
let bins = await SkillBinsCache.shared.currentBins()
skillAllow = allowlistResolutions.allSatisfy { bins.contains($0.executableName) }
} else {
skillAllow = false
}
return ExecApprovalEvaluation(
command: command,
displayCommand: displayCommand,
agentId: normalizedAgentId,
security: security,
ask: ask,
env: env,
resolution: allowlistResolutions.first,
allowlistResolutions: allowlistResolutions,
allowlistMatches: allowlistMatches,
allowlistSatisfied: allowlistSatisfied,
allowlistMatch: allowlistSatisfied ? allowlistMatches.first : nil,
skillAllow: skillAllow)
}
}
+189 -391
View File
@@ -90,6 +90,31 @@ enum ExecApprovalDecision: String, Codable, Sendable {
case deny
}
enum ExecAllowlistPatternValidationReason: String, Codable, Sendable, Equatable {
case empty
case missingPathComponent
var message: String {
switch self {
case .empty:
"Pattern cannot be empty."
case .missingPathComponent:
"Path patterns only. Include '/', '~', or '\\\\'."
}
}
}
enum ExecAllowlistPatternValidation: Sendable, Equatable {
case valid(String)
case invalid(ExecAllowlistPatternValidationReason)
}
struct ExecAllowlistRejectedEntry: Sendable, Equatable {
let id: UUID
let pattern: String
let reason: ExecAllowlistPatternValidationReason
}
struct ExecAllowlistEntry: Codable, Hashable, Identifiable {
var id: UUID
var pattern: String
@@ -222,13 +247,25 @@ enum ExecApprovalsStore {
}
agents.removeValue(forKey: "default")
}
if !agents.isEmpty {
var normalizedAgents: [String: ExecApprovalsAgent] = [:]
normalizedAgents.reserveCapacity(agents.count)
for (key, var agent) in agents {
if let allowlist = agent.allowlist {
let normalized = self.normalizeAllowlistEntries(allowlist, dropInvalid: false).entries
agent.allowlist = normalized.isEmpty ? nil : normalized
}
normalizedAgents[key] = agent
}
agents = normalizedAgents
}
return ExecApprovalsFile(
version: 1,
socket: ExecApprovalsSocketConfig(
path: socketPath.isEmpty ? nil : socketPath,
token: token.isEmpty ? nil : token),
defaults: file.defaults,
agents: agents)
agents: agents.isEmpty ? nil : agents)
}
static func readSnapshot() -> ExecApprovalsSnapshot {
@@ -306,7 +343,12 @@ enum ExecApprovalsStore {
}
static func ensureFile() -> ExecApprovalsFile {
var file = self.loadFile()
let url = self.fileURL()
let existed = FileManager().fileExists(atPath: url.path)
let loaded = self.loadFile()
let loadedHash = self.hashFile(loaded)
var file = self.normalizeIncoming(loaded)
if file.socket == nil { file.socket = ExecApprovalsSocketConfig(path: nil, token: nil) }
let path = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if path.isEmpty {
@@ -317,7 +359,9 @@ enum ExecApprovalsStore {
file.socket?.token = self.generateToken()
}
if file.agents == nil { file.agents = [:] }
self.saveFile(file)
if !existed || loadedHash != self.hashFile(file) {
self.saveFile(file)
}
return file
}
@@ -339,16 +383,9 @@ enum ExecApprovalsStore {
?? resolvedDefaults.askFallback,
autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills
?? resolvedDefaults.autoAllowSkills)
let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []))
.map { entry in
ExecAllowlistEntry(
id: entry.id,
pattern: entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
lastUsedAt: entry.lastUsedAt,
lastUsedCommand: entry.lastUsedCommand,
lastResolvedPath: entry.lastResolvedPath)
}
.filter { !$0.pattern.isEmpty }
let allowlist = self.normalizeAllowlistEntries(
(wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []),
dropInvalid: true).entries
let socketPath = self.expandPath(file.socket?.path ?? self.socketPath())
let token = file.socket?.token ?? ""
return ExecApprovalsResolved(
@@ -398,20 +435,30 @@ enum ExecApprovalsStore {
}
}
static func addAllowlistEntry(agentId: String?, pattern: String) {
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
@discardableResult
static func addAllowlistEntry(agentId: String?, pattern: String) -> ExecAllowlistPatternValidationReason? {
let normalizedPattern: String
switch ExecApprovalHelpers.validateAllowlistPattern(pattern) {
case .valid(let validPattern):
normalizedPattern = validPattern
case .invalid(let reason):
return reason
}
self.updateFile { file in
let key = self.agentKey(agentId)
var agents = file.agents ?? [:]
var entry = agents[key] ?? ExecApprovalsAgent()
var allowlist = entry.allowlist ?? []
if allowlist.contains(where: { $0.pattern == trimmed }) { return }
allowlist.append(ExecAllowlistEntry(pattern: trimmed, lastUsedAt: Date().timeIntervalSince1970 * 1000))
if allowlist.contains(where: { $0.pattern == normalizedPattern }) { return }
allowlist.append(ExecAllowlistEntry(
pattern: normalizedPattern,
lastUsedAt: Date().timeIntervalSince1970 * 1000))
entry.allowlist = allowlist
agents[key] = entry
file.agents = agents
}
return nil
}
static func recordAllowlistUse(
@@ -439,25 +486,21 @@ enum ExecApprovalsStore {
}
}
static func updateAllowlist(agentId: String?, allowlist: [ExecAllowlistEntry]) {
@discardableResult
static func updateAllowlist(agentId: String?, allowlist: [ExecAllowlistEntry]) -> [ExecAllowlistRejectedEntry] {
var rejected: [ExecAllowlistRejectedEntry] = []
self.updateFile { file in
let key = self.agentKey(agentId)
var agents = file.agents ?? [:]
var entry = agents[key] ?? ExecApprovalsAgent()
let cleaned = allowlist
.map { item in
ExecAllowlistEntry(
id: item.id,
pattern: item.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
lastUsedAt: item.lastUsedAt,
lastUsedCommand: item.lastUsedCommand,
lastResolvedPath: item.lastResolvedPath)
}
.filter { !$0.pattern.isEmpty }
let normalized = self.normalizeAllowlistEntries(allowlist, dropInvalid: true)
rejected = normalized.rejected
let cleaned = normalized.entries
entry.allowlist = cleaned
agents[key] = entry
file.agents = agents
}
return rejected
}
static func updateAgentSettings(agentId: String?, mutate: (inout ExecApprovalsAgent) -> Void) {
@@ -500,6 +543,14 @@ enum ExecApprovalsStore {
return digest.map { String(format: "%02x", $0) }.joined()
}
private static func hashFile(_ file: ExecApprovalsFile) -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = (try? encoder.encode(file)) ?? Data()
let digest = SHA256.hash(data: data)
return digest.map { String(format: "%02x", $0) }.joined()
}
private static func expandPath(_ raw: String) -> String {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed == "~" {
@@ -519,14 +570,101 @@ enum ExecApprovalsStore {
}
private static func normalizedPattern(_ pattern: String?) -> String? {
let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed.lowercased()
switch ExecApprovalHelpers.validateAllowlistPattern(pattern) {
case .valid(let normalized):
return normalized.lowercased()
case .invalid(.empty):
return nil
case .invalid:
let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed.lowercased()
}
}
private static func migrateLegacyPattern(_ entry: ExecAllowlistEntry) -> ExecAllowlistEntry {
let trimmedPattern = entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedResolved = entry.lastResolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let normalizedResolved = trimmedResolved.isEmpty ? nil : trimmedResolved
switch ExecApprovalHelpers.validateAllowlistPattern(trimmedPattern) {
case .valid(let pattern):
return ExecAllowlistEntry(
id: entry.id,
pattern: pattern,
lastUsedAt: entry.lastUsedAt,
lastUsedCommand: entry.lastUsedCommand,
lastResolvedPath: normalizedResolved)
case .invalid:
switch ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved) {
case .valid(let migratedPattern):
return ExecAllowlistEntry(
id: entry.id,
pattern: migratedPattern,
lastUsedAt: entry.lastUsedAt,
lastUsedCommand: entry.lastUsedCommand,
lastResolvedPath: normalizedResolved)
case .invalid:
return ExecAllowlistEntry(
id: entry.id,
pattern: trimmedPattern,
lastUsedAt: entry.lastUsedAt,
lastUsedCommand: entry.lastUsedCommand,
lastResolvedPath: normalizedResolved)
}
}
}
private static func normalizeAllowlistEntries(
_ entries: [ExecAllowlistEntry],
dropInvalid: Bool) -> (entries: [ExecAllowlistEntry], rejected: [ExecAllowlistRejectedEntry])
{
var normalized: [ExecAllowlistEntry] = []
normalized.reserveCapacity(entries.count)
var rejected: [ExecAllowlistRejectedEntry] = []
for entry in entries {
let migrated = self.migrateLegacyPattern(entry)
let trimmedPattern = migrated.pattern.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedResolvedPath = migrated.lastResolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let normalizedResolvedPath = trimmedResolvedPath.isEmpty ? nil : trimmedResolvedPath
switch ExecApprovalHelpers.validateAllowlistPattern(trimmedPattern) {
case .valid(let pattern):
normalized.append(
ExecAllowlistEntry(
id: migrated.id,
pattern: pattern,
lastUsedAt: migrated.lastUsedAt,
lastUsedCommand: migrated.lastUsedCommand,
lastResolvedPath: normalizedResolvedPath))
case .invalid(let reason):
if dropInvalid {
rejected.append(
ExecAllowlistRejectedEntry(
id: migrated.id,
pattern: trimmedPattern,
reason: reason))
} else if reason != .empty {
normalized.append(
ExecAllowlistEntry(
id: migrated.id,
pattern: trimmedPattern,
lastUsedAt: migrated.lastUsedAt,
lastUsedCommand: migrated.lastUsedCommand,
lastResolvedPath: normalizedResolvedPath))
}
}
}
return (normalized, rejected)
}
private static func mergeAgents(
current: ExecApprovalsAgent,
legacy: ExecApprovalsAgent) -> ExecApprovalsAgent
{
let currentAllowlist = self.normalizeAllowlistEntries(current.allowlist ?? [], dropInvalid: false).entries
let legacyAllowlist = self.normalizeAllowlistEntries(legacy.allowlist ?? [], dropInvalid: false).entries
var seen = Set<String>()
var allowlist: [ExecAllowlistEntry] = []
func append(_ entry: ExecAllowlistEntry) {
@@ -536,10 +674,10 @@ enum ExecApprovalsStore {
seen.insert(key)
allowlist.append(entry)
}
for entry in current.allowlist ?? [] {
for entry in currentAllowlist {
append(entry)
}
for entry in legacy.allowlist ?? [] {
for entry in legacyAllowlist {
append(entry)
}
@@ -552,286 +690,23 @@ enum ExecApprovalsStore {
}
}
struct ExecCommandResolution: Sendable {
let rawExecutable: String
let resolvedPath: String?
let executableName: String
let cwd: String?
static func resolve(
command: [String],
rawCommand: String?,
cwd: String?,
env: [String: String]?) -> ExecCommandResolution?
{
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedRaw.isEmpty, let token = self.parseFirstToken(trimmedRaw) {
return self.resolveExecutable(rawExecutable: token, cwd: cwd, env: env)
}
return self.resolve(command: command, cwd: cwd, env: env)
}
static func resolveForAllowlist(
command: [String],
rawCommand: String?,
cwd: String?,
env: [String: String]?) -> [ExecCommandResolution]
{
let shell = self.extractShellCommandFromArgv(command: command, rawCommand: rawCommand)
if shell.isWrapper {
guard let shellCommand = shell.command,
let segments = self.splitShellCommandChain(shellCommand)
else {
// Fail closed: if we cannot safely parse a shell wrapper payload,
// treat this as an allowlist miss and require approval.
return []
}
var resolutions: [ExecCommandResolution] = []
resolutions.reserveCapacity(segments.count)
for segment in segments {
guard let token = self.parseFirstToken(segment),
let resolution = self.resolveExecutable(rawExecutable: token, cwd: cwd, env: env)
else {
return []
}
resolutions.append(resolution)
}
return resolutions
}
guard let resolution = self.resolve(command: command, rawCommand: rawCommand, cwd: cwd, env: env) else {
return []
}
return [resolution]
}
static func resolve(command: [String], cwd: String?, env: [String: String]?) -> ExecCommandResolution? {
guard let raw = command.first?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
return nil
}
return self.resolveExecutable(rawExecutable: raw, cwd: cwd, env: env)
}
private static func resolveExecutable(
rawExecutable: String,
cwd: String?,
env: [String: String]?) -> ExecCommandResolution?
{
let expanded = rawExecutable.hasPrefix("~") ? (rawExecutable as NSString).expandingTildeInPath : rawExecutable
let hasPathSeparator = expanded.contains("/") || expanded.contains("\\")
let resolvedPath: String? = {
if hasPathSeparator {
if expanded.hasPrefix("/") {
return expanded
}
let base = cwd?.trimmingCharacters(in: .whitespacesAndNewlines)
let root = (base?.isEmpty == false) ? base! : FileManager().currentDirectoryPath
return URL(fileURLWithPath: root).appendingPathComponent(expanded).path
}
let searchPaths = self.searchPaths(from: env)
return CommandResolver.findExecutable(named: expanded, searchPaths: searchPaths)
}()
let name = resolvedPath.map { URL(fileURLWithPath: $0).lastPathComponent } ?? expanded
return ExecCommandResolution(
rawExecutable: expanded,
resolvedPath: resolvedPath,
executableName: name,
cwd: cwd)
}
private static func parseFirstToken(_ command: String) -> String? {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard let first = trimmed.first else { return nil }
if first == "\"" || first == "'" {
let rest = trimmed.dropFirst()
if let end = rest.firstIndex(of: first) {
return String(rest[..<end])
}
return String(rest)
}
return trimmed.split(whereSeparator: { $0.isWhitespace }).first.map(String.init)
}
private static func basenameLower(_ token: String) -> String {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "" }
let normalized = trimmed.replacingOccurrences(of: "\\", with: "/")
return normalized.split(separator: "/").last.map { String($0).lowercased() } ?? normalized.lowercased()
}
private static func extractShellCommandFromArgv(
command: [String],
rawCommand: String?) -> (isWrapper: Bool, command: String?)
{
guard let token0 = command.first?.trimmingCharacters(in: .whitespacesAndNewlines), !token0.isEmpty else {
return (false, nil)
}
let base0 = self.basenameLower(token0)
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let preferredRaw = trimmedRaw.isEmpty ? nil : trimmedRaw
if ["sh", "bash", "zsh", "dash", "ksh"].contains(base0) {
let flag = command.count > 1 ? command[1].trimmingCharacters(in: .whitespacesAndNewlines) : ""
guard flag == "-lc" || flag == "-c" else { return (false, nil) }
let payload = command.count > 2 ? command[2].trimmingCharacters(in: .whitespacesAndNewlines) : ""
let normalized = preferredRaw ?? (payload.isEmpty ? nil : payload)
return (true, normalized)
}
if base0 == "cmd.exe" || base0 == "cmd" {
guard let idx = command
.firstIndex(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "/c" })
else {
return (false, nil)
}
let tail = command.suffix(from: command.index(after: idx)).joined(separator: " ")
let payload = tail.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = preferredRaw ?? (payload.isEmpty ? nil : payload)
return (true, normalized)
}
return (false, nil)
}
private static func splitShellCommandChain(_ command: String) -> [String]? {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
var segments: [String] = []
var current = ""
var inSingle = false
var inDouble = false
var escaped = false
let chars = Array(trimmed)
var idx = 0
func appendCurrent() -> Bool {
let segment = current.trimmingCharacters(in: .whitespacesAndNewlines)
guard !segment.isEmpty else { return false }
segments.append(segment)
current.removeAll(keepingCapacity: true)
return true
}
while idx < chars.count {
let ch = chars[idx]
let next: Character? = idx + 1 < chars.count ? chars[idx + 1] : nil
if escaped {
current.append(ch)
escaped = false
idx += 1
continue
}
if ch == "\\", !inSingle {
current.append(ch)
escaped = true
idx += 1
continue
}
if ch == "'", !inDouble {
inSingle.toggle()
current.append(ch)
idx += 1
continue
}
if ch == "\"", !inSingle {
inDouble.toggle()
current.append(ch)
idx += 1
continue
}
if !inSingle, !inDouble {
if self.shouldFailClosedForUnquotedShell(ch: ch, next: next) {
// Fail closed on command/process substitution in allowlist mode.
return nil
}
let prev: Character? = idx > 0 ? chars[idx - 1] : nil
if let delimiterStep = self.chainDelimiterStep(ch: ch, prev: prev, next: next) {
guard appendCurrent() else { return nil }
idx += delimiterStep
continue
}
}
current.append(ch)
idx += 1
}
if escaped || inSingle || inDouble { return nil }
guard appendCurrent() else { return nil }
return segments
}
private static func shouldFailClosedForUnquotedShell(ch: Character, next: Character?) -> Bool {
if ch == "`" {
return true
}
if ch == "$", next == "(" {
return true
}
if ch == "<" || ch == ">", next == "(" {
return true
}
return false
}
private static func chainDelimiterStep(ch: Character, prev: Character?, next: Character?) -> Int? {
if ch == ";" || ch == "\n" {
return 1
}
if ch == "&" {
if next == "&" {
return 2
}
// Keep fd redirections like 2>&1 or &>file intact.
let prevIsRedirect = prev == ">"
let nextIsRedirect = next == ">"
return (!prevIsRedirect && !nextIsRedirect) ? 1 : nil
}
if ch == "|" {
if next == "|" || next == "&" {
return 2
}
return 1
}
return nil
}
private static func searchPaths(from env: [String: String]?) -> [String] {
let raw = env?["PATH"]
if let raw, !raw.isEmpty {
return raw.split(separator: ":").map(String.init)
}
return CommandResolver.preferredPaths()
}
}
enum ExecCommandFormatter {
static func displayString(for argv: [String]) -> String {
argv.map { arg in
let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "\"\"" }
let needsQuotes = trimmed.contains { $0.isWhitespace || $0 == "\"" }
if !needsQuotes { return trimmed }
let escaped = trimmed.replacingOccurrences(of: "\"", with: "\\\"")
return "\"\(escaped)\""
}.joined(separator: " ")
}
static func displayString(for argv: [String], rawCommand: String?) -> String {
let trimmed = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmed.isEmpty { return trimmed }
return self.displayString(for: argv)
}
}
enum ExecApprovalHelpers {
static func validateAllowlistPattern(_ pattern: String?) -> ExecAllowlistPatternValidation {
let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return .invalid(.empty) }
guard self.containsPathComponent(trimmed) else { return .invalid(.missingPathComponent) }
return .valid(trimmed)
}
static func isPathPattern(_ pattern: String?) -> Bool {
switch self.validateAllowlistPattern(pattern) {
case .valid:
true
case .invalid:
false
}
}
static func parseDecision(_ raw: String?) -> ExecApprovalDecision? {
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !trimmed.isEmpty else { return nil }
@@ -853,86 +728,9 @@ enum ExecApprovalHelpers {
let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? ""
return pattern.isEmpty ? nil : pattern
}
}
enum ExecAllowlistMatcher {
static func match(entries: [ExecAllowlistEntry], resolution: ExecCommandResolution?) -> ExecAllowlistEntry? {
guard let resolution, !entries.isEmpty else { return nil }
let rawExecutable = resolution.rawExecutable
let resolvedPath = resolution.resolvedPath
let executableName = resolution.executableName
for entry in entries {
let pattern = entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines)
if pattern.isEmpty { continue }
let hasPath = pattern.contains("/") || pattern.contains("~") || pattern.contains("\\")
if hasPath {
let target = resolvedPath ?? rawExecutable
if self.matches(pattern: pattern, target: target) { return entry }
} else if self.matches(pattern: pattern, target: executableName) {
return entry
}
}
return nil
}
static func matchAll(
entries: [ExecAllowlistEntry],
resolutions: [ExecCommandResolution]) -> [ExecAllowlistEntry]
{
guard !entries.isEmpty, !resolutions.isEmpty else { return [] }
var matches: [ExecAllowlistEntry] = []
matches.reserveCapacity(resolutions.count)
for resolution in resolutions {
guard let match = self.match(entries: entries, resolution: resolution) else {
return []
}
matches.append(match)
}
return matches
}
private static func matches(pattern: String, target: String) -> Bool {
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
let expanded = trimmed.hasPrefix("~") ? (trimmed as NSString).expandingTildeInPath : trimmed
let normalizedPattern = self.normalizeMatchTarget(expanded)
let normalizedTarget = self.normalizeMatchTarget(target)
guard let regex = self.regex(for: normalizedPattern) else { return false }
let range = NSRange(location: 0, length: normalizedTarget.utf16.count)
return regex.firstMatch(in: normalizedTarget, options: [], range: range) != nil
}
private static func normalizeMatchTarget(_ value: String) -> String {
value.replacingOccurrences(of: "\\\\", with: "/").lowercased()
}
private static func regex(for pattern: String) -> NSRegularExpression? {
var regex = "^"
var idx = pattern.startIndex
while idx < pattern.endIndex {
let ch = pattern[idx]
if ch == "*" {
let next = pattern.index(after: idx)
if next < pattern.endIndex, pattern[next] == "*" {
regex += ".*"
idx = pattern.index(after: next)
} else {
regex += "[^/]*"
idx = next
}
continue
}
if ch == "?" {
regex += "."
idx = pattern.index(after: idx)
continue
}
regex += NSRegularExpression.escapedPattern(for: String(ch))
idx = pattern.index(after: idx)
}
regex += "$"
return try? NSRegularExpression(pattern: regex, options: [.caseInsensitive])
private static func containsPathComponent(_ pattern: String) -> Bool {
pattern.contains("/") || pattern.contains("~") || pattern.contains("\\")
}
}
@@ -350,21 +350,7 @@ enum ExecApprovalsPromptPresenter {
@MainActor
private enum ExecHostExecutor {
private struct ExecApprovalContext {
let command: [String]
let displayCommand: String
let trimmedAgent: String?
let approvals: ExecApprovalsResolved
let security: ExecSecurity
let ask: ExecAsk
let autoAllowSkills: Bool
let env: [String: String]?
let resolution: ExecCommandResolution?
let allowlistResolutions: [ExecCommandResolution]
let allowlistMatches: [ExecAllowlistEntry]
let allowlistSatisfied: Bool
let skillAllow: Bool
}
private typealias ExecApprovalContext = ExecApprovalEvaluation
static func handle(_ request: ExecHostRequest) async -> ExecHostResponse {
let command = request.command.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
@@ -395,7 +381,7 @@ private enum ExecHostExecutor {
if ExecApprovalHelpers.requiresAsk(
ask: context.ask,
security: context.security,
allowlistMatch: context.allowlistSatisfied ? context.allowlistMatches.first : nil,
allowlistMatch: context.allowlistMatch,
skillAllow: context.skillAllow),
approvalDecision == nil
{
@@ -406,7 +392,7 @@ private enum ExecHostExecutor {
host: "node",
security: context.security.rawValue,
ask: context.ask.rawValue,
agentId: context.trimmedAgent,
agentId: context.agentId,
resolvedPath: context.resolution?.resolvedPath,
sessionKey: request.sessionKey))
@@ -447,7 +433,7 @@ private enum ExecHostExecutor {
? context.allowlistResolutions[idx].resolvedPath
: nil
ExecApprovalsStore.recordAllowlistUse(
agentId: context.trimmedAgent,
agentId: context.agentId,
pattern: match.pattern,
command: context.displayCommand,
resolvedPath: resolvedPath)
@@ -466,49 +452,12 @@ private enum ExecHostExecutor {
}
private static func buildContext(request: ExecHostRequest, command: [String]) async -> ExecApprovalContext {
let displayCommand = ExecCommandFormatter.displayString(
for: command,
rawCommand: request.rawCommand)
let agentId = request.agentId?.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedAgent = (agentId?.isEmpty == false) ? agentId : nil
let approvals = ExecApprovalsStore.resolve(agentId: trimmedAgent)
let security = approvals.agent.security
let ask = approvals.agent.ask
let autoAllowSkills = approvals.agent.autoAllowSkills
let env = self.sanitizedEnv(request.env)
let allowlistResolutions = ExecCommandResolution.resolveForAllowlist(
await ExecApprovalEvaluator.evaluate(
command: command,
rawCommand: request.rawCommand,
cwd: request.cwd,
env: env)
let resolution = allowlistResolutions.first
let allowlistMatches = security == .allowlist
? ExecAllowlistMatcher.matchAll(entries: approvals.allowlist, resolutions: allowlistResolutions)
: []
let allowlistSatisfied = security == .allowlist &&
!allowlistResolutions.isEmpty &&
allowlistMatches.count == allowlistResolutions.count
let skillAllow: Bool
if autoAllowSkills, !allowlistResolutions.isEmpty {
let bins = await SkillBinsCache.shared.currentBins()
skillAllow = allowlistResolutions.allSatisfy { bins.contains($0.executableName) }
} else {
skillAllow = false
}
return ExecApprovalContext(
command: command,
displayCommand: displayCommand,
trimmedAgent: trimmedAgent,
approvals: approvals,
security: security,
ask: ask,
autoAllowSkills: autoAllowSkills,
env: env,
resolution: resolution,
allowlistResolutions: allowlistResolutions,
allowlistMatches: allowlistMatches,
allowlistSatisfied: allowlistSatisfied,
skillAllow: skillAllow)
envOverrides: request.env,
agentId: request.agentId)
}
private static func persistAllowlistEntry(
@@ -525,7 +474,7 @@ private enum ExecHostExecutor {
continue
}
if seenPatterns.insert(pattern).inserted {
ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern)
ExecApprovalsStore.addAllowlistEntry(agentId: context.agentId, pattern: pattern)
}
}
}
@@ -586,10 +535,6 @@ private enum ExecHostExecutor {
payload: payload,
error: nil)
}
private static func sanitizedEnv(_ overrides: [String: String]?) -> [String: String] {
HostEnvSanitizer.sanitize(overrides: overrides)
}
}
private final class ExecApprovalsSocketServer: @unchecked Sendable {
@@ -0,0 +1,265 @@
import Foundation
struct ExecCommandResolution: Sendable {
let rawExecutable: String
let resolvedPath: String?
let executableName: String
let cwd: String?
static func resolve(
command: [String],
rawCommand: String?,
cwd: String?,
env: [String: String]?) -> ExecCommandResolution?
{
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedRaw.isEmpty, let token = self.parseFirstToken(trimmedRaw) {
return self.resolveExecutable(rawExecutable: token, cwd: cwd, env: env)
}
return self.resolve(command: command, cwd: cwd, env: env)
}
static func resolveForAllowlist(
command: [String],
rawCommand: String?,
cwd: String?,
env: [String: String]?) -> [ExecCommandResolution]
{
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: rawCommand)
if shell.isWrapper {
guard let shellCommand = shell.command,
let segments = self.splitShellCommandChain(shellCommand)
else {
// Fail closed: if we cannot safely parse a shell wrapper payload,
// treat this as an allowlist miss and require approval.
return []
}
var resolutions: [ExecCommandResolution] = []
resolutions.reserveCapacity(segments.count)
for segment in segments {
guard let token = self.parseFirstToken(segment),
let resolution = self.resolveExecutable(rawExecutable: token, cwd: cwd, env: env)
else {
return []
}
resolutions.append(resolution)
}
return resolutions
}
guard let resolution = self.resolve(command: command, rawCommand: rawCommand, cwd: cwd, env: env) else {
return []
}
return [resolution]
}
static func resolve(command: [String], cwd: String?, env: [String: String]?) -> ExecCommandResolution? {
let effective = ExecEnvInvocationUnwrapper.unwrapDispatchWrappersForResolution(command)
guard let raw = effective.first?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
return nil
}
return self.resolveExecutable(rawExecutable: raw, cwd: cwd, env: env)
}
private static func resolveExecutable(
rawExecutable: String,
cwd: String?,
env: [String: String]?) -> ExecCommandResolution?
{
let expanded = rawExecutable.hasPrefix("~") ? (rawExecutable as NSString).expandingTildeInPath : rawExecutable
let hasPathSeparator = expanded.contains("/") || expanded.contains("\\")
let resolvedPath: String? = {
if hasPathSeparator {
if expanded.hasPrefix("/") {
return expanded
}
let base = cwd?.trimmingCharacters(in: .whitespacesAndNewlines)
let root = (base?.isEmpty == false) ? base! : FileManager().currentDirectoryPath
return URL(fileURLWithPath: root).appendingPathComponent(expanded).path
}
let searchPaths = self.searchPaths(from: env)
return CommandResolver.findExecutable(named: expanded, searchPaths: searchPaths)
}()
let name = resolvedPath.map { URL(fileURLWithPath: $0).lastPathComponent } ?? expanded
return ExecCommandResolution(
rawExecutable: expanded,
resolvedPath: resolvedPath,
executableName: name,
cwd: cwd)
}
private static func parseFirstToken(_ command: String) -> String? {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard let first = trimmed.first else { return nil }
if first == "\"" || first == "'" {
let rest = trimmed.dropFirst()
if let end = rest.firstIndex(of: first) {
return String(rest[..<end])
}
return String(rest)
}
return trimmed.split(whereSeparator: { $0.isWhitespace }).first.map(String.init)
}
private enum ShellTokenContext {
case unquoted
case doubleQuoted
}
private struct ShellFailClosedRule {
let token: Character
let next: Character?
}
private static let shellFailClosedRules: [ShellTokenContext: [ShellFailClosedRule]] = [
.unquoted: [
ShellFailClosedRule(token: "`", next: nil),
ShellFailClosedRule(token: "$", next: "("),
ShellFailClosedRule(token: "<", next: "("),
ShellFailClosedRule(token: ">", next: "("),
],
.doubleQuoted: [
ShellFailClosedRule(token: "`", next: nil),
ShellFailClosedRule(token: "$", next: "("),
],
]
private static func splitShellCommandChain(_ command: String) -> [String]? {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
var segments: [String] = []
var current = ""
var inSingle = false
var inDouble = false
var escaped = false
let chars = Array(trimmed)
var idx = 0
func appendCurrent() -> Bool {
let segment = current.trimmingCharacters(in: .whitespacesAndNewlines)
guard !segment.isEmpty else { return false }
segments.append(segment)
current.removeAll(keepingCapacity: true)
return true
}
while idx < chars.count {
let ch = chars[idx]
let next: Character? = idx + 1 < chars.count ? chars[idx + 1] : nil
if escaped {
current.append(ch)
escaped = false
idx += 1
continue
}
if ch == "\\", !inSingle {
current.append(ch)
escaped = true
idx += 1
continue
}
if ch == "'", !inDouble {
inSingle.toggle()
current.append(ch)
idx += 1
continue
}
if ch == "\"", !inSingle {
inDouble.toggle()
current.append(ch)
idx += 1
continue
}
if !inSingle, self.shouldFailClosedForShell(ch: ch, next: next, inDouble: inDouble) {
// Fail closed on command/process substitution in allowlist mode,
// including command substitution inside double-quoted shell strings.
return nil
}
if !inSingle, !inDouble {
let prev: Character? = idx > 0 ? chars[idx - 1] : nil
if let delimiterStep = self.chainDelimiterStep(ch: ch, prev: prev, next: next) {
guard appendCurrent() else { return nil }
idx += delimiterStep
continue
}
}
current.append(ch)
idx += 1
}
if escaped || inSingle || inDouble { return nil }
guard appendCurrent() else { return nil }
return segments
}
private static func shouldFailClosedForShell(ch: Character, next: Character?, inDouble: Bool) -> Bool {
let context: ShellTokenContext = inDouble ? .doubleQuoted : .unquoted
guard let rules = self.shellFailClosedRules[context] else {
return false
}
for rule in rules {
if ch == rule.token, rule.next == nil || next == rule.next {
return true
}
}
return false
}
private static func chainDelimiterStep(ch: Character, prev: Character?, next: Character?) -> Int? {
if ch == ";" || ch == "\n" {
return 1
}
if ch == "&" {
if next == "&" {
return 2
}
// Keep fd redirections like 2>&1 or &>file intact.
let prevIsRedirect = prev == ">"
let nextIsRedirect = next == ">"
return (!prevIsRedirect && !nextIsRedirect) ? 1 : nil
}
if ch == "|" {
if next == "|" || next == "&" {
return 2
}
return 1
}
return nil
}
private static func searchPaths(from env: [String: String]?) -> [String] {
let raw = env?["PATH"]
if let raw, !raw.isEmpty {
return raw.split(separator: ":").map(String.init)
}
return CommandResolver.preferredPaths()
}
}
enum ExecCommandFormatter {
static func displayString(for argv: [String]) -> String {
argv.map { arg in
let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "\"\"" }
let needsQuotes = trimmed.contains { $0.isWhitespace || $0 == "\"" }
if !needsQuotes { return trimmed }
let escaped = trimmed.replacingOccurrences(of: "\"", with: "\\\"")
return "\"\(escaped)\""
}.joined(separator: " ")
}
static func displayString(for argv: [String], rawCommand: String?) -> String {
let trimmed = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmed.isEmpty { return trimmed }
return self.displayString(for: argv)
}
}
@@ -0,0 +1,108 @@
import Foundation
enum ExecCommandToken {
static func basenameLower(_ token: String) -> String {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "" }
let normalized = trimmed.replacingOccurrences(of: "\\", with: "/")
return normalized.split(separator: "/").last.map { String($0).lowercased() } ?? normalized.lowercased()
}
}
enum ExecEnvInvocationUnwrapper {
static let maxWrapperDepth = 4
private static let optionsWithValue = Set([
"-u",
"--unset",
"-c",
"--chdir",
"-s",
"--split-string",
"--default-signal",
"--ignore-signal",
"--block-signal",
])
private static let flagOptions = Set(["-i", "--ignore-environment", "-0", "--null"])
private static func isEnvAssignment(_ token: String) -> Bool {
let pattern = #"^[A-Za-z_][A-Za-z0-9_]*=.*"#
return token.range(of: pattern, options: .regularExpression) != nil
}
static func unwrap(_ command: [String]) -> [String]? {
var idx = 1
var expectsOptionValue = false
while idx < command.count {
let token = command[idx].trimmingCharacters(in: .whitespacesAndNewlines)
if token.isEmpty {
idx += 1
continue
}
if expectsOptionValue {
expectsOptionValue = false
idx += 1
continue
}
if token == "--" || token == "-" {
idx += 1
break
}
if self.isEnvAssignment(token) {
idx += 1
continue
}
if token.hasPrefix("-"), token != "-" {
let lower = token.lowercased()
let flag = lower.split(separator: "=", maxSplits: 1).first.map(String.init) ?? lower
if self.flagOptions.contains(flag) {
idx += 1
continue
}
if self.optionsWithValue.contains(flag) {
if !lower.contains("=") {
expectsOptionValue = true
}
idx += 1
continue
}
if lower.hasPrefix("-u") ||
lower.hasPrefix("-c") ||
lower.hasPrefix("-s") ||
lower.hasPrefix("--unset=") ||
lower.hasPrefix("--chdir=") ||
lower.hasPrefix("--split-string=") ||
lower.hasPrefix("--default-signal=") ||
lower.hasPrefix("--ignore-signal=") ||
lower.hasPrefix("--block-signal=")
{
idx += 1
continue
}
return nil
}
break
}
guard idx < command.count else { return nil }
return Array(command[idx...])
}
static func unwrapDispatchWrappersForResolution(_ command: [String]) -> [String] {
var current = command
var depth = 0
while depth < self.maxWrapperDepth {
guard let token = current.first?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty else {
break
}
guard ExecCommandToken.basenameLower(token) == "env" else {
break
}
guard let unwrapped = self.unwrap(current), !unwrapped.isEmpty else {
break
}
current = unwrapped
depth += 1
}
return current
}
}
@@ -0,0 +1,106 @@
import Foundation
enum ExecShellWrapperParser {
struct ParsedShellWrapper {
let isWrapper: Bool
let command: String?
static let notWrapper = ParsedShellWrapper(isWrapper: false, command: nil)
}
private enum Kind {
case posix
case cmd
case powershell
}
private struct WrapperSpec {
let kind: Kind
let names: Set<String>
}
private static let posixInlineFlags = Set(["-lc", "-c", "--command"])
private static let powershellInlineFlags = Set(["-c", "-command", "--command"])
private static let wrapperSpecs: [WrapperSpec] = [
WrapperSpec(kind: .posix, names: ["ash", "sh", "bash", "zsh", "dash", "ksh", "fish"]),
WrapperSpec(kind: .cmd, names: ["cmd.exe", "cmd"]),
WrapperSpec(kind: .powershell, names: ["powershell", "powershell.exe", "pwsh", "pwsh.exe"]),
]
static func extract(command: [String], rawCommand: String?) -> ParsedShellWrapper {
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let preferredRaw = trimmedRaw.isEmpty ? nil : trimmedRaw
return self.extract(command: command, preferredRaw: preferredRaw, depth: 0)
}
private static func extract(command: [String], preferredRaw: String?, depth: Int) -> ParsedShellWrapper {
guard depth < ExecEnvInvocationUnwrapper.maxWrapperDepth else {
return .notWrapper
}
guard let token0 = command.first?.trimmingCharacters(in: .whitespacesAndNewlines), !token0.isEmpty else {
return .notWrapper
}
let base0 = ExecCommandToken.basenameLower(token0)
if base0 == "env" {
guard let unwrapped = ExecEnvInvocationUnwrapper.unwrap(command) else {
return .notWrapper
}
return self.extract(command: unwrapped, preferredRaw: preferredRaw, depth: depth + 1)
}
guard let spec = self.wrapperSpecs.first(where: { $0.names.contains(base0) }) else {
return .notWrapper
}
guard let payload = self.extractPayload(command: command, spec: spec) else {
return .notWrapper
}
let normalized = preferredRaw ?? payload
return ParsedShellWrapper(isWrapper: true, command: normalized)
}
private static func extractPayload(command: [String], spec: WrapperSpec) -> String? {
switch spec.kind {
case .posix:
return self.extractPosixInlineCommand(command)
case .cmd:
return self.extractCmdInlineCommand(command)
case .powershell:
return self.extractPowerShellInlineCommand(command)
}
}
private static func extractPosixInlineCommand(_ command: [String]) -> String? {
let flag = command.count > 1 ? command[1].trimmingCharacters(in: .whitespacesAndNewlines) : ""
guard self.posixInlineFlags.contains(flag.lowercased()) else {
return nil
}
let payload = command.count > 2 ? command[2].trimmingCharacters(in: .whitespacesAndNewlines) : ""
return payload.isEmpty ? nil : payload
}
private static func extractCmdInlineCommand(_ command: [String]) -> String? {
guard let idx = command.firstIndex(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "/c" }) else {
return nil
}
let tail = command.suffix(from: command.index(after: idx)).joined(separator: " ")
let payload = tail.trimmingCharacters(in: .whitespacesAndNewlines)
return payload.isEmpty ? nil : payload
}
private static func extractPowerShellInlineCommand(_ command: [String]) -> String? {
for idx in 1..<command.count {
let token = command[idx].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if token.isEmpty { continue }
if token == "--" { break }
if self.powershellInlineFlags.contains(token) {
let payload = idx + 1 < command.count
? command[idx + 1].trimmingCharacters(in: .whitespacesAndNewlines)
: ""
return payload.isEmpty ? nil : payload
}
}
return nil
}
}
@@ -2,9 +2,34 @@ import Foundation
import OpenClawDiscovery
enum GatewayDiscoveryHelpers {
static func sshTarget(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
let host = self.sanitizedTailnetHost(gateway.tailnetDns) ?? gateway.lanHost
static func resolvedServiceHost(
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String?
{
self.resolvedServiceHost(gateway.serviceHost)
}
static func resolvedServiceHost(_ host: String?) -> String? {
guard let host = self.trimmed(host), !host.isEmpty else { return nil }
return host
}
static func serviceEndpoint(
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> (host: String, port: Int)?
{
self.serviceEndpoint(serviceHost: gateway.serviceHost, servicePort: gateway.servicePort)
}
static func serviceEndpoint(
serviceHost: String?,
servicePort: Int?) -> (host: String, port: Int)?
{
guard let host = self.resolvedServiceHost(serviceHost) else { return nil }
guard let port = servicePort, port > 0, port <= 65535 else { return nil }
return (host, port)
}
static func sshTarget(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
guard let host = self.resolvedServiceHost(for: gateway) else { return nil }
let user = NSUserName()
var target = "\(user)@\(host)"
if gateway.sshPort != 22 {
@@ -16,42 +41,37 @@ enum GatewayDiscoveryHelpers {
static func directUrl(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
self.directGatewayUrl(
serviceHost: gateway.serviceHost,
servicePort: gateway.servicePort,
lanHost: gateway.lanHost,
gatewayPort: gateway.gatewayPort)
servicePort: gateway.servicePort)
}
static func directGatewayUrl(
serviceHost: String?,
servicePort: Int?,
lanHost: String?,
gatewayPort: Int?) -> String?
servicePort: Int?) -> String?
{
// Security: do not route using unauthenticated TXT hints (tailnetDns/lanHost/gatewayPort).
// Prefer the resolved service endpoint (SRV + A/AAAA).
if let host = self.trimmed(serviceHost), !host.isEmpty,
let port = servicePort, port > 0
{
let scheme = port == 443 ? "wss" : "ws"
let portSuffix = port == 443 ? "" : ":\(port)"
return "\(scheme)://\(host)\(portSuffix)"
}
// Legacy fallback (best-effort): keep existing behavior when we couldn't resolve SRV.
guard let lanHost = self.trimmed(lanHost), !lanHost.isEmpty else { return nil }
let port = gatewayPort ?? 18789
return "ws://\(lanHost):\(port)"
}
static func sanitizedTailnetHost(_ host: String?) -> String? {
guard let host = self.trimmed(host), !host.isEmpty else { return nil }
if host.hasSuffix(".internal.") || host.hasSuffix(".internal") {
guard let endpoint = self.serviceEndpoint(serviceHost: serviceHost, servicePort: servicePort) else {
return nil
}
return host
// Security: for non-loopback hosts, force TLS to avoid plaintext credential/session leakage.
let scheme = self.isLoopbackHost(endpoint.host) ? "ws" : "wss"
let portSuffix = endpoint.port == 443 ? "" : ":\(endpoint.port)"
return "\(scheme)://\(endpoint.host)\(portSuffix)"
}
private static func trimmed(_ value: String?) -> String? {
value?.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func isLoopbackHost(_ rawHost: String) -> Bool {
let host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !host.isEmpty else { return false }
if host == "localhost" || host == "::1" || host == "0:0:0:0:0:0:0:1" {
return true
}
if host.hasPrefix("::ffff:127.") {
return true
}
return host.hasPrefix("127.")
}
}
@@ -303,7 +303,9 @@ struct GeneralSettings: View {
.disabled(self.remoteStatus == .checking || self.state.remoteUrl
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
Text("Direct mode requires a ws:// or wss:// URL (Tailscale Serve uses wss://<magicdns>).")
Text(
"Direct mode requires wss:// for remote hosts. ws:// is only allowed for localhost/127.0.0.1."
)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, self.remoteLabelWidth + 10)
@@ -546,7 +548,9 @@ extension GeneralSettings {
return
}
guard Self.isValidWsUrl(trimmedUrl) else {
self.remoteStatus = .failed("Gateway URL must start with ws:// or wss://")
self.remoteStatus = .failed(
"Gateway URL must use wss:// for remote hosts (ws:// only for localhost)"
)
return
}
} else {
@@ -603,11 +607,7 @@ extension GeneralSettings {
}
private static func isValidWsUrl(_ raw: String) -> Bool {
guard let url = URL(string: raw.trimmingCharacters(in: .whitespacesAndNewlines)) else { return false }
let scheme = url.scheme?.lowercased() ?? ""
guard scheme == "ws" || scheme == "wss" else { return false }
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return !host.isEmpty
GatewayRemoteConfig.normalizeGatewayUrl(raw) != nil
}
private static func sshCheckCommand(target: String, identity: String) -> [String]? {
@@ -675,22 +675,17 @@ extension GeneralSettings {
private func applyDiscoveredGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) {
MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID)
let host = gateway.tailnetDns ?? gateway.lanHost
guard let host else { return }
let user = NSUserName()
if self.state.remoteTransport == .direct {
if let url = GatewayDiscoveryHelpers.directUrl(for: gateway) {
self.state.remoteUrl = url
}
self.state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
} else {
self.state.remoteTarget = GatewayDiscoveryModel.buildSSHTarget(
user: user,
host: host,
port: gateway.sshPort)
self.state.remoteCliPath = gateway.cliPath ?? ""
self.state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
}
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: gateway.serviceHost ?? host,
port: gateway.servicePort ?? gateway.gatewayPort)
host: endpoint.host,
port: endpoint.port)
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
}
}
}
@@ -14,6 +14,9 @@ enum HostEnvSanitizer {
"RUBYOPT",
"BASH_ENV",
"ENV",
"SHELL",
"SHELLOPTS",
"PS4",
"GCONV_PATH",
"IFS",
"SSLKEYLOGFILE",
@@ -24,13 +27,40 @@ enum HostEnvSanitizer {
"LD_",
"BASH_FUNC_",
]
private static let blockedOverrideKeys: Set<String> = [
"HOME",
"ZDOTDIR",
]
private static let shellWrapperAllowedOverrideKeys: Set<String> = [
"TERM",
"LANG",
"LC_ALL",
"LC_CTYPE",
"LC_MESSAGES",
"COLORTERM",
"NO_COLOR",
"FORCE_COLOR",
]
private static func isBlocked(_ upperKey: String) -> Bool {
if self.blockedKeys.contains(upperKey) { return true }
return self.blockedPrefixes.contains(where: { upperKey.hasPrefix($0) })
}
static func sanitize(overrides: [String: String]?) -> [String: String] {
private static func filterOverridesForShellWrapper(_ overrides: [String: String]?) -> [String: String]? {
guard let overrides else { return nil }
var filtered: [String: String] = [:]
for (rawKey, value) in overrides {
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty else { continue }
if self.shellWrapperAllowedOverrideKeys.contains(key.uppercased()) {
filtered[key] = value
}
}
return filtered.isEmpty ? nil : filtered
}
static func sanitize(overrides: [String: String]?, shellWrapper: Bool = false) -> [String: String] {
var merged: [String: String] = [:]
for (rawKey, value) in ProcessInfo.processInfo.environment {
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -40,14 +70,19 @@ enum HostEnvSanitizer {
merged[key] = value
}
guard let overrides else { return merged }
for (rawKey, value) in overrides {
let effectiveOverrides = shellWrapper
? self.filterOverridesForShellWrapper(overrides)
: overrides
guard let effectiveOverrides else { return merged }
for (rawKey, value) in effectiveOverrides {
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty else { continue }
let upper = key.uppercased()
// PATH is part of the security boundary (command resolution + safe-bin checks). Never
// allow request-scoped PATH overrides from agents/gateways.
if upper == "PATH" { continue }
if self.blockedOverrideKeys.contains(upper) { continue }
if self.isBlocked(upper) { continue }
merged[key] = value
}
@@ -441,48 +441,25 @@ actor MacNodeRuntime {
guard !command.isEmpty else {
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: command required")
}
let displayCommand = ExecCommandFormatter.displayString(for: command, rawCommand: params.rawCommand)
let trimmedAgent = params.agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let agentId = trimmedAgent.isEmpty ? nil : trimmedAgent
let approvals = ExecApprovalsStore.resolve(agentId: agentId)
let security = approvals.agent.security
let ask = approvals.agent.ask
let autoAllowSkills = approvals.agent.autoAllowSkills
let sessionKey = (params.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
? params.sessionKey!.trimmingCharacters(in: .whitespacesAndNewlines)
: self.mainSessionKey
let runId = UUID().uuidString
let env = Self.sanitizedEnv(params.env)
let allowlistResolutions = ExecCommandResolution.resolveForAllowlist(
let evaluation = await ExecApprovalEvaluator.evaluate(
command: command,
rawCommand: params.rawCommand,
cwd: params.cwd,
env: env)
let resolution = allowlistResolutions.first
let allowlistMatches = security == .allowlist
? ExecAllowlistMatcher.matchAll(entries: approvals.allowlist, resolutions: allowlistResolutions)
: []
let allowlistSatisfied = security == .allowlist &&
!allowlistResolutions.isEmpty &&
allowlistMatches.count == allowlistResolutions.count
let allowlistMatch = allowlistSatisfied ? allowlistMatches.first : nil
let skillAllow: Bool
if autoAllowSkills, !allowlistResolutions.isEmpty {
let bins = await SkillBinsCache.shared.currentBins()
skillAllow = allowlistResolutions.allSatisfy { bins.contains($0.executableName) }
} else {
skillAllow = false
}
envOverrides: params.env,
agentId: params.agentId)
if security == .deny {
if evaluation.security == .deny {
await self.emitExecEvent(
"exec.denied",
payload: ExecEventPayload(
sessionKey: sessionKey,
runId: runId,
host: "node",
command: displayCommand,
command: evaluation.displayCommand,
reason: "security=deny"))
return Self.errorResponse(
req,
@@ -494,13 +471,13 @@ actor MacNodeRuntime {
req: req,
params: params,
context: ExecRunContext(
displayCommand: displayCommand,
security: security,
ask: ask,
agentId: agentId,
resolution: resolution,
allowlistMatch: allowlistMatch,
skillAllow: skillAllow,
displayCommand: evaluation.displayCommand,
security: evaluation.security,
ask: evaluation.ask,
agentId: evaluation.agentId,
resolution: evaluation.resolution,
allowlistMatch: evaluation.allowlistMatch,
skillAllow: evaluation.skillAllow,
sessionKey: sessionKey,
runId: runId))
if let response = approval.response { return response }
@@ -508,19 +485,19 @@ actor MacNodeRuntime {
let persistAllowlist = approval.persistAllowlist
self.persistAllowlistPatterns(
persistAllowlist: persistAllowlist,
security: security,
agentId: agentId,
security: evaluation.security,
agentId: evaluation.agentId,
command: command,
allowlistResolutions: allowlistResolutions)
allowlistResolutions: evaluation.allowlistResolutions)
if security == .allowlist, !allowlistSatisfied, !skillAllow, !approvedByAsk {
if evaluation.security == .allowlist, !evaluation.allowlistSatisfied, !evaluation.skillAllow, !approvedByAsk {
await self.emitExecEvent(
"exec.denied",
payload: ExecEventPayload(
sessionKey: sessionKey,
runId: runId,
host: "node",
command: displayCommand,
command: evaluation.displayCommand,
reason: "allowlist-miss"))
return Self.errorResponse(
req,
@@ -529,19 +506,19 @@ actor MacNodeRuntime {
}
self.recordAllowlistMatches(
security: security,
allowlistSatisfied: allowlistSatisfied,
agentId: agentId,
allowlistMatches: allowlistMatches,
allowlistResolutions: allowlistResolutions,
displayCommand: displayCommand)
security: evaluation.security,
allowlistSatisfied: evaluation.allowlistSatisfied,
agentId: evaluation.agentId,
allowlistMatches: evaluation.allowlistMatches,
allowlistResolutions: evaluation.allowlistResolutions,
displayCommand: evaluation.displayCommand)
if let permissionResponse = await self.validateScreenRecordingIfNeeded(
req: req,
needsScreenRecording: params.needsScreenRecording,
sessionKey: sessionKey,
runId: runId,
displayCommand: displayCommand)
displayCommand: evaluation.displayCommand)
{
return permissionResponse
}
@@ -550,10 +527,10 @@ actor MacNodeRuntime {
req: req,
params: params,
command: command,
env: env,
env: evaluation.env,
sessionKey: sessionKey,
runId: runId,
displayCommand: displayCommand)
displayCommand: evaluation.displayCommand)
}
private func handleSystemWhich(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
@@ -947,10 +924,6 @@ extension MacNodeRuntime {
UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false
}
private static func sanitizedEnv(_ overrides: [String: String]?) -> [String: String] {
HostEnvSanitizer.sanitize(overrides: overrides)
}
private nonisolated static func locationMode() -> OpenClawLocationMode {
let raw = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
return OpenClawLocationMode(rawValue: raw) ?? .off
@@ -520,11 +520,12 @@ final class NodePairingApprovalPrompter {
let preferred = GatewayDiscoveryPreferences.preferredStableID()
let gateway = model.gateways.first { $0.stableID == preferred } ?? model.gateways.first
guard let gateway else { return nil }
let host = (gateway.tailnetDns?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ??
gateway.lanHost?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty)
guard let host, !host.isEmpty else { return nil }
let port = gateway.sshPort > 0 ? gateway.sshPort : 22
return SSHTarget(host: host, port: port)
guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway),
let parsed = CommandResolver.parseSSHTarget(target)
else {
return nil
}
return SSHTarget(host: parsed.host, port: parsed.port)
}
private static func probeSSH(user: String, host: String, port: Int) async -> Bool {
@@ -26,20 +26,17 @@ extension OnboardingView {
GatewayDiscoveryPreferences.setPreferredStableID(gateway.stableID)
if self.state.remoteTransport == .direct {
if let url = GatewayDiscoveryHelpers.directUrl(for: gateway) {
self.state.remoteUrl = url
}
} else if let host = GatewayDiscoveryHelpers.sanitizedTailnetHost(gateway.tailnetDns) ?? gateway.lanHost {
let user = NSUserName()
self.state.remoteTarget = GatewayDiscoveryModel.buildSSHTarget(
user: user,
host: host,
port: gateway.sshPort)
OpenClawConfigFile.setRemoteGatewayUrl(
host: gateway.serviceHost ?? host,
port: gateway.servicePort ?? gateway.gatewayPort)
self.state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
} else {
self.state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
}
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
OpenClawConfigFile.setRemoteGatewayUrl(
host: endpoint.host,
port: endpoint.port)
} else {
OpenClawConfigFile.clearRemoteGatewayUrl()
}
self.state.remoteCliPath = gateway.cliPath ?? ""
self.state.connectionMode = .remote
MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID)
@@ -265,9 +265,11 @@ extension OnboardingView {
if self.state.remoteTransport == .direct {
return GatewayDiscoveryHelpers.directUrl(for: gateway) ?? "Gateway pairing only"
}
if let host = GatewayDiscoveryHelpers.sanitizedTailnetHost(gateway.tailnetDns) ?? gateway.lanHost {
let portSuffix = gateway.sshPort != 22 ? " · ssh \(gateway.sshPort)" : ""
return "\(host)\(portSuffix)"
if let target = GatewayDiscoveryHelpers.sshTarget(for: gateway),
let parsed = CommandResolver.parseSSHTarget(target)
{
let portSuffix = parsed.port != 22 ? " · ssh \(parsed.port)" : ""
return "\(parsed.host)\(portSuffix)"
}
return "Gateway pairing only"
}
@@ -223,6 +223,19 @@ enum OpenClawConfigFile {
}
}
static func clearRemoteGatewayUrl() {
self.updateGatewayDict { gateway in
guard var remote = gateway["remote"] as? [String: Any] else { return }
guard remote["url"] != nil else { return }
remote.removeValue(forKey: "url")
if remote.isEmpty {
gateway.removeValue(forKey: "remote")
} else {
gateway["remote"] = remote
}
}
}
private static func remoteGatewayUrl() -> URL? {
let root = self.loadDict()
guard let gateway = root["gateway"] as? [String: Any],
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.21</string>
<string>2026.2.23</string>
<key>CFBundleVersion</key>
<string>202602210</string>
<string>202602230</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>
@@ -105,16 +105,24 @@ struct SystemRunSettingsView: View {
.foregroundStyle(.secondary)
} else {
HStack(spacing: 8) {
TextField("Add allowlist pattern (case-insensitive globs)", text: self.$newPattern)
TextField("Add allowlist path pattern (case-insensitive globs)", text: self.$newPattern)
.textFieldStyle(.roundedBorder)
Button("Add") {
let pattern = self.newPattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !pattern.isEmpty else { return }
self.model.addEntry(pattern)
self.newPattern = ""
if self.model.addEntry(self.newPattern) == nil {
self.newPattern = ""
}
}
.buttonStyle(.bordered)
.disabled(self.newPattern.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
.disabled(!self.model.isPathPattern(self.newPattern))
}
Text("Path patterns only. Basename entries like \"echo\" are ignored.")
.font(.footnote)
.foregroundStyle(.secondary)
if let validationMessage = self.model.allowlistValidationMessage {
Text(validationMessage)
.font(.footnote)
.foregroundStyle(.orange)
}
if self.model.entries.isEmpty {
@@ -234,6 +242,7 @@ final class ExecApprovalsSettingsModel {
var autoAllowSkills = false
var entries: [ExecAllowlistEntry] = []
var skillBins: [String] = []
var allowlistValidationMessage: String?
var agentPickerIds: [String] {
[Self.defaultsScopeId] + self.agentIds
@@ -289,6 +298,7 @@ final class ExecApprovalsSettingsModel {
func selectAgent(_ id: String) {
self.selectedAgentId = id
self.allowlistValidationMessage = nil
self.loadSettings(for: id)
Task { await self.refreshSkillBins() }
}
@@ -301,6 +311,7 @@ final class ExecApprovalsSettingsModel {
self.askFallback = defaults.askFallback
self.autoAllowSkills = defaults.autoAllowSkills
self.entries = []
self.allowlistValidationMessage = nil
return
}
let resolved = ExecApprovalsStore.resolve(agentId: agentId)
@@ -310,6 +321,7 @@ final class ExecApprovalsSettingsModel {
self.autoAllowSkills = resolved.agent.autoAllowSkills
self.entries = resolved.allowlist
.sorted { $0.pattern.localizedCaseInsensitiveCompare($1.pattern) == .orderedAscending }
self.allowlistValidationMessage = nil
}
func setSecurity(_ security: ExecSecurity) {
@@ -367,32 +379,55 @@ final class ExecApprovalsSettingsModel {
Task { await self.refreshSkillBins(force: enabled) }
}
func addEntry(_ pattern: String) {
guard !self.isDefaultsScope else { return }
let trimmed = pattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.entries.append(ExecAllowlistEntry(pattern: trimmed, lastUsedAt: nil))
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
@discardableResult
func addEntry(_ pattern: String) -> ExecAllowlistPatternValidationReason? {
guard !self.isDefaultsScope else { return nil }
switch ExecApprovalHelpers.validateAllowlistPattern(pattern) {
case .valid(let normalizedPattern):
self.entries.append(ExecAllowlistEntry(pattern: normalizedPattern, lastUsedAt: nil))
let rejected = ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
self.allowlistValidationMessage = rejected.first?.reason.message
return rejected.first?.reason
case .invalid(let reason):
self.allowlistValidationMessage = reason.message
return reason
}
}
func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) {
guard !self.isDefaultsScope else { return }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
self.entries[index] = entry
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
@discardableResult
func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) -> ExecAllowlistPatternValidationReason? {
guard !self.isDefaultsScope else { return nil }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return nil }
var next = entry
switch ExecApprovalHelpers.validateAllowlistPattern(next.pattern) {
case .valid(let normalizedPattern):
next.pattern = normalizedPattern
case .invalid(let reason):
self.allowlistValidationMessage = reason.message
return reason
}
self.entries[index] = next
let rejected = ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
self.allowlistValidationMessage = rejected.first?.reason.message
return rejected.first?.reason
}
func removeEntry(id: UUID) {
guard !self.isDefaultsScope else { return }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
self.entries.remove(at: index)
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
let rejected = ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
self.allowlistValidationMessage = rejected.first?.reason.message
}
func entry(for id: UUID) -> ExecAllowlistEntry? {
self.entries.first(where: { $0.id == id })
}
func isPathPattern(_ pattern: String) -> Bool {
ExecApprovalHelpers.isPathPattern(pattern)
}
func refreshSkillBins(force: Bool = false) async {
guard self.autoAllowSkills else {
self.skillBins = []
@@ -15,7 +15,7 @@ struct ConnectOptions {
var clientMode: String = "ui"
var displayName: String?
var role: String = "operator"
var scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"]
var scopes: [String] = defaultOperatorConnectScopes
var help: Bool = false
static func parse(_ args: [String]) -> ConnectOptions {
@@ -0,0 +1,7 @@
let defaultOperatorConnectScopes: [String] = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.pairing",
]
@@ -251,7 +251,7 @@ actor GatewayWizardClient {
let clientMode = "ui"
let role = "operator"
// Explicit scopes; gateway no longer defaults empty scopes to admin.
let scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"]
let scopes = defaultOperatorConnectScopes
let client: [String: ProtoAnyCodable] = [
"id": ProtoAnyCodable(clientId),
"displayName": ProtoAnyCodable(Host.current().localizedName ?? "OpenClaw macOS Wizard CLI"),
@@ -281,8 +281,8 @@ actor GatewayWizardClient {
let identity = DeviceIdentityStore.loadOrCreate()
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
let scopesValue = scopes.joined(separator: ",")
var payloadParts = [
connectNonce == nil ? "v1" : "v2",
let payloadParts = [
"v2",
identity.deviceId,
clientId,
clientMode,
@@ -290,23 +290,19 @@ actor GatewayWizardClient {
scopesValue,
String(signedAtMs),
self.token ?? "",
connectNonce,
]
if let connectNonce {
payloadParts.append(connectNonce)
}
let payload = payloadParts.joined(separator: "|")
if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity)
{
var device: [String: ProtoAnyCodable] = [
let device: [String: ProtoAnyCodable] = [
"id": ProtoAnyCodable(identity.deviceId),
"publicKey": ProtoAnyCodable(publicKey),
"signature": ProtoAnyCodable(signature),
"signedAt": ProtoAnyCodable(signedAtMs),
"nonce": ProtoAnyCodable(connectNonce),
]
if let connectNonce {
device["nonce"] = ProtoAnyCodable(connectNonce)
}
params["device"] = ProtoAnyCodable(device)
}
@@ -333,29 +329,24 @@ actor GatewayWizardClient {
}
}
private func waitForConnectChallenge() async throws -> String? {
guard let task = self.task else { return nil }
do {
return try await AsyncTimeout.withTimeout(
seconds: self.connectChallengeTimeoutSeconds,
onTimeout: { ConnectChallengeError.timeout },
operation: {
while true {
let message = try await task.receive()
let frame = try await self.decodeFrame(message)
if case let .event(evt) = frame, evt.event == "connect.challenge" {
if let payload = evt.payload?.value as? [String: ProtoAnyCodable],
let nonce = payload["nonce"]?.value as? String
{
return nonce
}
}
private func waitForConnectChallenge() async throws -> String {
guard let task = self.task else { throw ConnectChallengeError.timeout }
return try await AsyncTimeout.withTimeout(
seconds: self.connectChallengeTimeoutSeconds,
onTimeout: { ConnectChallengeError.timeout },
operation: {
while true {
let message = try await task.receive()
let frame = try await self.decodeFrame(message)
if case let .event(evt) = frame, evt.event == "connect.challenge",
let payload = evt.payload?.value as? [String: ProtoAnyCodable],
let nonce = payload["nonce"]?.value as? String,
nonce.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
{
return nonce
}
})
} catch {
if error is ConnectChallengeError { return nil }
throw error
}
}
})
}
}
@@ -527,6 +527,7 @@ public struct AgentParams: Codable, Sendable {
public let groupchannel: String?
public let groupspace: String?
public let timeout: Int?
public let besteffortdeliver: Bool?
public let lane: String?
public let extrasystemprompt: String?
public let inputprovenance: [String: AnyCodable]?
@@ -553,6 +554,7 @@ public struct AgentParams: Codable, Sendable {
groupchannel: String?,
groupspace: String?,
timeout: Int?,
besteffortdeliver: Bool?,
lane: String?,
extrasystemprompt: String?,
inputprovenance: [String: AnyCodable]?,
@@ -578,6 +580,7 @@ public struct AgentParams: Codable, Sendable {
self.groupchannel = groupchannel
self.groupspace = groupspace
self.timeout = timeout
self.besteffortdeliver = besteffortdeliver
self.lane = lane
self.extrasystemprompt = extrasystemprompt
self.inputprovenance = inputprovenance
@@ -605,6 +608,7 @@ public struct AgentParams: Codable, Sendable {
case groupchannel = "groupChannel"
case groupspace = "groupSpace"
case timeout
case besteffortdeliver = "bestEffortDeliver"
case lane
case extrasystemprompt = "extraSystemPrompt"
case inputprovenance = "inputProvenance"
@@ -2170,6 +2174,132 @@ public struct SkillsStatusParams: Codable, Sendable {
}
}
public struct ToolsCatalogParams: Codable, Sendable {
public let agentid: String?
public let includeplugins: Bool?
public init(
agentid: String?,
includeplugins: Bool?)
{
self.agentid = agentid
self.includeplugins = includeplugins
}
private enum CodingKeys: String, CodingKey {
case agentid = "agentId"
case includeplugins = "includePlugins"
}
}
public struct ToolCatalogProfile: Codable, Sendable {
public let id: AnyCodable
public let label: String
public init(
id: AnyCodable,
label: String)
{
self.id = id
self.label = label
}
private enum CodingKeys: String, CodingKey {
case id
case label
}
}
public struct ToolCatalogEntry: Codable, Sendable {
public let id: String
public let label: String
public let description: String
public let source: AnyCodable
public let pluginid: String?
public let optional: Bool?
public let defaultprofiles: [AnyCodable]
public init(
id: String,
label: String,
description: String,
source: AnyCodable,
pluginid: String?,
optional: Bool?,
defaultprofiles: [AnyCodable])
{
self.id = id
self.label = label
self.description = description
self.source = source
self.pluginid = pluginid
self.optional = optional
self.defaultprofiles = defaultprofiles
}
private enum CodingKeys: String, CodingKey {
case id
case label
case description
case source
case pluginid = "pluginId"
case optional
case defaultprofiles = "defaultProfiles"
}
}
public struct ToolCatalogGroup: Codable, Sendable {
public let id: String
public let label: String
public let source: AnyCodable
public let pluginid: String?
public let tools: [ToolCatalogEntry]
public init(
id: String,
label: String,
source: AnyCodable,
pluginid: String?,
tools: [ToolCatalogEntry])
{
self.id = id
self.label = label
self.source = source
self.pluginid = pluginid
self.tools = tools
}
private enum CodingKeys: String, CodingKey {
case id
case label
case source
case pluginid = "pluginId"
case tools
}
}
public struct ToolsCatalogResult: Codable, Sendable {
public let agentid: String
public let profiles: [ToolCatalogProfile]
public let groups: [ToolCatalogGroup]
public init(
agentid: String,
profiles: [ToolCatalogProfile],
groups: [ToolCatalogGroup])
{
self.agentid = agentid
self.profiles = profiles
self.groups = groups
}
private enum CodingKeys: String, CodingKey {
case agentid = "agentId"
case profiles
case groups
}
}
public struct SkillsBinsParams: Codable, Sendable {}
public struct SkillsBinsResult: Codable, Sendable {
@@ -2306,15 +2436,39 @@ public struct CronJob: Codable, Sendable {
public struct CronListParams: Codable, Sendable {
public let includedisabled: Bool?
public let limit: Int?
public let offset: Int?
public let query: String?
public let enabled: AnyCodable?
public let sortby: AnyCodable?
public let sortdir: AnyCodable?
public init(
includedisabled: Bool?)
includedisabled: Bool?,
limit: Int?,
offset: Int?,
query: String?,
enabled: AnyCodable?,
sortby: AnyCodable?,
sortdir: AnyCodable?)
{
self.includedisabled = includedisabled
self.limit = limit
self.offset = offset
self.query = query
self.enabled = enabled
self.sortby = sortby
self.sortdir = sortdir
}
private enum CodingKeys: String, CodingKey {
case includedisabled = "includeDisabled"
case limit
case offset
case query
case enabled
case sortby = "sortBy"
case sortdir = "sortDir"
}
}
@@ -2374,6 +2528,60 @@ public struct CronAddParams: Codable, Sendable {
}
}
public struct CronRunsParams: Codable, Sendable {
public let scope: AnyCodable?
public let id: String?
public let jobid: String?
public let limit: Int?
public let offset: Int?
public let statuses: [AnyCodable]?
public let status: AnyCodable?
public let deliverystatuses: [AnyCodable]?
public let deliverystatus: AnyCodable?
public let query: String?
public let sortdir: AnyCodable?
public init(
scope: AnyCodable?,
id: String?,
jobid: String?,
limit: Int?,
offset: Int?,
statuses: [AnyCodable]?,
status: AnyCodable?,
deliverystatuses: [AnyCodable]?,
deliverystatus: AnyCodable?,
query: String?,
sortdir: AnyCodable?)
{
self.scope = scope
self.id = id
self.jobid = jobid
self.limit = limit
self.offset = offset
self.statuses = statuses
self.status = status
self.deliverystatuses = deliverystatuses
self.deliverystatus = deliverystatus
self.query = query
self.sortdir = sortdir
}
private enum CodingKeys: String, CodingKey {
case scope
case id
case jobid = "jobId"
case limit
case offset
case statuses
case status
case deliverystatuses = "deliveryStatuses"
case deliverystatus = "deliveryStatus"
case query
case sortdir = "sortDir"
}
}
public struct CronRunLogEntry: Codable, Sendable {
public let ts: Int
public let jobid: String
@@ -2381,11 +2589,18 @@ public struct CronRunLogEntry: Codable, Sendable {
public let status: AnyCodable?
public let error: String?
public let summary: String?
public let delivered: Bool?
public let deliverystatus: AnyCodable?
public let deliveryerror: String?
public let sessionid: String?
public let sessionkey: String?
public let runatms: Int?
public let durationms: Int?
public let nextrunatms: Int?
public let model: String?
public let provider: String?
public let usage: [String: AnyCodable]?
public let jobname: String?
public init(
ts: Int,
@@ -2394,11 +2609,18 @@ public struct CronRunLogEntry: Codable, Sendable {
status: AnyCodable?,
error: String?,
summary: String?,
delivered: Bool?,
deliverystatus: AnyCodable?,
deliveryerror: String?,
sessionid: String?,
sessionkey: String?,
runatms: Int?,
durationms: Int?,
nextrunatms: Int?)
nextrunatms: Int?,
model: String?,
provider: String?,
usage: [String: AnyCodable]?,
jobname: String?)
{
self.ts = ts
self.jobid = jobid
@@ -2406,11 +2628,18 @@ public struct CronRunLogEntry: Codable, Sendable {
self.status = status
self.error = error
self.summary = summary
self.delivered = delivered
self.deliverystatus = deliverystatus
self.deliveryerror = deliveryerror
self.sessionid = sessionid
self.sessionkey = sessionkey
self.runatms = runatms
self.durationms = durationms
self.nextrunatms = nextrunatms
self.model = model
self.provider = provider
self.usage = usage
self.jobname = jobname
}
private enum CodingKeys: String, CodingKey {
@@ -2420,11 +2649,18 @@ public struct CronRunLogEntry: Codable, Sendable {
case status
case error
case summary
case delivered
case deliverystatus = "deliveryStatus"
case deliveryerror = "deliveryError"
case sessionid = "sessionId"
case sessionkey = "sessionKey"
case runatms = "runAtMs"
case durationms = "durationMs"
case nextrunatms = "nextRunAtMs"
case model
case provider
case usage
case jobname = "jobName"
}
}
@@ -2570,6 +2806,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String
public let cwd: AnyCodable?
public let nodeid: AnyCodable?
public let host: AnyCodable?
public let security: AnyCodable?
public let ask: AnyCodable?
@@ -2583,6 +2820,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
id: String?,
command: String,
cwd: AnyCodable?,
nodeid: AnyCodable?,
host: AnyCodable?,
security: AnyCodable?,
ask: AnyCodable?,
@@ -2595,6 +2833,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
self.id = id
self.command = command
self.cwd = cwd
self.nodeid = nodeid
self.host = host
self.security = security
self.ask = ask
@@ -2609,6 +2848,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
case id
case command
case cwd
case nodeid = "nodeId"
case host
case security
case ask
@@ -2,7 +2,55 @@ import Foundation
import Testing
@testable import OpenClaw
/// These cases cover optional `security=allowlist` behavior.
/// Default install posture remains deny-by-default for exec on macOS node-host.
struct ExecAllowlistTests {
private struct ShellParserParityFixture: Decodable {
struct Case: Decodable {
let id: String
let command: String
let ok: Bool
let executables: [String]
}
let cases: [Case]
}
private struct WrapperResolutionParityFixture: Decodable {
struct Case: Decodable {
let id: String
let argv: [String]
let expectedRawExecutable: String?
}
let cases: [Case]
}
private static func loadShellParserParityCases() throws -> [ShellParserParityFixture.Case] {
let fixtureURL = self.fixtureURL(filename: "exec-allowlist-shell-parser-parity.json")
let data = try Data(contentsOf: fixtureURL)
let fixture = try JSONDecoder().decode(ShellParserParityFixture.self, from: data)
return fixture.cases
}
private static func loadWrapperResolutionParityCases() throws -> [WrapperResolutionParityFixture.Case] {
let fixtureURL = self.fixtureURL(filename: "exec-wrapper-resolution-parity.json")
let data = try Data(contentsOf: fixtureURL)
let fixture = try JSONDecoder().decode(WrapperResolutionParityFixture.self, from: data)
return fixture.cases
}
private static func fixtureURL(filename: String) -> URL {
var repoRoot = URL(fileURLWithPath: #filePath)
for _ in 0..<5 {
repoRoot.deleteLastPathComponent()
}
return repoRoot
.appendingPathComponent("test")
.appendingPathComponent("fixtures")
.appendingPathComponent(filename)
}
@Test func matchUsesResolvedPath() {
let entry = ExecAllowlistEntry(pattern: "/opt/homebrew/bin/rg")
let resolution = ExecCommandResolution(
@@ -14,7 +62,7 @@ struct ExecAllowlistTests {
#expect(match?.pattern == entry.pattern)
}
@Test func matchUsesBasenameForSimplePattern() {
@Test func matchIgnoresBasenamePattern() {
let entry = ExecAllowlistEntry(pattern: "rg")
let resolution = ExecCommandResolution(
rawExecutable: "rg",
@@ -22,11 +70,22 @@ struct ExecAllowlistTests {
executableName: "rg",
cwd: nil)
let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution)
#expect(match?.pattern == entry.pattern)
#expect(match == nil)
}
@Test func matchIgnoresBasenameForRelativeExecutable() {
let entry = ExecAllowlistEntry(pattern: "echo")
let resolution = ExecCommandResolution(
rawExecutable: "./echo",
resolvedPath: "/tmp/oc-basename/echo",
executableName: "echo",
cwd: "/tmp/oc-basename")
let match = ExecAllowlistMatcher.match(entries: [entry], resolution: resolution)
#expect(match == nil)
}
@Test func matchIsCaseInsensitive() {
let entry = ExecAllowlistEntry(pattern: "RG")
let entry = ExecAllowlistEntry(pattern: "/OPT/HOMEBREW/BIN/RG")
let resolution = ExecCommandResolution(
rawExecutable: "rg",
resolvedPath: "/opt/homebrew/bin/rg",
@@ -80,6 +139,55 @@ struct ExecAllowlistTests {
#expect(resolutions.isEmpty)
}
@Test func resolveForAllowlistFailsClosedOnQuotedCommandSubstitution() {
let command = ["/bin/sh", "-lc", "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\""]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\"",
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.isEmpty)
}
@Test func resolveForAllowlistFailsClosedOnQuotedBackticks() {
let command = ["/bin/sh", "-lc", "echo \"ok `/usr/bin/id`\""]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: "echo \"ok `/usr/bin/id`\"",
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.isEmpty)
}
@Test func resolveForAllowlistMatchesSharedShellParserFixture() throws {
let fixtures = try Self.loadShellParserParityCases()
for fixture in fixtures {
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: ["/bin/sh", "-lc", fixture.command],
rawCommand: fixture.command,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(!resolutions.isEmpty == fixture.ok)
if fixture.ok {
let executables = resolutions.map { $0.executableName.lowercased() }
let expected = fixture.executables.map { $0.lowercased() }
#expect(executables == expected)
}
}
}
@Test func resolveMatchesSharedWrapperResolutionFixture() throws {
let fixtures = try Self.loadWrapperResolutionParityCases()
for fixture in fixtures {
let resolution = ExecCommandResolution.resolve(
command: fixture.argv,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolution?.rawExecutable == fixture.expectedRawExecutable)
}
}
@Test func resolveForAllowlistTreatsPlainShInvocationAsDirectExec() {
let command = ["/bin/sh", "./script.sh"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
@@ -91,6 +199,30 @@ struct ExecAllowlistTests {
#expect(resolutions[0].executableName == "sh")
}
@Test func resolveForAllowlistUnwrapsEnvShellWrapperChains() {
let command = ["/usr/bin/env", "/bin/sh", "-lc", "echo allowlisted && /usr/bin/touch /tmp/openclaw-allowlist-test"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 2)
#expect(resolutions[0].executableName == "echo")
#expect(resolutions[1].executableName == "touch")
}
@Test func resolveForAllowlistUnwrapsEnvToEffectiveDirectExecutable() {
let command = ["/usr/bin/env", "FOO=bar", "/usr/bin/printf", "ok"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.count == 1)
#expect(resolutions[0].resolvedPath == "/usr/bin/printf")
#expect(resolutions[0].executableName == "printf")
}
@Test func matchAllRequiresEverySegmentToMatch() {
let first = ExecCommandResolution(
rawExecutable: "echo",
@@ -105,12 +237,12 @@ struct ExecAllowlistTests {
let resolutions = [first, second]
let partial = ExecAllowlistMatcher.matchAll(
entries: [ExecAllowlistEntry(pattern: "echo")],
entries: [ExecAllowlistEntry(pattern: "/usr/bin/echo")],
resolutions: resolutions)
#expect(partial.isEmpty)
let full = ExecAllowlistMatcher.matchAll(
entries: [ExecAllowlistEntry(pattern: "echo"), ExecAllowlistEntry(pattern: "touch")],
entries: [ExecAllowlistEntry(pattern: "/USR/BIN/ECHO"), ExecAllowlistEntry(pattern: "/usr/bin/touch")],
resolutions: resolutions)
#expect(full.count == 2)
}
@@ -29,6 +29,24 @@ import Testing
#expect(ExecApprovalHelpers.allowlistPattern(command: [], resolution: nil) == nil)
}
@Test func validateAllowlistPatternReturnsReasons() {
#expect(ExecApprovalHelpers.isPathPattern("/usr/bin/rg"))
#expect(ExecApprovalHelpers.isPathPattern(" ~/bin/rg "))
#expect(!ExecApprovalHelpers.isPathPattern("rg"))
if case .invalid(let reason) = ExecApprovalHelpers.validateAllowlistPattern(" ") {
#expect(reason == .empty)
} else {
Issue.record("Expected empty pattern rejection")
}
if case .invalid(let reason) = ExecApprovalHelpers.validateAllowlistPattern("echo") {
#expect(reason == .missingPathComponent)
} else {
Issue.record("Expected basename pattern rejection")
}
}
@Test func requiresAskMatchesPolicy() {
let entry = ExecAllowlistEntry(pattern: "/bin/ls", lastUsedAt: nil, lastUsedCommand: nil, lastResolvedPath: nil)
#expect(ExecApprovalHelpers.requiresAsk(
@@ -0,0 +1,75 @@
import Foundation
import Testing
@testable import OpenClaw
@Suite(.serialized)
struct ExecApprovalsStoreRefactorTests {
@Test
func ensureFileSkipsRewriteWhenUnchanged() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: stateDir) }
try await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) {
_ = ExecApprovalsStore.ensureFile()
let url = ExecApprovalsStore.fileURL()
let firstWriteDate = try Self.modificationDate(at: url)
try await Task.sleep(nanoseconds: 1_100_000_000)
_ = ExecApprovalsStore.ensureFile()
let secondWriteDate = try Self.modificationDate(at: url)
#expect(firstWriteDate == secondWriteDate)
}
}
@Test
func updateAllowlistReportsRejectedBasenamePattern() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: stateDir) }
await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) {
let rejected = ExecApprovalsStore.updateAllowlist(
agentId: "main",
allowlist: [
ExecAllowlistEntry(pattern: "echo"),
ExecAllowlistEntry(pattern: "/bin/echo"),
])
#expect(rejected.count == 1)
#expect(rejected.first?.reason == .missingPathComponent)
#expect(rejected.first?.pattern == "echo")
let resolved = ExecApprovalsStore.resolve(agentId: "main")
#expect(resolved.allowlist.map(\.pattern) == ["/bin/echo"])
}
}
@Test
func updateAllowlistMigratesLegacyPatternFromResolvedPath() async throws {
let stateDir = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-state-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: stateDir) }
await TestIsolation.withEnvValues(["OPENCLAW_STATE_DIR": stateDir.path]) {
let rejected = ExecApprovalsStore.updateAllowlist(
agentId: "main",
allowlist: [
ExecAllowlistEntry(pattern: "echo", lastUsedAt: nil, lastUsedCommand: nil, lastResolvedPath: " /usr/bin/echo "),
])
#expect(rejected.isEmpty)
let resolved = ExecApprovalsStore.resolve(agentId: "main")
#expect(resolved.allowlist.map(\.pattern) == ["/usr/bin/echo"])
}
}
private static func modificationDate(at url: URL) throws -> Date {
let attributes = try FileManager().attributesOfItem(atPath: url.path)
guard let date = attributes[.modificationDate] as? Date else {
struct MissingDateError: Error {}
throw MissingDateError()
}
return date
}
}
@@ -45,12 +45,7 @@ import Testing
// First send is the connect handshake request. Subsequent sends are request frames.
if currentSendCount == 0 {
guard case let .data(data) = message else { return }
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
(obj["type"] as? String) == "req",
(obj["method"] as? String) == "connect",
let id = obj["id"] as? String
{
if let id = GatewayWebSocketTestSupport.connectRequestID(from: message) {
self.connectRequestID.withLock { $0 = id }
}
return
@@ -65,7 +60,7 @@ import Testing
return
}
let response = Self.responseData(id: id)
let response = GatewayWebSocketTestSupport.okResponseData(id: id)
let handler = self.pendingReceiveHandler.withLock { $0 }
handler?(Result<URLSessionWebSocketTask.Message, Error>.success(.data(response)))
}
@@ -75,7 +70,7 @@ import Testing
try await Task.sleep(nanoseconds: UInt64(self.helloDelayMs) * 1_000_000)
}
let id = self.connectRequestID.withLock { $0 } ?? "connect"
return .data(Self.connectOkData(id: id))
return .data(GatewayWebSocketTestSupport.connectOkData(id: id))
}
func receive(
@@ -89,41 +84,6 @@ import Testing
handler?(Result<URLSessionWebSocketTask.Message, Error>.success(.data(data)))
}
private static func connectOkData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 2,
"server": { "version": "test", "connId": "test" },
"features": { "methods": [], "events": [] },
"snapshot": {
"presence": [ { "ts": 1 } ],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
}
}
"""
return Data(json.utf8)
}
private static func responseData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": { "ok": true }
}
"""
return Data(json.utf8)
}
}
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
@@ -38,17 +38,7 @@ import Testing
}
func send(_ message: URLSessionWebSocketTask.Message) async throws {
let data: Data? = switch message {
case let .data(d): d
case let .string(s): s.data(using: .utf8)
@unknown default: nil
}
guard let data else { return }
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
obj["type"] as? String == "req",
obj["method"] as? String == "connect",
let id = obj["id"] as? String
{
if let id = GatewayWebSocketTestSupport.connectRequestID(from: message) {
self.connectRequestID.withLock { $0 = id }
}
}
@@ -60,7 +50,7 @@ import Testing
case let .helloOk(ms):
delayMs = ms
let id = self.connectRequestID.withLock { $0 } ?? "connect"
msg = .data(Self.connectOkData(id: id))
msg = .data(GatewayWebSocketTestSupport.connectOkData(id: id))
case let .invalid(ms):
delayMs = ms
msg = .string("not json")
@@ -77,29 +67,6 @@ import Testing
self.pendingReceiveHandler.withLock { $0 = completionHandler }
}
private static func connectOkData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 2,
"server": { "version": "test", "connId": "test" },
"features": { "methods": [], "events": [] },
"snapshot": {
"presence": [ { "ts": 1 } ],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
}
}
"""
return Data(json.utf8)
}
}
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
@@ -42,17 +42,7 @@ import Testing
// First send is the connect handshake. Second send is the request frame.
if currentSendCount == 0 {
let data: Data? = switch message {
case let .data(d): d
case let .string(s): s.data(using: .utf8)
@unknown default: nil
}
guard let data else { return }
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
obj["type"] as? String == "req",
obj["method"] as? String == "connect",
let id = obj["id"] as? String
{
if let id = GatewayWebSocketTestSupport.connectRequestID(from: message) {
self.connectRequestID.withLock { $0 = id }
}
}
@@ -64,7 +54,7 @@ import Testing
func receive() async throws -> URLSessionWebSocketTask.Message {
let id = self.connectRequestID.withLock { $0 } ?? "connect"
return .data(Self.connectOkData(id: id))
return .data(GatewayWebSocketTestSupport.connectOkData(id: id))
}
func receive(
@@ -73,29 +63,6 @@ import Testing
self.pendingReceiveHandler.withLock { $0 = completionHandler }
}
private static func connectOkData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 2,
"server": { "version": "test", "connId": "test" },
"features": { "methods": [], "events": [] },
"snapshot": {
"presence": [ { "ts": 1 } ],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
}
}
"""
return Data(json.utf8)
}
}
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
@@ -32,24 +32,14 @@ import Testing
}
func send(_ message: URLSessionWebSocketTask.Message) async throws {
let data: Data? = switch message {
case let .data(d): d
case let .string(s): s.data(using: .utf8)
@unknown default: nil
}
guard let data else { return }
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
obj["type"] as? String == "req",
obj["method"] as? String == "connect",
let id = obj["id"] as? String
{
if let id = GatewayWebSocketTestSupport.connectRequestID(from: message) {
self.connectRequestID.withLock { $0 = id }
}
}
func receive() async throws -> URLSessionWebSocketTask.Message {
let id = self.connectRequestID.withLock { $0 } ?? "connect"
return .data(Self.connectOkData(id: id))
return .data(GatewayWebSocketTestSupport.connectOkData(id: id))
}
func receive(
@@ -63,29 +53,6 @@ import Testing
handler?(Result<URLSessionWebSocketTask.Message, Error>.failure(URLError(.networkConnectionLost)))
}
private static func connectOkData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 2,
"server": { "version": "test", "connId": "test" },
"features": { "methods": [], "events": [] },
"snapshot": {
"presence": [ { "ts": 1 } ],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
}
}
"""
return Data(json.utf8)
}
}
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
@@ -0,0 +1,98 @@
import Foundation
import OpenClawDiscovery
import Testing
@testable import OpenClaw
@Suite
struct GatewayDiscoveryHelpersTests {
private func makeGateway(
serviceHost: String?,
servicePort: Int?,
lanHost: String? = "txt-host.local",
tailnetDns: String? = "txt-host.ts.net",
sshPort: Int = 22,
gatewayPort: Int? = 18789) -> GatewayDiscoveryModel.DiscoveredGateway
{
GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: serviceHost,
servicePort: servicePort,
lanHost: lanHost,
tailnetDns: tailnetDns,
sshPort: sshPort,
gatewayPort: gatewayPort,
cliPath: "/tmp/openclaw",
stableID: UUID().uuidString,
debugID: UUID().uuidString,
isLocal: false)
}
@Test func sshTargetUsesResolvedServiceHostOnly() {
let gateway = self.makeGateway(
serviceHost: "resolved.example.ts.net",
servicePort: 18789,
sshPort: 2201)
guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) else {
Issue.record("expected ssh target")
return
}
let parsed = CommandResolver.parseSSHTarget(target)
#expect(parsed?.host == "resolved.example.ts.net")
#expect(parsed?.port == 2201)
}
@Test func sshTargetAllowsMissingResolvedServicePort() {
let gateway = self.makeGateway(
serviceHost: "resolved.example.ts.net",
servicePort: nil,
sshPort: 2201)
guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) else {
Issue.record("expected ssh target")
return
}
let parsed = CommandResolver.parseSSHTarget(target)
#expect(parsed?.host == "resolved.example.ts.net")
#expect(parsed?.port == 2201)
}
@Test func sshTargetRejectsTxtOnlyGateways() {
let gateway = self.makeGateway(
serviceHost: nil,
servicePort: nil,
lanHost: "txt-only.local",
tailnetDns: "txt-only.ts.net",
sshPort: 2222)
#expect(GatewayDiscoveryHelpers.sshTarget(for: gateway) == nil)
}
@Test func directUrlUsesResolvedServiceEndpointOnly() {
let tlsGateway = self.makeGateway(
serviceHost: "resolved.example.ts.net",
servicePort: 443)
#expect(GatewayDiscoveryHelpers.directUrl(for: tlsGateway) == "wss://resolved.example.ts.net")
let wsGateway = self.makeGateway(
serviceHost: "resolved.example.ts.net",
servicePort: 18789)
#expect(GatewayDiscoveryHelpers.directUrl(for: wsGateway) == "wss://resolved.example.ts.net:18789")
let localGateway = self.makeGateway(
serviceHost: "127.0.0.1",
servicePort: 18789)
#expect(GatewayDiscoveryHelpers.directUrl(for: localGateway) == "ws://127.0.0.1:18789")
}
@Test func directUrlRejectsTxtOnlyFallback() {
let gateway = self.makeGateway(
serviceHost: nil,
servicePort: nil,
lanHost: "txt-only.local",
tailnetDns: "txt-only.ts.net",
gatewayPort: 22222)
#expect(GatewayDiscoveryHelpers.directUrl(for: gateway) == nil)
}
}
@@ -225,7 +225,7 @@ import Testing
}
@Test func normalizeGatewayUrlRejectsNonLoopbackWs() {
let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://gateway")
let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://gateway.example:18789")
#expect(url == nil)
}
@@ -39,12 +39,7 @@ struct GatewayProcessManagerTests {
}
if currentSendCount == 0 {
guard case let .data(data) = message else { return }
if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
(obj["type"] as? String) == "req",
(obj["method"] as? String) == "connect",
let id = obj["id"] as? String
{
if let id = GatewayWebSocketTestSupport.connectRequestID(from: message) {
self.connectRequestID.withLock { $0 = id }
}
return
@@ -59,14 +54,14 @@ struct GatewayProcessManagerTests {
return
}
let response = Self.responseData(id: id)
let response = GatewayWebSocketTestSupport.okResponseData(id: id)
let handler = self.pendingReceiveHandler.withLock { $0 }
handler?(Result<URLSessionWebSocketTask.Message, Error>.success(.data(response)))
}
func receive() async throws -> URLSessionWebSocketTask.Message {
let id = self.connectRequestID.withLock { $0 } ?? "connect"
return .data(Self.connectOkData(id: id))
return .data(GatewayWebSocketTestSupport.connectOkData(id: id))
}
func receive(
@@ -75,41 +70,6 @@ struct GatewayProcessManagerTests {
self.pendingReceiveHandler.withLock { $0 = completionHandler }
}
private static func connectOkData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 2,
"server": { "version": "test", "connId": "test" },
"features": { "methods": [], "events": [] },
"snapshot": {
"presence": [ { "ts": 1 } ],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
}
}
"""
return Data(json.utf8)
}
private static func responseData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": { "ok": true }
}
"""
return Data(json.utf8)
}
}
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
@@ -0,0 +1,63 @@
import OpenClawKit
import Foundation
extension WebSocketTasking {
// Keep unit-test doubles resilient to protocol additions.
func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
pongReceiveHandler(nil)
}
}
enum GatewayWebSocketTestSupport {
static func connectRequestID(from message: URLSessionWebSocketTask.Message) -> String? {
let data: Data? = switch message {
case let .data(d): d
case let .string(s): s.data(using: .utf8)
@unknown default: nil
}
guard let data else { return nil }
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
guard (obj["type"] as? String) == "req", (obj["method"] as? String) == "connect" else {
return nil
}
return obj["id"] as? String
}
static func connectOkData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 2,
"server": { "version": "test", "connId": "test" },
"features": { "methods": [], "events": [] },
"snapshot": {
"presence": [ { "ts": 1 } ],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 }
}
}
"""
return Data(json.utf8)
}
static func okResponseData(id: String) -> Data {
let json = """
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": { "ok": true }
}
"""
return Data(json.utf8)
}
}
@@ -0,0 +1,36 @@
import Testing
@testable import OpenClaw
struct HostEnvSanitizerTests {
@Test func sanitizeBlocksShellTraceVariables() {
let env = HostEnvSanitizer.sanitize(overrides: [
"SHELLOPTS": "xtrace",
"PS4": "$(touch /tmp/pwned)",
"OPENCLAW_TEST": "1",
])
#expect(env["SHELLOPTS"] == nil)
#expect(env["PS4"] == nil)
#expect(env["OPENCLAW_TEST"] == "1")
}
@Test func sanitizeShellWrapperAllowsOnlyExplicitOverrideKeys() {
let env = HostEnvSanitizer.sanitize(
overrides: [
"LANG": "C",
"LC_ALL": "C",
"OPENCLAW_TOKEN": "secret",
"PS4": "$(touch /tmp/pwned)",
],
shellWrapper: true)
#expect(env["LANG"] == "C")
#expect(env["LC_ALL"] == "C")
#expect(env["OPENCLAW_TOKEN"] == nil)
#expect(env["PS4"] == nil)
}
@Test func sanitizeNonShellWrapperKeepsRegularOverrides() {
let env = HostEnvSanitizer.sanitize(overrides: ["OPENCLAW_TOKEN": "secret"])
#expect(env["OPENCLAW_TOKEN"] == "secret")
}
}
@@ -13,7 +13,8 @@ import Testing
configpath: nil,
statedir: nil,
sessiondefaults: nil,
authmode: nil)
authmode: nil,
updateavailable: nil)
let hello = HelloOk(
type: "hello",
@@ -1,3 +1,4 @@
import Foundation
import OpenClawDiscovery
import SwiftUI
import Testing
@@ -25,4 +26,36 @@ struct OnboardingViewSmokeTests {
let order = OnboardingView.pageOrder(for: .local, showOnboardingChat: false)
#expect(!order.contains(8))
}
@Test func selectRemoteGatewayClearsStaleSshTargetWhenEndpointUnresolved() async {
let override = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
.appendingPathComponent("openclaw.json")
.path
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
let state = AppState(preview: true)
state.remoteTransport = .ssh
state.remoteTarget = "user@old-host:2222"
let view = OnboardingView(
state: state,
permissionMonitor: PermissionMonitor.shared,
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
let gateway = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Unresolved",
serviceHost: nil,
servicePort: nil,
lanHost: "txt-host.local",
tailnetDns: "txt-host.ts.net",
sshPort: 22,
gatewayPort: 18789,
cliPath: "/tmp/openclaw",
stableID: UUID().uuidString,
debugID: UUID().uuidString,
isLocal: false)
view.selectRemoteGateway(gateway)
#expect(state.remoteTarget.isEmpty)
}
}
}
@@ -62,6 +62,31 @@ struct OpenClawConfigFileTests {
}
}
@MainActor
@Test
func clearRemoteGatewayUrlRemovesOnlyUrlField() async {
let override = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
.appendingPathComponent("openclaw.json")
.path
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
OpenClawConfigFile.saveDict([
"gateway": [
"remote": [
"url": "wss://old-host:111",
"token": "tok",
],
],
])
OpenClawConfigFile.clearRemoteGatewayUrl()
let root = OpenClawConfigFile.loadDict()
let remote = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
#expect((remote["url"] as? String) == nil)
#expect((remote["token"] as? String) == "tok")
}
}
@Test
func stateDirOverrideSetsConfigPath() async {
let dir = FileManager().temporaryDirectory
@@ -127,6 +127,14 @@ private enum ConnectChallengeError: Error {
case timeout
}
private let defaultOperatorConnectScopes: [String] = [
"operator.admin",
"operator.read",
"operator.write",
"operator.approvals",
"operator.pairing",
]
public actor GatewayChannelActor {
private let logger = Logger(subsystem: "ai.openclaw", category: "gateway")
private var task: WebSocketTaskBox?
@@ -146,8 +154,8 @@ public actor GatewayChannelActor {
private var lastAuthSource: GatewayAuthSource = .none
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
// Remote gateways (tailscale/wan) can take a bit longer to deliver the connect.challenge event,
// and we must include the nonce once the gateway requires v2 signing.
// Remote gateways (tailscale/wan) can take longer to deliver connect.challenge.
// Connect now requires this nonce before we send device-auth.
private let connectTimeoutSeconds: Double = 12
private let connectChallengeTimeoutSeconds: Double = 6.0
// Some networks will silently drop idle TCP/TLS flows around ~30s. The gateway tick is server->client,
@@ -318,7 +326,7 @@ public actor GatewayChannelActor {
let primaryLocale = Locale.preferredLanguages.first ?? Locale.current.identifier
let options = self.connectOptions ?? GatewayConnectOptions(
role: "operator",
scopes: ["operator.admin", "operator.approvals", "operator.pairing"],
scopes: defaultOperatorConnectScopes,
caps: [],
commands: [],
permissions: [:],
@@ -391,8 +399,8 @@ public actor GatewayChannelActor {
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
let connectNonce = try await self.waitForConnectChallenge()
let scopesValue = scopes.joined(separator: ",")
var payloadParts = [
connectNonce == nil ? "v1" : "v2",
let payloadParts = [
"v2",
identity?.deviceId ?? "",
clientId,
clientMode,
@@ -400,23 +408,19 @@ public actor GatewayChannelActor {
scopesValue,
String(signedAtMs),
authToken ?? "",
connectNonce,
]
if let connectNonce {
payloadParts.append(connectNonce)
}
let payload = payloadParts.joined(separator: "|")
if includeDeviceIdentity, let identity {
if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) {
var device: [String: ProtoAnyCodable] = [
let device: [String: ProtoAnyCodable] = [
"id": ProtoAnyCodable(identity.deviceId),
"publicKey": ProtoAnyCodable(publicKey),
"signature": ProtoAnyCodable(signature),
"signedAt": ProtoAnyCodable(signedAtMs),
"nonce": ProtoAnyCodable(connectNonce),
]
if let connectNonce {
device["nonce"] = ProtoAnyCodable(connectNonce)
}
params["device"] = ProtoAnyCodable(device)
}
}
@@ -545,33 +549,26 @@ public actor GatewayChannelActor {
}
}
private func waitForConnectChallenge() async throws -> String? {
guard let task = self.task else { return nil }
do {
return try await AsyncTimeout.withTimeout(
seconds: self.connectChallengeTimeoutSeconds,
onTimeout: { ConnectChallengeError.timeout },
operation: { [weak self] in
guard let self else { return nil }
while true {
let msg = try await task.receive()
guard let data = self.decodeMessageData(msg) else { continue }
guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else { continue }
if case let .event(evt) = frame, evt.event == "connect.challenge" {
if let payload = evt.payload?.value as? [String: ProtoAnyCodable],
let nonce = payload["nonce"]?.value as? String {
return nonce
}
}
private func waitForConnectChallenge() async throws -> String {
guard let task = self.task else { throw ConnectChallengeError.timeout }
return try await AsyncTimeout.withTimeout(
seconds: self.connectChallengeTimeoutSeconds,
onTimeout: { ConnectChallengeError.timeout },
operation: { [weak self] in
guard let self else { throw ConnectChallengeError.timeout }
while true {
let msg = try await task.receive()
guard let data = self.decodeMessageData(msg) else { continue }
guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else { continue }
if case let .event(evt) = frame, evt.event == "connect.challenge",
let payload = evt.payload?.value as? [String: ProtoAnyCodable],
let nonce = payload["nonce"]?.value as? String,
nonce.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
{
return nonce
}
})
} catch {
if error is ConnectChallengeError {
self.logger.warning("gateway connect challenge timed out")
return nil
}
throw error
}
}
})
}
private func waitForConnectResponse(reqId: String) async throws -> ResponseFrame {
@@ -527,6 +527,7 @@ public struct AgentParams: Codable, Sendable {
public let groupchannel: String?
public let groupspace: String?
public let timeout: Int?
public let besteffortdeliver: Bool?
public let lane: String?
public let extrasystemprompt: String?
public let inputprovenance: [String: AnyCodable]?
@@ -553,6 +554,7 @@ public struct AgentParams: Codable, Sendable {
groupchannel: String?,
groupspace: String?,
timeout: Int?,
besteffortdeliver: Bool?,
lane: String?,
extrasystemprompt: String?,
inputprovenance: [String: AnyCodable]?,
@@ -578,6 +580,7 @@ public struct AgentParams: Codable, Sendable {
self.groupchannel = groupchannel
self.groupspace = groupspace
self.timeout = timeout
self.besteffortdeliver = besteffortdeliver
self.lane = lane
self.extrasystemprompt = extrasystemprompt
self.inputprovenance = inputprovenance
@@ -605,6 +608,7 @@ public struct AgentParams: Codable, Sendable {
case groupchannel = "groupChannel"
case groupspace = "groupSpace"
case timeout
case besteffortdeliver = "bestEffortDeliver"
case lane
case extrasystemprompt = "extraSystemPrompt"
case inputprovenance = "inputProvenance"
@@ -2170,6 +2174,132 @@ public struct SkillsStatusParams: Codable, Sendable {
}
}
public struct ToolsCatalogParams: Codable, Sendable {
public let agentid: String?
public let includeplugins: Bool?
public init(
agentid: String?,
includeplugins: Bool?)
{
self.agentid = agentid
self.includeplugins = includeplugins
}
private enum CodingKeys: String, CodingKey {
case agentid = "agentId"
case includeplugins = "includePlugins"
}
}
public struct ToolCatalogProfile: Codable, Sendable {
public let id: AnyCodable
public let label: String
public init(
id: AnyCodable,
label: String)
{
self.id = id
self.label = label
}
private enum CodingKeys: String, CodingKey {
case id
case label
}
}
public struct ToolCatalogEntry: Codable, Sendable {
public let id: String
public let label: String
public let description: String
public let source: AnyCodable
public let pluginid: String?
public let optional: Bool?
public let defaultprofiles: [AnyCodable]
public init(
id: String,
label: String,
description: String,
source: AnyCodable,
pluginid: String?,
optional: Bool?,
defaultprofiles: [AnyCodable])
{
self.id = id
self.label = label
self.description = description
self.source = source
self.pluginid = pluginid
self.optional = optional
self.defaultprofiles = defaultprofiles
}
private enum CodingKeys: String, CodingKey {
case id
case label
case description
case source
case pluginid = "pluginId"
case optional
case defaultprofiles = "defaultProfiles"
}
}
public struct ToolCatalogGroup: Codable, Sendable {
public let id: String
public let label: String
public let source: AnyCodable
public let pluginid: String?
public let tools: [ToolCatalogEntry]
public init(
id: String,
label: String,
source: AnyCodable,
pluginid: String?,
tools: [ToolCatalogEntry])
{
self.id = id
self.label = label
self.source = source
self.pluginid = pluginid
self.tools = tools
}
private enum CodingKeys: String, CodingKey {
case id
case label
case source
case pluginid = "pluginId"
case tools
}
}
public struct ToolsCatalogResult: Codable, Sendable {
public let agentid: String
public let profiles: [ToolCatalogProfile]
public let groups: [ToolCatalogGroup]
public init(
agentid: String,
profiles: [ToolCatalogProfile],
groups: [ToolCatalogGroup])
{
self.agentid = agentid
self.profiles = profiles
self.groups = groups
}
private enum CodingKeys: String, CodingKey {
case agentid = "agentId"
case profiles
case groups
}
}
public struct SkillsBinsParams: Codable, Sendable {}
public struct SkillsBinsResult: Codable, Sendable {
@@ -2306,15 +2436,39 @@ public struct CronJob: Codable, Sendable {
public struct CronListParams: Codable, Sendable {
public let includedisabled: Bool?
public let limit: Int?
public let offset: Int?
public let query: String?
public let enabled: AnyCodable?
public let sortby: AnyCodable?
public let sortdir: AnyCodable?
public init(
includedisabled: Bool?)
includedisabled: Bool?,
limit: Int?,
offset: Int?,
query: String?,
enabled: AnyCodable?,
sortby: AnyCodable?,
sortdir: AnyCodable?)
{
self.includedisabled = includedisabled
self.limit = limit
self.offset = offset
self.query = query
self.enabled = enabled
self.sortby = sortby
self.sortdir = sortdir
}
private enum CodingKeys: String, CodingKey {
case includedisabled = "includeDisabled"
case limit
case offset
case query
case enabled
case sortby = "sortBy"
case sortdir = "sortDir"
}
}
@@ -2374,6 +2528,60 @@ public struct CronAddParams: Codable, Sendable {
}
}
public struct CronRunsParams: Codable, Sendable {
public let scope: AnyCodable?
public let id: String?
public let jobid: String?
public let limit: Int?
public let offset: Int?
public let statuses: [AnyCodable]?
public let status: AnyCodable?
public let deliverystatuses: [AnyCodable]?
public let deliverystatus: AnyCodable?
public let query: String?
public let sortdir: AnyCodable?
public init(
scope: AnyCodable?,
id: String?,
jobid: String?,
limit: Int?,
offset: Int?,
statuses: [AnyCodable]?,
status: AnyCodable?,
deliverystatuses: [AnyCodable]?,
deliverystatus: AnyCodable?,
query: String?,
sortdir: AnyCodable?)
{
self.scope = scope
self.id = id
self.jobid = jobid
self.limit = limit
self.offset = offset
self.statuses = statuses
self.status = status
self.deliverystatuses = deliverystatuses
self.deliverystatus = deliverystatus
self.query = query
self.sortdir = sortdir
}
private enum CodingKeys: String, CodingKey {
case scope
case id
case jobid = "jobId"
case limit
case offset
case statuses
case status
case deliverystatuses = "deliveryStatuses"
case deliverystatus = "deliveryStatus"
case query
case sortdir = "sortDir"
}
}
public struct CronRunLogEntry: Codable, Sendable {
public let ts: Int
public let jobid: String
@@ -2381,11 +2589,18 @@ public struct CronRunLogEntry: Codable, Sendable {
public let status: AnyCodable?
public let error: String?
public let summary: String?
public let delivered: Bool?
public let deliverystatus: AnyCodable?
public let deliveryerror: String?
public let sessionid: String?
public let sessionkey: String?
public let runatms: Int?
public let durationms: Int?
public let nextrunatms: Int?
public let model: String?
public let provider: String?
public let usage: [String: AnyCodable]?
public let jobname: String?
public init(
ts: Int,
@@ -2394,11 +2609,18 @@ public struct CronRunLogEntry: Codable, Sendable {
status: AnyCodable?,
error: String?,
summary: String?,
delivered: Bool?,
deliverystatus: AnyCodable?,
deliveryerror: String?,
sessionid: String?,
sessionkey: String?,
runatms: Int?,
durationms: Int?,
nextrunatms: Int?)
nextrunatms: Int?,
model: String?,
provider: String?,
usage: [String: AnyCodable]?,
jobname: String?)
{
self.ts = ts
self.jobid = jobid
@@ -2406,11 +2628,18 @@ public struct CronRunLogEntry: Codable, Sendable {
self.status = status
self.error = error
self.summary = summary
self.delivered = delivered
self.deliverystatus = deliverystatus
self.deliveryerror = deliveryerror
self.sessionid = sessionid
self.sessionkey = sessionkey
self.runatms = runatms
self.durationms = durationms
self.nextrunatms = nextrunatms
self.model = model
self.provider = provider
self.usage = usage
self.jobname = jobname
}
private enum CodingKeys: String, CodingKey {
@@ -2420,11 +2649,18 @@ public struct CronRunLogEntry: Codable, Sendable {
case status
case error
case summary
case delivered
case deliverystatus = "deliveryStatus"
case deliveryerror = "deliveryError"
case sessionid = "sessionId"
case sessionkey = "sessionKey"
case runatms = "runAtMs"
case durationms = "durationMs"
case nextrunatms = "nextRunAtMs"
case model
case provider
case usage
case jobname = "jobName"
}
}
@@ -2570,6 +2806,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String
public let cwd: AnyCodable?
public let nodeid: AnyCodable?
public let host: AnyCodable?
public let security: AnyCodable?
public let ask: AnyCodable?
@@ -2583,6 +2820,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
id: String?,
command: String,
cwd: AnyCodable?,
nodeid: AnyCodable?,
host: AnyCodable?,
security: AnyCodable?,
ask: AnyCodable?,
@@ -2595,6 +2833,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
self.id = id
self.command = command
self.cwd = cwd
self.nodeid = nodeid
self.host = host
self.security = security
self.ask = ask
@@ -2609,6 +2848,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
case id
case command
case cwd
case nodeid = "nodeId"
case host
case security
case ask
@@ -0,0 +1,48 @@
export function reconnectDelayMs(
attempt,
opts = { baseMs: 1000, maxMs: 30000, jitterMs: 1000, random: Math.random },
) {
const baseMs = Number.isFinite(opts.baseMs) ? opts.baseMs : 1000;
const maxMs = Number.isFinite(opts.maxMs) ? opts.maxMs : 30000;
const jitterMs = Number.isFinite(opts.jitterMs) ? opts.jitterMs : 1000;
const random = typeof opts.random === "function" ? opts.random : Math.random;
const safeAttempt = Math.max(0, Number.isFinite(attempt) ? attempt : 0);
const backoff = Math.min(baseMs * 2 ** safeAttempt, maxMs);
return backoff + Math.max(0, jitterMs) * random();
}
export async function deriveRelayToken(gatewayToken, port) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
enc.encode(gatewayToken),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign(
"HMAC",
key,
enc.encode(`openclaw-extension-relay-v1:${port}`),
);
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
export async function buildRelayWsUrl(port, gatewayToken) {
const token = String(gatewayToken || "").trim();
if (!token) {
throw new Error(
"Missing gatewayToken in extension settings (chrome.storage.local.gatewayToken)",
);
}
const relayToken = await deriveRelayToken(token, port);
return `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(relayToken)}`;
}
export function isRetryableReconnectError(err) {
const message = err instanceof Error ? err.message : String(err || "");
if (message.includes("Missing gatewayToken")) {
return false;
}
return true;
}
+513 -67
View File
@@ -1,3 +1,5 @@
import { buildRelayWsUrl, isRetryableReconnectError, reconnectDelayMs } from './background-utils.js'
const DEFAULT_PORT = 18792
const BADGE = {
@@ -12,8 +14,6 @@ let relayWs = null
/** @type {Promise<void>|null} */
let relayConnectPromise = null
let debuggerListenersInstalled = false
let nextSession = 1
/** @type {Map<number, {state:'connecting'|'connected', sessionId?:string, targetId?:string, attachOrder?:number}>} */
@@ -26,6 +26,18 @@ const childSessionToTab = new Map()
/** @type {Map<number, {resolve:(v:any)=>void, reject:(e:Error)=>void}>} */
const pending = new Map()
// Per-tab operation locks prevent double-attach races.
/** @type {Set<number>} */
const tabOperationLocks = new Set()
// Tabs currently in a detach/re-attach cycle after navigation.
/** @type {Set<number>} */
const reattachPending = new Set()
// Reconnect state for exponential backoff.
let reconnectAttempt = 0
let reconnectTimer = null
function nowStack() {
try {
return new Error().stack || ''
@@ -55,6 +67,63 @@ function setBadge(tabId, kind) {
void chrome.action.setBadgeTextColor({ tabId, color: '#FFFFFF' }).catch(() => {})
}
// Persist attached tab state to survive MV3 service worker restarts.
async function persistState() {
try {
const tabEntries = []
for (const [tabId, tab] of tabs.entries()) {
if (tab.state === 'connected' && tab.sessionId && tab.targetId) {
tabEntries.push({ tabId, sessionId: tab.sessionId, targetId: tab.targetId, attachOrder: tab.attachOrder })
}
}
await chrome.storage.session.set({
persistedTabs: tabEntries,
nextSession,
})
} catch {
// chrome.storage.session may not be available in all contexts.
}
}
// Rehydrate tab state on service worker startup. Fast path — just restores
// maps and badges. Relay reconnect happens separately in background.
async function rehydrateState() {
try {
const stored = await chrome.storage.session.get(['persistedTabs', 'nextSession'])
if (stored.nextSession) {
nextSession = Math.max(nextSession, stored.nextSession)
}
const entries = stored.persistedTabs || []
// Phase 1: optimistically restore state and badges.
for (const entry of entries) {
tabs.set(entry.tabId, {
state: 'connected',
sessionId: entry.sessionId,
targetId: entry.targetId,
attachOrder: entry.attachOrder,
})
tabBySession.set(entry.sessionId, entry.tabId)
setBadge(entry.tabId, 'on')
}
// Phase 2: validate asynchronously, remove dead tabs.
for (const entry of entries) {
try {
await chrome.tabs.get(entry.tabId)
await chrome.debugger.sendCommand({ tabId: entry.tabId }, 'Runtime.evaluate', {
expression: '1',
returnByValue: true,
})
} catch {
tabs.delete(entry.tabId)
tabBySession.delete(entry.sessionId)
setBadge(entry.tabId, 'off')
}
}
} catch {
// Ignore rehydration errors.
}
}
async function ensureRelayConnection() {
if (relayWs && relayWs.readyState === WebSocket.OPEN) return
if (relayConnectPromise) return await relayConnectPromise
@@ -63,9 +132,7 @@ async function ensureRelayConnection() {
const port = await getRelayPort()
const gatewayToken = await getGatewayToken()
const httpBase = `http://127.0.0.1:${port}`
const wsUrl = gatewayToken
? `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(gatewayToken)}`
: `ws://127.0.0.1:${port}/extension`
const wsUrl = await buildRelayWsUrl(port, gatewayToken)
// Fast preflight: is the relay server up?
try {
@@ -74,12 +141,6 @@ async function ensureRelayConnection() {
throw new Error(`Relay server not reachable at ${httpBase} (${String(err)})`)
}
if (!gatewayToken) {
throw new Error(
'Missing gatewayToken in extension settings (chrome.storage.local.gatewayToken)',
)
}
const ws = new WebSocket(wsUrl)
relayWs = ws
@@ -99,42 +160,144 @@ async function ensureRelayConnection() {
}
})
ws.onmessage = (event) => void onRelayMessage(String(event.data || ''))
ws.onclose = () => onRelayClosed('closed')
ws.onerror = () => onRelayClosed('error')
if (!debuggerListenersInstalled) {
debuggerListenersInstalled = true
chrome.debugger.onEvent.addListener(onDebuggerEvent)
chrome.debugger.onDetach.addListener(onDebuggerDetach)
// Bind permanent handlers. Guard against stale socket: if this WS was
// replaced before its close fires, the handler is a no-op.
ws.onmessage = (event) => {
if (ws !== relayWs) return
void whenReady(() => onRelayMessage(String(event.data || '')))
}
ws.onclose = () => {
if (ws !== relayWs) return
onRelayClosed('closed')
}
ws.onerror = () => {
if (ws !== relayWs) return
onRelayClosed('error')
}
})()
try {
await relayConnectPromise
reconnectAttempt = 0
} finally {
relayConnectPromise = null
}
}
// Relay closed — update badges, reject pending requests, auto-reconnect.
// Debugger sessions are kept alive so they survive transient WS drops.
function onRelayClosed(reason) {
relayWs = null
for (const [id, p] of pending.entries()) {
pending.delete(id)
p.reject(new Error(`Relay disconnected (${reason})`))
}
for (const tabId of tabs.keys()) {
void chrome.debugger.detach({ tabId }).catch(() => {})
setBadge(tabId, 'connecting')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: disconnected (click to re-attach)',
})
reattachPending.clear()
for (const [tabId, tab] of tabs.entries()) {
if (tab.state === 'connected') {
setBadge(tabId, 'connecting')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: relay reconnecting…',
})
}
}
tabs.clear()
tabBySession.clear()
childSessionToTab.clear()
scheduleReconnect()
}
function scheduleReconnect() {
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
const delay = reconnectDelayMs(reconnectAttempt)
reconnectAttempt++
console.log(`Scheduling reconnect attempt ${reconnectAttempt} in ${Math.round(delay)}ms`)
reconnectTimer = setTimeout(async () => {
reconnectTimer = null
try {
await ensureRelayConnection()
reconnectAttempt = 0
console.log('Reconnected successfully')
await reannounceAttachedTabs()
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.warn(`Reconnect attempt ${reconnectAttempt} failed: ${message}`)
if (!isRetryableReconnectError(err)) {
return
}
scheduleReconnect()
}
}, delay)
}
function cancelReconnect() {
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
reconnectAttempt = 0
}
// Re-announce all attached tabs to the relay after reconnect.
async function reannounceAttachedTabs() {
for (const [tabId, tab] of tabs.entries()) {
if (tab.state !== 'connected' || !tab.sessionId || !tab.targetId) continue
// Verify debugger is still attached.
try {
await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
expression: '1',
returnByValue: true,
})
} catch {
tabs.delete(tabId)
if (tab.sessionId) tabBySession.delete(tab.sessionId)
setBadge(tabId, 'off')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay (click to attach/detach)',
})
continue
}
// Send fresh attach event to relay.
try {
const info = /** @type {any} */ (
await chrome.debugger.sendCommand({ tabId }, 'Target.getTargetInfo')
)
const targetInfo = info?.targetInfo
sendToRelay({
method: 'forwardCDPEvent',
params: {
method: 'Target.attachedToTarget',
params: {
sessionId: tab.sessionId,
targetInfo: { ...targetInfo, attached: true },
waitingForDebugger: false,
},
},
})
setBadge(tabId, 'on')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: attached (click to detach)',
})
} catch {
setBadge(tabId, 'on')
}
}
await persistState()
}
function sendToRelay(payload) {
@@ -159,10 +322,18 @@ async function maybeOpenHelpOnce() {
function requestFromRelay(command) {
const id = command.id
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject })
const timer = setTimeout(() => {
pending.delete(id)
reject(new Error('Relay request timeout (30s)'))
}, 30000)
pending.set(id, {
resolve: (v) => { clearTimeout(timer); resolve(v) },
reject: (e) => { clearTimeout(timer); reject(e) },
})
try {
sendToRelay(command)
} catch (err) {
clearTimeout(timer)
pending.delete(id)
reject(err instanceof Error ? err : new Error(String(err)))
}
@@ -233,8 +404,9 @@ async function attachTab(tabId, opts = {}) {
throw new Error('Target.getTargetInfo returned no targetId')
}
const sessionId = `cb-tab-${nextSession++}`
const attachOrder = nextSession
const sid = nextSession++
const sessionId = `cb-tab-${sid}`
const attachOrder = sid
tabs.set(tabId, { state: 'connected', sessionId, targetId, attachOrder })
tabBySession.set(sessionId, tabId)
@@ -258,11 +430,33 @@ async function attachTab(tabId, opts = {}) {
}
setBadge(tabId, 'on')
await persistState()
return { sessionId, targetId }
}
async function detachTab(tabId, reason) {
const tab = tabs.get(tabId)
// Send detach events for child sessions first.
for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
if (parentTabId === tabId) {
try {
sendToRelay({
method: 'forwardCDPEvent',
params: {
method: 'Target.detachedFromTarget',
params: { sessionId: childSessionId, reason: 'parent_detached' },
},
})
} catch {
// Relay may be down.
}
childSessionToTab.delete(childSessionId)
}
}
// Send detach event for main session.
if (tab?.sessionId && tab?.targetId) {
try {
sendToRelay({
@@ -273,21 +467,17 @@ async function detachTab(tabId, reason) {
},
})
} catch {
// ignore
// Relay may be down.
}
}
if (tab?.sessionId) tabBySession.delete(tab.sessionId)
tabs.delete(tabId)
for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
if (parentTabId === tabId) childSessionToTab.delete(childSessionId)
}
try {
await chrome.debugger.detach({ tabId })
} catch {
// ignore
// May already be detached.
}
setBadge(tabId, 'off')
@@ -295,6 +485,8 @@ async function detachTab(tabId, reason) {
tabId,
title: 'OpenClaw Browser Relay (click to attach/detach)',
})
await persistState()
}
async function connectOrToggleForActiveTab() {
@@ -302,33 +494,53 @@ async function connectOrToggleForActiveTab() {
const tabId = active?.id
if (!tabId) return
const existing = tabs.get(tabId)
if (existing?.state === 'connected') {
await detachTab(tabId, 'toggle')
return
}
tabs.set(tabId, { state: 'connecting' })
setBadge(tabId, 'connecting')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: connecting to local relay…',
})
// Prevent concurrent operations on the same tab.
if (tabOperationLocks.has(tabId)) return
tabOperationLocks.add(tabId)
try {
await ensureRelayConnection()
await attachTab(tabId)
} catch (err) {
tabs.delete(tabId)
setBadge(tabId, 'error')
if (reattachPending.has(tabId)) {
reattachPending.delete(tabId)
setBadge(tabId, 'off')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay (click to attach/detach)',
})
return
}
const existing = tabs.get(tabId)
if (existing?.state === 'connected') {
await detachTab(tabId, 'toggle')
return
}
// User is manually connecting — cancel any pending reconnect.
cancelReconnect()
tabs.set(tabId, { state: 'connecting' })
setBadge(tabId, 'connecting')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: relay not running (open options for setup)',
title: 'OpenClaw Browser Relay: connecting to local relay…',
})
void maybeOpenHelpOnce()
// Extra breadcrumbs in chrome://extensions service worker logs.
const message = err instanceof Error ? err.message : String(err)
console.warn('attach failed', message, nowStack())
try {
await ensureRelayConnection()
await attachTab(tabId)
} catch (err) {
tabs.delete(tabId)
setBadge(tabId, 'error')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: relay not running (open options for setup)',
})
void maybeOpenHelpOnce()
const message = err instanceof Error ? err.message : String(err)
console.warn('attach failed', message, nowStack())
}
} finally {
tabOperationLocks.delete(tabId)
}
}
@@ -337,14 +549,12 @@ async function handleForwardCdpCommand(msg) {
const params = msg?.params?.params || undefined
const sessionId = typeof msg?.params?.sessionId === 'string' ? msg.params.sessionId : undefined
// Map command to tab
const bySession = sessionId ? getTabBySessionId(sessionId) : null
const targetId = typeof params?.targetId === 'string' ? params.targetId : undefined
const tabId =
bySession?.tabId ||
(targetId ? getTabByTargetId(targetId) : null) ||
(() => {
// No sessionId: pick the first connected tab (stable-ish).
for (const [id, tab] of tabs.entries()) {
if (tab.state === 'connected') return id
}
@@ -434,20 +644,256 @@ function onDebuggerEvent(source, method, params) {
},
})
} catch {
// ignore
// Relay may be down.
}
}
function onDebuggerDetach(source, reason) {
async function onDebuggerDetach(source, reason) {
const tabId = source.tabId
if (!tabId) return
if (!tabs.has(tabId)) return
void detachTab(tabId, reason)
// User explicitly cancelled or DevTools replaced the connection — respect their intent
if (reason === 'canceled_by_user' || reason === 'replaced_with_devtools') {
void detachTab(tabId, reason)
return
}
// Check if tab still exists — distinguishes navigation from tab close
let tabInfo
try {
tabInfo = await chrome.tabs.get(tabId)
} catch {
// Tab is gone (closed) — normal cleanup
void detachTab(tabId, reason)
return
}
if (tabInfo.url?.startsWith('chrome://') || tabInfo.url?.startsWith('chrome-extension://')) {
void detachTab(tabId, reason)
return
}
if (reattachPending.has(tabId)) return
const oldTab = tabs.get(tabId)
const oldSessionId = oldTab?.sessionId
const oldTargetId = oldTab?.targetId
if (oldSessionId) tabBySession.delete(oldSessionId)
tabs.delete(tabId)
for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
if (parentTabId === tabId) childSessionToTab.delete(childSessionId)
}
if (oldSessionId && oldTargetId) {
try {
sendToRelay({
method: 'forwardCDPEvent',
params: {
method: 'Target.detachedFromTarget',
params: { sessionId: oldSessionId, targetId: oldTargetId, reason: 'navigation-reattach' },
},
})
} catch {
// Relay may be down.
}
}
reattachPending.add(tabId)
setBadge(tabId, 'connecting')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: re-attaching after navigation…',
})
const delays = [300, 700, 1500]
for (let attempt = 0; attempt < delays.length; attempt++) {
await new Promise((r) => setTimeout(r, delays[attempt]))
if (!reattachPending.has(tabId)) return
try {
await chrome.tabs.get(tabId)
} catch {
reattachPending.delete(tabId)
setBadge(tabId, 'off')
return
}
if (!relayWs || relayWs.readyState !== WebSocket.OPEN) {
reattachPending.delete(tabId)
setBadge(tabId, 'error')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: relay disconnected during re-attach',
})
return
}
try {
await attachTab(tabId)
reattachPending.delete(tabId)
return
} catch {
// continue retries
}
}
reattachPending.delete(tabId)
setBadge(tabId, 'off')
void chrome.action.setTitle({
tabId,
title: 'OpenClaw Browser Relay: re-attach failed (click to retry)',
})
}
chrome.action.onClicked.addListener(() => void connectOrToggleForActiveTab())
// Tab lifecycle listeners — clean up stale entries.
chrome.tabs.onRemoved.addListener((tabId) => void whenReady(() => {
reattachPending.delete(tabId)
if (!tabs.has(tabId)) return
const tab = tabs.get(tabId)
if (tab?.sessionId) tabBySession.delete(tab.sessionId)
tabs.delete(tabId)
for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
if (parentTabId === tabId) childSessionToTab.delete(childSessionId)
}
if (tab?.sessionId && tab?.targetId) {
try {
sendToRelay({
method: 'forwardCDPEvent',
params: {
method: 'Target.detachedFromTarget',
params: { sessionId: tab.sessionId, targetId: tab.targetId, reason: 'tab_closed' },
},
})
} catch {
// Relay may be down.
}
}
void persistState()
}))
chrome.tabs.onReplaced.addListener((addedTabId, removedTabId) => void whenReady(() => {
const tab = tabs.get(removedTabId)
if (!tab) return
tabs.delete(removedTabId)
tabs.set(addedTabId, tab)
if (tab.sessionId) {
tabBySession.set(tab.sessionId, addedTabId)
}
for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
if (parentTabId === removedTabId) {
childSessionToTab.set(childSessionId, addedTabId)
}
}
setBadge(addedTabId, 'on')
void persistState()
}))
// Register debugger listeners at module scope so detach/event handling works
// even when the relay WebSocket is down.
chrome.debugger.onEvent.addListener((...args) => void whenReady(() => onDebuggerEvent(...args)))
chrome.debugger.onDetach.addListener((...args) => void whenReady(() => onDebuggerDetach(...args)))
chrome.action.onClicked.addListener(() => void whenReady(() => connectOrToggleForActiveTab()))
// Refresh badge after navigation completes — service worker may have restarted
// during navigation, losing ephemeral badge state.
chrome.webNavigation.onCompleted.addListener(({ tabId, frameId }) => void whenReady(() => {
if (frameId !== 0) return
const tab = tabs.get(tabId)
if (tab?.state === 'connected') {
setBadge(tabId, relayWs && relayWs.readyState === WebSocket.OPEN ? 'on' : 'connecting')
}
}))
// Refresh badge when user switches to an attached tab.
chrome.tabs.onActivated.addListener(({ tabId }) => void whenReady(() => {
const tab = tabs.get(tabId)
if (tab?.state === 'connected') {
setBadge(tabId, relayWs && relayWs.readyState === WebSocket.OPEN ? 'on' : 'connecting')
}
}))
chrome.runtime.onInstalled.addListener(() => {
// Useful: first-time instructions.
void chrome.runtime.openOptionsPage()
})
// MV3 keepalive via chrome.alarms — more reliable than setInterval across
// service worker restarts. Checks relay health and refreshes badges.
chrome.alarms.create('relay-keepalive', { periodInMinutes: 0.5 })
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== 'relay-keepalive') return
await initPromise
if (tabs.size === 0) return
// Refresh badges (ephemeral in MV3).
for (const [tabId, tab] of tabs.entries()) {
if (tab.state === 'connected') {
setBadge(tabId, relayWs && relayWs.readyState === WebSocket.OPEN ? 'on' : 'connecting')
}
}
// If relay is down and no reconnect is in progress, trigger one.
if (!relayWs || relayWs.readyState !== WebSocket.OPEN) {
if (!relayConnectPromise && !reconnectTimer) {
console.log('Keepalive: WebSocket unhealthy, triggering reconnect')
await ensureRelayConnection().catch(() => {
// ensureRelayConnection may throw without triggering onRelayClosed
// (e.g. preflight fetch fails before WS is created), so ensure
// reconnect is always scheduled on failure.
if (!reconnectTimer) {
scheduleReconnect()
}
})
}
}
})
// Rehydrate state on service worker startup. Split: rehydration is the gate
// (fast), relay reconnect runs in background (slow, non-blocking).
const initPromise = rehydrateState()
initPromise.then(() => {
if (tabs.size > 0) {
ensureRelayConnection().then(() => {
reconnectAttempt = 0
return reannounceAttachedTabs()
}).catch(() => {
scheduleReconnect()
})
}
})
// Shared gate: all state-dependent handlers await this before accessing maps.
async function whenReady(fn) {
await initPromise
return fn()
}
// Relay check handler for the options page. The service worker has
// host_permissions and bypasses CORS preflight, so the options page
// delegates token-validation requests here.
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type !== 'relayCheck') return false
const { url, token } = msg
const headers = token ? { 'x-openclaw-relay-token': token } : {}
fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(2000) })
.then(async (res) => {
const contentType = String(res.headers.get('content-type') || '')
let json = null
if (contentType.includes('application/json')) {
try {
json = await res.json()
} catch {
json = null
}
}
sendResponse({ status: res.status, ok: res.ok, contentType, json })
})
.catch((err) => sendResponse({ status: 0, ok: false, error: String(err) }))
return true
})
+1 -1
View File
@@ -9,7 +9,7 @@
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"permissions": ["debugger", "tabs", "activeTab", "storage"],
"permissions": ["debugger", "tabs", "activeTab", "storage", "alarms", "webNavigation"],
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
"background": { "service_worker": "background.js", "type": "module" },
"action": {
@@ -0,0 +1,57 @@
const PORT_GUIDANCE = 'Use gateway port + 3 (for gateway 18789, relay is 18792).'
function hasCdpVersionShape(data) {
return !!data && typeof data === 'object' && 'Browser' in data && 'Protocol-Version' in data
}
export function classifyRelayCheckResponse(res, port) {
if (!res) {
return { action: 'throw', error: 'No response from service worker' }
}
if (res.status === 401) {
return { action: 'status', kind: 'error', message: 'Gateway token rejected. Check token and save again.' }
}
if (res.error) {
return { action: 'throw', error: res.error }
}
if (!res.ok) {
return { action: 'throw', error: `HTTP ${res.status}` }
}
const contentType = String(res.contentType || '')
if (!contentType.includes('application/json')) {
return {
action: 'status',
kind: 'error',
message: `Wrong port: this is likely the gateway, not the relay. ${PORT_GUIDANCE}`,
}
}
if (!hasCdpVersionShape(res.json)) {
return {
action: 'status',
kind: 'error',
message: `Wrong port: expected relay /json/version response. ${PORT_GUIDANCE}`,
}
}
return { action: 'status', kind: 'ok', message: `Relay reachable and authenticated at http://127.0.0.1:${port}/` }
}
export function classifyRelayCheckException(err, port) {
const message = String(err || '').toLowerCase()
if (message.includes('json') || message.includes('syntax')) {
return {
kind: 'error',
message: `Wrong port: this is not a relay endpoint. ${PORT_GUIDANCE}`,
}
}
return {
kind: 'error',
message: `Relay not reachable/authenticated at http://127.0.0.1:${port}/. Start OpenClaw browser relay and verify token.`,
}
}
+16 -25
View File
@@ -1,3 +1,6 @@
import { deriveRelayToken } from './background-utils.js'
import { classifyRelayCheckException, classifyRelayCheckResponse } from './options-validation.js'
const DEFAULT_PORT = 18792
function clampPort(value) {
@@ -13,12 +16,6 @@ function updateRelayUrl(port) {
el.textContent = `http://127.0.0.1:${port}/`
}
function relayHeaders(token) {
const t = String(token || '').trim()
if (!t) return {}
return { 'x-openclaw-relay-token': t }
}
function setStatus(kind, message) {
const status = document.getElementById('status')
if (!status) return
@@ -33,27 +30,21 @@ async function checkRelayReachable(port, token) {
setStatus('error', 'Gateway token required. Save your gateway token to connect.')
return
}
const ctrl = new AbortController()
const t = setTimeout(() => ctrl.abort(), 1200)
try {
const res = await fetch(url, {
method: 'GET',
headers: relayHeaders(trimmedToken),
signal: ctrl.signal,
const relayToken = await deriveRelayToken(trimmedToken, port)
// Delegate the fetch to the background service worker to bypass
// CORS preflight on the custom x-openclaw-relay-token header.
const res = await chrome.runtime.sendMessage({
type: 'relayCheck',
url,
token: relayToken,
})
if (res.status === 401) {
setStatus('error', 'Gateway token rejected. Check token and save again.')
return
}
if (!res.ok) throw new Error(`HTTP ${res.status}`)
setStatus('ok', `Relay reachable and authenticated at http://127.0.0.1:${port}/`)
} catch {
setStatus(
'error',
`Relay not reachable/authenticated at http://127.0.0.1:${port}/. Start OpenClaw browser relay and verify token.`,
)
} finally {
clearTimeout(t)
const result = classifyRelayCheckResponse(res, port)
if (result.action === 'throw') throw new Error(result.error)
setStatus(result.kind, result.message)
} catch (err) {
const result = classifyRelayCheckException(err, port)
setStatus(result.kind, result.message)
}
}
+3
View File
@@ -82,6 +82,9 @@ fi
mkdir -p "$OPENCLAW_CONFIG_DIR"
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
# Seed device-identity parent eagerly for Docker Desktop/Windows bind mounts
# that reject creating new subdirectories from inside the container.
mkdir -p "$OPENCLAW_CONFIG_DIR/identity"
export OPENCLAW_CONFIG_DIR
export OPENCLAW_WORKSPACE_DIR
+92 -1
View File
@@ -349,7 +349,8 @@ Notes:
## Storage & history
- Job store: `~/.openclaw/cron/jobs.json` (Gateway-managed JSON).
- Run history: `~/.openclaw/cron/runs/<jobId>.jsonl` (JSONL, auto-pruned).
- Run history: `~/.openclaw/cron/runs/<jobId>.jsonl` (JSONL, auto-pruned by size and line count).
- Isolated cron run sessions in `sessions.json` are pruned by `cron.sessionRetention` (default `24h`; set `false` to disable).
- Override store path: `cron.store` in config.
## Configuration
@@ -362,10 +363,21 @@ Notes:
maxConcurrentRuns: 1, // default 1
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
webhookToken: "replace-with-dedicated-webhook-token", // optional bearer token for webhook mode
sessionRetention: "24h", // duration string or false
runLog: {
maxBytes: "2mb", // default 2_000_000 bytes
keepLines: 2000, // default 2000
},
},
}
```
Run-log pruning behavior:
- `cron.runLog.maxBytes`: max run-log file size before pruning.
- `cron.runLog.keepLines`: when pruning, keep only the newest N lines.
- Both apply to `cron/runs/<jobId>.jsonl` files.
Webhook behavior:
- Preferred: set `delivery.mode: "webhook"` with `delivery.to: "https://..."` per job.
@@ -380,6 +392,85 @@ Disable cron entirely:
- `cron.enabled: false` (config)
- `OPENCLAW_SKIP_CRON=1` (env)
## Maintenance
Cron has two built-in maintenance paths: isolated run-session retention and run-log pruning.
### Defaults
- `cron.sessionRetention`: `24h` (set `false` to disable run-session pruning)
- `cron.runLog.maxBytes`: `2_000_000` bytes
- `cron.runLog.keepLines`: `2000`
### How it works
- Isolated runs create session entries (`...:cron:<jobId>:run:<uuid>`) and transcript files.
- The reaper removes expired run-session entries older than `cron.sessionRetention`.
- For removed run sessions no longer referenced by the session store, OpenClaw archives transcript files and purges old deleted archives on the same retention window.
- After each run append, `cron/runs/<jobId>.jsonl` is size-checked:
- if file size exceeds `runLog.maxBytes`, it is trimmed to the newest `runLog.keepLines` lines.
### Performance caveat for high volume schedulers
High-frequency cron setups can generate large run-session and run-log footprints. Maintenance is built in, but loose limits can still create avoidable IO and cleanup work.
What to watch:
- long `cron.sessionRetention` windows with many isolated runs
- high `cron.runLog.keepLines` combined with large `runLog.maxBytes`
- many noisy recurring jobs writing to the same `cron/runs/<jobId>.jsonl`
What to do:
- keep `cron.sessionRetention` as short as your debugging/audit needs allow
- keep run logs bounded with moderate `runLog.maxBytes` and `runLog.keepLines`
- move noisy background jobs to isolated mode with delivery rules that avoid unnecessary chatter
- review growth periodically with `openclaw cron runs` and adjust retention before logs become large
### Customize examples
Keep run sessions for a week and allow bigger run logs:
```json5
{
cron: {
sessionRetention: "7d",
runLog: {
maxBytes: "10mb",
keepLines: 5000,
},
},
}
```
Disable isolated run-session pruning but keep run-log pruning:
```json5
{
cron: {
sessionRetention: false,
runLog: {
maxBytes: "5mb",
keepLines: 3000,
},
},
}
```
Tune for high-volume cron usage (example):
```json5
{
cron: {
sessionRetention: "12h",
runLog: {
maxBytes: "3mb",
keepLines: 1500,
},
},
}
```
## CLI quickstart
One-shot reminder (UTC ISO, auto-delete after success):
+17 -15
View File
@@ -182,9 +182,7 @@ The `metadata.openclaw` object supports:
The `handler.ts` file exports a `HookHandler` function:
```typescript
import type { HookHandler } from "../../src/hooks/hooks.js";
const myHandler: HookHandler = async (event) => {
const myHandler = async (event) => {
// Only trigger on 'new' command
if (event.type !== "command" || event.action !== "new") {
return;
@@ -305,13 +303,15 @@ Message events include rich context about the message:
#### Example: Message Logger Hook
```typescript
import type { HookHandler } from "../../src/hooks/hooks.js";
import { isMessageReceivedEvent, isMessageSentEvent } from "../../src/hooks/internal-hooks.js";
const isMessageReceivedEvent = (event: { type: string; action: string }) =>
event.type === "message" && event.action === "received";
const isMessageSentEvent = (event: { type: string; action: string }) =>
event.type === "message" && event.action === "sent";
const handler: HookHandler = async (event) => {
if (isMessageReceivedEvent(event)) {
const handler = async (event) => {
if (isMessageReceivedEvent(event as { type: string; action: string })) {
console.log(`[message-logger] Received from ${event.context.from}: ${event.context.content}`);
} else if (isMessageSentEvent(event)) {
} else if (isMessageSentEvent(event as { type: string; action: string })) {
console.log(`[message-logger] Sent to ${event.context.to}: ${event.context.content}`);
}
};
@@ -364,9 +364,7 @@ This hook does something useful when you issue `/new`.
### 4. Create handler.ts
```typescript
import type { HookHandler } from "../../src/hooks/hooks.js";
const handler: HookHandler = async (event) => {
const handler = async (event) => {
if (event.type !== "command" || event.action !== "new") {
return;
}
@@ -793,13 +791,17 @@ Test your handlers in isolation:
```typescript
import { test } from "vitest";
import { createHookEvent } from "./src/hooks/hooks.js";
import myHandler from "./hooks/my-hook/handler.js";
test("my handler works", async () => {
const event = createHookEvent("command", "new", "test-session", {
foo: "bar",
});
const event = {
type: "command",
action: "new",
sessionKey: "test-session",
timestamp: new Date(),
messages: [],
context: { foo: "bar" },
};
await myHandler(event);
+54 -7
View File
@@ -397,7 +397,9 @@ Example:
`allowlist` behavior:
- guild must match `channels.discord.guilds` (`id` preferred, slug accepted)
- optional sender allowlists: `users` (IDs or names) and `roles` (role IDs only); if either is configured, senders are allowed when they match `users` OR `roles`
- optional sender allowlists: `users` (stable IDs recommended) and `roles` (role IDs only); if either is configured, senders are allowed when they match `users` OR `roles`
- direct name/tag matching is disabled by default; enable `channels.discord.dangerouslyAllowNameMatching: true` only as break-glass compatibility mode
- names/tags are supported for `users`, but IDs are safer; `openclaw security audit` warns when name/tag entries are used
- if a guild has `channels` configured, non-listed channels are denied
- if a guild has no `channels` block, all channels in that allowlisted guild are allowed
@@ -424,7 +426,7 @@ Example:
}
```
If you only set `DISCORD_BOT_TOKEN` and do not create a `channels.discord` block, runtime fallback is `groupPolicy="open"` (with a warning in logs).
If you only set `DISCORD_BOT_TOKEN` and do not create a `channels.discord` block, runtime fallback is `groupPolicy="allowlist"` (with a warning in logs), even if `channels.defaults.groupPolicy` is `open`.
</Tab>
@@ -562,7 +564,9 @@ Default slash command settings:
<Accordion title="Live stream preview">
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives.
- `channels.discord.streamMode` controls preview streaming (`off` | `partial` | `block`, default: `off`).
- `channels.discord.streaming` controls preview streaming (`off` | `partial` | `block` | `progress`, default: `off`).
- `progress` is accepted for cross-channel consistency and maps to `partial` on Discord.
- `channels.discord.streamMode` is a legacy alias and is auto-migrated.
- `partial` edits a single preview message as tokens arrive.
- `block` emits draft-sized chunks (use `draftChunk` to tune size and breakpoints).
@@ -572,7 +576,7 @@ Default slash command settings:
{
channels: {
discord: {
streamMode: "partial",
streaming: "partial",
},
},
}
@@ -584,7 +588,7 @@ Default slash command settings:
{
channels: {
discord: {
streamMode: "block",
streaming: "block",
draftChunk: {
minChars: 200,
maxChars: 800,
@@ -624,6 +628,49 @@ Default slash command settings:
</Accordion>
<Accordion title="Thread-bound sessions for subagents">
Discord can bind a thread to a session target so follow-up messages in that thread keep routing to the same session (including subagent sessions).
Commands:
- `/focus <target>` bind current/new thread to a subagent/session target
- `/unfocus` remove current thread binding
- `/agents` show active runs and binding state
- `/session ttl <duration|off>` inspect/update auto-unfocus TTL for focused bindings
Config:
```json5
{
session: {
threadBindings: {
enabled: true,
ttlHours: 24,
},
},
channels: {
discord: {
threadBindings: {
enabled: true,
ttlHours: 24,
spawnSubagentSessions: false, // opt-in
},
},
},
}
```
Notes:
- `session.threadBindings.*` sets global defaults.
- `channels.discord.threadBindings.*` overrides Discord behavior.
- `spawnSubagentSessions` must be true to auto-create/bind threads for `sessions_spawn({ thread: true })`.
- If thread bindings are disabled for an account, `/focus` and related thread binding operations are unavailable.
See [Sub-agents](/tools/subagents) and [Configuration Reference](/gateway/configuration-reference).
</Accordion>
<Accordion title="Reaction notifications">
Per-guild reaction notification mode:
@@ -722,7 +769,7 @@ Default slash command settings:
Notes:
- allowlists can use `pk:<memberId>`
- member display names are matched by name/slug
- member display names are matched by name/slug only when `channels.discord.dangerouslyAllowNameMatching: true`
- lookups use original message ID and are time-window constrained
- if lookup fails, proxied messages are treated as bot messages and dropped unless `allowBots=true`
@@ -976,7 +1023,7 @@ High-signal Discord fields:
- command: `commands.native`, `commands.useAccessGroups`, `configWrites`, `slashCommand.*`
- reply/history: `replyToMode`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
- delivery: `textChunkLimit`, `chunkMode`, `maxLinesPerMessage`
- streaming: `streamMode`, `draftChunk`, `blockStreaming`, `blockStreamingCoalesce`
- streaming: `streaming` (legacy alias: `streamMode`), `draftChunk`, `blockStreaming`, `blockStreamingCoalesce`
- media/retry: `mediaMaxMb`, `retry`
- actions: `actions.*`
- presence: `activity`, `status`, `activityType`, `activityUrl`
+4 -2
View File
@@ -153,7 +153,8 @@ Configure your tunnel's ingress rules to only route the webhook path:
Use these identifiers for delivery and allowlists:
- Direct messages: `users/<userId>` (recommended) or raw email `name@example.com` (mutable principal).
- Direct messages: `users/<userId>` (recommended).
- Raw email `name@example.com` is mutable and only used for direct allowlist matching when `channels.googlechat.dangerouslyAllowNameMatching: true`.
- Deprecated: `users/<email>` is treated as a user id, not an email allowlist.
- Spaces: `spaces/<spaceId>`.
@@ -171,7 +172,7 @@ Use these identifiers for delivery and allowlists:
botUser: "users/1234567890", // optional; helps mention detection
dm: {
policy: "pairing",
allowFrom: ["users/1234567890", "name@example.com"],
allowFrom: ["users/1234567890"],
},
groupPolicy: "allowlist",
groups: {
@@ -194,6 +195,7 @@ Notes:
- Service account credentials can also be passed inline with `serviceAccount` (JSON string).
- Default webhook path is `/googlechat` if `webhookPath` isnt set.
- `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode).
- Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled.
- `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth).
- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
+1 -1
View File
@@ -21,7 +21,7 @@ title: grammY
- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls).
- **Sessions:** direct chats collapse into the agent main session (`agent:<agentId>:<mainKey>`); groups use `agent:<agentId>:telegram:group:<chatId>`; replies route back to the same channel.
- **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`, `channels.telegram.webhookHost`.
- **Live stream preview:** optional `channels.telegram.streaming` sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming.
- **Live stream preview:** `channels.telegram.streaming` (`off | partial | block | progress`) sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming.
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
Open questions
+6 -2
View File
@@ -190,6 +190,7 @@ Notes:
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`.
Quick mental model (evaluation order for group messages):
@@ -253,7 +254,10 @@ Notes:
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
- `tools`: allow/deny tools for the whole group.
- `toolsBySender`: per-sender overrides within the group (keys are sender IDs/usernames/emails/phone numbers depending on the channel). Use `"*"` as a wildcard.
- `toolsBySender`: per-sender overrides within the group.
Use explicit key prefixes:
`id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard.
Legacy unprefixed keys are still accepted and matched as `id:` only.
Resolution order (most specific wins):
@@ -273,7 +277,7 @@ Example (Telegram):
"-1001234567890": {
tools: { deny: ["exec", "read", "write"] },
toolsBySender: {
"123456789": { alsoAllow: ["exec"] },
"id:123456789": { alsoAllow: ["exec"] },
},
},
},
+1
View File
@@ -158,6 +158,7 @@ imsg send <handle> "test"
Group sender allowlist: `channels.imessage.groupAllowFrom`.
Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks fall back to `allowFrom` when available.
Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
Mention gating for groups:
+1
View File
@@ -25,6 +25,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
- [Synology Chat](/channels/synology-chat) — Synology NAS Chat via outgoing+incoming webhooks (plugin, installed separately).
- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
+10 -3
View File
@@ -1,6 +1,10 @@
---
title: IRC
description: Connect OpenClaw to IRC channels and direct messages.
summary: "IRC plugin setup, access controls, and troubleshooting"
read_when:
- You want to connect OpenClaw to IRC channels or DMs
- You are configuring IRC allowlists, group policy, or mention gating
---
Use IRC when you want OpenClaw in classic channels (`#room`) and direct messages.
@@ -53,7 +57,8 @@ Config keys:
- Per-channel controls (channel + sender + mention rules): `channels.irc.groups["#channel"]`
- `channels.irc.groupPolicy="open"` allows unconfigured channels (**still mention-gated by default**)
Allowlist entries can use nick or `nick!user@host` forms.
Allowlist entries should use stable sender identities (`nick!user@host`).
Bare nick matching is mutable and only enabled when `channels.irc.dangerouslyAllowNameMatching: true`.
### Common gotcha: `allowFrom` is for DMs, not channels
@@ -159,7 +164,7 @@ Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your
"*": {
deny: ["group:runtime", "group:fs", "gateway", "nodes", "cron", "browser"],
},
eigen: {
"id:eigen": {
deny: ["gateway", "nodes", "cron"],
},
},
@@ -172,7 +177,9 @@ Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your
Notes:
- `toolsBySender` keys can be a nick (e.g. `"eigen"`) or a full hostmask (`"eigen!~eigen@174.127.248.171"`) for stronger identity matching.
- `toolsBySender` keys should use `id:` for IRC sender identity values:
`id:eigen` or `id:eigen!~eigen@174.127.248.171` for stronger matching.
- Legacy unprefixed keys are still accepted and matched as `id:` only.
- The first matching sender policy wins; `"*"` is the wildcard fallback.
For more on group access vs mention-gating (and how they interact), see: [/channels/groups](/channels/groups).
+1
View File
@@ -118,6 +118,7 @@ Allowlists and policies:
- `channels.line.groupPolicy`: `allowlist | open | disabled`
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom`
- Runtime note: if `channels.line` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
LINE IDs are case-sensitive. Valid IDs look like:
+1
View File
@@ -195,6 +195,7 @@ Notes:
## Rooms (groups)
- Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset.
- Runtime note: if `channels.matrix` is completely missing, runtime falls back to `groupPolicy="allowlist"` for room checks (even if `channels.defaults.groupPolicy` is set).
- Allowlist rooms with `channels.matrix.groups` (room IDs or aliases; names are resolved to IDs when directory search finds a single exact match):
```json5
+3 -1
View File
@@ -101,8 +101,10 @@ Notes:
## Channels (groups)
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs recommended).
- `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`.
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
- Runtime note: if `channels.mattermost` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
## Targets for outbound delivery
+7 -2
View File
@@ -87,7 +87,9 @@ Disable with:
**DM access**
- Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
- `channels.msteams.allowFrom` accepts AAD object IDs, UPNs, or display names. The wizard resolves names to IDs via Microsoft Graph when credentials allow.
- `channels.msteams.allowFrom` should use stable AAD object IDs.
- UPNs/display names are mutable; direct matching is disabled by default and only enabled with `channels.msteams.dangerouslyAllowNameMatching: true`.
- The wizard can resolve names to IDs via Microsoft Graph when credentials allow.
**Group access**
@@ -454,7 +456,8 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
- `channels.msteams.webhook.port` (default `3978`)
- `channels.msteams.webhook.path` (default `/api/messages`)
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
- `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.
- `channels.msteams.allowFrom`: DM allowlist (AAD object IDs recommended). The wizard resolves names to IDs during setup when Graph access is available.
- `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching.
- `channels.msteams.textChunkLimit`: outbound text chunk size.
- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
@@ -469,6 +472,8 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
- `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.
- `channels.msteams.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
- `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
- `toolsBySender` keys should use explicit prefixes:
`id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only).
- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
## Routing & Sessions
+1
View File
@@ -195,6 +195,7 @@ Groups:
- `channels.signal.groupPolicy = open | allowlist | disabled`.
- `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
- Runtime note: if `channels.signal` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
## How it works (behavior)
+24 -5
View File
@@ -165,12 +165,13 @@ For actions/directory reads, user token can be preferred when configured. For wr
Channel allowlist lives under `channels.slack.channels`.
Runtime note: if `channels.slack` is completely missing (env-only setup) and `channels.defaults.groupPolicy` is unset, runtime falls back to `groupPolicy="open"` and logs a warning.
Runtime note: if `channels.slack` is completely missing (env-only setup), runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
Name/ID resolution:
- channel allowlist entries and DM allowlist entries are resolved at startup when token access allows
- unresolved entries are kept as configured
- inbound authorization matching is ID-first by default; direct username/slug matching requires `channels.slack.dangerouslyAllowNameMatching: true`
</Tab>
@@ -191,6 +192,8 @@ For actions/directory reads, user token can be preferred when configured. For wr
- `skills`
- `systemPrompt`
- `tools`, `toolsBySender`
- `toolsBySender` key format: `id:`, `e164:`, `username:`, `name:`, or `"*"` wildcard
(legacy unprefixed keys still map to `id:` only)
</Tab>
</Tabs>
@@ -241,7 +244,7 @@ Manual reply tags are supported:
- `[[reply_to_current]]`
- `[[reply_to:<id>]]`
Note: `replyToMode="off"` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored.
Note: `replyToMode="off"` disables **all** reply threading in Slack, including explicit `[[reply_to_*]]` tags. This differs from Telegram, where explicit tags are still honored in `"off"` mode. The difference reflects the platform threading models: Slack threads hide messages from the channel, while Telegram replies remain visible in the main chat flow.
## Media, chunking, and delivery
@@ -465,14 +468,29 @@ openclaw pairing list slack
OpenClaw supports Slack native text streaming via the Agents and AI Apps API.
By default, streaming is enabled. Disable it per account:
`channels.slack.streaming` controls live preview behavior:
- `off`: disable live preview streaming.
- `partial` (default): replace preview text with the latest partial output.
- `block`: append chunked preview updates.
- `progress`: show progress status text while generating, then send final text.
`channels.slack.nativeStreaming` controls Slack's native streaming API (`chat.startStream` / `chat.appendStream` / `chat.stopStream`) when `streaming` is `partial` (default: `true`).
Disable native Slack streaming (keep draft preview behavior):
```yaml
channels:
slack:
streaming: false
streaming: partial
nativeStreaming: false
```
Legacy keys:
- `channels.slack.streamMode` (`replace | status_final | append`) is auto-migrated to `channels.slack.streaming`.
- boolean `channels.slack.streaming` is auto-migrated to `channels.slack.nativeStreaming`.
### Requirements
1. Enable **Agents and AI Apps** in your Slack app settings.
@@ -496,9 +514,10 @@ Primary reference:
High-signal Slack fields:
- mode/auth: `mode`, `botToken`, `appToken`, `signingSecret`, `webhookPath`, `accounts.*`
- DM access: `dm.enabled`, `dmPolicy`, `allowFrom` (legacy: `dm.policy`, `dm.allowFrom`), `dm.groupEnabled`, `dm.groupChannels`
- compatibility toggle: `dangerouslyAllowNameMatching` (break-glass; keep off unless needed)
- channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention`
- threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `nativeStreaming`
- ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly`
## Related
+127
View File
@@ -0,0 +1,127 @@
---
summary: "Synology Chat webhook setup and OpenClaw config"
read_when:
- Setting up Synology Chat with OpenClaw
- Debugging Synology Chat webhook routing
title: "Synology Chat"
---
# Synology Chat (plugin)
Status: supported via plugin as a direct-message channel using Synology Chat webhooks.
The plugin accepts inbound messages from Synology Chat outgoing webhooks and sends replies
through a Synology Chat incoming webhook.
## Plugin required
Synology Chat is plugin-based and not part of the default core channel install.
Install from a local checkout:
```bash
openclaw plugins install ./extensions/synology-chat
```
Details: [Plugins](/tools/plugin)
## Quick setup
1. Install and enable the Synology Chat plugin.
2. In Synology Chat integrations:
- Create an incoming webhook and copy its URL.
- Create an outgoing webhook with your secret token.
3. Point the outgoing webhook URL to your OpenClaw gateway:
- `https://gateway-host/webhook/synology` by default.
- Or your custom `channels.synology-chat.webhookPath`.
4. Configure `channels.synology-chat` in OpenClaw.
5. Restart gateway and send a DM to the Synology Chat bot.
Minimal config:
```json5
{
channels: {
"synology-chat": {
enabled: true,
token: "synology-outgoing-token",
incomingUrl: "https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=...",
webhookPath: "/webhook/synology",
dmPolicy: "allowlist",
allowedUserIds: ["123456"],
rateLimitPerMinute: 30,
allowInsecureSsl: false,
},
},
}
```
## Environment variables
For the default account, you can use env vars:
- `SYNOLOGY_CHAT_TOKEN`
- `SYNOLOGY_CHAT_INCOMING_URL`
- `SYNOLOGY_NAS_HOST`
- `SYNOLOGY_ALLOWED_USER_IDS` (comma-separated)
- `SYNOLOGY_RATE_LIMIT`
- `OPENCLAW_BOT_NAME`
Config values override env vars.
## DM policy and access control
- `dmPolicy: "allowlist"` is the recommended default.
- `allowedUserIds` accepts a list (or comma-separated string) of Synology user IDs.
- `dmPolicy: "open"` allows any sender.
- `dmPolicy: "disabled"` blocks DMs.
- Pairing approvals work with:
- `openclaw pairing list synology-chat`
- `openclaw pairing approve synology-chat <CODE>`
## Outbound delivery
Use numeric Synology Chat user IDs as targets.
Examples:
```bash
openclaw message send --channel synology-chat --target 123456 --text "Hello from OpenClaw"
openclaw message send --channel synology-chat --target synology-chat:123456 --text "Hello again"
```
Media sends are supported by URL-based file delivery.
## Multi-account
Multiple Synology Chat accounts are supported under `channels.synology-chat.accounts`.
Each account can override token, incoming URL, webhook path, DM policy, and limits.
```json5
{
channels: {
"synology-chat": {
enabled: true,
accounts: {
default: {
token: "token-a",
incomingUrl: "https://nas-a.example.com/...token=...",
},
alerts: {
token: "token-b",
incomingUrl: "https://nas-b.example.com/...token=...",
webhookPath: "/webhook/synology-alerts",
dmPolicy: "allowlist",
allowedUserIds: ["987654"],
},
},
},
},
}
```
## Security notes
- Keep `token` secret and rotate it if leaked.
- Keep `allowInsecureSsl: false` unless you explicitly trust a self-signed local NAS cert.
- Inbound webhook requests are token-verified and rate-limited per sender.
- Prefer `dmPolicy: "allowlist"` for production.
+31 -4
View File
@@ -47,6 +47,7 @@ Status: production-ready for bot DMs + groups via grammY. Long polling is the de
```
Env fallback: `TELEGRAM_BOT_TOKEN=...` (default account only).
Telegram does **not** use `openclaw channels login telegram`; configure token in config/env, then start gateway.
</Step>
@@ -148,6 +149,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
`groupAllowFrom` is used for group sender filtering. If not set, Telegram falls back to `allowFrom`.
`groupAllowFrom` entries must be numeric Telegram user IDs.
Runtime note: if `channels.telegram` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group policy evaluation (even if `channels.defaults.groupPolicy` is set).
Example: allow any member in one specific group:
@@ -226,8 +228,9 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Requirement:
- `channels.telegram.streaming` is `true` (default)
- legacy `channels.telegram.streamMode` values are auto-mapped to `streaming`
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `off`)
- `progress` maps to `partial` on Telegram (compat with cross-channel naming)
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
This works in direct chats and groups/topics.
@@ -669,6 +672,29 @@ openclaw message send --channel telegram --target @name --message "hi"
- Node 22+ + custom fetch/proxy can trigger immediate abort behavior if AbortSignal types mismatch.
- Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress can cause intermittent Telegram API failures.
- If logs include `TypeError: fetch failed` or `Network request for 'getUpdates' failed!`, OpenClaw now retries these as recoverable network errors.
- On VPS hosts with unstable direct egress/TLS, route Telegram API calls through `channels.telegram.proxy`:
```yaml
channels:
telegram:
proxy: socks5://user:pass@proxy-host:1080
```
- Node 22+ defaults to `autoSelectFamily=true` (except WSL2) and `dnsResultOrder=ipv4first`.
- If your host is WSL2 or explicitly works better with IPv4-only behavior, force family selection:
```yaml
channels:
telegram:
network:
autoSelectFamily: false
```
- Environment overrides (temporary):
- `OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1`
- `OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY=1`
- `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first`
- Validate DNS answers:
```bash
@@ -708,10 +734,11 @@ Primary reference:
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
- `channels.telegram.streaming`: `true | false` (live stream preview; default: true).
- `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `off`; `progress` maps to `partial`).
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled.
- `channels.telegram.network.dnsResultOrder`: override DNS result order (`ipv4first` or `verbatim`). Defaults to `ipv4first` on Node 22+.
- `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
- `channels.telegram.webhookUrl`: enable webhook mode (requires `channels.telegram.webhookSecret`).
- `channels.telegram.webhookSecret`: webhook secret (required when webhookUrl is set).
+1 -1
View File
@@ -171,7 +171,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
- if `groupAllowFrom` is unset, runtime falls back to `allowFrom` when available
- sender allowlists are evaluated before mention/reply activation
Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is effectively `open`.
Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is `allowlist` (with a warning log), even if `channels.defaults.groupPolicy` is set.
</Tab>
+4
View File
@@ -1,6 +1,10 @@
---
title: CI Pipeline
description: How the OpenClaw CI pipeline works
summary: "CI job graph, scope gates, and local command equivalents"
read_when:
- You need to understand why a CI job did or did not run
- You are debugging failing GitHub Actions checks
---
# CI Pipeline
+7
View File
@@ -49,6 +49,13 @@ openclaw acp client --server-args --url wss://gateway-host:18789 --token-file ~/
openclaw acp client --server "node" --server-args openclaw.mjs acp --url ws://127.0.0.1:19001
```
Permission model (client debug mode):
- Auto-approval is allowlist-based and only applies to trusted core tool IDs.
- `read` auto-approval is scoped to the current working directory (`--cwd` when set).
- Unknown/non-core tool names, out-of-scope reads, and dangerous tools always require explicit prompt approval.
- Server-provided `toolCall.kind` is treated as untrusted metadata (not an authorization source).
## How to use this
Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want
+21
View File
@@ -0,0 +1,21 @@
---
summary: "CLI reference for `openclaw clawbot` (legacy alias namespace)"
read_when:
- You maintain older scripts using `openclaw clawbot ...`
- You need migration guidance to current commands
title: "clawbot"
---
# `openclaw clawbot`
Legacy alias namespace kept for backwards compatibility.
Current supported alias:
- `openclaw clawbot qr` (same behavior as [`openclaw qr`](/cli/qr))
## Migration
Prefer modern top-level commands directly:
- `openclaw clawbot qr` -> `openclaw qr`
+35
View File
@@ -0,0 +1,35 @@
---
summary: "CLI reference for `openclaw completion` (generate/install shell completion scripts)"
read_when:
- You want shell completions for zsh/bash/fish/PowerShell
- You need to cache completion scripts under OpenClaw state
title: "completion"
---
# `openclaw completion`
Generate shell completion scripts and optionally install them into your shell profile.
## Usage
```bash
openclaw completion
openclaw completion --shell zsh
openclaw completion --install
openclaw completion --shell fish --install
openclaw completion --write-state
openclaw completion --shell bash --write-state
```
## Options
- `-s, --shell <shell>`: shell target (`zsh`, `bash`, `powershell`, `fish`; default: `zsh`)
- `-i, --install`: install completion by adding a source line to your shell profile
- `--write-state`: write completion script(s) to `$OPENCLAW_STATE_DIR/completions` without printing to stdout
- `-y, --yes`: skip install confirmation prompts
## Notes
- `--install` writes a small "OpenClaw Completion" block into your shell profile and points it at the cached script.
- Without `--install` or `--write-state`, the command prints the script to stdout.
- Completion generation eagerly loads command trees so nested subcommands are included.
+5
View File
@@ -23,6 +23,11 @@ Note: one-shot (`--at`) jobs delete after success by default. Use `--keep-after-
Note: recurring jobs now use exponential retry backoff after consecutive errors (30s → 1m → 5m → 15m → 60m), then return to normal schedule after the next successful run.
Note: retention/pruning is controlled in config:
- `cron.sessionRetention` (default `24h`) prunes completed isolated run sessions.
- `cron.runLog.maxBytes` + `cron.runLog.keepLines` prune `~/.openclaw/cron/runs/<jobId>.jsonl`.
## Common edits
Update delivery settings without changing the message:
+43
View File
@@ -0,0 +1,43 @@
---
summary: "CLI reference for `openclaw daemon` (legacy alias for gateway service management)"
read_when:
- You still use `openclaw daemon ...` in scripts
- You need service lifecycle commands (install/start/stop/restart/status)
title: "daemon"
---
# `openclaw daemon`
Legacy alias for Gateway service management commands.
`openclaw daemon ...` maps to the same service control surface as `openclaw gateway ...` service commands.
## Usage
```bash
openclaw daemon status
openclaw daemon install
openclaw daemon start
openclaw daemon stop
openclaw daemon restart
openclaw daemon uninstall
```
## Subcommands
- `status`: show service install state and probe Gateway health
- `install`: install service (`launchd`/`systemd`/`schtasks`)
- `uninstall`: remove service
- `start`: start service
- `stop`: stop service
- `restart`: restart service
## Common options
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--deep`, `--json`
- `install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
- lifecycle (`uninstall|start|stop|restart`): `--json`
## Prefer
Use [`openclaw gateway`](/cli/gateway) for current docs and examples.
+1
View File
@@ -27,6 +27,7 @@ Notes:
- Interactive prompts (like keychain/OAuth fixes) only run when stdin is a TTY and `--non-interactive` is **not** set. Headless runs (cron, Telegram, no terminal) will skip prompts.
- `--fix` (alias for `--repair`) writes a backup to `~/.openclaw/openclaw.json.bak` and drops unknown config keys, listing each removal.
- State integrity checks now detect orphan transcript files in the sessions directory and can archive them as `.deleted.<timestamp>` to reclaim space safely.
## macOS: `launchctl` env overrides
+20 -1
View File
@@ -16,6 +16,7 @@ This page describes the current CLI behavior. If commands change, update this do
- [`onboard`](/cli/onboard)
- [`configure`](/cli/configure)
- [`config`](/cli/config)
- [`completion`](/cli/completion)
- [`doctor`](/cli/doctor)
- [`dashboard`](/cli/dashboard)
- [`reset`](/cli/reset)
@@ -33,6 +34,7 @@ This page describes the current CLI behavior. If commands change, update this do
- [`system`](/cli/system)
- [`models`](/cli/models)
- [`memory`](/cli/memory)
- [`directory`](/cli/directory)
- [`nodes`](/cli/nodes)
- [`devices`](/cli/devices)
- [`node`](/cli/node)
@@ -46,10 +48,13 @@ This page describes the current CLI behavior. If commands change, update this do
- [`hooks`](/cli/hooks)
- [`webhooks`](/cli/webhooks)
- [`pairing`](/cli/pairing)
- [`qr`](/cli/qr)
- [`plugins`](/cli/plugins) (plugin commands)
- [`channels`](/cli/channels)
- [`security`](/cli/security)
- [`skills`](/cli/skills)
- [`daemon`](/cli/daemon) (legacy alias for gateway service commands)
- [`clawbot`](/cli/clawbot) (legacy alias namespace)
- [`voicecall`](/cli/voicecall) (plugin; if installed)
## Global flags
@@ -94,7 +99,9 @@ openclaw [--dev] [--profile <name>] <command>
get
set
unset
completion
doctor
dashboard
security
audit
reset
@@ -108,6 +115,7 @@ openclaw [--dev] [--profile <name>] <command>
remove
login
logout
directory
skills
list
info
@@ -145,6 +153,13 @@ openclaw [--dev] [--profile <name>] <command>
stop
restart
run
daemon
status
install
uninstall
start
stop
restart
logs
system
event
@@ -231,6 +246,9 @@ openclaw [--dev] [--profile <name>] <command>
pairing
list
approve
qr
clawbot
qr
docs
dns
setup
@@ -303,13 +321,14 @@ Options:
- `--non-interactive`
- `--mode <local|remote>`
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|custom-api-key|skip>`
- `--auth-choice <setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|mistral-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|custom-api-key|skip>`
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
- `--token <token>` (non-interactive; used with `--auth-choice token`)
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
- `--anthropic-api-key <key>`
- `--openai-api-key <key>`
- `--mistral-api-key <key>`
- `--openrouter-api-key <key>`
- `--ai-gateway-api-key <key>`
- `--moonshot-api-key <key>`

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