Summary:
- The PR reuses a request-scoped cold manifest registry/runtime context across plugin status and inspect report paths, threads that context through provider/setup/metadata helpers, adds targeted coverage, and adds a changelog entry.
ClawSweeper fixups:
- Included follow-up commit: fix(plugins): preserve setup auto-enable lookup
Validation:
- ClawSweeper review passed for head 4d8e8e2d24.
- Required merge gates passed before the squash merge.
Prepared head SHA: 4d8e8e2d24
Review: https://github.com/openclaw/openclaw/pull/75620#issuecomment-4359143053
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Summary:
- The PR changes Discord message-action discovery to inspect configured accounts without resolving bot tokens, resolves scoped channel SecretRefs during message-tool execution even with an injected config snapshot, adds regression tests and a changelog entry, and restores a tool-display serializer export.
ClawSweeper fixups:
- Included follow-up commit: fix(discord): avoid resolving token during action discovery
- Included follow-up commit: fix(tools): restore tool display serializer export
Validation:
- ClawSweeper review passed for head a2cd832d01.
- Required merge gates passed before the squash merge.
Prepared head SHA: a2cd832d01
Review: https://github.com/openclaw/openclaw/pull/75424#issuecomment-4357825074
Co-authored-by: Clawdbot <clawdbot@apilab.us>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Summary:
- The PR rewires Control UI/WebChat New Chat to create and switch to a dashboard session through `sessions.create`, adds guarded UI/session helper logic and regression tests, and updates the changelog.
ClawSweeper fixups:
- Included follow-up commit: fix(webchat): create dashboard sessions from New Chat
Validation:
- ClawSweeper review passed for head 983c634ec0.
- Required merge gates passed before the squash merge.
Prepared head SHA: 983c634ec0
Review: https://github.com/openclaw/openclaw/pull/73725#issuecomment-4338023497
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
* fix: async transcript I/O to unblock gateway event loop
Two related fixes for event-loop starvation caused by synchronous file
operations on session transcript files during gateway hot paths.
## sessions.list: yield between transcript reads (#75330)
Extract filterAndSortSessionEntries() from listSessionsFromStore() and
add a new listSessionsFromStoreAsync() that yields to the event loop
via setImmediate every 10 session rows. The sessions.list RPC handler
now uses the async version.
The synchronous version is kept for callers that need it (sessions-
resolve visibility checks, embedded backends, subagent tools).
The dominant blocker is readSessionTitleFieldsFromTranscript(), which
performs fs.statSync + fs.openSync + fs.readSync (head) + fs.readSync
(tail) for every session row that requests derived titles or last-
message previews. With 100+ sessions, this blocks the event loop for
32-64 seconds, starving WebSocket heartbeats, channel I/O, and
concurrent RPC.
## session compaction: async file copy (#75414)
Add captureCompactionCheckpointSnapshotAsync() using fs.promises for
stat, copyFile, and unlink instead of fsSync equivalents. Switch both
compact.ts and compact.queued.ts to the async version.
The synchronous copyFileSync of large transcript files (20MB+ observed
in production) was blocking the event loop for the entire copy duration
— one reporter measured a 43-minute event loop block from a single
compaction checkpoint capture.
Refs: #75330, #75414
* test: cover async transcript I/O responsiveness
* fix: avoid sync checkpoint metadata reads
Fix three child-process stdin write paths that let async EPIPE errors
escape to uncaughtException and crash the gateway.
extensions/imessage/src/client.ts (the actual #75438 crash path):
- Add child.stdin.on('error') listener in start() to catch async EPIPE
and reject all pending requests via failAll().
- Add write callback to request() stdin.write() that rejects the
specific pending request on error, instead of leaving it hanging
until timeout.
src/agents/mcp-stdio-transport.ts:
- Fix write callback race in send(): previously resolved the promise
immediately when write() returned true, then the write callback with
EPIPE would fire after the promise was already fulfilled. Now always
settles the promise from the write callback so the outcome is known
before resolving.
src/process/exec.ts:
- Add stdin.on('error') before writing input so EPIPE from a
prematurely-exited child is swallowed — the process exit handler
reports the real status.
One reporter observed a gateway crash after 10.5 hours of stable
uptime — a single EPIPE on an iMessage RPC child process stdin write
killed the gateway with code 1.
Fixes: #75438
* fix(agents): trim trailing assistant turns and rewrite blank user messages in session repair
Session-file repair now:
- Trims trailing assistant messages so the JSONL never ends on
role=assistant, preventing the Anthropic 400 prefill-loop that
fires when thinking is enabled. (#75271)
- Rewrites blank-only user messages to a synthetic '(continue)'
placeholder instead of dropping them, so strict providers
(Qwen/mlx-vlm, Anthropic) no longer reject transcripts missing
a user turn. (#75313)
Closes#75271, closes#75313.
* refactor: clean up comments in session-file repair
* fix(agents): preserve trailing assistant tool-call turns during session trim
Mirror the outbound guard (stripTrailingAssistantPrefillTurns):
skip assistant entries containing toolCall/toolUse/functionCall
blocks so transcript repair can synthesize missing tool results.
Addresses PR review feedback from clawsweeper on #75606.
Summary:
- The PR updates the shared status reaction controller to track active remove-capable reactions, defer cleanup until clear/restoreInitial, adjust controller and Slack lifecycle tests, add a changelog entry, and carries qrcode runtime-dependency mirror hunks from its older base.
ClawSweeper fixups:
- Included follow-up commit: fix: limit status reaction restore cleanup
- Included follow-up commit: chore: merge main into status reaction cleanup
- Included follow-up commit: fix: mirror qrcode runtime dependency
Validation:
- ClawSweeper review passed for head f3efcb4fd3.
- Required merge gates passed before the squash merge.
Prepared head SHA: f3efcb4fd3
Review: https://github.com/openclaw/openclaw/pull/75582#issuecomment-4358876584
Co-authored-by: Peter Steinberger <steipete@steipete-macstudio.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Add inline docker build commands for npm-installed users who don't have the
source checkout scripts. Update all docs referencing sandbox-setup.sh,
sandbox-common-setup.sh and sandbox-browser-setup.sh to note they are
source-checkout-only and link to the new inline instructions.
Fixes#75485.
Adds `agents.defaults.skipOptionalBootstrapFiles` for optional workspace bootstrap files, validates the supported filenames, and propagates the option through workspace bootstrap callers.
Also preserves legacy setup detection when `USER.md` or `IDENTITY.md` are intentionally skipped, documents the config field, and includes focused regression coverage.
Landing follow-up included small CI unblockers for current-base drift: removing an unused Brave runtime dependency, fixing Telegram RTT lint, and preserving compatible gateway-bindable plugin registry cache reuse when runtime ensures disable bundled dependency installation.
* fix(agents,failover): propagate sessionId/lane/provider attribution through FailoverError
Adds optional `sessionId` and `lane` fields to `FailoverError` and threads
them — together with the existing `provider`, `model`, `profileId` — through
`describeFailoverError` and `coerceToFailoverError` context, so structured
error log ingestion can attribute exhausted-fallback wrapper errors back
to the originating request instead of dropping the per-profile metadata
when the final wrapper is built.
Fixes#42713.
* fix: preserve failover error attribution
---------
Co-authored-by: Altay <altay@uinaf.dev>
Adds the SDK-facing tools.invoke Gateway RPC for #74705.
Reuses the /tools/invoke policy path for tool policy, deny-list, owner filtering, before-tool-call hooks, session/agent scoping, and plugin approval handling. Returns typed SDK approval/refusal/success results while preserving HTTP compatibility and uses idempotencyKey as the stable tool-call id.
Includes protocol schema exports, method scope/list registration, SDK helper/types, docs, generated Swift models, tests, and changelog credit.
Stabilize the media stream readiness regression test by waiting for the early audio send directly and closing the WebSocket in cleanup before server shutdown.
Thanks @PfanP.
Fix voice-call CLI gateway delegation by returning protocol-shaped errors and running delegated continue turns through operation-id polling instead of one long Gateway RPC.\n\nThanks @serrurco and @DougButdorf.
Use the existing SSRF hostname/IP classifier for Voice Call and Google Meet webhook exposure checks so bracketed IPv6 loopback, unique-local, link-local, and IPv4-mapped local/private addresses fail before Twilio/Meet joins while public hostnames are not rejected by prefix accidents.
Thanks @clawsweeper, @donkeykong91, and @PfanP.
Summary:
- The PR adds payment-credential redaction patterns and a key-aware structured field redaction helper, wires it into tool payload sanitization, and updates focused tests, logging docs, and the changelog.
ClawSweeper fixups:
- No separate fixup commits were needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head 5f5f1fadbb.
- Required merge gates passed before the squash merge.
Prepared head SHA: 5f5f1fadbb
Review: https://github.com/openclaw/openclaw/pull/75230#issuecomment-4355538755
Co-authored-by: stainlu <stainlu@newtype-ai.org>
Fix Twilio voice-call startup so accepted media streams register immediately, realtime transcription readiness gates only the initial greeting, and early inbound media is preserved while STT connects.
Fixes#75197.
Thanks @PfanP and @donkeykong91.
Summary:
- The PR updates auto-reply message-tool availability and fallback policy, qa-channel group target support, qa-lab scenario coverage, generated config metadata, docs, and the changelog for group visible replies.
ClawSweeper fixups:
- No separate fixup commits were needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head adbec93b8a.
- Required merge gates passed before the squash merge.
Prepared head SHA: adbec93b8a
Review: https://github.com/openclaw/openclaw/pull/75382#issuecomment-4357590733
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): extract abortable from runEmbeddedAttempt to release captured run scope on hung provider abort (#74182)
* test(agents): drop synthetic WeakRef retention test for abortable
* feat(scripts): add embedded-run-abort-leak harness for runtime closure-leak validation
* feat(scripts): add production mode to leak harness importing real abortable
* docs(changelog): add #74182 fix entry for embedded-run abort closure release
Summary:
- The PR removes the auto-reply runtime warning for visible-reply defaults, adds doctor preview warnings and tests for message-tool visibility policy mismatches, and updates the group/channel docs and changelog wording.
ClawSweeper fixups:
- No separate fixup commits were needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head 1f96b3b568.
- Required merge gates passed before the squash merge.
Prepared head SHA: 1f96b3b568
Review: https://github.com/openclaw/openclaw/pull/75367#issuecomment-4357475980
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Keep async music generation completions on the requester-session wake path even when direct-send completion is enabled.
Also aligns config help, generated schema text, public docs, and the changelog so tools.media.asyncCompletion.directSend no longer claims to direct-send music completions.
Verification:
- pnpm test src/agents/tools/music-generate-background.test.ts src/agents/tools/video-generate-background.test.ts
- pnpm exec oxfmt --check --threads=1 src/agents/tools/media-generate-background-shared.ts src/agents/tools/music-generate-background.ts src/agents/tools/music-generate-background.test.ts src/config/schema.help.ts src/config/types.tools.ts docs/automation/tasks.md docs/gateway/config-tools.md CHANGELOG.md
- pnpm config:schema:check
- pnpm config:docs:check
- pnpm check:changelog-attributions
- git diff --check
- OPENCLAW_TESTBOX=1 pnpm check:changed
Harden gateway recovery diagnostics and media delivery.\n\n- Accept gateway send asVoice and map it to outbound audioAsVoice.\n- Preserve generated Swift protocol models for the gateway send schema.\n- Keep the broader recovery hardening for install/update/status/vector/TTS paths in one reviewed PR.\n\nProof:\n- Focused local gateway/outbound/update/status/doctor/sqlite-vec tests passed.\n- oxfmt --check and git diff --check passed.\n- Testbox OPENCLAW_TESTBOX=1 pnpm check:changed passed at 2f5ef650e97763a61ff43c28e61707db84c50060.\n- GitHub required checks are green at the merge SHA; the qa-lab parity gate is optional/surface-only and was still pending.
Preserve the existing wrapped OpenAI Codex stream so PI OAuth bearer injection reaches ChatGPT/Codex Responses, and scope native Codex payload sanitization to the ChatGPT backend.\n\nThanks @keshavbotagent.
* fix(security): stop implicit tool grants from config sections (#47487)
Configured tool sections (tools.exec, tools.fs) no longer implicitly
widen restrictive profiles (messaging, minimal). Previously, having a
tools.exec section anywhere in config — even just safety settings like
security: "allowlist" — would automatically add exec and process to the
profile's allowed tools, defeating the purpose of the restrictive
profile.
The same pattern existed in tool-fs-policy.ts where tools.fs presence
would add read/write/edit to the profile allowlist for root expansion.
Changes:
- pi-tools.policy.ts: Stop merging implicit grants into profileAlsoAllow.
Renamed resolveImplicitProfileAlsoAllow → detectImplicitProfileGrants
and use it only for a startup warning that tells users to add explicit
alsoAllow entries.
- tool-fs-policy.ts: Remove the implicit read/write/edit grant from
resolveEffectiveToolFsRootExpansionAllowed when tools.fs is present.
Root expansion now requires actual read access via profile or alsoAllow.
- Updated 4 existing tests and added 3 new regression tests.
Migration: users who relied on tools.exec or tools.fs implicitly granting
access under a restrictive profile should add explicit alsoAllow entries:
tools:
profile: "messaging"
alsoAllow: ["exec", "process"] # was implicit, now required
exec: { security: "allowlist" }
Fixes#47487
* fix: address tool policy review feedback
Wire the Control UI chat slash-command menu to the composer with stable listbox and option IDs, active-descendant updates, and a live status announcement. Keep the native textarea role conforming while preserving the menu relationships and tests.
* fix(qqbot): align clear-storage command with actual downloads directory
The /bot-clear-storage command previously targeted
~/.openclaw/media/qqbot/downloads/{appId}/, but inbound attachments
and outbound fallback downloads are stored directly under
~/.openclaw/media/qqbot/downloads/ without appId subdivision.
This mismatch caused the clear command to report 'no files to clean'
while downloaded files continued to occupy disk space.
Changes:
- Replace resolveQqbotDownloadsDirForApp(appId) with
resolveQqbotDownloadsDir() that returns the downloads root
- Use getQQBotMediaPath('downloads') instead of manual path assembly
- Remove appId-based path validation (no longer needed)
- Update usage text to reflect the new scope
* refactor(qqbot): unify slash command auth and c2cOnly gating in registry
Previously, slash command authorization and group-chat rejection were
scattered across individual handlers and a hardcoded GROUP_EXCLUDED set.
This led to inconsistent behavior: commandAuthorized was hardcoded to
true in the pre-dispatch path, some handlers checked allowFrom while
others did not, and group users received no response for auth-gated
commands.
Changes:
1. Add resolveSlashCommandAuth() (new file slash-command-auth.ts)
- Requires sender to appear in an explicit non-wildcard allowFrom
list; wildcard ['*'] does not grant admin command access
- Group messages use groupAllowFrom, falling back to allowFrom
2. Fix commandAuthorized in slash-command-handler.ts
- Replace hardcoded 'true' with resolveSlashCommandAuth() call
3. Add c2cOnly field to SlashCommand interface
- Commands declare c2cOnly: true instead of checking ctx.type
inside their handler
- Registry rejects c2cOnly commands in group chat before auth
check, returning a user-friendly hint
4. Remove GROUP_EXCLUDED hardcoded set from register-basic.ts
- /bot-help now filters by cmd.c2cOnly dynamically
5. Clean up handler-level auth and scene checks
- Remove hasExplicitCommandAllowlist check from register-logs
- Remove ctx.type !== 'c2c' guards from all c2cOnly handlers
- Improve rejection message to mention the correct config field
(allowFrom for c2c, groupAllowFrom for group)
6. Mark commands: bot-upgrade, bot-streaming, bot-logs,
bot-clear-storage, bot-approve as c2cOnly: true
* fix(qqbot): pass allowQQBotDataDownloads when sending slash command file attachments
The /bot-logs command writes temporary log files to the QQBot data
downloads directory (~/.openclaw/qqbot/downloads/), but sendDocument
was called without allowQQBotDataDownloads: true. This caused
resolveOutboundMediaPath to reject the file path as outside the
allowed media roots, silently failing the file attachment while
the text reply was sent successfully.
Add { allowQQBotDataDownloads: true } to the sendDocument call in
slash-command-handler.ts so file-bearing slash command results
(currently only /bot-logs) can deliver their attachments.
* feat(qqbot): add /bot-me command to display sender user ID
Add a new /bot-me slash command that returns the sender's user ID
(openid). This helps users quickly find the value they need to add
to allowFrom or groupAllowFrom configuration for admin command
access.
Marked as c2cOnly since the user ID is sensitive information.
* feat(qqbot): update response timeout
* feat(qqbot): add engine import boundary test and bump version
- Add engine-import-boundary.test.ts to enforce that engine/ sources
only import from openclaw/plugin-sdk/* and never reach into other
openclaw internals directly. Scans all 110 source files recursively.
- Bump plugin version to 2026.4.27.
* fix(qqbot): unify slash command auth, c2cOnly gating, and file delivery (#73616) (thanks @cxyhhhhh)
---------
Co-authored-by: sliverp <870080352@qq.com>
* fix(cron): warn when --agent is not specified on cron add
Warn users when creating a cron job without specifying the --agent flag,
so they know the job will run with the default agent (main).
Fixes#42196
* fix(cron): warn when cron add omits --agent
* fix(cron): name default agent in warning
---------
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(pdf): resolve standard fonts from pdfjs package root
Resolve PDF.js standard fonts via pdfjs-dist/package.json instead of a
relative ../../node_modules path so the fallback renderer does not depend
on emitted dist chunk layout.
Add focused regression coverage that asserts the forwarded
standardFontDataUrl matches the installed pdfjs-dist package root and
exists on disk.
* fix(pdf): resolve pdfjs standard fonts from package root
* fix(pdf): use PDF.js font URL separator
---------
Co-authored-by: Dr JCai <jingxiao.cai@gmail.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Derive context-window guard thresholds from the effective model window, keeping 10% hard-min and 20% warning ratios with 4k/8k floors.
Stop the embedded runner from forcing old fixed guard overrides so runtime admission uses the dynamic resolver.
Validation:
- CI run 25151866833 passed, including build-artifacts and checks-node-channels.
- Parity gate 25151866868 passed.
- Testbox pnpm test:channels passed: 54 files / 433 tests.
Fixes#42999.
Prepared head SHA: 9c80383639
* fix(skills): scan nested subdirectories for grouped skill layouts
Previously, skill discovery only checked immediate children of the
skills root for SKILL.md files. Skills organized in subdirectories
(e.g. ~/.openclaw/skills/coze/koze-retrieval/SKILL.md) were silently
ignored.
Now, when an immediate child directory does not contain a SKILL.md,
its own children are checked one level deeper. This supports grouped
skill layouts while keeping the scan depth bounded (max 2 levels) to
avoid unbounded filesystem traversal.
The existing per-source skill count limits and containment checks
still apply to nested discoveries.
Fixes#56915
* test(skills): cover nested grouped skill discovery
* fix(skills): cache contained-path checks and cap nested scans
- Reuse skillDirRealPath captured during the collection phase so the load
loop no longer re-runs resolveContainedSkillPath on the same directory.
- Apply the per-root candidate cap (and the matching warning log) when
descending into nested grouped skill directories, matching the outer
scan's behavior.
Addresses Greptile P2 feedback on PR #72534.
* fix(skills): load grouped skill directories under skills roots
* fix(clownfish): address review for ghcrawl-156697-autonomous-smoke (1)
---------
Co-authored-by: Otto Deng <otto@ottodeng.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: Otto Deng <ottodeng2@github.local>
Removes the win32 exclusion from supportsNativeJitiRuntime() and adds { allowWindows: true } to all tryNativeRequireJavaScriptModule call sites, so bundled plugin modules use native require() instead of Jiti on Windows. Also adds an attempted-load counter to the debug timing log and a changelog entry.
Fixes#68656
Co-authored-by: Galin Iliev <galiniliev@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Disable the Control UI refresh button while chat is disconnected, loading, sending, running, or streaming.
This prevents manual chat-history refresh from racing active run/stream state and adds browser render coverage for the disabled-state matrix.
Closes#65522.
Validation:
- Exact PR head `1511a086614a727fc4200730e7ad9622134bb7d3` reached `CLEAN` merge state.
- GitHub CI for the exact head completed with no failed or pending checks.
Adds the memory runtime quality shard to the PR CodeQL guard while preserving provider/plugin overlap only for the memory files that share those contracts.
Adds the Plugin SDK reply runtime quality shard to the PR CodeQL guard while keeping reply runtime changes on the existing plugin and package-contract shards.
* fix: configs that used the previously documented WhatsApp exposeErrorText key now fail valida...
* fix(clawsweeper): address review for clawsweeper-commit-openclaw-openclaw-4cba08df01ea (1)
---------
Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Splits the previous wall-of-prose docs/ci.md into discoverable sections
while preserving every operator-relevant detail:
- Lead orientation paragraph kept; cross-links to umbrella and prerelease
- Pipeline overview anchors the job table at the top
- Fail-fast order tightened; superseded-run/concurrency notes folded in
- Scope and routing surfaces ci-changed-scope.mjs, the routing-only fast
path, the Windows scope rule, Vitest shard balancing, the Android
dual-flavor rule, and the check-dependencies (Knip + unused-file
allowlist) pass that was buried in the lead
- Manual dispatches groups examples + include_android + target_ref
- Runners and Local equivalents tables/blocks preserved
- Full Release Validation: release_profile and rerun_group bulleted;
verifier-only rerun guidance and the shared release-package-under-test
artifact called out
- Live and E2E shards: native-live shard names listed, live-media-runner
image and openclaw-live-test:<sha> with OPENCLAW_SKIP_DOCKER_BUILD=1
broken out
- Package Acceptance split into Jobs / Candidate sources / Suite profiles
/ Legacy compatibility windows / Examples / debugging
- Install smoke: fast vs full paths, main-push policy, Bun gate
- Local Docker E2E: scheduler tunables in a table, reusable workflow
flow, release-path chunks list, rerun helpers
- Plugin Prerelease, QA Lab, CodeQL each get their own discoverable
sections; CodeQL uses tables for security and quality categories
instead of paragraph walls (kept the new provider-runtime-boundary
shard in the PR-quality-guard list)
- Maintenance workflows groups Docs Agent, Test Performance Agent, and
Duplicate PRs After Merge
- Local check gates and changed routing turn boundary lane rules into
bullets and keep the explicit-mapping prose
- Testbox validation kept; Related links preserved
Audited every workflow name and CodeQL category against
.github/workflows/ — no stale references. File goes from 527 to 413
lines while preserving shard names, env vars, profiles, chunks, and
legacy-compat windows. Layout obeys oxfmt.
Bumps the docker-images group with 1 update in the / directory: debian.
Updates `debian` from `4724b8c` to `f9c6a2f`
---
updated-dependencies:
- dependency-name: debian
dependency-version: bookworm-slim
dependency-type: direct:production
dependency-group: docker-images
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Adds the provider runtime quality shard to the PR CodeQL guard, keeps PR quality analysis path-sharded by surface, and fixes selector overlap for Plugin SDK/package-contract paths.
Adds the gateway runtime quality shard to the PR CodeQL guard, keeps PR quality analysis path-sharded by surface, and documents the shard selector behavior.
When an exec-approval followup run has no deliverable route and no
gateway-internal channel, buildAgentFollowupArgs was passing channel=undefined
to the spawned agent. This left defaults.messageProvider=undefined in the
followup run, causing tools.elevated.allowFrom.<provider> checks to always
fail with provider=null after the user approved an async elevated command.
Thread turnSourceChannel through buildAgentFollowupArgs and use it as a
fallback when sessionOnlyOriginChannel is absent. Fixes#74646.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Feishu delivers empty-text events (e.g. {"text":""}) when users send
blank messages or when a media-only message produces no text content.
Writing a blank user turn to the session file causes downstream LLM
providers such as MiniMax to reject requests with:
invalid params, messages must not be empty (2013)
Guard at the point after media resolution: if ctx.content.trim() is
empty AND mediaList is empty, log the skip and return without queuing
a reply. This preserves all existing behaviour for text, media, and
mixed messages.
Regression test: dispatch a DM with {"text":""} (no media), assert
mockDispatchReplyFromConfig is not called.
Closes#74634. Thanks @xdengli.
* fix(acp): fall through to thread-bound resolution when token is unresolvable (#66299)
resolveAcpTargetSessionKey returned an error immediately when an explicit
session token was supplied but could not be resolved as a key/id/label.
This blocked the thread-bound and requester-session fallback paths from
ever being reached.
Discord slash commands auto-fill the current thread ID as a positional
ACP target. That value is not a session identifier, so the gateway lookup
returns null, and the command returned 'Unable to resolve session target'
instead of falling through to the thread-bound session that was already
known via the binding context.
Fix: when the token lookup returns null, skip the early-exit error and
fall through to thread-bound → requester-session → error in the normal
way. The 'Missing session key' error still surfaces when neither fallback
produces a binding.
Adds a focused regression test: unresolvable token + bound thread session
→ steer command reaches the thread-bound session, not an error.
Fixes#66299
* fix(changelog): add Thanks @martingarramon attribution for #66299
Per clawsweeper P2 review — every new CHANGELOG entry must credit
at least one author. martingarramon authored the issue analysis and
explicitly invited the PR.
* fix(acp): preserve bad-token diagnostics after thread fallback
---------
Co-authored-by: clawsweeper-repair <clawsweeper-repair@users.noreply.github.com>
Closes#74587. AI-assisted, fully tested.
The previous deprecation warning ("set config.modelFallback explicitly
if you want a fallback model") read naturally as runtime failover —
model A errors → switch to model B. The actual semantics in
`getModelRef` are different: `modelFallback` is the **last candidate
in the chain-resolution walk**, consulted only when `config.model`,
the current run's model, AND the agent's configured default have all
resolved to nothing. There is no error-recovery / retry-with-different-model
path.
The mismatch wastes real debug time. The issue filer reports ~1 hour of
cycles before reading source revealed the gap; users without source
access can debug for much longer assuming runtime failover exists.
## Fix
Rewrite the warning string to:
1. State the deprecation (preserved).
2. Describe `modelFallback`'s actual semantics — chain-resolution
last-resort, gated on the three earlier candidates resolving to
nothing.
3. Explicitly disclaim the wrong mental model — "it is NOT a runtime
failover that substitutes a different model when the resolved model
errors out" — so a quick read can't lead the operator astray.
No behavior change, only operator-facing copy. Surrounding code paths
(`getModelRef`, `hasDeprecatedModelFallbackPolicy`, the warn caller in
`register()`) are untouched.
## Tests
`extensions/active-memory/index.test.ts` extends the existing
deprecation-warning assertion to pin both the positive copy
(`chain-resolution`, `last-resort`) and the negative disclaimer
(`NOT a runtime failover`), so a future "let's reword this" change
that reintroduces the failover-implying language fails the test
instead of silently regressing.
`pnpm test extensions/active-memory/index.test.ts` — 94 passed.
`pnpm exec oxfmt --check` — clean. `pnpm exec oxlint` — 0 warnings,
0 errors.
## AI-assisted PR
- [x] Mark as AI-assisted (Claude). Lightly tested via the targeted
Vitest extension shard; not exercised against a live Ollama / AM
rollout because the change is a log-string update, not behavior.
- [x] Confirm I understand what the code does: yes — `getModelRef`
walks four candidates (`config.model`, `currentRunModel`,
`configuredDefaultModel`, `config.modelFallback`) and returns the
first non-null parse; `modelFallback` is purely a default-when-empty
selector, not a runtime failover.
Previously both a planned probe skip (probe:false path) and a transport timeout
returned checked:false, so the renderer's !checked early return would silently
suppress diagnostics for key-optional providers even when the gateway had timed out.
- Add `skipped?: boolean` to GatewayMemoryProbe: true for gateway-confirmed skip,
false for timeout/unavailable paths
- Renderer now guards on `probe.skipped` instead of `!probe.checked`, so timeouts
fall through to the existing warning path
- Update doctor-memory-search inline type and buildGatewayProbeWarning signature
- Update skipped-probe tests to pass { skipped: true }; add regression test for
key-optional timeout (lmstudio gateway timeout now warns)
Addresses clawsweeper P2: src/commands/doctor-memory-search.ts:416
clawsweeper P1: probeGatewayMemoryStatus always returned checked: true
on successful RPC, silently discarding payload.embedding.checked === false
from the SKIPPED_MEMORY_EMBEDDING_PROBE gateway response. The renderer
guard in noteMemorySearchHealth (added in prior commit) never saw checked:
false in real execution — only on timeout paths.
Fix: propagate checked flag from payload.embedding.checked so a skipped
gateway probe surfaces as checked: false to the renderer, allowing the
key-optional provider guard to suppress the false-positive warning.
Add adapter-level regression test that verifies the skipped payload shape
from doctor.memory.status reaches GatewayMemoryProbe as checked: false.
When `openclaw doctor` runs without --deep, the gateway probe is skipped
and returns { checked: false, ready: false } (SKIPPED_MEMORY_EMBEDDING_PROBE).
Key-optional providers (ollama, lmstudio, local) were incorrectly shown
"could not confirm embeddings are ready" in this case, misleading users
into thinking their fully-functional embedding setup had an issue.
Guard the key-optional provider path: if probe.checked is false (probe
was skipped, not run), return early without warning. A skipped probe
carries no readiness signal — it is not a failure.
- Adds two focused regression tests for ollama and lmstudio with
skipped probe (checked: false) → expect note() not called
- Updates the prior test that expected a warning on checked:false
to reflect the corrected behaviour
Fixes#74608
* feat(file-transfer): add bundled plugin for binary file ops on nodes
New extensions/file-transfer/ plugin exposing four agent tools
(file_fetch, dir_list, dir_fetch, file_write) and four matching
node-host commands (file.fetch, dir.list, dir.fetch, file.write).
Lets agents read and write files on paired nodes by absolute path,
bypassing the bash output cap (200KB) and the live tool-result
text cap that would otherwise truncate base64 payloads.
Public surface
--------------
- file_fetch({ node, path, maxBytes? })
Image MIMEs return image content blocks; small text (<=8 KB) inlines
as text content; everything else returns a saved-media-path text
block. sha256-verified end-to-end.
- dir_list({ node, path, pageToken?, maxEntries? })
Structured directory listing — name, path, size, mimeType, isDir,
mtime. Paginated. No content transfer.
- dir_fetch({ node, path, maxBytes?, includeDotfiles? })
Server-side tar -czf streamed back, unpacked into the gateway media
store, returns a manifest of saved paths. Single round-trip.
60s wall-clock timeouts on tar create/unpack. tar -xzf without -P
rejects absolute paths in archive entries.
- file_write({ node, path, contentBase64, mimeType?, overwrite?,
createParents? })
Atomic write (temp + rename). Refuses to overwrite by default.
Refuses to write through symlinks (lstat check). Buffer-side
sha256 (no read-back race). Pair with file_fetch to round-trip
files between nodes — DO NOT use exec/cp for file copies.
All four commands gated by:
- dangerous-by-default node command policy
(gateway.nodes.allowCommands opt-in)
- per-node path policy (gateway.nodes.fileTransfer)
- optional operator approval prompt (ask: off | on-miss | always)
16 MB raw byte ceiling per single-frame round-trip (25 MB WS frame
with ~33% base64 overhead and JSON envelope). 8 MB defaults.
Path policy and approvals
-------------------------
Default behavior is DENY. The operator must explicitly opt in:
{
"gateway": {
"nodes": {
"fileTransfer": {
"<nodeId-or-displayName>": {
"ask": "off" | "on-miss" | "always",
"allowReadPaths": ["~/Screenshots/**", "/tmp/**"],
"allowWritePaths": ["~/Downloads/**"],
"denyPaths": ["**/.ssh/**", "**/.aws/**"],
"maxBytes": 16777216
},
"*": { "ask": "on-miss" }
}
}
}
}
ask modes:
off — silent: allow if matched, deny if not (default)
on-miss — silent allow if matched; prompt on miss
always — prompt every call (denyPaths still hard-deny)
denyPaths always wins. allow-always from the prompt persists the
exact path back into allowReadPaths/allowWritePaths via
mutateConfigFile so subsequent matching calls go silent.
Reuses existing primitives — no new gateway methods:
plugin.approval.request / plugin.approval.waitDecision
decision: allow-once | allow-always | deny
Pre-flight against requested path AND post-flight against the
canonicalPath returned by the node — closes symlink-escape attacks
where the requested path matched policy but realpath resolves
somewhere else.
Audit log
---------
JSONL at ~/.openclaw/audit/file-transfer.jsonl. Records every
decision (allow/allowed-once/allowed-always/denied/error) with
timestamp, op, nodeId, displayName, requestedPath, canonicalPath,
decision, error code, sizeBytes, sha256, durationMs. Best-effort
writes; never propagates failure.
Plugin layout
-------------
extensions/file-transfer/
index.ts definePluginEntry, nodeHostCommands
openclaw.plugin.json contracts.tools registration
package.json
src/node-host/{file-fetch,dir-list,dir-fetch,file-write}.ts
src/tools/{file-fetch,dir-list,dir-fetch,file-write}-tool.ts
src/shared/
mime.ts single-source extension->MIME map + image/text sets
errors.ts shared error code enum and helpers
params.ts shared param-validation helpers + GatewayCallOptions
policy.ts evaluateFilePolicy, persistAllowAlways
approval.ts plugin.approval.request wrapper
gatekeep.ts one-stop policy + approval + audit orchestrator
audit.ts JSONL audit sink
Core touch points
-----------------
- src/infra/node-commands.ts: NODE_FILE_FETCH_COMMAND,
NODE_DIR_LIST_COMMAND, NODE_DIR_FETCH_COMMAND,
NODE_FILE_WRITE_COMMAND, NODE_FILE_COMMANDS array
- src/gateway/node-command-policy.ts: all four added to
DEFAULT_DANGEROUS_NODE_COMMANDS
- src/security/audit-extra.sync.ts: audit detail mentions file ops
- src/agents/tools/nodes-tool-media.ts: MEDIA_INVOKE_ACTIONS entry
for file.fetch redirects raw nodes(action=invoke) callers to the
dedicated file_fetch tool to prevent base64 context bloat
- src/agents/tools/nodes-tool.ts: nodes tool description points to
the dedicated file_fetch tool
Known limitations / follow-ups
------------------------------
- No tests in this PR. For a security-sensitive surface this is a
gap; will follow up with a test pass.
- Direct CLI invocation (openclaw nodes invoke --command file.fetch)
bypasses the plugin policy entirely. Plugin-side gating is the
realistic threat model (agent on iMessage requesting paths it
shouldn't), but for true defense-in-depth, policy belongs in the
gateway-side node.invoke dispatch. Move-policy-to-core is a
separate PR.
- file_watch (long-lived filesystem event subscription) is not
included; it needs a new node-protocol primitive for streaming
event channels and was descoped from this PR.
- dir_fetch includeDotfiles: true is the only supported mode;
BSD tar exclude patterns reliably collapse dotfile filtering
to an empty archive. Reliable filtering needs a
`find ! -name ".*" | tar -T -` pipeline; deferred.
- dir_fetch du -sk preflight is a heuristic (du * 4 vs maxBytes);
the mid-stream byte cap is the actual safety net.
* test(file-transfer): add unit tests for handlers, policy, and shared utilities
Adds 77 tests covering:
- handleFileFetch: validation, fs errors, sha256, size cap, symlink canonicalization
- handleFileWrite: validation, atomic write, overwrite policy, parent dir handling, symlink refusal, integrity check, size cap
- handleDirList: validation, fs errors, sorted listing, dotfile inclusion, pagination
- handleDirFetch: validation, fs errors, gzipped tar with sha256, mid-stream byte cap
- evaluateFilePolicy: default-deny, denyPaths-wins, allow matching, ask modes (off/on-miss/always), node-id/displayName/'*' resolution
- persistAllowAlways: append, dedupe, create-on-missing
- shared/mime: extension lookup, image/text inline sets
- shared/errors: err helper, classifyFsError, throwFromNodePayload
Also fixes accumulated lint regressions in the prod source flagged once these
files moved into the changed-gate scope (parseInt -> Number.parseInt, redundant
type casts removed, single-statement if bodies wrapped in braces).
* fix(file-transfer): address PR review feedback (security + availability)
Reviewer findings addressed (greptile + aisle):
- policy: persistAllowAlways no longer escalates per-node approvals to the
'*' wildcard entry; allow-always now writes under the specific node's
own entry, never the wildcard (greptile P1 SECURITY).
- policy: add literal '..' segment short-circuit in evaluateFilePolicy,
raised before glob match. Stops "/allowed/../etc/passwd" from passing
preflight against "/allowed/**" globs (aisle MEDIUM CWE-22).
- file-write: replace no-op base64 try/catch with actual round-trip
validation. Buffer.from(s, "base64") never throws — invalid input
silently decoded to garbage bytes. Now re-encodes and compares
modulo padding/url-variant chars (greptile P1 SECURITY).
- file-write: document the parent-symlink residual risk and rely on the
existing gateway-side post-flight policy check; full rollback requires
a node-side file.unlink which is deferred to a follow-up. Initial
segment-walk attempt was reverted because it false-positives on system
symlinks like macOS /var → /private/var (aisle HIGH CWE-59).
- dir-fetch tool: add preValidateTarball pass that runs `tar -tzvf` and
rejects symlinks, hardlinks, absolute paths, '..' traversal,
uncompressed sizes >64MB, and entry counts >5000 — before any
extraction. Drops --no-overwrite-dir (GNU-only flag rejected by BSD
tar on macOS) (aisle HIGH x2 CWE-22 + CWE-409, greptile P2).
- dir-fetch tool: stream-hash files via fs.open + read loop instead of
fs.readFile to avoid full-buffer reads on large extracted entries.
- dir-fetch handler: replace spawnSync in countTarEntries with async
spawn + bounded buffer so tar -tzf can't park the node-host event
loop for up to 10s on a slow filesystem (greptile P1 AVAIL).
- audit: clear auditDirPromise on rejection so a transient mkdir
failure doesn't permanently silence the audit log (greptile P2).
New tests: wildcard escalation rejection, base64 malformed/url-variant,
'..' traversal short-circuit (3 cases). 84/84 passing.
* fix(file-transfer): CI failures + second-round PR review feedback
CI failures on previous push:
- Declare runtime deps (minimatch, typebox) in package.json — failed the
extension-runtime-dependencies contract test that scans imports.
- Switch policy.ts and policy.test.ts off the broad
openclaw/plugin-sdk/config-runtime barrel and onto the narrow
openclaw/plugin-sdk/config-mutation + runtime-config-snapshot subpaths.
This satisfies the deprecated-internal-config-api architecture guard.
Second-round Aisle findings:
- policy: traversal-segment check now treats backslash and forward slash
as equivalent, so a Windows node can't be hit with mixed-separator
"C:\\allowed\\..\\Windows\\system.ini" (Aisle HIGH CWE-22).
- dir-fetch tool: replace the single fragile `tar -tvzf` parser pass
(which broke for filenames containing whitespace) with two robust
passes: `tar -tzf` for paths only (one per line, no parsing of
fixed columns) and `tar -tzvf` for type chars only (FIRST CHAR of each
line, never the path column). Also reject backslash-containing entry
names. Drops the in-process uncompressed-size cap because reliably
parsing sizes from tar output is fragile and Aisle flagged it as a
bypass primitive — entry-count cap stays (Aisle HIGH CWE-22, MED).
Tests still 84/84 passing.
* fix(file-transfer): third-round PR review feedback
Aisle's re-analysis on b63daa6a05 surfaced 3 actionable findings:
- nodes.invoke bypass (HIGH CWE-285): generic nodes.action="invoke" let
agents call dir.list/dir.fetch/file.write directly, skipping the
file-transfer plugin's gatekeep + policy + approval flow. Only file.fetch
was redirected to its dedicated tool. Add the other three to
MEDIA_INVOKE_ACTIONS so the redirect-or-deny logic in
nodes-tool-commands fires for all four. The dedicated tools enforce
policy; the generic invoke surface no longer has a way to skip them
without an explicit allowMediaInvokeCommands opt-in.
- prototype pollution in persistAllowAlways (MED CWE-1321): a paired
node with displayName "__proto__" / "prototype" / "constructor" would
mutate the fileTransfer object's prototype when persisting allow-always.
Reject those keys explicitly. Switch the existing-key lookup to
Object.prototype.hasOwnProperty.call so a key like "constructor"
doesn't accidentally match Object.prototype.constructor.
- decompression-bomb cap in dir_fetch (MED CWE-409): compressed tar is
bounded upstream, but a highly compressible bomb can still expand to
gigabytes. Enforce DIR_FETCH_MAX_UNCOMPRESSED_BYTES (64MB) summed
across extracted files and DIR_FETCH_MAX_SINGLE_FILE_BYTES (16MB) per
entry, both checked during the post-extract walk. On bust, rm -rf the
rootDir and audit-log + throw UNCOMPRESSED_TOO_LARGE.
Tests: 85/85 passing (added prototype-pollution rejection test).
Aisle's HIGH parent-symlink finding remains documented as deferred — full
rollback requires a node-side file.unlink command which is out of scope
for this PR. The gateway-side post-flight policy check still detects and
loudly errors on canonical-path mismatches.
* fix(file-transfer): refuse symlink traversal by default with followSymlinks opt-in
Closes the deferred Aisle HIGH parent-symlink finding. Instead of
detecting the escape in a post-flight gateway check after the file is
already written, the node-side handler now refuses pre-flight if any
component of the requested path resolves through a symlink.
Behavior:
- Reads (file.fetch / dir.list / dir.fetch): node realpath()s the
requested path. If canonical != requested AND followSymlinks=false,
return SYMLINK_REDIRECT { canonicalPath } — no I/O happens.
- Writes (file.write): node realpath()s the parent dir. Same refusal
rule. The lstat-on-final check is kept to catch the case where the
target file itself is an existing symlink.
- Opt-in: set gateway.nodes.fileTransfer.<node>.followSymlinks=true to
bring back the previous "follow + post-flight check" behavior.
Operator UX: the SYMLINK_REDIRECT response includes the canonical path
so the operator can either update their allow list to the canonical form
or set followSymlinks=true on that node. On macOS, /var → /private/var
and /tmp → /private/tmp are system aliases that trip the new check, so
operators using those paths need followSymlinks=true OR canonical-path
allowlists.
Wiring:
- Add followSymlinks?: boolean to NodeFilePolicyConfig.
- evaluateFilePolicy returns followSymlinks (default false) on its
ok=true branches.
- gatekeep propagates it via GatekeepOutcome.
- Each tool passes it as a node.invoke param.
- Each handler honors it pre-flight before any read/write.
Tests updated: 89/89 passing.
- realpath(mkdtemp()) so existing happy-path tests don't trip the new
default on macOS where mkdtemp lands under symlinked /var/folders.
- New tests: SYMLINK_REDIRECT refusal for file.fetch and file.write
parent traversal; opt-in passthrough when followSymlinks=true.
- New policy test: followSymlinks propagation default false / true.
* fix(file-transfer): close two more aisle findings on 069bd66
Aisle re-analysis on 069bd66 surfaced two issues my earlier round-three
fix missed:
- HIGH (CWE-284): file.fetch / dir.fetch / dir.list / file.write were
still bypassable via the generic nodes.action="invoke" surface when
the operator had set allowMediaInvokeCommands=true. That flag was
meant to opt in to base64-bloat for camera/screen, not to disable
path policy on file-transfer. Split the redirect map: introduce
POLICY_REDIRECT_INVOKE_COMMANDS (file-transfer only) which ALWAYS
rerouts to its dedicated tool regardless of the bloat flag. Camera
and screen continue to use the bloat-only redirect (suppressed by
allowMediaInvokeCommands=true). Confirmed by clawsweeper P1.
- MED (CWE-276): tar -xzf in dir_fetch unpack preserved archive
ownership and permissions, so a malicious node could plant
setuid/setgid or world-writable files on a gateway running with
elevated privileges. Add --no-same-owner --no-same-permissions
(both flags are portable across BSD tar / GNU tar).
Tests: 89/89 passing.
* chore(file-transfer): drop file_watch from plugin description
Phase 5 (file_watch) was deferred earlier in this PR. Strip the watch
mention from the plugin description in package.json,
openclaw.plugin.json, and index.ts so the metadata reflects what's
actually shipped (file_fetch, dir_list, dir_fetch, file_write).
Closes clawsweeper P3.
* fix(file-transfer): hash before rename and allow zero-byte round-trip
Two of Peter's review findings on PR #74134:
- P2 (file-write integrity): hash the decoded buffer + compare against
expectedSha256 BEFORE temp+rename. Previously the rename happened
first, then the sha check unlinked the target on mismatch — with
overwrite=true a bad caller hash could replace + delete the original.
Now a hash mismatch returns INTEGRITY_FAILURE without touching disk.
Added a regression test that asserts the original file survives.
- P2/P3 (zero-byte round-trip): the tool layer's truthy checks on
contentBase64 and base64 rejected the empty string, blocking zero-byte
files from round-tripping through file_fetch -> file_write. Switched
to type-checks (typeof === "string") and added zero-byte tests at the
handler layer for both fetch and write (sha matches the known empty
digest).
Tests: 92/92 passing.
* fix(file-transfer): declare gateway.nodes.fileTransfer in core config schema
Peter's P1/P2 finding: the plugin reads/writes gateway.nodes.fileTransfer
via casts through unknown because the strict zod schema and OpenClawConfig
type didn't declare it. That meant `openclaw config validate` would
reject the very examples in the plugin's own documentation.
- Add fileTransfer block to gateway.nodes in src/config/zod-schema.ts
with the full per-node entry shape (ask, allowReadPaths,
allowWritePaths, denyPaths, maxBytes, followSymlinks).
- Add GatewayNodeFileTransferEntry + the fileTransfer field on
GatewayNodesConfig in src/config/types.gateway.ts.
- Drop the `as unknown` casts in the extension's policy.ts now that
gateway.nodes.fileTransfer is properly typed end-to-end.
- Regenerate docs/.generated/config-baseline.sha256.
Tests: 92/92 passing. pnpm config:docs:check OK.
* fix(file-transfer): enforce path policy at gateway dispatch
Closes Peter's P1 review finding on PR #74134.
The agent-tool-only redirect added in earlier commits left CLI
(`openclaw nodes invoke`), plugin-runtime, and raw `node.invoke` callers
able to skip the file-transfer path policy entirely. The fix moves the
security boundary down to the gateway: every code path that reaches
`node.invoke` for file.fetch / dir.list / dir.fetch / file.write now
runs the same allow/deny check.
- New: src/gateway/file-transfer-dispatch.ts with
`evaluateFileTransferDispatchPolicy` and `isFileTransferCommand`. Same
semantics as the extension-side `evaluateFilePolicy` minus the
operator-prompt flow (prompts stay at the agent-tool layer; the
gateway is silent enforcement).
- src/gateway/server-methods/nodes.ts: after the existing command
allowlist check, run the new gate before forwarding. Denies emit
INVALID_REQUEST with a structured `{ command, code, reason }`.
- Decision matrix mirrors the extension: NO_POLICY (no entry for
this node) deny, denyPaths-wins, '..' traversal short-circuit
(with backslash separator handling), allowPaths match → allow,
no allow match → deny.
- 19 new unit tests covering each branch including identity
resolution (nodeId/displayName/'*'), prototype-pollution-safe lookup,
and read-vs-write allow-list separation.
Note on allow-once approvals: the agent tool's interactive
`allow-once` decision now has to flow through the dedicated tool's
pre-flight (which forwards an approved request); raw `nodes.invoke`
callers cannot benefit from one-time approvals because the gateway is
silent. allow-always (which persists to allowReadPaths/allowWritePaths)
continues to work transparently because by the time the next request
hits the gateway the path is in the persisted allow list.
Tests: 92 extension + 19 gateway = 111 total, all passing.
* fix(file-transfer): enforce node policy in gateway
* fix(file-transfer): use plugin node policy only
* fix(file-transfer): harden node policy edge cases
* fix(file-transfer): close review hardening gaps
* fix(file-transfer): harden node invoke policy
* fix(file-transfer): align runtime dependency versions
* fix(file-transfer): keep minimatch extension-owned
* refactor(file-transfer): remove unused approval gate
* fix(file-transfer): require canonical node policy authorization
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
* fix(clawsweeper): address review for automerge-openclaw-openclaw-74134 (1)
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
* fix(file-transfer): recheck dir fetch archive policy after fetch
* fix(file-transfer): name file-transfer tool in invoke redirect
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: clawsweeper-repair <clawsweeper-repair@users.noreply.github.com>
Preserve the Feishu-local cleanup path while matching the Lark SDK record value shapes: recursively delete default-empty strings, nulls, arrays, and nested text spans, but keep meaningful links, attachments, users, locations, numbers, and booleans.\n\nCarries forward #40602. Thanks @boat2moon.
For 323985f4ca (Val Alexander/@BunsDev): adds a Control UI/exports
entry covering the sidebar-trigger affordance alignment across the
resizable divider, mobile layout, and exported-HTML transcript template.
The other Val/@BunsDev fix (b1c515270e) was already covered by the
existing "Control UI/mobile: persist mobile chat settings" entry.
The rest of the last 24h's missing-CHANGELOG candidates are either:
- already covered by adjacent entries (Shakker manifest auth-evidence
series under "CLI/models: keep manifest auth-evidence credentials
visible", Discord application id + Cloudflare 429 under "Channels/
Discord: cool down Cloudflare/Error 1015 HTML 429", config patch
follow-ups under "Plugins/runtime-deps: add openclaw plugins deps",
etc.);
- internal/test/CI/refactor with no operator surface;
- Clawsweeper-bot self-fixes for already-merged PRs;
- Peter-only with no external collaborator (per the
attribution rule against thanking @steipete).
* fix(models): block stale openai-codex/gpt-5.4-mini inline entries via unconditional suppression (#74451)
Suppress explicitly user-configured openai-codex/gpt-5.4-mini inline entries
so a stale models config written by `openclaw doctor --fix` cannot bypass the
manifest capability block and cause repeated assistant-turn failures when the
runtime switches to that model on ChatGPT-backed Codex accounts.
Adds `unconditionalOnly` flag to `buildManifestBuiltInModelSuppressionResolver`
and a `shouldUnconditionallySuppress` helper. Inside `resolveExplicitModelWithRegistry`,
inline matches are now gated on unconditional suppressions (no `when` clause)
before returning. Conditional suppressions such as the qwen Coding Plan endpoint
guard remain bypassable by explicit user configuration, preserving the existing
`resolves explicitly configured qwen3.6-plus before Coding Plan built-in suppression`
behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(changelog): add missing reporter attribution for #74451 models suppression fix
* docs: credit codex mini suppression contributors
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
* fix(voice-call): close in-flight limiter fail-open on empty remote address
The webhook in-flight limiter (createWebhookInFlightLimiter in
src/plugin-sdk/webhook-request-guards.ts) returns true unconditionally
when tryAcquire is called with an empty key — that is its by-contract
fail-open path used to mean 'caller is opting out of the limiter'.
The voice-call webhook handler reached that path silently: it computed
'req.socket.remoteAddress ?? ""' and passed the empty string straight
into tryAcquire. Whenever req.socket.remoteAddress was absent (closed
socket, edge proxy quirk), the limiter became a no-op and the request
proceeded directly to readBody without any concurrency cap.
Fix: when remoteAddress is missing, log a warning and fall back to a
constant non-empty key ('__voice_call_no_remote__') so all such
requests share one in-flight bucket instead of bypassing the limiter
entirely. The bucket size stays maxInFlightPerKey (default 8), which
is the right defense-in-depth posture against slow-body attacks
arriving with stripped IP info.
Scoped to voice-call only. Other consumers of the SDK helper
(bluebubbles via openclaw/plugin-sdk/webhook-ingress) are not changed
to avoid drive-by edits to plugins this PR does not own. The shared
SDK contract (empty key = bypass) is left as-is and documented
implicitly by the fix's comment block.
The existing 8-concurrent test in webhook.test.ts continues to assert
the limiter engages on the happy path; no new test added since the
private handleRequest path is not unit-test exposed and the change is
two-line auditable from the diff alone.
* test(voice-call): cover missing webhook remote address limiter
* test: align changed package sdk routing
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Tighten Google Vertex ADC manifest evidence to canonical project env vars and canonical ADC fallback paths only.
Local proof:
- OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/agents/model-auth.profiles.test.ts src/plugins/manifest-registry.test.ts src/secrets/provider-env-vars.dynamic.test.ts
- pnpm exec oxfmt --check --threads=1 docs/plugins/manifest.md extensions/google/openclaw.plugin.json src/agents/model-auth-env.ts src/agents/model-auth.profiles.test.ts src/plugins/manifest.ts
- git diff --check origin/main...HEAD
CI note: checks-node-core-support-boundary was red on an unrelated tooling assertion in test/scripts/test-projects.test.ts for packages/sdk/src/index.test.ts routing; that file and scripts/test-projects.mjs are unchanged from origin/main.
* fix(pi-embedded): strip [tool calls omitted] from user-facing text
The internal replay placeholder '[tool calls omitted]' was leaking
into channel output (e.g. Telegram) after aborted tool calls.
Fix: strip the placeholder early in sanitizeUserFacingText so all
channels are protected by default. The replay transcript path in
turns.ts is unaffected — it uses the placeholder internally.
Fixes#74573.
Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
* fix(pi-embedded): preserve whitespace when stripping placeholder
* test(pi-embedded): document replay placeholder sanitization
* fix(pi-embedded): strip consecutive replay placeholders
---------
Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
Adds the focused MCP/process/tool-execution CodeQL security shard and documents it in CI docs.
Proof:
- Branch CodeQL security run https://github.com/openclaw/openclaw/actions/runs/25132942030 passed on 9d8ca2bae7.
- New mcp-process-tool-boundary analysis 1200250367 returned 0 results.
- Branch open CodeQL alerts: none.
- Workflow Sanity, Blacksmith Testbox, Blacksmith Build Artifacts Testbox, and OpenGrep PR Diff passed.
Remove the maintainer PAT fallback from the ClawSweeper dispatch workflow so missing app auth fails closed instead of attributing downstream automation to a human token.
Fail setup-code generation when gateway.remote.url is configured but malformed, instead of falling back to a bind-derived URL and issuing a bootstrap token.
Let route-question searches match people-routing metadata from natural-language prompts, and allow wiki_apply evidence provenance fields that the markdown parser already supports.
Adds six missing entries for commits that landed without their own
CHANGELOG.md update, picked from the last six hours of origin/main and
attributed to the original contributors.
Changes:
- Control UI/i18n locale registry expansion + new docs glossaries
(297f4c6e60, 0126692bf5 by @vincentkoc).
- Gateway/diagnostics opt-in startup timeline (097eed8cd8, d001c3436b,
e69da9d578 by @shakkernerd).
Fixes:
- Matrix `verify confirm-sas` cross-signing close (86956f71e6 by
@nklock; #74542).
- `openclaw status` channel context-window overrides (eb7d89f4b9 by
@HemantSudarshan).
- Sandbox Docker daemon graceful when sandbox mode is off (2dadc82cf4
by @kaseonedge; #73671).
- Control UI mobile chat settings persisted via Lit state (b1c515270e
by @BunsDev).
Skipped Peter-only commits with no external collaborator (per the
maintainer-attribution rule against thanking @steipete) and the model
list auth-index series (already covered by the existing "Models/UI:
hide unauthenticated providers" entry).
* fix: improve error message in optimizeImageToJpeg to include actual error details
* fix: improve error message to include configured input for Model does not support images
* fix(media): surface vision pipeline diagnostics
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(matrix): close owner-side device verification loop on SAS confirm
After SAS confirm via the `openclaw matrix verify confirm-sas` CLI, the
operator's Element X stayed in "Verifying…" because three things on the
bot side did not happen before the verb returned:
1. confirmVerificationSas didn't await the rust-crypto verifier promise.
`Verifier.verify()` resolves only after both sides exchange MACs and
the protocol fully settles, including cross-signing-key uploads
triggered by `crossSignDevice`. Returning early meant Element X's
next /keys/query saw an inconsistent state and the prompt persisted.
2. The 30s auto-confirm path (used when the operator initiates from
their phone) explicitly passed `{ trustOwnDevice: false }`, so the
bot never cross-signed its own device on this path. The check inside
trustOwnDeviceAfterConfirmedSas already gates on isSelfVerification,
so flipping the flag is safe — non-self requests remain a no-op.
3. The standalone `confirmMatrixVerificationSas` action did not call
`trustOwnIdentityAfterSelfVerification` (only the higher-level
`runMatrixSelfVerification` path did). Without that call, the bot
had not signed the operator's master key, so Element X had no path
to clear the prompt without a passive sync tick.
Three additive edits:
- verification-manager.ts (confirmVerificationSas): await
session.verifyPromise after confirmSasForSession returns.
verifyPromise is the .then().catch() chain set by
ensureVerificationStarted, which already routes rejections into
session.error, so awaiting it cannot double-throw.
- verification-manager.ts (maybeAutoConfirmSas): pass
{ trustOwnDevice: true } so the auto-confirm path also cross-signs
the bot device for self-verifications.
- actions/verification.ts (confirmMatrixVerificationSas): mirror the
trustOwnIdentityAfterSelfVerification call from
completeMatrixSelfVerification when the returned summary indicates
isSelfVerification.
Tests:
- verification-manager.test.ts: flipped the existing "auto-confirmed
self-verification" assertion (now expects trustOwnDeviceAfterSas to
be called); added two new tests for verifyPromise await and
rejection-on-summary.error.
- actions/verification.test.ts: two new tests asserting
confirmMatrixVerificationSas calls trustOwnIdentityAfterSelfVerification
on self-verifications and not on remote verifications.
Verified end-to-end against matrix.thepolycule.ca (Synapse 1.145.0+ess.1,
MAS-fronted): after `verify confirm-sas`, Element X's device-list view
shows the bot device with a green shield and no pending Verify prompt.
* fix(matrix): guard owner trust after failed SAS verification
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(security): resolve model aliases before audit classification
Before classification, model strings are now resolved through the alias
index so that configured aliases (e.g. 'gpt-prev') are translated to
their canonical provider/key form (e.g. 'openai/gpt-5.4') before hygene
and tier checks run.
Fixes#74455.
Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
* fix(security): share audit model alias resolution
---------
Signed-off-by: Blasius Patrick <blasius.patrick@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Move the mobile chat settings dropdown open state into Lit-owned app state.
- Render the dropdown open class and ARIA disclosure attributes from state.
- Add Escape, outside pointer, tab-change cleanup, and focus restoration.
- Cover closed/open render state and mounted app dismissal flows with browser tests.
Validation:
- pnpm test ui/src/ui/app-render.helpers.browser.test.ts ui/src/ui/navigation.browser.test.ts
- pnpm exec oxfmt --check --threads=1 ui/src/ui/app.ts ui/src/ui/app-view-state.ts ui/src/ui/app-render.helpers.ts ui/src/ui/app-render.helpers.browser.test.ts ui/src/ui/navigation.browser.test.ts
- node scripts/run-oxlint.mjs --tsconfig tsconfig.oxlint.core.json ui/src/ui/app.ts ui/src/ui/app-view-state.ts ui/src/ui/app-render.helpers.ts ui/src/ui/app-render.helpers.browser.test.ts ui/src/ui/navigation.browser.test.ts
Previously, models from unconfigured providers were shown with an
"auth missing" hint, flooding the picker with 900+ unusable entries.
Now addModelSelectOption early-returns when the provider has no auth,
so only usable models appear in /models and the web chat dropdown.
Fixes#74423
* feat(security): add GHSA detector-review pipeline and OpenGrep CI workflows [AI-assisted]
Stand up an end-to-end pipeline that turns every published openclaw GitHub
Security Advisory into a reusable OpenGrep rule, and wire the compiled rules
into manual-dispatch GitHub Actions workflows that publish SARIF to GitHub
Code Scanning.
The pipeline is harness-agnostic: any coding-agent CLI (Rovo Dev, Claude
Code, Codex, OpenCode, or anything you can shell out to) can drive it via
the runner script's --harness flag. Built-in adapters cover the four common
harnesses; --harness-cmd '<template>' supports anything else with shell-style
{prompt}/{model}/{output_file} substitution.
Pipeline pieces:
- scripts/run-ghsa-detector-review-batch.mjs runs your chosen coding harness
in parallel against every advisory using the agent-agnostic detector-review
spec at security/detector-review/detector-review-spec.md. Each case
produces an opengrep general-rule.yml (precise) and broad-rule.yml
(review-aid), plus a coverage-validated report against the vulnerable
commit's changed files.
- scripts/compile-opengrep-rules.mjs walks a run directory, rewrites each
rule's id to ghsa-detector.<ghsa>.<orig-id>, injects ghsa/advisory-url/
detector-bucket/source-rule-id metadata, and uses opengrep itself to drop
rules with InvalidRuleSchemaError so the published super-configs load
cleanly.
Compiled outputs:
- security/opengrep/precise.yml (336 rules)
- security/opengrep/broad.yml (459 rules)
- security/opengrep/compile-manifest.json (per-rule provenance map)
CI workflows (manual workflow_dispatch only):
- .github/workflows/opengrep-precise.yml
- .github/workflows/opengrep-broad.yml
Both install a pinned opengrep, run opengrep scan against src/, upload SARIF
to Code Scanning under categories opengrep-precise / opengrep-broad, and use
continue-on-error: true so findings never block the workflow.
Detector-review spec and assets:
- security/detector-review/detector-review-spec.md the agent-agnostic spec
the runner injects into each per-case prompt
- security/detector-review/references/{detector-rubric,report-template}.md
- security/detector-review/scripts/init_case.py
- security/prompt-suffix-coverage-first.md mandatory prompt addendum that
enforces coverage-first validation (rule must catch the OG vuln, not just
pass synthetic fixtures)
Docs:
- security/README.md end-to-end flow, supported harnesses, regen recipe
- security/opengrep/README.md compiled-config details + recompile recipe
* security: tighten GHSA OpenGrep detector workflow
* chore: refine precise opengrep workflow
* chore: remove stale opengrep metadata
* fix: harden GHSA OpenGrep workflow
* ci: split OpenGrep diff and full scans
* chore: remove performance-only opengrep rule
* ci: use OpenGrep installer path
* chore: enforce opengrep rule metadata provenance
* chore: generalize opengrep rule compilation
* docs: align opengrep rulepack guidance
* chore: support generic opengrep rule sources
* fix: validate opengrep rulepack-only changes
---------
Co-authored-by: Jesse Merhi <security-engineering@atlassian.com>
* feat(nvidia): add NVIDIA provider with onboarding flow
Add the NVIDIA build.nvidia.com API as a bundled provider. Default model
is nvidia/nvidia/nemotron-3-super-120b-a12b: first segment is the provider
id, remaining "nvidia/nemotron-3-super-120b-a12b" is the literal upstream
model id (which happens to start with "nvidia/" because NVIDIA is also the
model maker).
Supporting core change: introduce a provider capability flag
nativeIdsIncludeProviderPrefix so providers whose native catalog ids
intentionally include their provider prefix (OpenRouter) opt into self-prefix
dedupe in modelKey, without hardcoding provider names in core. Providers
whose ids merely happen to start with their own name (NVIDIA) leave the flag
unset and get the full <provider>/<model-id> concatenation.
- extensions/nvidia/*: new plugin, catalog, onboarding, tests, docs
- extensions/openrouter/index.ts: declare nativeIdsIncludeProviderPrefix
- src/plugins/types.ts: add field to ProviderPlugin
- src/plugins/registry.ts: populate self-prefix set on registration
- src/agents/provider-self-prefix.ts: sync accessor used by modelKey
- src/agents/model-ref-shared.ts: modelKey consults the flag
- test updates for affected surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(model-picker): simplify literal-prefix display to label-only
* fix(model-picker): pass workspaceDir/env to allowlist literal-prefix resolution
* chore: untrack generated baseline JSON artifacts (gitignored)
* fix(nvidia): show literal model ref in picker and onboarding notes
* fix(nvidia): show hint whenever display label differs from stored config
* fix(nvidia): drop redundant hint from Keep current label
* fix(nvidia): restore literal double-prefix display labels
* fix(picker): handle literal-prefix fast path
* fix(picker): show literal keep label
* fix(docs): update nvidia provider docs
* fix(nvidia): update test helper imports
* fix(changelog): add nvidia provider entry
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Prevent hidden channel lifecycle runs from staying stuck as running
Hidden channel-routed runs were dropping session keys on lifecycle events at
our shared agent-event bus. Gateway lifecycle persistence then had to rely on
run-context lookup surviving until the terminal event, which is unnecessarily
fragile for the exact sessions that are intentionally hidden from Control UI.
This keeps session keys on hidden lifecycle events only, preserving the existing
privacy boundary for assistant/tool traffic while making terminal session-state
persistence explicit and test-covered.
Constraint: Hidden channel runs must stay out of Control UI chat/tool streams
Rejected: Broaden sessionKey preservation to every hidden event | would expose more hidden traffic than needed
Confidence: medium
Scope-risk: narrow
Reversibility: clean
Directive: If hidden-run event redaction changes again, keep lifecycle persistence independent from ephemeral run-context lookup
Tested: pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/infra/agent-events.ts src/infra/agent-events.test.ts; pnpm tsgo:core; pnpm tsgo:extensions; pnpm tsgo:core:test; pnpm tsgo:extensions:test; pnpm test src/infra/agent-events.test.ts; pnpm test src/gateway/server-chat.agent-events.test.ts; pnpm test src/gateway/session-lifecycle-state.test.ts; pnpm lint:extensions:bundled; codex exec review returned ship it
Not-tested: Live gateway reproduction against Knox's local stuck-session install
* Clarify hidden lifecycle redaction and cover context fallback
The follow-up review asked for two things: document why the separate error
stream stays redacted for hidden runs, and cover the registered-context fallback
branch for hidden lifecycle events when callers omit sessionKey.
Constraint: Hidden assistant/tool/error diagnostics must remain redacted from Control UI
Rejected: Preserve sessionKey on the generic error stream | terminal persistence already flows through lifecycle phase:error, so widening the visible identity surface is unnecessary
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep hidden-run identity exceptions tightly scoped to terminal lifecycle persistence unless a concrete downstream consumer requires more
Tested: pnpm exec oxfmt --write --threads=1 src/infra/agent-events.ts src/infra/agent-events.test.ts; pnpm test src/infra/agent-events.test.ts; pnpm test src/gateway/server-chat.agent-events.test.ts; pnpm test src/gateway/session-lifecycle-state.test.ts
Not-tested: Full repo gate rerun; previous branch-wide gates remain from the parent PR commit
* fix(gateway): keep hidden agent broadcasts redacted
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
`shouldRemoveRuntimeDepsLock` previously trusted `isAlive(owner.pid)`
alone when deciding whether a lock could be reclaimed. That works fine
on a normal host: when the writer dies the PID is gone and `isAlive`
returns false. Inside Docker it does not — every Node gateway process
runs as PID 1 (or PID 7 with `init: true`) in its container PID
namespace, so a stale lock left behind by a previous incarnation looks
"alive" to the new one. The 5-minute lock-wait timeout then fires and
the supervisor restarts, and the cycle repeats indefinitely. Operators
have to manually remove `.openclaw-runtime-deps.lock` to recover.
This change records `pidStartTimeMs` alongside `pid` and `createdAtMs`
when the lock is acquired, and consults it in the staleness check.
When both sides have start-time evidence and they disagree, the lock
is treated as stale; otherwise the existing PID-alive-means-fresh
behavior is preserved exactly. The capture point uses
`Date.now() - process.uptime() * 1000` once at module load, and the
read side uses `/proc/<pid>/stat` field 22 on Linux (returning null
elsewhere so legacy semantics still apply on macOS/Windows hosts).
This is strictly additive on the wire format and the predicate:
existing lock files without `pidStartTimeMs` continue to take the same
code path they did before, and platforms that cannot resolve a live
PID's start-time fall back to the same legacy behavior.
Refs #74346.
Make the topbar OpenClaw breadcrumb a semantic Overview link, wire the existing navigate event at the app shell, and preserve prefixed Control UI base paths.\n\nValidation:\n- pnpm test ui/src/ui/navigation.browser.test.ts\n- pnpm exec oxfmt --check --threads=1 ui/src/ui/components/dashboard-header.ts ui/src/ui/app-render.ts ui/src/ui/navigation.browser.test.ts\n- git diff --check origin/main...HEAD
Render the command palette as a native modal dialog with labelled combobox/listbox semantics, stable active-descendant wiring, and guarded close behavior.\n\nValidated with targeted command palette tests and formatter checks.
DeepSeek models had no provider-policy-api.ts, so materializeRuntimeConfig
filled contextWindow with DEFAULT_CONTEXT_TOKENS (200k) and cost with zeros
for all DeepSeek models. This caused premature session compaction at ~125k
instead of using the full 1M window, and zero-cost display for v4 models.
Add a normalizeConfig surface that hydrates missing contextWindow, maxTokens,
and cost from the bundled DeepSeek model catalog for matching model ids.
Explicit user overrides are preserved.
Fixes#74245
Make the chat sidebar divider accessible and input-method agnostic.\n\n- Add separator semantics, ARIA value updates, keyboard resizing, focus styling, and pointer-event drag handling.\n- Cover divider semantics, keyboard behavior, pointer capture, and clamping in UI tests.\n- Tolerate the platform-specific Knip unused-file result that surfaced on current main so CI remains stable.
Summary:
- Make browser-local assistant avatar overrides win over stale missing IDENTITY.md avatar metadata.
- Show the selected assistant image in Personal settings and chat instead of a false File not found state.
- Add focused Control UI coverage for assistant avatar override and clear behavior.
Validation:
- pnpm test ui/src/ui/app-render.assistant-avatar.test.ts ui/src/ui/views/config-quick.test.ts ui/src/ui/controllers/assistant-identity.test.ts -- --reporter=verbose
- pnpm tsgo:core:test
- pnpm deadcode:dependencies
- pnpm deadcode:unused-files
- CI green on PR #74260
Adds focused regression coverage for dead owner PID runtime-deps install locks so stale lock recovery remains PID-first and does not wait on age when the recorded owner process is gone.
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
- docs/concepts/active-memory.md: extend the "Useful tuning fields" config
table with the new `config.circuitBreakerMaxTimeouts` and
`config.circuitBreakerCooldownMs` keys (with their schema-declared ranges
and defaults) added by 89cd2b6362, so operators tuning Active Memory
recall after consecutive timeouts can find the knobs alongside
`cacheTtlMs`.
- docs/plugins/memory-lancedb.md: extend the "Commands" section with the
new `openclaw memory query` subcommand 6b44dce0c8 registered when
memory-lancedb is the active memory plugin, including the `--cols`,
`--filter`, `--limit`, and `--order-by` options and the safety bounds
(200-character filter cap, sanitized character allowlist, positive
integer limit, in-memory order-by).
Extend MIRRORED_CORE_RUNTIME_DEP_NAMES from ["semver", "tslog"] to
also include @agentclientprotocol/sdk, @lydell/node-pty, croner,
dotenv, jiti, json5, jszip, markdown-it, tar, and web-push.
These are all declared as direct dependencies in the openclaw root
package.json and imported by core source code (src/acp/*, src/cron/*,
src/config/*, src/infra/{archive,backup,dotenv,push-web}.ts,
src/markdown/ir.ts, src/plugin-sdk/root-alias.cjs,
src/plugins/jiti-loader-cache.ts, src/process/supervisor/adapters/pty.ts,
etc), but the existing collectMirroredPackageRuntimeDeps allowlist only
covered semver and tslog.
The dynamic collectRootDistMirroredRuntimeDeps scan does pick up
imports that have an extension package.json owner (for example
memory-core declares chokidar, matrix declares jiti and markdown-it).
For deps with no extension owner, or for setups where the owning
extension is not enabled, those imports never make it into the
runtime-deps mirror and Node fails to resolve them at runtime, e.g.:
Cannot find package 'chokidar' imported from
.../plugin-runtime-deps/openclaw-<ver>/dist/qmd-manager-...js
Also add a static drift guard test that walks src/ for value imports of
root-package runtime deps and fails when one is neither in
MIRRORED_CORE_RUNTIME_DEP_NAMES nor declared by any extension's
package.json (with an explicit allowlist for known-transitive or
build/type-only imports such as chalk, ipaddr.js, file-type,
proxy-agent, typescript, qrcode). The guard caught @lydell/node-pty
during this change.
Refs #74199.
* fix(memory): add LIKE fallback when FTS5 MATCH throws and log silent search errors
When searchKeyword FTS5 MATCH fails (e.g. unicode61 tokenizer rejects
certain query patterns), the search now falls back to a LIKE-based query
instead of silently returning zero results. The four .catch(() => [])
sites in the search orchestrator now log warnings so failures are
visible in diagnostics.
Fixes#74036
* fix(memory): split LIKE fallback into per-token clauses and log MATCH errors
* fix(agents): recognize flat JSON billing payloads and snake_case error codes
Two independent fixes for billing error detection:
1. isErrorPayloadObject/parseApiErrorInfo now recognize flat JSON like
{"error":"string_code","message":"..."} where error is a string code
at the top level, not just nested {"error":{"type":"...","message":"..."}}
envelopes.
2. isBillingErrorMessage now matches "insufficient_balance" (underscore)
and "Insufficient MBT balance" (one word between insufficient/balance)
via two new patterns in the billing pattern list.
Together these prevent raw JSON from leaking to user-facing chat when
providers return 402-style flat payloads.
Fixes#74079
* fix(agents): remove redundant billing pattern and fix misleading regex comment
Adds a Vercel AI Gateway provider thinking-profile resolver for trusted OpenAI and Anthropic upstream refs, preserving catalog compat fallback for unsupported/base-only refs.
Includes provider tests, docs, and changelog coverage. Supersedes #41561.
Co-authored-by: Zcg2021 <80769518+Zcg2021@users.noreply.github.com>
* fix(tui): clear stale streaming after unbound final events
* fix(clownfish): address review for ghcrawl-156749-autonomous-smoke (1)
* fix(tui): address stale streaming review
Repair WhatsApp group inbound recovery after repeated reconnect churn while keeping the fallback scoped to reconnect metadata.
Canonical issue: #66920. Related evidence: #7433, #63855, #70856.
Thanks to legonhilltech-jpg, octopuslabs-fl, Kanorin-chan, and stuswan for the reports and reproduction details.
Add reasoningDefault support under agents.defaults and preserve the existing per-agent/session/inline override order.
Includes authorization gating for configured reasoning state, /status coverage, config schema/docs baseline updates, and regression tests for the reply and status paths. Also carries the related cron startup-run preservation fix and CI test stabilization needed for this PR branch.
Validated locally with pnpm check:changed, the focused Vitest bundle for touched gateway/cron/auto-reply/plugin-sdk/tooling tests, pnpm config:docs:check, and git diff --check. GitHub checks are green on the merged head; Greptile latest visible review is 4/5 with no P0/P1 findings.
Introduce a native dialog-backed Control UI modal primitive and migrate the exec approval, gateway URL confirmation, and dreaming restart confirmation prompts to it.
The modal primitive provides aria-modal semantics, shadow-root-local labels/descriptions, focus trapping, safe initial focus, Escape cancellation, and focus restoration while preserving the existing prompt content and decision semantics.
Validation:
- pnpm lint --threads=8
- pnpm --dir ui test src/ui/components/modal-dialog.test.ts src/ui/views/exec-approval.test.ts src/ui/navigation.browser.test.ts
- pnpm test:ui
- pnpm exec oxfmt --check --threads=1 ui/src/ui/components/modal-dialog.ts ui/src/styles/config-quick.test.ts
- git diff --check
CI note: checks-node-core-support-boundary is failing in test/scripts/docker-build-helper.test.ts on an unrelated package-acceptance assertion; the failing files are identical to origin/main and outside this UI-only PR.
## Summary
- Addresses the remaining Gateway RSS/session-accumulation path tracked by #54155.
- Narrows the fix to the structuredClone/session-store cache memory growth described in #45438.
- Preserves prior report context from #57699, #62717, #66886, #69977, and #70717 as validation evidence.
## Validation
- pnpm -s vitest run src/config/sessions/store.pruning.test.ts src/config/sessions/store.pruning.integration.test.ts src/gateway/sessions-resolve-store.test.ts
- pnpm check:changed
## Credit
Thanks @the-lobsternaut for #54155 and @markus-lassfolk plus the #45438 commenters for isolating the structuredClone/native-memory behavior.
ProjectClownfish replacement details:
- Cluster: ghcrawl-156648-autonomous-smoke
- Source PRs: none
- Credit: Credit #54155 reporter @the-lobsternaut for the multi-day Gateway RSS/session-accumulation report.; Credit #45438 reporter @markus-lassfolk and commenters for isolating the structuredClone/session-store native-memory path.; Preserve prior closed-report context from #57699, #62717, #66886, #69977, and #70717 in the PR body as reproduction evidence, not as new close targets.
- Validation: pnpm -s vitest run src/config/sessions/store.pruning.test.ts src/config/sessions/store.pruning.integration.test.ts src/gateway/sessions-resolve-store.test.ts; pnpm check:changed
Fix the Control UI Set Default action to persist agents.list[].default instead of the unsupported agents.defaultId config key.\n\nCloses #65565.\n\nThanks @luyao618.
Add the existing desktop cron-session visibility toggle to the mobile chat settings dropdown, reusing the shared session filtering state and cron filter icon path.
Also add focused browser render coverage for the mobile dropdown so the cron filter button, hidden-count title, active/pressed state, and click behavior are covered.
Validated:
- pnpm exec oxfmt --check --threads=1 ui/src/ui/app-render.helpers.browser.test.ts
- pnpm test ui/src/ui/app-render.helpers.browser.test.ts ui/src/ui/app-render.helpers.node.test.ts
- pnpm lint --threads=8
Thanks @luzhidong.
Fail Discord startup closed when the bot identity cannot be resolved, and keep mention gating active when configured mention patterns can still detect required mentions without a bot id.\n\nFixes #42219. Carries forward source PRs #46856 by @education-01 and #49218 by @BenediktSchackenberg. #46847 was already closed as a duplicate; #42675 was security-routed separately and left out of the replacement source.
* fix: Found one bug in the new compile-cache prune path: it removes a d
* fix(postinstall): keep compile cache pruning resilient
---------
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
For 7b07a0ab8f: the Tencent Yuanbao bot was added to docs/channels/index.md
and docs/docs.json with that SHA, but the root README.md "Supported
channels include" line still listed all the other Chinese-platform
channels (WeChat, QQ) without Yuanbao. Adds it before WebChat so the
README reflects the same channel surface as the docs.
* 'main' of https://github.com/openclaw/openclaw:
fix: exclude test support from raw fetch guard
fix(ollama): preserve aborts with stream timeouts
ci: require maintainer permission for command reactions
docs(hooks/bundled/readme): cover session compaction and message events
refactor: share docker e2e harness runner
fix: keep browser test fetch out of runtime scan
The bundled hooks README listed only command/agent/gateway events and
ended with a stale "More event types coming soon (session lifecycle,
agent errors, etc.)" line, but production code now triggers:
- session:compact:before / session:compact:after via
src/agents/pi-embedded-runner/compaction-hooks.ts
- message:received via src/auto-reply/reply/dispatch-from-config.ts
- message:sent via src/infra/outbound/deliver.ts
Updates the "Event Types" list with the four real production event
names, drops the stale coming-soon line, and aligns the InternalHookEvent
interface example with the actual InternalHookEventType union (adds
"message" and refreshes the action examples). HOOK.md authors that target
session lifecycle or message routing now have a real surface to subscribe
to instead of relying on tribal knowledge or the type definitions.
For bdba90a20b: apps/ios/README.md "What Works Now (Concrete)" section
omitted the authenticated background `node.presence.alive` beacon
feature that shipped on iOS first, even though apps/android/README.md
already lists it on the rebuild checklist. Adds a matching bullet so
the iOS README reflects the gateway last-seen metadata update path
across foreground/background transitions.
Fix Telegram portrait video distortion by probing video dimensions through the shared media helper and passing width/height to sendVideo.
Validation:
- Targeted Telegram/media tests passed locally.
- Plugin SDK API baseline check passed locally.
- Formatter and git diff whitespace checks passed locally.
CI note: current boundary drift observed on prior run came from existing src/plugin-sdk/discord.ts and src/plugin-sdk/telegram-account.ts, not this PR diff.
Fixes#73621.
Preserve queued Control UI chat messages across in-UI session switches by saving the active queue per session before reset and restoring it when switching back. Route the overview session selector through the shared switchChatSession helper so it follows the same queue lifecycle.
Validation:
- OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test ui/src/ui/app-render.helpers.node.test.ts
- pnpm tsgo:test:ui
- pnpm exec oxfmt --check --threads=1 ui/src/ui/app-render.helpers.node.test.ts ui/src/ui/app-render.helpers.ts ui/src/ui/app-render.ts ui/src/ui/app-view-state.ts ui/src/ui/app.ts
Fix startup and per-turn provider registry hot paths by keeping primary-model startup discovery on metadata-only provider entries and by keeping capability provider fallback loads scoped to manifest-derived owners, including explicit empty scopes when no bundled owner exists.
Evidence:
- Reproduces the reported code paths from #73729, #73835, and #73793: startup prewarm was able to enter provider/model discovery that loaded plugin runtime, and capability lookups could bypass active registry reuse or broaden fallback registry loads.
- Fix threads providerDiscoveryEntriesOnly through models-config planning into plugin discovery.
- Fix reuses active non-memory/non-speech capability providers even with explicit plugins.entries.
- Fix keeps fallback registry loads scoped with onlyPluginIds, including [] for no-owner media capability checks.
- Local targeted tests passed for gateway startup, models config, provider discovery, capability providers, and web provider runtimes.
- Testbox pnpm check:changed passed.
- Testbox pnpm build passed.
- GitHub CI required checks passed on e5e6fe1d52.
Fixes#73729.
Fixes#73835.
Fixes#73793.
Supersedes #73794.
Fixes openclaw#73559. Extracts a shared wrapEmbeddedAgentStreamFn helper and applies it to both provider-owned and boundary-aware fallback paths in resolveEmbeddedAgentStreamFn, forwarding the resolved OAuth bearer (resolvedApiKey → authStorage → options.apiKey) and run abort signal so models routing through openai-codex-responses and other boundary-aware transports stop failing with 401 Missing bearer auth header.
Adds two missing changelog entries for previously merged fixes that
landed without their own CHANGELOG.md updates:
- Gateway/readiness covers 75ba8398f9 (`fix(gateway): expose event loop
health in readiness`), which adds a new `eventLoop` block (p99/max
delay, utilization, CPU core ratio, `degraded` flag) to authenticated
`/readyz` responses. The same SHA already documented the surface in
docs/cli/gateway.md but had no changelog line.
- CLI/update covers 09cb0b0e64 (`fix(cli): ignore stale memory cleanup
after package update`), which moves the memory-state import inside
the best-effort teardown try/catch so hashed-chunk replacement during
`openclaw update` no longer surfaces as exit-time errors.
No changelog backfill for 68ef37011e (Ollama unused destructure cleanup —
no user-facing change), 1f41b8b44b (already covered by the
"Gateway/reload: bound default restart deferral" entry), df9d26eb43 and
d55c7ea997 (jointly covered by the existing "Active Memory: register
the prompt-build hook with the configured recall timeout" entry), or
the gauntlet/CI/QA-test commits which are internal infrastructure with
no end-user behavior change.
Suppress raw failed edit/write warning payloads when the assistant already delivered a user-facing error reply for the same turn, while keeping the fallback warning for unresolved, ambiguous, or success-looking mutating failures.
Fixes#39631.
Refs #51065, #39636, #39717, and #39406.
Validation:
- Testbox tbx_01kqbqxw1yqpyyxb25vvjkrc90: OPENCLAW_TESTBOX=1 pnpm test:serial src/agents/pi-embedded-runner/run/payloads.errors.test.ts
- Testbox tbx_01kqbqxw1yqpyyxb25vvjkrc90: OPENCLAW_TESTBOX=1 pnpm check:changed
- CI run 25086475010: success on ea33538add
- Parity gate run 25086474949: success on ea33538add
Bias group-chat prompt composition toward using subagents for tool-heavy work, keeping maintainer-channel responsiveness higher.\n\nValidated locally with focused prompt/auto-reply tests before opening the PR.
Adds a narrow CodeQL Critical Quality shard for the Control UI/control-plane surface and fixes the custom-theme font-family ReDoS finding discovered by the new shard.
Adds a Slack attachment vision reference covering downloaded media handling, PDF/file limits, thread-starter media fallback, multi-attachment behavior, and known troubleshooting cases.
Fixes#51355
Thanks @haroldfabla2-hue.
* fix(onboarding): skip redundant install prompt when only one source exists
When the channel-setup flow asks 'Install <plugin>?' after the user has
already picked the channel in the previous menu, and the only real
install source available is npm (or local), the prompt degenerates into
'<that source> vs Skip'. The user already expressed intent by picking
the channel, so re-confirming adds friction without offering a
meaningful choice.
Resolve directly to the available source in that case. Keep the prompt
when both npm and local sources exist so the user can still pick which
to use, and keep it when no real source exists (the prompt then only
offers Skip, which is informative).
* fix ci
* fix ci
* fix(channel-setup): skip redundant install prompt when only one source exists
Add autoConfirmSingleSource opt-in parameter to promptInstallChoice /
ensureOnboardingPluginInstalled / ensureChannelSetupPluginInstalled.
When set and only one real install source (npm or local, not both)
exists, the 'Install <plugin>? / Skip' prompt is skipped and the
single source is used directly.
Only channel-setup.ts passes autoConfirmSingleSource: true — the user
already expressed intent by picking the channel in the previous menu,
so re-confirming adds friction without a meaningful choice. The
onboarding and quickstart entry points keep the existing prompt
behavior unchanged.
Also fix findBundledPluginSourceInMap mock type in
onboarding-plugin-install.test.ts to avoid TS2345.
* fix(tests): revert auto-confirm test expectations and fix mock leak
- Revert 'offers registry npm specs' test to expect the prompt
(autoConfirmSingleSource not passed)
- Revert channel-setup 'does not default to bundled local path' test
to expect the prompt
- Reset findBundledPluginSourceInMap and
resolveBundledInstallPlanForCatalogEntry mocks after the bundled
prompt test to prevent cross-test leakage
* fix ci
* docs(changelog): add #73419
* fix(logs): find active log file across date boundaries
Fixes#42875
When gateway runs across midnight, openclaw channels logs was looking
for today's log file instead of the active one. This change makes
the CLI find the most recently modified log file as a fallback.
(cherry picked from commit fba6b88e8644365360f82802cbe25039a091409d)
* fix(channels): resolve active log file for channel logs
(cherry picked from commit ee87397a4323f04fdd37a2fc136de02e648a92d5)
---------
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
For 054b2e1b7e: docs/install/docker.md "Storage and persistence" now
records that the bundled docker-compose.yml falls back to
${HOME}/.openclaw (and ${HOME}/.openclaw/workspace for the workspace
mount), or /tmp/.openclaw when HOME is also unset, when
OPENCLAW_CONFIG_DIR / OPENCLAW_WORKSPACE_DIR are not provided. That
matches the new default expressions in the compose file and prevents an
empty-source volume spec on bare environments.
* test(ci): route plugin prerelease coverage to plugin shard
* test(ci): add plugin prerelease suite to CI
* fix(ci): preserve pnpm path in plugin prerelease shard
* fix(ci): avoid inheriting secrets for plugin prerelease suite
For 771846c5fa: docs/providers/bedrock.md "Advanced configuration" now
includes a "Claude Opus 4.7 temperature" accordion describing that
OpenClaw automatically omits `temperature` for Opus 4.7 Bedrock refs
(foundation model ids, named profiles, application inference profiles
whose underlying model resolves to Opus 4.7, and dotted `opus-4.7`
variants with regional prefixes), since Bedrock rejects the parameter on
that model. The fix has no user-facing knob, but Opus 4.7 Bedrock users
need to know the request shape changes silently.
Three findings from the second pass:
1. **MEDIUM — Cross-chat short message ID guard bypassed on empty chat
context (CWE-285).** When `requireKnownShortId=true` and `chatContext`
was missing or `{}`, `resolveBlueBubblesMessageId` would still resolve
the short id. Short ids are allocated from a single global counter
across every account and chat, so an action call without a chat
scope could silently apply to the wrong conversation. Throw "requires
a chat scope" instead. The previous behavior was an explicit
"fail-open" choice with a comment acknowledging the risk; the
underlying assumption (downstream call carries chatGuid) does not
hold for every action handler. Test rewritten to expect fail-closed.
2. **LOW — Unsanitized messageId reflected in cross-chat guard error
(CWE-117 / CWE-200).** The thrown error embedded the raw inputId
(and the raw chatGuid / chatIdentifier from the cached entry until
the previous pass). Replace the inputId with a shape descriptor
(`<short:N-digit>` or `<uuid:prefix…>`) so cross-chat errors no
longer leak any concrete identifier. Combined with the chat
identifier redaction in describeChatForError (already in place),
the error is fully redacted.
3. **LOW — PII exposure via verbose logs (CWE-532).** Untrusted webhook
identifiers (senderId / messageId / action) were already passed
through `sanitizeForLog`, but the helper only stripped control
characters — it did not redact secrets such as `?password=` query
strings or `Authorization: Bearer …` headers that occasionally
bleed into error chains. Extend `sanitizeForLog` to redact those
patterns. All call sites benefit immediately.
Four findings on this PR, all addressed in this commit:
1. **Cross-chat guard bypass when ctx.chatGuid present but cached lacks chatGuid**
(CWE-697). Earlier `isCrossChatMismatch` gated chatIdentifier and chatId
fallback comparisons on `!ctxChatGuid`, which let any non-empty
ctx.chatGuid suppress the fallback checks when the cached entry happened
to lack chatGuid — letting a short id from chat A be reused while acting
in chat B. Rewrite the function so chatIdentifier/chatId comparisons
run independently based on availability on each side, not on whether
ctx.chatGuid happens to be present.
2. **Sensitive chat identifiers exposed via thrown cross-chat error**
(CWE-200). `describeChatForError` interpolated raw chatGuid /
chatIdentifier / chatId into the error message — these can leak phone
numbers / email addresses / chat GUIDs into agent transcripts, tool
results, remote channel deliveries, or third-party log aggregators.
Surface only the *shape* of the chat target with `=<redacted>` values.
3. **Group reaction drop-guard bypass via whitespace chatIdentifier**.
Earlier guard treated "" as missing but accepted " " / "\t". Trim
chatGuid/chatIdentifier before the missing-check so a webhook sender
supplying whitespace cannot satisfy the guard and have peerId degrade
to the literal "group".
4. **Log injection via webhook senderId/messageId in verbose log lines**
(CWE-117). Untrusted webhook fields were interpolated directly into
`logVerbose` calls without sanitization, allowing log forging if a
sender carried CR/LF/control bytes. Wrap with the existing
`sanitizeForLog()` helper at all such sites.
Test updates: monitor-reply-cache.test.ts cross-chat error assertions
now expect `chatGuid=<redacted>` instead of raw values.
resolveBlueBubblesOutboundSessionRoute classified all `chat_guid:`
prefixed targets as groups:
const isGroup =
parsed.kind === "chat_id" ||
parsed.kind === "chat_guid" ||
parsed.kind === "chat_identifier";
But BlueBubbles also encodes DM chatGuids in the same `chat_guid:`
form — they look like `iMessage;-;+15551234567` (the `;-;` separator
is the DM marker; groups use `;+;`). Treating those as groups gave
the same DM two different sessionKeys depending on how the caller
addressed it:
- handle form (`bluebubbles:imessage:+15551234567`)
→ peer.kind = "direct", from = `bluebubbles:+15551234567`
- chat_guid form (`bluebubbles:chat_guid:iMessage;-;+15551234567`)
→ peer.kind = "group", from = `group:iMessage;-;+15551234567`
When a bound DM session was looked up against the second form, no
binding matched and the outbound landed in a freshly-synthesized
"group" sessionKey — a degenerate session that the next inbound
message also failed to find, surfacing the conversation in the
wrong place.
Use resolveGroupFlagFromChatGuid (already used by monitor-normalize
to read the same marker for inbound webhooks) so both directions
agree on what counts as a group. Unknown chatGuid shapes still
fall back to "group" to preserve prior behavior — we never
silently downgrade a real group to direct.
Tests: extensions/bluebubbles/src/session-route.test.ts (new)
- chat_guid `;-;` → direct
- chat_guid `;+;` → group
- chat_guid with no recognizable marker → group (back-compat)
- handle target → direct
- chat_id / chat_identifier → group (unchanged)
- DM addressed two ways converges on the same peer kind
Local patch for upstream consideration. Latent bug introduced by
0f7cd59824 (BlueBubbles: move outbound session routing behind plugin
boundary), not commonly hit because most outbound DM call sites use
the handle form, but a real foot-gun for callers that pass the
chat_guid form.
processReaction's peerId calculation:
const peerId = reaction.isGroup
? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
: reaction.senderId;
reads as "if it's a group with at least one chat hint, use that hint;
otherwise fall through to either the literal string 'group' (group case)
or the sender id (DM case)". Two failure modes hide here:
1. BlueBubbles fires a `message-reaction` event with `isGroup: true` but
omits chatGuid AND chatId AND chatIdentifier — peerId becomes the
literal "group" and resolveBlueBubblesConversationRoute synthesizes
a session key unrelated to any real binding. The reaction surfaces in
whatever session the binding fallback picks, never the right one.
2. The same payload arrives with isGroup misclassified as false (BB's
group-flag inference relies on chatGuid, explicit isGroup, or
participants > 2 — none of which are guaranteed for reaction events;
monitor.webhook.test-helpers.ts even ships a default reaction fixture
with no chatGuid and isGroup defaulted to false). peerId then becomes
reaction.senderId and the event is enqueued into the sender's DM
session — the group tapback shows up inside an unrelated 1:1
transcript Chris was looking at.
Neither outcome is recoverable without a chat hint — without chatGuid,
chatId, or chatIdentifier we cannot identify which group the reaction
belongs to. Drop the event with a verbose-log and let the agent miss
that reaction rather than route it incorrectly. DM reactions (which
legitimately may arrive with no chat hint and only a sender) keep
working because the guard is gated on `reaction.isGroup === true`.
A latent risk remains: if BB ever sends an isGroup-misclassified-as-false
payload, this guard does not catch it. That would require teaching
normalize to surface group-flag confidence, which is a larger change
left for follow-up.
Tests (extensions/bluebubbles/src/monitor.test.ts):
- Group reaction with no chat identifiers → not enqueued
- Group reaction with at least one chat identifier → still enqueued
(regression sentinel for the new guard)
Local patch for upstream consideration.
The cross-chat guard added in the prior commit (resolveBlueBubblesMessageId
with chatContext) only ran on numeric short ids — `if (/^\d+$/.test(trimmed))`.
Full GUID input fell through to `return trimmed` with no chat check.
Once the short-id guard started rejecting cross-chat reuses, agents would
retry the same call with the full GUID copied from history or a previous
tool result. That second attempt bypassed the guard entirely and the
group reaction landed in the DM anyway — exactly the symptom the prior
commit was meant to close.
Apply the same `isCrossChatMismatch` check to full GUID input. Cache miss
still falls through (callers may legitimately supply a fresh-from-the-wire
GUID the cache hasn't observed yet), but cache hits with a chat mismatch
throw with a remediation hint pointed at the chat target rather than at
the id format — telling an agent to "retry with the full GUID" makes no
sense when it already supplied one.
Tests (extensions/bluebubbles/src/monitor-reply-cache.test.ts):
- UUID + same chat → resolves
- UUID + different chat → throws (this is the regression)
- UUID + cache miss → passes through (preserves behavior for fresh GUIDs)
- UUID + empty chatContext → passes through (preserves prior behavior)
- UUID error message hints at the chat target, not the id format
- chatIdentifier fallback applies to UUID input too
Local patch for upstream consideration — completes the cross-chat guard
started in the prior commit so both id forms are protected symmetrically.
When a BlueBubbles inbound webhook arrives without `chatGuid`, processMessage
falls back to `resolveChatGuidForTarget` to look it up. The previous fallback
target was:
isGroup && (chatId || chatIdentifier)
? <chat_id or chat_identifier>
: { kind: "handle", address: message.senderId }
That `else` branch quietly covered two very different cases:
1. DM with no chatGuid — resolving via sender handle is correct, the chat
IS the conversation with that handle.
2. **Group with no chatGuid AND no chatId AND no chatIdentifier** — resolving
via sender handle yields *that sender's DM chatGuid*, then the rest of
processMessage uses it for ack reactions, mark-read, outbound reply cache,
typing indicators, and outboundTarget.
Case 2 is reachable: `monitor.webhook.test-helpers.ts` ships a default
`createMessageReactionPayloadForTest` payload with no chatGuid/chatId/
chatIdentifier and `isGroup` defaulted to `false`, mirroring real BlueBubbles
reaction/tapback webhooks. When a group reaction or tapback arrives in that
shape and isGroup is later corrected to true (or the message takes the same
poisoned path), `chatGuidForActions` becomes the sender's DM chatGuid. The
poisoned chatGuid then writes the outbound reply cache (line ~1395) with the
wrong chat, defeating the cross-chat short-id guard added in
9912472289 — a later short id resolved against that cache cannot detect the
mismatch and the agent's reaction/reply silently lands in the DM.
Symptom Chris observed (recurring after 9912472289 baked): group messages
getting reacted to from the agent's side show up in a DM transcript with
that sender, attached to a message GUID the user can no longer locate in
the DM.
Extract the fallback target construction into
`buildBlueBubblesInboundChatResolveTarget` so the rule is testable in
isolation and the wrong fallback can never be reached again:
- Group inbound + chatId present → `chat_id`
- Group inbound + chatIdentifier present → `chat_identifier`
- **Group inbound + neither → return null (caller skips chatGuid-dependent actions)**
- DM inbound → `handle` (unchanged: the conversation IS that sender)
processMessage now logs at verbose when the group case returns null instead
of silently degrading to the sender's DM.
Tests: extensions/bluebubbles/src/monitor-processing-chat-resolve.test.ts
covers the eight branches (group with id, group with identifier, group
preferring id, group with neither, blank/non-finite/null variants, DM, DM
with chat_id present, DM with empty sender).
Local patch for upstream consideration — pairs with the short-id chat guard
landed in the previous commit.
BlueBubbles short message ids (numeric aliases like "1", "5" that agents
use instead of full GUIDs to save tokens) are allocated from a single
global counter across every account and every chat. Nothing in
resolveBlueBubblesMessageId verified that the resolved GUID was actually
in the chat the caller was acting on, so any time an agent reused or
mis-remembered a short id — especially common after a long group
conversation — the id could silently point at a different chat entirely.
Symptom Chris observed: reactions/tapbacks and quoted replies authored
inside a group would intermittently land in a DM, targeting an old
message the user could no longer see. Tool call looks successful, chat
archive shows a group reaction appearing in the DM transcript.
Add an optional chatContext parameter to resolveBlueBubblesMessageId
(chatGuid / chatIdentifier / chatId). When provided, look up the
cached reply entry for the resolved GUID and compare. A clear mismatch
(same identifier present on both sides, different values) throws with a
message that lists both chats and points at "use the full GUID", so the
agent fails fast and retries with a disambiguated id. Ambiguous cases
(either side missing all identifiers) pass through to preserve existing
behavior for callers that cannot supply chat hints. The comparison
mirrors resolveReplyContextFromCache so outbound and inbound paths agree
on scope.
Update every call site that resolves a short id for outbound BB traffic
to pass chatContext:
- extensions/bluebubbles/src/actions.ts: react, edit, unsend, reply
(build context from chat* params, then to/target, then the tool's
currentChannelId)
- extensions/bluebubbles/src/channel.ts sendText: derive context from
the `to` target
- extensions/bluebubbles/src/media-send.ts: same
- extensions/bluebubbles/src/monitor-processing.ts deliver path: pass
the chat already resolved for routing
Add buildBlueBubblesChatContextFromTarget to targets.ts so callers can
project a raw target string (`chat_guid:...`, `chat_id:42`,
`imessage:+1...`, bare handle) into the context shape.
Tests:
- extensions/bluebubbles/src/monitor-reply-cache.test.ts (new, 8 cases):
same-chat resolves, cross-chatGuid throws, ambiguous passes,
chatIdentifier fallback, chatId fallback, full GUID input bypasses,
error message identifies both chats, unknown short id still errors.
- extensions/bluebubbles/src/actions.test.ts: update the react short-id
assertion to verify chatContext now flows through.
Local patch for upstream consideration — same root cause affects every
BB user; plan is to open a separate upstream PR once this bakes locally.
For c2d31a5e59: docs/gateway/security/index.md "External content
special-token sanitization" section already mentions the outbound
sanitizer with `<tool_call>` and `<function_calls>` examples, but it
predates the new internal-runtime-scaffolding stripping that targets
`<system-reminder>` and `<previous_response>` tags. Adds those two tags
as explicit examples and notes the final channel delivery boundary so
operators reading the security page see the same coverage exposed by
the c2d31a5e59 sanitizer.
Persist the NVIDIA_API_KEY marker in generated catalog output and mark bundled NVIDIA Chat Completions models as string-content compatible.\n\nFixes #73013.\nFixes #50107.\nRefs #73014.
For 195f704c74: docs/channels/groups.md "Visible replies" section now
records that native slash commands (Discord, Telegram, and other surfaces
with native command support) reply visibly even when
`messages.groupChat.visibleReplies` is `"message_tool"`, so the channel-
native command UI gets the response it expects. Text-typed `/...` commands
and ordinary chat turns still follow the configured group default.
For 891c7d9f1c: docs/plugins/hooks.md "Quick start" now lists the `priority`
and new `timeoutMs` opts that `api.on(...)` accepts, explaining that the
per-hook budget aborts a slow handler instead of letting plugin setup or
recall work consume the caller's configured model timeout. The change is
traceable to the new `OpenClawPluginApi.on` `{ priority?; timeoutMs? }`
signature and `PluginHookRegistration.timeoutMs` field added in the same
SHA.
Fixes#73502.
Active Memory now allows its hidden recall sub-agent to use both bundled memory tool contracts: memory_recall for memory-lancedb and memory_search/memory_get for memory-core. The prompt prefers memory_recall when available and falls back to the legacy tool pair when that is the active backend surface.
Also updates Active Memory docs, QA mock fixtures, and debug parsing compatibility for the two recall paths.
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test (local full suite failed in unrelated plugin/logging shards; PR-specific docs/changelog checks and GitHub checks passed)
- GitHub status checks for c2c5a94df8 completed without failure
Co-authored-by: WuKongAI-CMU <210765158+WuKongAI-CMU@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix: Discord read/search timeout, session-key fallback, and gateway execution mode
- Add 15s timeout to readMessagesDiscord and searchMessagesDiscord so they
fail fast instead of hanging indefinitely (#73431)
- Fall back to CommandTargetSessionKey in dispatchReplyFromConfig when
SessionKey is empty, so Discord inbound message:received hooks fire
reliably (#73431, refs #33038)
- Add resolveExecutionMode to Discord channel actions routing read/search
through gateway timeout path, matching Telegram's pattern (#73431)
* fix: move timeout to fetch layer, drop send.messages wrapper
Inject AbortSignal.timeout into the Discord proxy-request-client fetch
wrapper so every Discord REST call gets a 15s timeout at the HTTP level.
This replaces the Promise.race wrapper in send.messages.ts — cleaner,
covers all calls, and actually aborts the TCP connection.
* fix: remove unused callerController variable in proxy-request-client test
* fix: remove unnecessary mergeAbortSignal helper
- docs/plugins/hooks.md: add `cron_changed` to the Lifecycle hook catalog and
a Gateway lifecycle paragraph describing its typed event payload, run
status, delivery status, and removed-event job snapshot, so plugin authors
picking up f155a5f955 (#72773) have a canonical reference beyond the
sdk-overview bullet that already shipped in the same SHA.
- docs/help/environment.md: add a "Legacy environment variables" section for
aa1834a3ff so users see that `CLAWDBOT_*` and `MOLTBOT_*` prefixes are now
ignored and trigger an `OPENCLAW_LEGACY_ENV_VARS` deprecation warning,
with a rename example to `OPENCLAW_*`.
For 058b57867e: docs/providers/qwen.md "Qwen 3.6 Plus availability"
accordion now records that the bundled catalog still does not advertise
`qwen3.6-plus` on Coding Plan endpoints, but explicitly configured
`models.providers.qwen.models` entries for that model are honored on
Coding Plan baseUrls so subscribers whose plan enables it can opt in. The
upstream API still decides whether the call succeeds.
Aligns Gateway history and session list thinking-default resolution so backend session state matches the Control UI default label:
- `chat.history` now falls back through the shared Gateway session thinking-default resolver.
- Explicit session overrides still win, then owning `agents.list[].thinkingDefault`, then global/model/catalog defaults.
- `sessions.list` catalog-aware thinking defaults are covered by focused regressions.
PR by @jpreagan.
Validated in Blacksmith Testbox `tbx_01kq9t1aeqrz1mj598vvqv9dpg`:
- `pnpm test:serial src/gateway/session-utils.test.ts src/gateway/server.sessions.gateway-server-sessions-a.test.ts src/gateway/server.chat.gateway-server-chat.test.ts` (141 passed)
- `OPENCLAW_TESTBOX=1 pnpm check:changed`
Add opt-in `sandbox.docker.gpus` config plumbing for Docker sandbox containers.
- thread the optional GPU passthrough field through config types, schema, resolution, and Docker create args
- reject empty config values and emit `--gpus` as a separate Docker argv pair
- document the Docker-only behavior and credit the original contributor in the changelog
Fixes#57976.
Carries forward #58124 from @cyan-ember.
Co-authored-by: cyan-ember <5855097+cyan-ember@users.noreply.github.com>
Route Memory Wiki bridge-mode status, doctor, and bridge import CLI paths through Gateway RPC when bridge artifact reads are active, while preserving local/offline fallbacks.
Harden Gateway CLI rendering and imported-source writes: validate RPC response shapes, bound response strings before rendering/JSON serialization, sanitize/escape terminal-controlled output, avoid redundant JSON forwarding, and replace imported source pages through a temp-file rename path with symlink and hardlink regressions.
Fixes#65722Fixes#65976Fixes#66082Fixes#67979Fixes#68371Fixes#68828Fixes#69019Fixes#70181Fixes#70242Fixes#70842
Thanks @moorsecopers99, @vincentkoc, and @prasad-yashdeep.
For 47dc9f7fc0: docs/gateway/sandboxing.md now warns under "Build the default
image" that OpenClaw no longer silently retags plain debian:bookworm-slim as
openclaw-sandbox:bookworm-slim when the default image is missing. Sandbox runs
fail with a build instruction so the python3 tooling required by sandbox
write/edit helpers is preserved instead of being silently dropped.
- docs/concepts/model-providers.md: add proxy-route shaping rule for the
09ec5d2c4d fix that suppresses implicit Anthropic beta headers
(`claude-code-20250219`, `interleaved-thinking-2025-05-14`, OAuth markers)
on non-direct endpoints, parallel to the existing OpenAI
`compat.supportsDeveloperRole` rule.
- docs/gateway/cli-backends.md: add a "Fallback prelude from claude-cli
sessions" section for a96f1fa5ef so users know that non-CLI fallback
candidates after a claude-cli failure are now seeded with a context prelude
harvested from Claude Code's `~/.claude/projects/` JSONL (preferring the
latest `/compact` summary, coalescing tool blocks, skipping same-provider
`--resume` fallbacks).
The required-typed param introduced in 9987e7797f broke
attempt-execution.cli.test.ts and auth-profile-runtime-contract.test.ts
which construct runAgentAttempt params without an originalProvider field.
Make it optional and explicitly require the typeof check before passing
to isClaudeCliProvider so a missing field correctly skips the seed
(defensive default for fallback paths that didn't plumb the original
provider through, no-op for non-fallback paths).
Addresses review on #72069:
- Codex P1 ("Gate Claude prelude seeding by source provider"): the
guard checked the *current* fallback candidate but not the failed
attempt. A session that still carried a stale
cliSessionBindings["claude-cli"] from an unrelated past run would
inject Claude transcript context into a fallback chain that started
on a different provider (e.g. openai -> openai-codex), leaking
irrelevant prior conversation. Plumb `originalProvider` (the
user-requested provider for the chain) through to runAgentAttempt
and require `isClaudeCliProvider(originalProvider)` before reading
Claude history.
- Codex P2 ("Prefer latest compact boundary when summary is missing"):
the resolver always preferred the most recent explicit summary, so
a later compaction without its own summary entry (rare crash case)
paired stale summary text with post-latest-boundary turns. Restructure
readClaudeCliFallbackSeed to queue summaries into pendingSummary and
flush each boundary's pair atomically. A boundary with no preceding
summary now correctly falls back to the boundary's own content
rather than serving an older summary alongside fresh turns.
- Greptile P2 (newest-first break vs sparse coverage): the
formatFallbackTurns walk intentionally stops on the first oversized
turn so the prelude stays a contiguous "what was happening just
before the failure" window. Document the design choice inline so a
future maintainer doesn't reflexively change it to skip-and-continue.
Tests:
- New gateway cases for the boundary-without-summary edge case and
for trailing summaries written without a paired boundary.
- existing 33 attempt-execution + 14 cli-session-history tests still
pass; broader src/agents/command suite stays green (63/63).
Local default oxlint did not run --type-aware so the warning was missed
on the initial commit; CI surfaced it via check-lint. Hoist the heading
into a named const so its length is read directly without the assertion.
When a claude-cli attempt failed with a fallbackable error (e.g. a 402
billing limit), the next candidate -- typically a non-CLI provider --
ran with no prior conversation context. Claude Code keeps its own
JSONL session under ~/.claude/projects/, but the fallback runner only
sees what OpenClaw assembles from its own transcript, which is empty
for claude-cli sessions. The fallback model therefore behaved as if
the conversation just started, even though Claude later resumed fine.
Resolution mirrors what Claude Code itself does on resume after
compaction: prefer the explicit `/compact` summary, then append the
most recent post-boundary turns up to a char budget. Concretely:
- `readClaudeCliFallbackSeed` (gateway): walks the Claude JSONL with
awareness of `type: "summary"` and `type: "system",
subtype: "compact_boundary"` entries. Pre-boundary turns are dropped
(they are represented by the summary); post-boundary turns become
the recent-window. Multiple compactions are handled by preferring
the latest summary. Path safety reuses the existing
`resolveClaudeCliSessionFilePath` validation.
- `formatClaudeCliFallbackPrelude` / `buildClaudeCliFallbackContext\
Prelude` (agents helpers): format the harvested seed into a labeled
prelude. Tool blocks are coalesced to compact "(tool call: name)" /
"(tool result: …)" hints to keep the prompt budget honest. Newest
turns are kept first when truncating; the summary is clearly
labeled "(truncated)" if it overflows.
- `resolveFallbackRetryPrompt`: gains an optional
`priorContextPrelude` that prepends before the existing retry
marker. Empty/whitespace preludes are ignored; first-attempt prompts
are unchanged.
- `runAgentAttempt`: builds the prelude when `isFallbackRetry === true`
AND the new candidate is non-claude-cli AND a Claude-cli session
binding is present. Same-provider fallbacks (claude-cli to
claude-cli) are unaffected because Claude's own --resume still works.
Verified the new tests (12 in cli-session-history, 12 added to
attempt-execution) catch the regression: removing the prelude prepend
in resolveFallbackRetryPrompt makes both new prelude cases fail,
restoring the original cold-start behavior.
References:
- https://code.claude.com/docs/en/how-claude-code-works
- "Inside Claude Code: The Session File Format"
https://databunny.medium.com/inside-claude-code-the-session-file-format-and-how-to-inspect-it-b9998e66d56b
* fix: add CJK error patterns to failover classification
Chinese LLM providers (ZhipuAI/GLM, Bailian, Kimi/Moonshot, DeepSeek,
etc.) return error messages in Chinese. The existing failover
classification only matches English patterns, causing these errors to
fall through as unclassified — surfacing raw provider errors to users
instead of triggering model fallback.
Real production example: ZhipuAI error code 1234 returns
'网络错误,错误id:xxx,请联系客服。' (network error). This was not
matched by the existing 'network error' English pattern, so no failover
was triggered despite having a configured fallback model.
Changes:
- Add Chinese patterns to all error categories in failover-matches.ts:
timeout, serverError, rateLimit, billing, auth, overloaded
- Add Chinese network error detection in formatTransportErrorCopy()
for user-friendly error messages
- Add comprehensive test coverage for all CJK error categories
Follows the existing precedent set by Chinese context overflow patterns
in isContextOverflowError().
* fix: narrow billing pattern and fix placeholder issue URL
- Change '账户余额' to '账户余额不足' to avoid false positives on
messages that merely mention account balance (per greptile review)
- Replace XXXXX placeholder with actual issue #56242
* fix: wire CJK auth failover patterns
* fix: classify CJK provider failover errors
* fix: place failover changelog entry in unreleased
---------
Co-authored-by: Altay <altay@uinaf.dev>
When a bundled plugin (e.g. plugin-sdk loaded transitively) is resolved via a
pluginRoot already inside the existing plugin-runtime-deps cache, its path
does not match the `dist/extensions/<plugin>` shape, so
resolveBundledPluginPackageRoot() returns null and the caller falls back to
the raw pluginRoot. resolveExistingExternalBundledRuntimeDepsRoots() then
rejected the path because the relative segment crossed a directory separator,
causing the resolver to mint a fresh `openclaw-unknown-<pathhash>` cache
beside the real versioned one. The two caches raced replaceNodeModulesDir()
and triggered ENOTEMPTY crash loops.
Treat any descendant of `<base>/openclaw-*` as belonging to that cache key
so nested resolutions return the existing versioned root instead of creating
a self-referential zombie cache.
Fixes#72956
createSubsystemLogger writes through writeConsoleLine, which intentionally
bypasses the patched console.* capture handler in src/logging/console.ts to
avoid recursion. That bypass also skipped the sink-boundary
redactSensitiveText() gate, so secrets reaching subsystem loggers as
message strings or formatted meta could appear verbatim on the terminal —
a follow-up to the file-transport redaction landed in #67953, tracked
under #64046.
Apply redactSensitiveText() at the writeConsoleLine() exit, immediately
after the existing Windows surrogate sanitization and before dispatching
to the rawConsole sink. This covers all subsystem console paths
(trace/debug/info/warn/error/fatal and .raw) because they share the same
writeConsoleLine() exit, matching the redact-at-sink-boundary pattern
already used in console.ts and the file transport.
Closes#73284
Rewrites the always-on reply handling so group/channel rooms default to message-tool-visible output, while `messages.groupChat.visibleReplies: \"automatic\"` preserves legacy auto-posting.\n\nThanks @scoootscooob.
Persist refreshed `zca-js` session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local Zalo Personal session.
- Adds stable credential cookie signatures so equivalent cookie-jar reorderings do not rewrite credentials.
- Adds regression coverage for reordered live cookie jars preserving credential file content and mtime.
- Updates CHANGELOG.md: (#73277) Thanks @darkamenosa.
Co-authored-by: Tuyen <hxtxmu@gmail.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
Clean up local Claude stdio one-shot runs before returning from embedded `openclaw agent --local`, including bundle MCP loopback teardown for local process resources.
Keeps gateway-owned MCP loopback cleanup internal to the Gateway, documents the local-vs-gateway behavior, and aligns the stale OpenAI provider-runtime fixture with the current unsupported Codex mini route.
Cap detached Dream Diary narrative subagent runs across cron dreaming sweeps so multi-workspace runs cannot fan out unbounded subagent sessions.
Adds regression coverage that queued detached narratives resume and clean up, plus a unit-fast lane correction for the security symlink audit test.
* fix(tui): clear stale streaming after orphaned finals
* fix(tui): clear stale streaming after orphaned finals
* fix(tui): clear stale streaming after orphaned finals
Fix config writes so in-process reload notifications use the canonical post-write source snapshot, matching the file watcher path.
Adds regression coverage for the runtime source snapshot and changelog credit.
* fix(export): fix broken template placeholders in session export HTML
The {{MARKED_JS}}, {{HIGHLIGHT_JS}}, and {{JS}} placeholders in the
export HTML template were split across multiple lines by a code
formatter, turning them into JS block statements instead of template
tokens. The generateHtml() function uses .replace('{{MARKED_JS}}', ...)
which requires contiguous strings, so the vendor JS and app code were
never injected — producing a 2MB HTML file that opens with styles and
session data but renders blank (no JS to parse/display the data).
Fix: collapse placeholders to single-line {{TOKEN}} format and add
prettier-ignore comments to prevent re-formatting.
Introduced in 9d403fd.
* fix(export): use function replacers for vendor JS injection
String.replace() interprets $ sequences ($&, $$, $', etc.) in
replacement strings. The minified vendor libraries (highlight.min.js,
marked.min.js) and the template JS contain literal $ characters that
get mutated during injection — e.g. $& becomes the matched placeholder
text, $$ becomes a single $.
Fix: use arrow function replacers for JS content so replacement text
is injected verbatim without $ interpretation. CSS and session data
use string replacers since they don't contain problematic $ patterns.
Flagged by Codex review (P2).
* ci: retrigger checks
* fix(export-session): restore inline export scripts
---------
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Closes the gap left by #72496 on the parallel `messages.tts.providers.<id>` site. After #72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).
Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from #72496.
Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.
Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.
Fixes#73109. Refs #72496.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Raise the Bonjour stuck-announcing watchdog threshold from 8s to 20s and align watchdog timer coverage so healthy 12-13s LAN announcements do not trigger false-positive advertiser teardown.
Replaces JavaScriptCore catalog evaluation with a bounded fail-closed object-literal parser for the generated macOS model catalog.\n\nValidation: macos-node, macos-swift, security-fast, security-scm-fast, security-dependency-audit, workflow sanity checks passed on PR #73112.
Several `openclaw <parent>` commands (channels, plugins, approvals, devices,
cron, mcp) were exiting with code 1 when invoked bare, while printing the
same help-style content that `<parent> --help` produces (which exits 0).
This broke `&&` chains and surfaced a misleading
`ELIFECYCLE Command failed with exit code 1.` line under pnpm.
Add a small `applyParentDefaultHelpAction(cmd)` helper in
`src/cli/program/parent-default-help.ts` that attaches a default action
which prints the parent's own help and sets `process.exitCode = 0`. The
helper is a no-op when the parent already has its own action (e.g.
`agents` defaulting to `agents list`), so existing intentional defaults
are preserved.
Apply it to the six core parents listed in #73077.
On ARM64 devices (e.g. Raspberry Pi 4), resolvePluginProviders takes ~20s
on first call. Three bugs cause this cost to be paid repeatedly:
1. ensureOpenClawModelsJson readyCache fingerprint includes models.json
mtime. After a write, the stored fingerprint (pre-write mtime) never
matches again, forcing every caller to re-run planOpenClawModelsJson.
2. readyCache has one entry per file path. Agents with different configs
(e.g. main agent vs active-memory subagent) overwrite each other's
entry, so neither benefits from caching.
3. resolveExplicitModelWithRegistry calls shouldSuppressBuiltInModel →
resolveProviderPluginsForCatalogHooks on every agent run. The internal
cache key includes the full config, so callers with slightly different
configs each pay the full provider-load cost.
Fixes:
- Remove modelsFileMtimeMs from fingerprint (bug 1)
- Add noopCache to MODELS_JSON_STATE keyed by (path, mtime) — a noop
result is config-agnostic, so any caller can reuse it (bug 2)
- Cache resolveExplicitModelWithRegistry by (provider, modelId, agentDir),
stable for the lifetime of a gateway session (bug 3)
Measured on Raspberry Pi 4 (ARM64):
active-memory subagent preprocessing: 66-75s → ~3s (warm)
active-memory total elapsed: ~96s → ~14s (warm)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- stream fallback Memory Core vector scoring with SQLite iterate() and a bounded top-K result set
- add regression coverage and live-main lint/boundary helper repairs
- supersedes #73069
Thanks @parkertoddbrooks.
A local containment profile uses plugins.enabled=false to stop plugin and channel runtime churn. The previous startup path still built plugin lookup tables and doctor stale scans despite the global disable, which made the switch noisy and slow.
Constraint: plugins.enabled=false must leave channel blocker warnings intact while treating stale plugin config as inert.
Rejected: Clear user plugin config automatically | would mutate a reversible containment setting.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce plugin registry discovery before checking plugins.enabled.
Tested: pnpm test src/gateway/server-startup-plugins.test.ts src/config/plugin-auto-enable.core.test.ts src/commands/doctor/shared/stale-plugin-config.test.ts src/commands/doctor/shared/preview-warnings.test.ts
Tested: pnpm check:changed
Tested: pnpm build
* fix(agents): resolve model aliases in sessions_spawn
normalizeModelSelection() only trims the input — it never resolves
aliases through the model alias index. When a user passes an alias
like 'opus' to sessions_spawn, the child session gets patched with
the raw string, which the gateway cannot match to any provider.
Add resolveModelThroughAliases() to check bare strings against the
configured alias map before returning from
resolveSubagentSpawnModelSelection().
Fixes#57532
Refs #50736
* refactor: address review feedback on alias resolution
- Accept pre-built ModelAliasIndex instead of rebuilding per call
- Narrow helper signature to (string, ModelAliasIndex) → string
- Remove unreachable ?? raw fallback
Co-Authored-By: greptile-apps[bot]
* fix(agents): resolve sessions_spawn model aliases
---------
Co-authored-by: HowdyDooToYou <HowdyDooToYou@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Conservatively filter macOS CodeQL SARIF by dropping only findings where every location is SwiftPM build output. Verified with workflow sanity, local jq filtering, PR CI, and a failed-job rerun for an unrelated stalled Vitest shard.
Document the redaction surface added in f3e8c50df3: custom logging.redactPatterns now apply to Control UI tool start args, partial/final result payloads, derived exec output, and patch summaries on top of the built-in defaults.
Allow the memory index suite to exceed the global 120s test timeout when it runs inside a packed extension shard. The scoped Vitest config is reset after the file.
Harden the macOS CodeQL SARIF filter to drop only findings whose primary location is SwiftPM build output. Verified with workflow sanity, local jq filtering, full PR CI, and profile=macos-security branch proof in 18m44s.
Filter SwiftPM dependency build results from the manual macOS CodeQL shard before upload. Verified with workflow sanity, local jq filtering, and profile=macos-security branch proof in 15m54s. PR CI has the same unrelated extensions/memory-core timeout failure currently present on main.
End-to-end testing on macOS + BlueBubbles + ElevenLabs walked through three CAF flavors before landing on the format Apple's Messages.app actually emits when a user records a native iMessage voice memo:
- PCM int16 @ 44.1 kHz CAF: BlueBubbles' internal `afconvert -f m4af -d aac` conversion fails; the original CAF reaches iMessage but renders with 0 s duration.
- AAC @ 22.05 kHz mono CAF: BlueBubbles' conversion succeeds and the server silently downgrades the delivery, sending the converted MP3 as a generic audio attachment.
- **Opus @ 24 kHz mono CAF**: byte-identical to the descriptor block Apple's Messages.app produces; BlueBubbles passes it through unchanged and iMessage renders a native voice-memo bubble with proper duration and waveform UI.
Adds an opt-in `tts.voice.preferAudioFileFormat` channel capability and a macOS `afconvert`-backed pre-transcode in the speech-core pipeline. BlueBubbles declares `preferAudioFileFormat: "caf"`. Other channels are unaffected. Falls back to the original buffer when the host platform, the source/target pair, or the transcoder process can't produce the preferred container — so non-Darwin hosts and unsupported provider combinations are unchanged.
Also adds a `caff` magic-byte sniff in `src/media/mime.ts` so the auto-reply host-local-media validator (which uses `file-type` and didn't recognize CAF natively) accepts the buffer instead of dropping it as "⚠️ Media failed."
Fixes#72506.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remediate current-profile CodeQL findings for file SecretRef id validation and release workflow job permissions. Includes changelog credit. Thanks @vincentkoc.
Feishu config defaults groupPolicy to 'allowlist'. Inbound group handling read groupAllowFrom and called isFeishuGroupAllowed before resolveFeishuReplyPolicy was reached, so a config that only set channels.feishu.groups.<chat_id>.requireMention=false (with no groupAllowFrom) was rejected with 'group not in groupAllowFrom' before per-group requireMention could take effect. Treat the explicit presence of a group entry under channels.feishu.groups as the operator's allowlist signal: if groupConfig is defined, skip the empty-allowlist rejection. resolveFeishuReplyPolicy still owns mention gating, and existing groupConfig.enabled=false / groupAllowFrom-driven rejections are preserved. Adds a regression test that exercises the reporter's exact config shape and confirms inbound text reaches finalize/dispatch.
The ACP dispatch path calls applyMediaUnderstanding without the agentDir
parameter. This prevents the media understanding pipeline from locating
agent-specific models.json and auth profiles, causing image understanding
to fail silently for non-visual models configured with a separate image
understanding model.
The non-ACP reply path (get-reply.ts) already passes agentDir correctly.
This aligns the ACP path with the same behavior.
Closes#55046
AI-assisted (built with Hermes orchestration).
Address review feedback on PR #72888. triggerInternalHook passes the
same event reference to all handlers sequentially. Mutating evt.context
leaks pluginConfig to subsequent handlers and causes cross-plugin
overwrites. Shallow-copy event and context instead.
When plugins register hooks via api.registerHook(), pluginConfig from
openclaw.json was not available in the hook event context. Plugins that
accessed ctx.pluginConfig or event.context.pluginConfig received
undefined, causing silent failures or fallback to defaults.
Changes:
- Add pluginConfig parameter to registerHook() function
- Wrap handler to inject pluginConfig into event.context before invocation
- Pass params.pluginConfig through createApi() call site
Fixes#72880
Closes#72837. The 15s narrative-subagent timeout was empirically too
tight for warm-gateway runs across light, REM, and deep phases —
gpt-5.4-mini latency through OpenAI alone routinely brushes 12s+, so the
first sweep after a restart deterministically times out across all three
phases. 60s gives realistic LLM-call headroom while still capping the
worst case at one minute, preserving the original comment's "don't leave
parent cron running for minutes" constraint.
Test: updates the matching toMatchObject assertion in
dreaming-narrative.test.ts from 15_000 to 60_000.
When the system hostname exceeds 63 bytes (common with Kubernetes pod
names), the @homebridge/ciao DNS label encoder throws an AssertionError
that crashes the gateway on startup.
Add truncateToDnsLabel() that safely truncates UTF-8 strings at byte
boundaries, applied to both the service instance name and hostname
before passing them to ciao.
Closes#37705
AI-assisted (built with Hermes orchestration).
Commit 2cd23957c0 ("build: use slim docker runtime") switched the
runtime image from `node:24-bookworm` (full) to `node:24-bookworm-slim`.
The slim base does not ship `ca-certificates`, and the runtime stage's
`apt-get install` line was not updated to add it.
Result on the resulting image:
- `/etc/ssl/certs/` is empty (`ls /etc/ssl/certs/ | wc -l` == 0)
- `dpkg -l ca-certificates` reports `un` (not installed)
- `update-ca-certificates` is missing in `$PATH` (exit 127)
- every HTTPS outbound from the gateway dies at TLS handshake with
`error setting certificate file: /etc/ssl/certs/ca-certificates.crt`
- channel plugins that use `node fetch` (telegram/discord/slack)
crash-loop with `Network request for 'deleteWebhook' failed!`
and pin the gateway main thread at ~100% CPU on retry.
Verified by rebuilding the runtime image with this patch and
confirming inside the container:
- `ls /etc/ssl/certs/ | wc -l` -> 285
- `curl -4 https://api.telegram.org/` -> 302
- `curl -4 https://www.google.com/` -> 200
- channel plugins (telegram/discord/slack) register cleanly,
gateway main-thread CPU returns to idle.
Add `ca-certificates` to the apt-install list and call
`update-ca-certificates` to populate the CA bundle.
Signed-off-by: ryuhaneul <luj.moonlight@gmail.com>
When the post-update completion cache refresh times out (slow disk,
large bundled plugin tree, Docker overlayfs), the user previously saw
the opaque 'Completion cache update failed: Error: spawnSync
/usr/bin/node ETIMEDOUT'. Detect ETIMEDOUT specifically, surface
'timed out after 30s', and append a manual refresh hint pointing at
'openclaw completion --write-state' so users know it's non-fatal and
how to recover.
Fixes#72842
Add an end anchor to the type/subtype match and explicitly accept the
RFC 9110 ;parameter tail. Inputs like "image/png<script>" or
"application/json garbage" now return undefined instead of silently
matching the leading prefix.
Closes#9795
Remove the two Unreleased Coven ACP/runtime changelog bullets that were reintroduced after the Coven extension removal.\n\nVerification:\n- rg -n -i "coven" CHANGELOG.md Swabble/CHANGELOG.md extensions/matrix/CHANGELOG.md apps/ios/CHANGELOG.md\n- git diff --check origin/main..HEAD\n- PR checks passed on head 767c274b0f
* fix(ui): discard stale config state on explicit reload
* fix(clownfish): address review for ghcrawl-156594-autonomous-smoke (1)
* fix(clownfish): address review for ghcrawl-156594-autonomous-smoke (1)
* test(ui): align channel config host state
* fix(gateway): preserve runtime-backed health state
* fix(clownfish): address review for ghcrawl-207035-agentic-merge (1)
* fix(gateway): harden health snapshot exposure
Both lanes had only one paragraph each in qa-e2e-automation.md. Adds a
"Telegram and Discord QA reference" section verified against
extensions/qa-lab/src/live-transports/{telegram,discord}/* with:
- shared CLI flags table (--scenario, --output-dir, --repo-root, --sut-account,
--provider-mode, --model, --alt-model, --fast, --credential-source,
--credential-role) — none of these were enumerated for either lane.
- Telegram QA: 8 scenario ids
(telegram-canary/-mention-gating/-mentioned-message-reply/-help-command/
-commands-command/-tools-compact-command/-whoami-command/-context-command),
output artifact paths (telegram-qa-report.md, -summary.json,
-observed-messages.json), and the redaction toggle.
- Discord QA: 3 scenario ids
(discord-canary/-mention-gating/-native-help-command-registration), output
artifact paths, and the SUT-application-id-must-match-bot-user-id check.
- Convex credential pool: documents Discord support (only Telegram was
mentioned before) and the per-kind payload shapes for the
admin/add validator. Cross-links to testing.md for the broker endpoint
contract.
Slims the duplicate Operator-flow paragraphs for Telegram and Discord into a
single one-block pointer that links to the new reference section.
Reorg
- Rename the architecture page title to "QA overview" (slug stays
/concepts/qa-e2e-automation so inbound links keep working).
- Move "Adding a channel to QA" + scenario-helper-name reference from
testing.md into qa-e2e-automation.md under "Transport adapters". Architecture
belongs with the architecture page.
- Drop the duplicate live-transport coverage table from testing.md; canonical
copy stays in qa-e2e-automation.md under a new "Live transport coverage"
heading so qa-matrix.md can deep-link to it.
- Slim testing.md QA-specific runners section to ops only, with cross-links.
Audit (against extensions/qa-lab/src/cli.ts, qa-channel/src/config-schema.ts,
and live-transport runtimes)
- qa-e2e-automation.md gains a "Command surface" table covering all 14
openclaw qa <subcommand> forms; previously only ~7 of 14 were named.
- Document missing OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT and
OPENCLAW_QA_DISCORD_CAPTURE_CONTENT env vars (Matrix already had it).
- Cross-link qa coverage from the Reporting section.
- qa-channel.md completes the config-key list (enabled, name, accounts,
defaultAccount were missing from the schema doc) and pollTimeoutMs range.
- Drop stale "Follow-up work" framing in qa-channel.md (provider/model matrix,
scenario discovery, orchestration) — all three already shipped.
- Replace "vertical slice" language with current behavior; fix misplaced
debugger-UI paragraph.
Discoverability
- Add a Note callout to testing.md pointing at the three QA pages
(QA overview, Matrix QA, QA channel) so maintainers landing on testing.md
see the QA stack in the prologue.
Glossary entries for the renamed/new doc titles.
Adds a focused reference for the Docker-backed Matrix QA lane (CLI flags,
seven scenario profiles, eight env vars including the redaction toggle and
Tuwunel image override, scenario taxonomy, output artifact layout, and triage
tips). Source-of-truth checked against extensions/qa-matrix/src/cli.ts,
shared/live-transport-cli.ts, runners/contract/{runtime,scenario-catalog}.ts,
and substrate/harness.runtime.ts.
Registered in docs/docs.json alongside QA E2E automation.
Add the opt-in Coven ACP runtime bridge as a bundled extension while keeping ACPX as the default path.
Security hardening included before merge:
- fail closed by default instead of silently falling back;
- bounded health/socket requests and daemon response sizes;
- fixed Coven socket trust anchor and symlink/path validation;
- reject untrusted harness/session/event ids before exposing them;
- sanitize daemon-controlled terminal/status/error strings;
- use incremental event polling with bounded dedupe state;
- clean up launched Coven sessions before fallback when daemon ids are invalid.
Validation:
- pnpm test extensions/coven/src/config.test.ts extensions/coven/src/client.test.ts extensions/coven/src/runtime.test.ts
- pnpm check:changed
- GitHub CI green on a64eac20b9
- Greptile Review green
Preserve contributor credit and land the narrowed sessions_spawn ACP-field handling with follow-up transcript redaction and ACP resume ownership hardening. Targeted Blacksmith validation passed for the touched sessions/ACP tests.
The talk.config discovery RPC was handing the source-snapshot's
talkProviderConfig (with the unresolved SecretRef wrapper still on
apiKey) to speechProvider.resolveTalkConfig. ElevenLabs/OpenAI's
strict normalizeResolvedSecretInputString helper threw 'unresolved
SecretRef' there, so iOS / macOS / Control UI Talk overlays never
learned the configured provider and silently fell back to local
AVSpeechSynthesizer ('robot voice') even though talk.realtime.session
and talk.speak both worked end-to-end with the same SecretRef.
Prefer the runtime-resolved provider config when calling
resolveTalkConfig, strip the apiKey field if it's still a SecretRef
wrapper at the call site, and restore the source-shaped apiKey onto
the response so the UI keeps the SecretRef context. Redaction strips
the value when includeSecrets=false.
Adds a regression test using a strict resolver speech provider that
mirrors ElevenLabs/OpenAI behavior so the path stays covered for
SecretRef apiKeys.
Fixes#72496
Thanks @omarshahine
Keep Google Live Talk browser sessions on the supported WebSocket/gateway-relay paths instead of falling back to browser WebRTC, remove stale browser-native voice controls that bypass Talk/TTS provider settings, and harden the Google Live URL plus realtime relay resource controls.
Verification:
- pnpm test ui/src/ui/realtime-talk.test.ts ui/src/ui/realtime-talk-google-live.test.ts src/gateway/talk-realtime-relay.test.ts src/gateway/server-methods/talk.test.ts
- pnpm check:changed
* feat(qqbot): implement unified media upload handling and introduce chunked upload support
This commit enhances the media upload functionality by introducing a unified `sendMedia` method that consolidates the previous separate methods for sending images, voice messages, videos, and files. Key changes include:
- Added `uploadChunked` function for future chunked media uploads, currently marked as not implemented.
- Introduced `MediaSource` abstraction to handle various media types (URLs, base64, local files, buffers) uniformly.
- Updated existing media handling logic to utilize the new `sendMedia` method, ensuring consistent media processing across different types.
- Enhanced error handling and validation for media uploads, including MIME type checks and file size limits.
These changes aim to streamline the media upload process and prepare for future enhancements in handling larger files through chunked uploads.
* feat(qqbot): enhance media upload capabilities with chunked upload support
This commit updates the media upload functionality by implementing chunked upload support for larger files. Key changes include:
- Revised the `SKILL.md` documentation to clarify media file size limits and local file path requirements.
- Introduced a new test suite for the chunked media upload functionality, ensuring robust error handling and upload processes.
- Updated the media handling logic to enforce per-file-type upload ceilings, allowing for seamless integration of chunked uploads.
- Enhanced error handling for daily upload limits, providing user-friendly messages when limits are exceeded.
These improvements aim to streamline the media upload process and accommodate larger files effectively.
* feat(qqbot): add C2C streaming API support for message delivery
This commit introduces support for the QQ C2C official `stream_messages` API, enabling single-message typing-style updates. Key changes include:
- Updated the configuration schema to include a new `c2cStreamApi` boolean option for enabling the C2C streaming API.
- Enhanced the `QQBotAccountConfig` interface to accommodate the new streaming option.
- Implemented a `StreamingController` to manage the lifecycle of C2C stream messages, ensuring proper handling of media tags and message boundaries.
- Updated the outbound dispatch logic to utilize the new streaming capabilities, allowing for more dynamic message delivery in one-to-one chats.
These enhancements aim to improve the responsiveness and interactivity of message delivery within the QQBot framework.
* feat(qqbot): implement group chat support and unify adapter/DI architecture
- Implement group message history tracking with pending history buffer
(record on skip, render on @-mention reply)
- Add mention detection and gating: explicit @bot, implicit quote-reply,
ignoreOtherMentions, configurable activation mode (mention/always)
- Add group activation resolution with session store persistence
- Add message queue with per-peer FIFO and group message merging
(batch multiple rapid messages into one merged payload)
- Add deliver debounce to merge rapid outbound text bursts into
single messages, with media flush and maxWait cap
- Add group config resolution: per-group prompt, history limit,
wildcard and specific group overrides
- Enrich history attachments with local paths from processAttachments
so that history context renders downloaded paths instead of ephemeral
QQ CDN URLs
- Merge ports/ directory into adapter/ as single entry point
- Expand EngineAdapters to 5 required ports: history, mentionGate,
audioConvert, outboundAudio, commands
- Remove global register/get singletons in favor of constructor
injection and one-time init
- Add createEngineAdapters() in bridge/gateway.ts as single assembly point
- Extract monolithic buildInboundContext into 11 discrete stages:
access, content, quote, refidx, group-gate, envelope, assembly
- Extract group chat modules: history, mention, activation,
message-gating, deliver-debounce
- Extract config/group.ts, utils/attachment-tags.ts
* feat(qqbot): add /bot-streaming command for C2C message streaming control
This commit introduces the `/bot-streaming` command, allowing users to enable or disable streaming for message delivery in C2C chats. Key changes include:
- Implementation of the `isStreamingConfigEnabled` function to check the current streaming configuration.
- Command handler for `/bot-streaming` that provides usage instructions and manages the streaming state.
- Updates to the command's response messages to inform users of the current streaming status and how to toggle it.
These enhancements aim to improve user experience by providing a straightforward way to manage streaming message delivery in private chats.
* feat(qqbot): extract interaction handler and add remote config query/update support
- Extract INTERACTION_CREATE handler from gateway.ts into a dedicated
interaction-handler.ts module for better separation of concerns
- Add config query (type=2001) and config update (type=2002) interaction
branches that read/write claw_cfg via runtime.config API
- Register INTERACTION intent (1<<26) in FULL_INTENTS to receive
INTERACTION_CREATE events from the gateway
- Add InteractionType constants (CONFIG_QUERY, CONFIG_UPDATE)
- Extend GatewayPluginRuntime with optional config API (loadConfig,
writeConfigFile) for interaction handler access
- Add QQBotAccountConfigView interface for typed config field access
- Extend acknowledgeInteraction to accept optional data payload for
rich ACK responses (e.g. claw_cfg snapshot)
- Export getFrameworkVersion from slash-commands-impl for version
reporting in config snapshots
- Remove unused eslint-disable directive in streaming-media-send.ts
* feat(qqbot): enhance account management and logging capabilities
- Introduced `toGatewayAccount` function to map resolved QQBot accounts to the engine's gateway account structure.
- Added `persistAccountCredentialSnapshot` function to streamline credential backup during gateway events.
- Updated the `qqbotPlugin` to utilize the new account mapping and credential persistence functions, improving the handling of account data.
- Enhanced logging functionality by modifying the `EngineLogger` interface to support metadata in log messages.
- Implemented new commands for managing logs and clearing storage, providing users with better control over their data and system resources.
- Registered multiple built-in commands for improved user interaction, including `/bot-logs` for exporting logs and `/bot-clear-storage` for managing downloaded files.
- Updated configuration schemas to reflect new options and improve clarity for users.
* fix(qqbot): resolve oxlint errors and update raw-fetch allowlist
- Replace unnecessary `else` after `return` in outbound-media-send.ts (6 occurrences)
- Use `Number.parseInt` instead of global `parseInt` in outbound.ts and streaming-media-send.ts
- Use `Number.isNaN` instead of global `isNaN` in register-basic.ts
- Prefer `**` over `Math.pow` in media-chunked.ts
- Convert interface with call signature to function type in commands.port.ts
- Update api-client.ts allowlist line number (108→124) and add media-chunked.ts:552 to raw-fetch allowlist
* docs(qqbot): translate streaming-c2c.ts header comments to English
* feat(qqbot): add voiceMediaTypes
* feat: restore dispatch changes
* fix(qqbot): align test files with updated engine interfaces after rebase
- inbound-attachments.test: replace removed registerAudioConvertAdapter
with AudioConvertPort, pass audioConvert in ProcessContext
- inbound-pipeline.self-echo.test: add required adapters field to
InboundPipelineDeps mock (history, mentionGate, audioConvert,
outboundAudio, commands)
- outbound-dispatch.test: add required skipped field to InboundContext
* fix(qqbot): update test assertions to match refactored engine interfaces
- inbound-pipeline.self-echo.test: self-echo blocking was moved upstream;
update test to expect non-blocked pipeline behavior
- outbound-dispatch.test: TTS voice path now uses unified sendMedia
instead of sendVoiceMessage; add sendMedia mock and update assertion
- format-ref-entry.test: attachment format changed from [image: ...]
to MEDIA: tag syntax via renderAttachmentTags; update expected output
* refactor(qqbot): migrate from deprecated config API to current/replaceConfigFile
Replace all usages of deprecated runtime config methods:
- loadConfig() → current()
- writeConfigFile(cfg) → replaceConfigFile({ nextConfig, afterWrite })
Updated files:
- bridge/narrowing.ts: writeOpenClawConfigThroughRuntime
- adapter/commands.port.ts: ApproveRuntimeGetter type signature
- commands/builtin/register-approve.ts: loadExecConfig, writeExecConfig, reset
- commands/builtin/register-streaming.ts: config read/write
- gateway/interaction-handler.ts: config query/update handlers
- gateway/types.ts: GatewayPluginRuntime.config interface
* feat(qqbot): update package.json
* fix(qqbot): replace deprecated config-runtime import with config-types subpath
Bundled plugin lint requires focused plugin-sdk subpaths.
- gateway.ts: openclaw/plugin-sdk/config-runtime → config-types
- narrowing.ts: openclaw/plugin-sdk/config-runtime → config-types
* feat(qqbot): group chat support, C2C streaming, chunked media upload, and architecture refactor (#70624) (thanks @cxyhhhhh)
---------
Co-authored-by: Bobby <zkd8907@live.com>
Co-authored-by: sliverp <870080352@qq.com>
## Summary
- render cron job prompts and run summaries through the sanitized markdown pipeline in the Control UI
- keep system-event cron payloads plain and prevent markdown link clicks from triggering row selection
- handle failed runs with missing or empty summaries without duplicating or hiding the error text
## Verification
- pnpm test ui/src/ui/views/cron.test.ts
- pnpm test src/plugins/doctor-contract-registry.test.ts src/plugins/setup-registry.test.ts
- pnpm check:changed
- GitHub CI green on 251f01a3b0
Trace to edb3e84898 (fix: clean stale plugin channel config). When
openclaw doctor --fix removes a missing channel plugin, it also cascades
the cleanup to dangling channel config, heartbeat targets, and channel
model overrides, preventing gateway boot loops after failed plugin
reinstalls. Added an Accordion 11d to docs/gateway/doctor.md listing the
exact config keys that get pruned alongside the plugin entry.
Fixes#68160.
Drops stale optionality from the hello-ok auth schema and keeps generated Swift models, macOS fixtures, browser client types, protocol docs, and merged-base test boundaries aligned.
Harden WebChat input history handling so draft, navigation, and render-state behavior stay consistent across the chat UI.
Validated locally on the rebased PR head 742a5f22f1:
- CI=true OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- CI=true OPENCLAW_LOCAL_CHECK=0 pnpm test:changed
Closes#38702.
Trace to fee16865b2 (fix(agents): accept LAN local auth markers) and the
companion 0dd2844991 (fix: preserve Ollama local marker auth). The fix
extends ollama-local marker handling to any custom OpenAI-compatible
provider whose baseUrl resolves to loopback, a private LAN, .local, or a
bare hostname, so persisted local markers no longer fail with missing-auth
errors for non-Ollama-typed local providers (LM Studio, vLLM, LiteLLM).
The Ollama provider page already covers ollama-local for Ollama-typed
providers; this note lives in docs/gateway/local-models.md where custom
OpenAI-compatible local stacks are documented.
The publish workflow rsyncs source docs/ into the publish repo with --delete,
but explicitly protects locale directories so translation files survive
non-translation-pipeline syncs. When an English source file is renamed (for
example install/migrating-matrix.md -> channels/matrix-migration.md), the
locale copies at <locale>/install/migrating-matrix.md become orphans:
deleted from the English nav but still present on disk.
Mintlify's hosted build appears to silently fall back to the previous
deployment when nav references a path with mixed locale availability, so
recent docs changes (the migration hub rework, matrix-migration move) are
not propagating to docs.openclaw.ai even though every CI run reports
success and the publish repo has the right English content.
Add a pruneOrphanLocaleDocs() pass that walks every generated-locale
directory in the publish target and removes any .md/.mdx file whose
matching English path no longer exists in source docs. Runs after rsync
and before composing docs.json so the regenerated nav and the on-disk
files stay consistent. Verified the logic against the live publish repo:
identifies all ja-JP/es/pt-BR/ko/de/fr/ar/it/tr/uk/id/pl/zh-CN orphans of
install/migrating-matrix.md (12 entries) and would also catch any future
renames the same way.
The Matrix migration guide is plugin-upgrade content (encrypted-state recovery,
device verification, room-key restore) rather than a cross-system import or
machine move, so it belongs alongside the Matrix channel docs rather than under
Install > Maintenance > Migrating.
- Move docs/install/migrating-matrix.md to docs/channels/matrix-migration.md
- Update inbound link in docs/channels/matrix.md
- Update the migrating.md hub: replace the Matrix Card with a one-line link in 'Upgrade a plugin in place'
- Refresh Related list on the moved page (link Matrix push rules and Migration guide hub)
- docs.json: remove install/migrating-matrix from Maintenance > Migrating, slot channels/matrix-migration between channels/matrix and channels/matrix-push-rules in the Mainstream channels group, and add a /install/migrating-matrix -> /channels/matrix-migration redirect
- install/migrating: convert to a hub page with three clear paths (CardGroup for cross-system imports linking Claude+Hermes, machine-to-machine move with Steps and AccordionGroup, plugin upgrade Card linking Matrix)
- install/migrating-claude: align with Hermes page structure (add Restart-and-verify Step, JSON output for automation, Troubleshooting AccordionGroup with 4 entries, cross-link to Hermes guide)
- cli/migrate: tighten intro to mention both bundled providers and link the migration hub
- docs.json: move Maintenance group to immediately after Install overview, nest the four migrating pages (migrating, migrating-claude, migrating-hermes, migrating-matrix) under a 'Migrating' subgroup so they collapse into a dropdown
Adds a raw config pending-changes diff panel in Control UI raw mode, with JSON5 parsing, sensitive-value redaction until explicit reveal, bounded diff work, and tests for redaction/reveal and stale reveal-state reset.
Also aligns provider manifest contract coverage for google-vertex and Qwen aliases to unblock the rebased CI matrix.
Supersedes stale PRs #48621 and #46654. PR #48621 had gone stale without maintainer follow-up, so this maintainer-authored PR carries the implementation forward transparently while preserving changelog credit for the original contributor and @BunsDev.
* fix(cli): keep nodes list aligned with nodes status
* fix(clownfish): address review for ghcrawl-156588-autonomous-smoke (1)
* fix(cli): keep nodes list aligned with nodes status
Trace to b642ebece9 (fix(feishu): do not treat @all as a bot mention).
Document the new behavior in the mention requirement section: broadcast-only
@all/@_all messages no longer wake the bot, while messages that combine @all
with a direct bot mention still count as a bot mention.
Add a bundled Claude migration provider for Claude Code and Claude Desktop imports.\n\nIncludes source discovery, preview/apply behavior for instructions, MCP servers, skills and command prompts, archive/manual handling for unsafe Claude state, docs, labeler, and tests.
Display friendly agent identity labels in the Control UI Sessions key column when identity data is available, keep raw-key fallback behavior, and allow filtering by agent identity name.
This is the maintainer-owned replacement for #54212 by @dingtao416. Thanks @dingtao416 for the original feature idea and implementation direction.
Includes follow-up fixes from maintainer review automation: normalized key-cell classes, own-property identity lookup, and friendly-label tooltips.
Validation:
- pnpm test ui/src/ui/format.test.ts ui/src/ui/views/sessions.test.ts
- pnpm check:changed
Closes#54163.
Supersedes #54212.
Require Control UI updates to observe a real gateway process replacement, surface skipped/error update outcomes, and verify the running gateway version after restart.\n\nAdds update.status restart-sentinel plumbing, docs, generated protocol model updates, and changelog attribution.\n\nLocal verification:\n- pnpm test src/gateway/server-methods/update.test.ts src/cli/gateway-cli/run-loop.test.ts src/infra/restart-sentinel.test.ts src/infra/process-respawn.test.ts src/infra/update-runner.test.ts ui/src/ui/app-gateway.node.test.ts ui/src/ui/controllers/config.test.ts\n- git diff --check\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/gateway/protocol.md docs/gateway/configuration.md docs/web/control-ui.md\n- pnpm docs:check-mdx
Trace to 8bdfa58cbb (fix(migrations): avoid partial Hermes config apply after
conflict). Hermes apply now marks remaining dependent config items as
"blocked by earlier apply conflict" when a conflict surfaces mid-apply,
instead of writing them partially. Document the user-visible reason string
and where to find blocked items in the migration report.
Routes stateful Google Meet tool actions through the gateway-owned runtime so create/join/status/speak/leave share the same session owner instead of losing tool-created realtime sessions after the agent turn.
Also preserves structured gateway error details for missing session ids and tightens node-host child cleanup for already-closed sessions.
Fixes#72440.
Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
Require explicit confirmation before applying restart-impacting Dreaming mode changes in the Control UI.
- Add pending/confirm/loading state for the Dreaming toggle path
- Render a restart confirmation dialog before sending the config patch
- Sync Control UI locale metadata and cover the confirmation flow in browser tests
Fixes#63804
- cli/migrate: convert flat reference into structured Mintlify page (Tip pointer, ParamField for flags, AccordionGroup for safety model, sub-sections for Hermes provider with what's imported, .env keys, archive-only state, and plugin contract)
- install/migrating-hermes: new dedicated user guide modeled after migrating-matrix.md (Tabs for onboarding vs CLI, AccordionGroup for what gets imported, Steps for recommended flow, Warning for --overwrite, Troubleshooting accordions)
- docs.json: add install/migrating-hermes to Maintenance group alongside migrating and migrating-matrix
Remote proof:
- CI run 24982271745 passed on 6122e13c9f.
- Blacksmith Testbox tbx_01kq6vwehcszjfpp52f0pb3v1q passed focused Google Meet formatting, docs/link checks, realtime consult runtime tests, Google Meet tests, extension test typecheck, the core-unit-fast-support shard, and the core support boundary shard.
Thanks @BsnizND.
Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
Fixes#72523.
Remote proof:
- CI run 24980529154 passed on 29f825bea5.
- Blacksmith Testbox tbx_01kq6tsgbaxgstxmtearwy9n4w passed focused formatting, Google Meet tests, Google realtime provider tests, and extension test typecheck.
Thanks @BsnizND.
Co-authored-by: BSnizND <199837910+BsnizND@users.noreply.github.com>
Trace to db09f68ce5 (Support SecretRef for voice-call credentials and bundled
plugin SecretInputs #72607). The reference page docs/reference/secretref-credential-surface.md
listed the new entries in the same SHA, but docs/plugins/voice-call.md showed
only plain-string credentials without pointing to the SecretRef surface.
- platforms/android: blockquote Note for Android app status, Note for canvas host port
- platforms/macos: Tip component for app vs CLI discovery comparison
- plugins/zalouser, channels/zalouser: blockquote Warning components for unofficial automation risk
- channels/pairing: convert two Important paragraphs to Note components for DM-vs-group scope and silent-upgrade behavior
- start/openclaw: workspace-as-memory Tip component
- automation/tasks: drop 'this page covers' filler in Note
- automation/auth-monitoring, clawflow, cron-vs-heartbeat: collapse 'this page moved... See X' redirects to single direct sentences
- testing-live: Tip components for model-discovery and authoritative-list guidance
- debugging: --dev flag Note and non-dev gateway stop Tip
- testing: narrowing live tests Tip
- tools/lobster: optional-plugin allowlist Note
- tools/acp-agents-setup: blockquote Important to Warning component
- memory: replace en-dash list separators with em-dashes, sentence-case Further reading link titles
- messages: rewrite filler 'this page ties together' opener to a direct one
- delegate-architecture: convert 4 blockquote security warnings to Warning and Note components
- system-prompt: convert blockquote daily-memory note to Note component
- channels: convert Tip prose to component, fix /channels/index link, sentence-case heading
- configure: convert Note and Tip prose to components
- devices: convert Note and Warning prose to components
- models: sentence-case scan/status subheadings
- agents: clean up related links and Title Case body link
Add the legacy `models.providers.*.api: "openai"` → `"openai-completions"`
migration to doctor's Current migrations list, and note the gateway startup
behavior that skips providers with future or unknown api enum values instead
of failing closed.
Traces to:
- 6a7980e984 fix(doctor): migrate legacy OpenAI provider api
- 147f4f50f5 fix(gateway): skip stale model provider api entries
Polish the Control UI quick settings dashboard layout.
- Rework quick settings into a 12-column desktop grid with matched top-row card heights.
- Pair Personal with a right-side Appearance/Automations stack on large screens while preserving tablet/mobile ordering.
- Add render/style guards plus an Unreleased changelog entry crediting @BunsDev.
Validated with focused UI tests, formatting, git diff checks, local changed gate, and full PR CI.
Add manifest-owned GitHub Copilot token support for non-interactive onboarding, including documented env fallback, ref-mode tokenRef storage, saved-profile reuse, and default model wiring that preserves existing primary model configuration.
Validation:
- pnpm test extensions/github-copilot/index.test.ts src/plugins/contracts/registry.contract.test.ts src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts
- pnpm check:changed
- CI green on aadac2c8d4
Resolve Feishu group chat labels through getChatInfo so session labels prefer human-readable group names over raw chat IDs.\n\nPreserve topic/thread label priority and defer the lookup until after broadcast dedup claims to avoid duplicate account API calls.\n\nValidation:\n- pnpm test extensions/feishu/src/bot-group-name.test.ts extensions/feishu/src/bot.broadcast.test.ts\n- pnpm check:changed\n- GitHub CI green on c154dc0a41fd715dce95ef1fb5d0c269533b8c22\n\nCloses #35675
Address Clownfish follow-up on Telegram native draft finalization. Requires real streamed assistant partials before materializing drafts, clears stale native draft previews, and keeps media/buttons on normal send path.
Fixes the post-merge review follow-ups from #72471 by deduping stale pre-compaction state entries and preserving parent-before-child ordering for successor transcripts.
Add command-level sentinel coverage proving channel setup metadata, onboarding auth choices, and models-list provider ownership stay on manifest/registry paths without importing plugin runtime.\n\nLocal verification:\n- pnpm exec oxfmt --check --threads=1 src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm test:serial src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed\n- clean rebase sanity: git diff --check origin/main...HEAD\n\nPR CI had known unrelated main-red failures matching latest main run 24970053892; the new sentinel test passed in CI.
* fix(telegram): send fresh finals for stale previews
* test(telegram): cover stale preview send fallback
* fix(telegram): keep stale archived preview fallback
* fix(telegram): clear stale active previews
* fix(telegram): reset preview state after fresh finals
Fixes #70678.\n\nKeeps quiet but healthy WhatsApp linked-device sessions connected by tracking WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Also cleans up transport activity listeners on failed connection-open paths.\n\nCarries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.\n\nValidation:\n- pnpm test:serial extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts extensions/whatsapp/src/connection-controller.test.ts\n- pnpm check:changed\n- codex review --base origin/main
description: Use gitcrawl for OpenClaw issue and PR archive search, duplicate discovery, related-thread clustering, and local GitHub mirror freshness checks.
metadata:
openclaw:
requires:
bins:
- gitcrawl
---
# Gitcrawl
Use this skill before live GitHub search when triaging OpenClaw issues or PRs.
`gitcrawl` is the local candidate-discovery layer. It is fast, includes open and closed threads, and can surface duplicate attempts, related issues, and already-landed fixes. It is not the final source of truth for comments, labels, merges, closes, or current CI.
- Treat `gitcrawl` as stale if `doctor` shows no target thread, an old `last_sync_at`, missing embeddings for neighbor/search commands, or a clearly wrong open/closed state.
- If stale data blocks the decision, refresh the portable store first:
- Run expensive update commands such as `gitcrawl sync --include-comments` only when the user asked to update the local store or stale data is blocking the decision.
- The sync default is all GitHub thread states; pass `--state open`, `--state closed`, or `--state all` only when a task requires a narrower or explicit scope.
## Boundaries
- Use `gitcrawl` for candidates, clusters, and historical context.
- Use `gh`, `gh api`, and the current checkout for live state before commenting, labeling, closing, reopening, merging, or filing a PR review.
- Do not close or label based only on `gitcrawl` similarity. Require matching problem intent plus live verification.
- If `gitcrawl` is unavailable, say so and fall back to targeted `gh search` rather than blocking normal maintainer work.
short_description:"Search local OpenClaw issue and PR history before live GitHub triage"
default_prompt:"Use $gitcrawl to inspect OpenClaw issue and PR history, find related threads and duplicate candidates, then verify actionable decisions with live GitHub."
@@ -7,20 +7,21 @@ description: Review, triage, close, label, comment on, or land OpenClaw PRs/issu
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
## Start issue and PR triage with ghcrawl
## Start issue and PR triage with gitcrawl
-Anytime you inspect OpenClaw issues or PRs, check local `ghcrawl` data first for related threads, duplicate attempts, and already-landed fixes.
-Use`ghcrawl`for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
-If`ghcrawl`is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
-Do not run expensive/update commands such as `ghcrawl refresh`, `ghcrawl embed`, or `ghcrawl cluster` unless the user asked to update the local store or the stale data is blocking the decision.
-Use `$gitcrawl` first anytime you inspect OpenClaw issues or PRs.
-Check local`gitcrawl`data first for related threads, duplicate attempts, and already-landed fixes.
-Use`gitcrawl`for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
-If `gitcrawl` is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
- Do not run expensive/update commands such as `gitcrawl sync --include-comments`, future enrichment commands, or broad reclustering unless the user asked to update the local store or stale data is blocking the decision.
- Prefer `ghcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Prefer `gitcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Use `--repo openclaw/openclaw` with `--match title,body` first when using `gh search`.
- Add `--match comments` when triaging follow-up discussion or closed-as-duplicate chains.
- Do not stop at the first 500 results when the task requires a full search.
default_prompt:"Use $openclaw-pre-release-plugin-testing to plan or run pre-release OpenClaw plugin validation across package, lifecycle, doctor, gateway, SDK, and live-ish proof."
description: Fix only small, high-certainty OpenClaw bugs from a pasted issue/PR list after deep code review.
---
# OpenClaw Small Bugfix Sweep
Batch workflow for pasted OpenClaw issue/PR refs.
Execute, do not summarize.
Triage does not commit, push, create PRs, comment, close, label, land, or merge.
## Companion Skills
Use `$gitcrawl` first, `$openclaw-pr-maintainer` for live GitHub hygiene, `$github-deep-review` posture for source tracing, and `$openclaw-testing` for proof.
## Loop
For each ref:
1. Read live target with `gh`.
2. Check `gitcrawl` for related, duplicate, closed, or already-fixed threads.
3. Read body, comments, linked refs, changed files, current code, adjacent tests, and dependency contracts when relevant.
4. Trace the real runtime path.
5. For issues: fix locally only if this is a bug, current code proves root cause, the implicated path is clear, and a narrow patch is cleaner than refactor.
6. For PRs: decide `ready-to-merge`, `needs-fixup`, or `skip`; do not alter PR branches unless explicitly asked.
7. Add focused regression proof when practical for local issue fixes or PR readiness checks.
8. Run the smallest meaningful gate.
9. Continue until every pasted ref is fixed or classified.
No subagents unless explicitly requested.
## Skip If
- not a bug
- config/docs/workflow/release/support/dependency/product work
- repro or root cause is uncertain
- larger refactor or owner-boundary change is cleaner
- already fixed on current `main`
- dependency behavior is guessed
- no focused proof is feasible
Skip with terse reason. Do not pad with low-confidence fixes.
## Fix Rules
- owner module first; generic seam only when required
- existing patterns/helpers/types
- no drive-by refactors
- tests near failing surface
- docs only for changed public behavior
- no commit/push/create PR/comment/close/label/land/merge unless explicitly asked
## PR Rules
-`ready-to-merge`: code is good, current head checked, required proof is green or clearly pending only external CI; list for maintainer merge or `@clawsweeper automerge`
-`needs-fixup`: small bug is clear, but PR branch needs changes; list exact files/tests and wait for explicit fix/push/automerge instruction
-`skip`: broad, stale, speculative, config/product/security/release, owner-boundary, or refactor-sized
- if source PR is untrusted/uneditable, do not create a replacement PR during sweep
Use this skill for test-memory investigations. Do not guess from RSS alone when heap snapshots are available. Treat snapshot-name deltas as triage evidence, not proof, until retainers or dominators support the call.
For **runtime fixes** (e.g., closure leaks in long-running services like the gateway), see [Validating runtime fixes](#validating-runtime-fixes-not-test-memory) below — that uses a dedicated harness, not the test-parallel snapshot machinery.
## Workflow
1. Reproduce the failing shape first.
@@ -63,6 +65,38 @@ Use this skill for test-memory investigations. Do not guess from RSS alone when
Read the top positive deltas first. Large positive growth in module-transform artifacts suggests lane isolation; large positive growth in runtime objects suggests a real leak. If the names alone do not settle it, open the same snapshot pair in DevTools and inspect retainers/dominators for the top rows before declaring root cause.
## Validating runtime fixes (not test-memory)
The workflow above is for diagnosing Vitest worker memory growth. For
validating that a runtime/closure fix actually releases captured state, use the
dedicated harness:
-`pnpm leak:embedded-run` — runs `scripts/embedded-run-abort-leak.ts`. Loops N
aborted runs in a function-shaped scope mimicking `runEmbeddedAttempt`,
writes heap snapshots, and reports a PASS/FAIL verdict on retention growth
using `FinalizationRegistry` for tracked-instance counting plus RSS delta.
Modes:
-`closure-extracted` (default) — production fix shape (helper at module scope).
-`closure-inline` — pre-fix shape (closure inside the runner scope). Use as a
sensitivity check: if it passes you've broken the harness, not fixed a bug.
-`synthetic-leak` — deliberately retains via a module-level bucket. Use to
confirm the harness can detect leaks before trusting a PASS on a real fix.
Snapshots land in `.tmp/embedded-run-abort-leak/`. Diff with the same script
short_description:"Benchmark and fix slow OpenClaw tests"
default_prompt:"Use $openclaw-test-performance to reassess the OpenClaw test benchmark, identify the next real hotspot, fix it without losing coverage, update the report, and commit scoped changes."
short_description:"Benchmark tests, plugin suites, CPU, RSS, and heap growth"
default_prompt:"Use $openclaw-test-performance to reassess OpenClaw test and plugin-suite performance, collect wall/import/CPU/RSS metrics, investigate memory growth when needed, fix the next real hotspot without losing coverage, update the report, and commit scoped changes."
description: Choose, run, rerun, or debug OpenClaw tests, CI checks, Docker E2E lanes, release validation, and the cheapest safe verification path.
---
# OpenClaw Testing
Use this skill when deciding what to test, debugging failures, rerunning CI,
or validating a change without wasting hours.
## Read First
-`docs/reference/test.md` for local test commands.
-`docs/ci.md` for CI scope, release checks, Docker chunks, and runner behavior.
- Scoped `AGENTS.md` files before editing code under a subtree.
## Default Rule
Prove the touched surface first. Do not reflexively run the whole suite.
1. Inspect the diff and classify the touched surface:
- source: `pnpm changed:lanes --json`, then `pnpm check:changed`
- tests only: `pnpm test:changed`
- one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
2. Reproduce narrowly before fixing.
3. Fix root cause.
4. Rerun the same narrow proof.
5. Broaden only when the touched contract demands it.
## Guardrails
- Do not kill unrelated processes or tests. If something is running elsewhere, treat it as owned by the user or another agent.
- Do not run expensive local Docker, full release checks, full `pnpm test`, or full `pnpm check` unless the user asks or the change genuinely requires it.
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
- For Blacksmith Testbox proof, reuse only an id warmed and claimed in this
operator session. `blacksmith testbox list` is diagnostics only; a listed id
can have a local key and still carry stale rsync state from another lane.
After warmup, run `pnpm testbox:claim --id <id>`, then prefer
`pnpm testbox:run --id <id> -- "<command>"` for OpenClaw gates so stale
org-visible ids fail fast before syncing. Claims older than 12 hours are
stale unless `OPENCLAW_TESTBOX_CLAIM_TTL_MINUTES` is explicitly set for long
work.
## Local Test Shortcuts
```bash
pnpm changed:lanes --json
pnpm check:changed # changed typecheck/lint/guards; no Vitest
default_prompt:"Use $openclaw-testing to choose the cheapest safe test or CI verification path, inspect failures, and rerun only the relevant OpenClaw lane."
If `uvx --from pr-search-cli pr-search ...` fails because `uvx` or the `pr-search` launcher is not available, ask the user to make that command work before continuing.
If `prtags auth status` shows that the user is not logged in, ask the user to run:
```bash
@@ -90,19 +67,19 @@ Resume only after the missing tool or login state has been fixed.
## Read-Path Default
For read-only GitHub operations in this workflow, use `ghr` as the default CLI.
Treat it as a drop-in replacement for the `gh` read operations you would normally use for PRs, issues, comments, reviews, and duplicate-search evidence.
For candidate discovery in this workflow, use `gitcrawl` first.
Treat it as the local history and clustering layer for related issues, duplicate attempts, and closed threads.
Only fall back to `gh` when `ghr` is failing for a concrete reason, such as:
Use live `gh` or `gh api` for the target thread and for any candidate before making an actionable judgment.
Use live GitHub when `gitcrawl` is missing or stale for a concrete reason, such as:
- the mirrored object is not present yet
- the mirror data is clearly stale or incomplete for the decision you need to make
-the `ghr` command errors, times out, or does not expose the specific read you need
- the target or candidate is not present yet
- the local data is clearly stale or incomplete for the decision you need to make
-`gitcrawl` errors, times out, or lacks the needed neighbor/search data
When you fall back to `gh`, note that you did so and why.
When you fall back to live GitHub search, note that you did so and why.
If `ghr` is missing a fresh PR or issue but `gh` can read it, you may use `gh` for the read-side judgment.
If a later `prtags` target-level write fails because the same object is still missing from `ghreplica`, stop and report that the mirror has not caught up yet instead of forcing the write.
If a later `prtags` target-level write fails because its own mirror has not caught up, stop and report that the curation backend is missing the target object instead of forcing a fallback write.
## Goal
@@ -118,14 +95,12 @@ For each target PR or issue:
Use the tools with these boundaries:
-`ghreplica` is the raw evidence source
- use `ghr` first for normal GitHub read operations in this workflow
-use it for title/body/comment search, related PRs, overlapping files, overlapping ranges, and current PR or issue status
-resort to `gh` only when `ghr` cannot provide the needed read cleanly
-`pr-search-cli` is candidate generation and ranking
- use it to suggest likely duplicate PRs or issue-cluster context
- do not treat it as final truth
- do not create or expand a duplicate group only because `pr-search-cli` put multiple PRs in the same issue or duplicate cluster
-`gitcrawl` is candidate generation and historical context
- use it first for local title/body search, neighbors, clusters, and closed-thread discovery
-treat every candidate as a lead until live GitHub confirms it
-`gh` is live GitHub truth
-use it for target state, body, comments, reviews, files, linked issues, and current open/closed/merged status
- use `gh search` only when `gitcrawl` is stale, missing data, or cannot express the needed query
-`prtags` is the maintainer curation layer
- use it to create or reuse one duplicate group
- use it to save the duplicate status, confidence, rationale, and group summary
@@ -182,7 +157,7 @@ Examples:
## Evidence Checklist
Before declaring a duplicate, gather evidence from at least two categories.
Same-issue or same-cluster output from `pr-search-cli` counts only as candidate generation, not as one of the required proof categories by itself.
`gitcrawl` neighbors, search hits, and cluster membership count as candidate generation, not as enough proof by themselves.
For PRs:
@@ -205,21 +180,18 @@ If you only have wording similarity, that is not enough.
## Step 1: Read The Target
Start by reading the target itself.
Use `ghr` first for this step even if you would normally reach for `gh`.
Do not widen an existing group just because `pr-search-cli` placed several PRs under the same issue or duplicate cluster.
Do not widen an existing group just because `gitcrawl` placed several PRs or issues near each other.
Confirm that the actual implementation path and maintainer intent still match before adding the new member.
Create a new group only when no existing group clearly fits.
@@ -423,8 +377,8 @@ prtags annotation group set <group-id> \
When the evidence is incomplete, set `duplicate_status=candidate` and lower the confidence.
If a per-PR or per-issue annotation write fails because `prtags` cannot resolve the target through `ghreplica`, do not force a fallback write path.
Keep the group state you were able to write, report that the mirror is still missing the target object, and defer the target-level annotation until `ghreplica` catches up.
If a per-PR or per-issue annotation write fails because `prtags` cannot resolve the target, do not force a fallback write path.
Keep the group state you were able to write, report that the curation backend is still missing the target object, and defer the target-level annotation until `prtags` catches up.
short_description:"Find duplicate PRs and issues, group them in prtags, and let prtags sync the GitHub comment"
default_prompt:"Use $tag-duplicate-prs-issues to decide whether an OpenClaw PR or issue is a duplicate, gather evidence with ghreplica and pr-search-cli, group related items in prtags, and save the duplicate judgment."
short_description:"Find duplicate PRs and issues with gitcrawl, group them in prtags, and let prtags sync the GitHub comment"
default_prompt:"Use $tag-duplicate-prs-issues to decide whether an OpenClaw PR or issue is a duplicate, gather candidates with gitcrawl, verify live state with GitHub, group related items in prtags, and save the duplicate judgment."
if ! grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|automerge|autoclose)\b' "$body_file"; then
for key in CI GITHUB_ACTIONS GITHUB_WORKSPACE GITHUB_REPOSITORY GITHUB_RUN_ID GITHUB_RUN_NUMBER GITHUB_RUN_ATTEMPT GITHUB_REF GITHUB_REF_NAME GITHUB_SHA GITHUB_EVENT_NAME GITHUB_ACTOR RUNNER_OS RUNNER_ARCH RUNNER_TEMP RUNNER_TOOL_CACHE; do
write_export "$key"
done
} > "${env_file}.tmp"
mv "${env_file}.tmp" "$env_file"
{
echo "# Docker containers visible from the hydrated runner"
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
description:Existing release tag or current full 40-character workflow-branch commit SHA to validate (for example v2026.4.12 or 0123456789abcdef0123456789abcdef01234567)
description:Branch, tag, or full commit SHA to validate
required:true
type:string
expected_sha:
description:Optional full SHA that ref must resolve to
required:false
default:""
type:string
provider:
description:Provider lane for cross-OS onboarding and the end-to-end agent turn
required:false
@@ -25,28 +30,59 @@ on:
- fresh
- upgrade
- both
release_profile:
description:Release coverage profile for live/Docker/provider breadth
required:false
default:stable
type:choice
options:
- minimum
- stable
- full
rerun_group:
description:Release check group to run
required:false
default:all
type:choice
options:
- all
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
live_suite_filter:
description:Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
fi
echo "- This run will execute cross-OS release validation, install smoke, QA Lab parity, Matrix, and Telegram lanes, and the non-Parallels Docker/live/openwebui coverage from the CI migration plan."
- Owner boundary: fix owner-specific behavior in the owner module. Shared/core gets generic seams only; no owner ids, dependency strings, defaults, migrations, or recovery policy. If a bug names an extension or its dependency, start in that extension and add a generic core seam only when multiple owners need it.
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
- Formatting: use `oxfmt`, not Prettier. Prefer`pnpm format:check` / `pnpm format`; for targeted files use `pnpm exec oxfmt --check --threads=1 <files...>` or `pnpm exec oxfmt --write --threads=1 <files...>`.
- Linting: use repo wrappers (`pnpm lint:*`, `scripts/run-oxlint.mjs`); do not invoke generic JS formatters/lints unless a repo script uses them.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
-Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
-Blacksmith/Testbox: on maintainer machines with Blacksmith access, broad/shared validation defaults to Testbox. This includes `pnpm check`, `pnpm check:changed`, `pnpm test`, `pnpm test:changed`, Docker/E2E/live/package/build gates, and any command likely to fan out across many Vitest projects. Do not start those broad gates locally unless the user explicitly asks for local proof or sets `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`.
- Local validation: targeted edit loops only, such as `pnpm test <specific-file>`, targeted formatter checks, and small lint/type probes. If a local command expands beyond targeted proof, stop it and move the broad gate to Testbox.
- Testbox use: run from repo root, pre-warm early with `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`, reuse the returned `tbx_...` id for all `run`/`download` commands, and stop boxes you created before handoff. Timeout bins: `90` minutes default, `240` multi-hour, `720` all-day, `1440` overnight; anything above `1440` needs explicit approval and cleanup.
- Testbox full-suite profile: `blacksmith testbox run --id <ID> "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test"`. For installable package proof, prefer the GitHub `Package Acceptance` workflow over ad hoc Testbox commands.
## GitHub / CI
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without Peter asking.
- PR scan/triage: no unsolicited PR comments/reviews. Report in chat only unless explicitly asked, or a close/duplicate action needs a reason comment.
- GitHub search boolean text is fussy. If `OR` queries return empty, split exact terms and search title/body/comments separately before concluding no hits.
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
- PR execution artifacts/screenshots: attach them to the PR, comment, or an external artifact store. Do not add `.github/pr-assets` or other PR-only assets to the repo.
- PR review answer must explicitly cover: what bug/behavior we are trying to fix; PR/issue URL(s) and affected endpoint/surface; whether this is the best possible fix, with high-certainty evidence from code, tests, CI, and shipped/current behavior.
- When working on an issue or PR, always end the user-facing final answer with the full GitHub URL.
- CI polling: exact SHA, needed fields only. Example: `gh api repos/<owner>/<repo>/actions/runs/<id> --jq '{status,conclusion,head_sha,updated_at,name,path}'`.
- Post-land wait: minimal. Exact landed SHA only. If superseded on `main`, same-branch `cancel-in-progress` cancellations are expected; stop once local touched-surface proof exists. Never wait for newer unrelated `main` unless asked.
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push for code/test/runtime/config changes: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Before handoff/push for code/test/runtime/config changes: run`pnpm check:changed` in Testbox by default on maintainer machines. Tests-only: run`pnpm test:changed` in Testbox by default. Full prod sweep: run`pnpm check` in Testbox. Use local only for narrow targeted proof or when explicitly requested.
- If `pnpm test:changed` or `pnpm check:changed` selects broad/shared lanes, it belongs in Testbox; do not let it continue locally after it fans out.
- Docs/changelog-only and CI/workflow metadata-only changes are not changed-gate work by default. Use `git diff --check` plus the relevant formatter/docs/workflow sanity check; escalate to `pnpm check:changed` only when scripts, test config, generated docs/API, package metadata, or runtime/build behavior changed.
- Rebase sanity: after a green `pnpm check:changed`, a clean rebase onto current
`origin/main` does not require rerunning the full changed gate when the rebase
- Vitest. Colocated `*.test.ts`; e2e `*.e2e.test.ts`; example models `sonnet-4.6`, `gpt-5.4`.
- Avoid brittle tests that grep workflow/docs strings for operator policy. Prefer executable behavior, parsed config/schema checks, or live run proof; put release/CI policy reminders in AGENTS/docs instead.
- Prefer injection; if module mocking, mock narrow local `*.runtime.ts`, not broad barrels or `openclaw/plugin-sdk/*`.
- Share fixtures/builders; delete duplicate assertions; assert behavior that can regress here.
- Do not edit baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.
- Do not run multiple independent `pnpm test`/Vitest commands concurrently in the same worktree. They can race on `node_modules/.experimental-vitest-cache` and fail with `ENOTEMPTY`. Use one grouped `pnpm test ...` invocation, run targeted lanes sequentially, or set distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` values when true parallel Vitest processes are needed.
- Test workers max 16. Memory pressure: `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
-Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete`.
-Docs final answers: when doc files changed, end with the relevant full `https://docs.openclaw.ai/...` URL(s).
- Changelog user-facing only; fixing an issue or landing/merging a PR needs one unless pure test/internal.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @codex`, `Thanks @openclaw`, or `Thanks @steipete`.
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
- Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
- Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel.
- Memory wiki: keep prompt digest tiny. The prompt should only say the wiki exists, prefer `wiki_search` / `wiki_get`, start from `reports/person-agent-directory.md` for people routing, use search modes (`find-person`, `route-question`, `source-evidence`, `raw-claim`) when useful, and verify contact data before use.
- People wiki provenance: generated identity, social, contact, and "fun detail" notes need explicit source class/confidence (`maintainer-whois`, Discrawl sample/stat, GitHub profile, maintainer repo file). Do not promote inferred details to facts.
- Rebrand/migration/config warnings: run `openclaw doctor`.
- Never edit `node_modules`.
- Local-only `.agents` ignores: `.git/info/exclude`, not repo `.gitignore`.
If you believe you've found a security issue in OpenClaw, please report it privately.
If you believe you've found a security issue in OpenClaw, report it privately first.
## Reporting
This policy does two things: it gives researchers a clear disclosure path, and it spells out the trust model maintainers use when triaging reports. OpenClaw is local-first agent infrastructure for trusted operators; it is not designed as a shared multi-tenant boundary between adversarial users on one gateway.
The fastest useful reports show a current, reproducible boundary bypass with demonstrated impact. Scanner output, prompt-injection-only chains, or reports that rely on hostile users sharing one trusted gateway are usually not security vulnerabilities under this model.
Security work is shared across a number of OpenClaw maintainers, including engineers and security researchers from organizations such as NVIDIA and Tencent. See the [maintainer list](CONTRIBUTING.md#maintainers).
## Report a Security Issue
Report vulnerabilities directly to the repository where the issue lives:
@@ -15,22 +21,50 @@ Report vulnerabilities directly to the repository where the issue lives:
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 OpenClaw core issues, submit through a private [GitHub Security Advisory](https://github.com/openclaw/openclaw/security/advisories/new). Do not open a public issue or PR that discloses an unpatched vulnerability, exploit path, secret, or security-sensitive proof of concept.
Maintainers may close, hide, delete, or otherwise take down public issues and PRs that disclose vulnerabilities or active security issues. We will redirect those reports through the private disclosure process so the issue can be triaged and fixed without giving attackers a public playbook.
For full reporting instructions see our [Trust page](https://trust.openclaw.ai).
### Required in Reports
OpenClaw does not currently run a paid bug bounty program. Please still disclose responsibly so we can fix real issues quickly. The best way to help the project right now is to send high-signal reports and, when practical, focused PRs.
1.**Title**
2.**Severity Assessment**
3.**Impact**
4.**Affected Component**
5.**Technical Reproduction**
6.**Demonstrated Impact**
7.**Environment**
8.**Remediation Advice**
### What We Need
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.
Make the report easy to reproduce and easy to route:
### Report Acceptance Gate (Triage Fast Path)
- What you found and why you believe it is security-relevant.
- The affected component, version, and commit SHA when possible.
- Reproduction steps or a proof of concept against latest `main` or the latest released version.
- The actual impact, including which OpenClaw trust boundary is crossed.
- Any remediation advice or focused patch you can provide.
Reports without reproduction steps, demonstrated impact, and remediation advice are deprioritized. We receive a high volume of AI-generated scanner findings, so we prioritize vetted reports from researchers who can show how the issue crosses an OpenClaw security boundary.
### What Usually Is Not a Security Bug
These patterns are usually not vulnerabilities by themselves:
- Prompt injection without a policy, auth, approval, sandbox, or tool-boundary bypass.
- A trusted operator using an intentional local feature, such as local shell access or browser/script execution.
- A malicious plugin after a trusted operator installs or enables it.
- Multiple adversarial users sharing one Gateway host/config and expecting per-user isolation.
- Scanner-only, dependency-only, or stale-path reports without a working repro and demonstrated OpenClaw impact.
- Public internet exposure or risky deployment choices that the docs already recommend against.
If you are unsure, report privately. We would rather route a careful report than miss a real boundary issue.
### 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 Posture and Report Rules
The sections below are the normative posture maintainers use for report triage. The headings are editorial; the policy text defines the boundary.
### Detailed Report Acceptance Gate
For fastest triage, include all of the following:
@@ -47,7 +81,7 @@ For fastest triage, include all of the following:
Reports that miss these requirements may be closed as `invalid` or `no-action`.
### Common False-Positive Patterns
### Detailed False-Positive Patterns
These are frequently reported but are typically closed with no code change:
@@ -64,6 +98,7 @@ These are frequently reported but are typically closed with no code change:
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- Reports that only show an ACP tool can indirectly execute, mutate, orchestrate sessions, or reach another tool/runtime without demonstrating bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. ACP silent approval is intentionally limited to narrow readonly classes; parity-only indirect-command findings are hardening, not vulnerabilities.
- Reports that only show untrusted media bytes reaching a maintained native decoder dependency (for example Sharp/libvips/libheif) without proving the shipped dependency version is vulnerable and demonstrating crash, memory corruption, data exposure, or a boundary bypass through OpenClaw. JavaScript header sniffing and image dimension fast-paths are preflight/UX checks, not the security boundary for native decoder correctness.
- Reports whose only impact is transient extra memory, CPU, or allocation work from decoding, base64 expansion, media transcoding, serialization, or other format conversion after the input was already accepted under OpenClaw's configured size/trust limits, including base64 decode-before-size-estimate findings. These are performance issues, not vulnerabilities, unless the report demonstrates unauthenticated amplification, bypass of configured limits, crash/process termination, persistent resource exhaustion, data exposure, or another documented boundary bypass.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.
- Reports that depend on replacing or rewriting an already-approved executable path on a trusted host (same-path inode/content swap) without showing an untrusted path to perform that write.
@@ -75,27 +110,13 @@ These are frequently reported but are typically closed with no code change:
- Claims that Microsoft Teams `fileConsent/invoke``uploadInfo.uploadUrl` is attacker-controlled without demonstrating one of: auth boundary bypass, a real authenticated Teams/Bot Framework event carrying attacker-chosen URL, or compromise of the Microsoft/Bot trust path.
- Scanner-only claims against stale/nonexistent paths, or claims without a working repro.
- Reports that restate an already-fixed issue against later released versions without showing the vulnerable path still exists in the shipped tag or published artifact for that later version.
- SSRF reports against the operator-managed HTTP/WebSocket proxy-routing feature whose only claim is that ordinary process-local HTTP clients (`fetch`, `node:http`, `node:https`, WebSocket clients, axios/got/node-fetch-style clients) can reach an internal, metadata, private, or otherwise sensitive destination when proxy routing is disabled, missing, or the operator-managed proxy policy allows it. For this feature, OpenClaw provides fail-closed proxy routing when enabled; the external proxy's destination policy is operator infrastructure, not an OpenClaw-controlled security boundary. See [Network proxy](https://docs.openclaw.ai/security/network-proxy).
### 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.
## Bug Bounties
OpenClaw is a labor of love. There is no bug bounty program and no budget for paid reports. Please still disclose responsibly so we can fix issues quickly.
The best way to help the project right now is by sending PRs.
## Maintainers: GHSA Updates via CLI
### Maintainer GHSA Updates via CLI
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)
### Operator Trust Model
OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boundary.
@@ -120,7 +141,7 @@ OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boun
- 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.
## Trusted Plugin Concept (Core)
### Trusted Plugins
Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
@@ -128,7 +149,7 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Plugin behavior such as reading env/files or running host commands is expected inside this trust boundary.
- Security reports must show a boundary bypass (for example unauthenticated plugin load, allowlist/policy bypass, or sandbox/path-safety bypass), not only malicious behavior from a trusted-installed plugin.
## Out of Scope
### Out of Scope
- Public Internet Exposure
- Using OpenClaw in ways that the docs recommend not to
@@ -148,11 +169,13 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Reports whose only claim is that an ACP-exposed tool can indirectly execute commands, mutate host state, or reach another privileged tool/runtime without demonstrating a bypass of ACP prompt/approval, allowlist enforcement, sandboxing, or another documented trust boundary. These are hardening-only findings, not vulnerabilities.
- Reports whose only claim is that exec approvals do not semantically model every interpreter/runtime loader form, subcommand, flag combination, package script, or transitive module/config import. Exec approvals bind exact request context and best-effort direct local file operands; they are not a complete semantic model of everything a runtime may load.
- Reports whose only claim is parser reachability in an up-to-date maintained dependency without showing that the exact shipped dependency build is vulnerable. We keep native media dependencies current; dependency exposure alone is not a vulnerability.
- Reports whose only claim is resource overhead from decode/encode, base64 expansion, media transcoding, serialization, or format-conversion order after input has already passed the applicable configured acceptance limits, including base64 decode-before-size-estimate findings. These are performance-only and should be ignored for GHSA triage unless the report demonstrates unauthenticated amplification, limit bypass, crash/process termination, persistent exhaustion, data exposure, or another documented boundary bypass.
- 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.
- Reports whose only claim is that a platform-provided upload destination URL is untrusted (for example Microsoft Teams `fileConsent/invoke``uploadInfo.uploadUrl`) without proving attacker control in an authenticated production flow.
- SSRF reports limited to the operator-managed HTTP/WebSocket proxy-routing feature where the demonstrated mitigation is to enable/configure `proxy.enabled` with a filtering `proxy.proxyUrl`/`OPENCLAW_PROXY_URL`, or where impact depends on a permissive/misconfigured operator proxy. This only covers normal process-local HTTP(S)/WebSocket egress (`fetch`, Node HTTP(S), and similar JavaScript clients); non-HTTP egress and other features are assessed separately. See [Network proxy](https://docs.openclaw.ai/security/network-proxy).
- 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)
### One-User Trust Model
OpenClaw's security model is "personal assistant" (one trusted operator, potentially many agents), not "shared multi-tenant bus."
@@ -174,7 +197,7 @@ OpenClaw's security model is "personal assistant" (one trusted operator, potenti
- 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.
## Context Visibility and Allowlists
### Context Visibility and Allowlists
OpenClaw distinguishes:
@@ -192,7 +215,7 @@ Reports that only show supplemental-context visibility differences are typically
Hardening roadmap may add explicit visibility modes (for example `all`, `allowlist`, `allowlist_quote`) so operators can opt into stricter context filtering with predictable tradeoffs.
## Agent and Model Assumptions
### 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.
@@ -200,7 +223,7 @@ Hardening roadmap may add explicit visibility modes (for example `all`, `allowli
- Hook/webhook-driven payloads should be treated as untrusted content; keep unsafe bypass flags disabled unless doing tightly scoped debugging (`hooks.gmail.allowUnsafeExternalContent`, `hooks.mappings[].allowUnsafeExternalContent`).
- Weak model tiers are generally easier to prompt-inject. For tool-enabled or hook-driven agents, prefer strong modern model tiers and strict tool policy (for example `tools.profile: "messaging"` or stricter), plus sandboxing where possible.
## Gateway and Node trust concept
### Gateway and Node Trust Concept
OpenClaw separates routing from execution, but both remain inside the same operator trust boundary:
@@ -211,7 +234,7 @@ OpenClaw separates routing from execution, but both remain inside the same opera
- Differences in command-risk warning heuristics between exec surfaces (`gateway`, `node`, `sandbox`) do not, by themselves, constitute a security-boundary bypass.
- For untrusted-user isolation, split by trust boundary: separate gateways and separate OS users/hosts per boundary.
## Workspace Memory Trust Boundary
### Workspace Memory Trust Boundary
`MEMORY.md` and `memory/*.md` are plain workspace files and are treated as trusted local operator state.
@@ -220,7 +243,7 @@ OpenClaw separates routing from execution, but both remain inside the same opera
- 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
### Plugin Trust Boundary
Plugins/extensions are loaded **in-process** with the Gateway and are treated as trusted code.
@@ -228,7 +251,7 @@ Plugins/extensions are loaded **in-process** with the Gateway and are treated as
- Runtime helpers (for example `runtime.system.runCommandWithTimeout`) are convenience APIs, not a sandbox boundary.
- Only install plugins you trust, and prefer `plugins.allow` to pin explicit trusted plugin ids.
## Temp Folder Boundary (Media/Sandbox)
### Temp Folder Boundary
OpenClaw uses a dedicated temp root for local media handoff and sandbox-adjacent temp artifacts:
For threat model + hardening guidance (including `openclaw security audit --deep` and `--fix`), see:
-`https://docs.openclaw.ai/gateway/security`
### Tool filesystem hardening
#### Tool Filesystem Hardening
-`tools.exec.applyPatch.workspaceOnly: true` (recommended): keeps `apply_patch` writes/deletes within the configured workspace directory.
-`tools.fs.workspaceOnly: true` (optional): restricts `read`/`write`/`edit`/`apply_patch` paths and native prompt image auto-load paths to the workspace directory.
- Avoid setting `tools.exec.applyPatch.workspaceOnly: false` unless you fully trust who can trigger tool execution.
### Sub-agent delegation hardening
#### Sub-Agent Delegation Hardening
- Keep `sessions_spawn` denied unless you explicitly need delegated runs.
- Keep `agents.list[].subagents.allowAgents` narrow, and only include agents with sandbox settings you trust.
@@ -265,7 +288,7 @@ For threat model + hardening guidance (including `openclaw security audit --deep
-`sandbox: "require"` rejects the spawn unless the target child runtime is sandboxed.
- This prevents a less-restricted session from delegating work into an unsandboxed child by mistake.
### Web Interface Safety
#### Web Interface Safety
OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for **local use only**.
@@ -317,12 +340,39 @@ docker run --read-only --cap-drop=ALL \
## Security Scanning
This project uses `detect-secrets` for automated secret detection in CI/CD.
See `.detect-secrets.cfg` for configuration and `.secrets.baseline` for the baseline.
OpenClaw uses several security and release-validation layers. No single scanner is treated as the boundary.
Run locally:
### Secret Detection
OpenClaw uses `detect-secrets` with a checked-in baseline and local exclusion notes (`.secrets.baseline`, `.detect-secrets.cfg`). Secret-resolution behavior is also covered by the dedicated secrets test surface.
Run the baseline scan locally:
```bash
pip install detect-secrets==1.5.0
detect-secrets scan --baseline .secrets.baseline
```
### Static Analysis
CI runs CodeQL across core TypeScript, GitHub Actions, Android, macOS, and high-risk runtime boundaries using `.github/workflows/codeql*.yml` and `.github/codeql/*.yml`.
OpenGrep provides a high-precision Semgrep-compatible layer. PRs run a changed-path scan; maintainers can run a full repository scan when needed. The rulepack lives under `security/opengrep/`, with `.semgrepignore` as the shared exclusion file.
Run the local OpenGrep wrapper after installing `opengrep`:
```bash
scripts/run-opengrep.sh --changed --sarif --error
pnpm check:opengrep-rule-metadata
```
### E2E and Live Validation
Security-relevant behavior is also covered by runtime validation, not only static scanning:
-`pnpm test:e2e` for repo E2E coverage.
-`pnpm test:live` for live provider/runtime coverage.
-`pnpm test:docker:all` for Docker-packaged runtime scenarios.
- Package acceptance and scheduled live/E2E workflows for release-path validation.
These lanes exercise packaged installs, gateway/runtime behavior, live model/provider paths, Docker scenarios, and platform smoke tests. They complement scanners by proving the security-sensitive flows still behave correctly in real runtime environments.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.