Skip to content

fix(voice-call): close webhook in-flight limiter fail-open on empty remote address#74453

Merged
clawsweeper[bot] merged 3 commits intoopenclaw:mainfrom
davidangularme:fix/voice-call-webhook-rate-limit-proxy
Apr 30, 2026
Merged

fix(voice-call): close webhook in-flight limiter fail-open on empty remote address#74453
clawsweeper[bot] merged 3 commits intoopenclaw:mainfrom
davidangularme:fix/voice-call-webhook-rate-limit-proxy

Conversation

@davidangularme
Copy link
Copy Markdown
Contributor

@davidangularme davidangularme commented Apr 29, 2026

Summary

The voice-call webhook handler silently bypasses its in-flight rate limiter when req.socket.remoteAddress is unavailable. This PR closes that fail-open path with a constant fallback key.

Bug

extensions/voice-call/src/webhook.ts:619 (pre-PR):

const inFlightKey = req.socket.remoteAddress ?? "";

The shared SDK helper at src/plugin-sdk/webhook-request-guards.ts:111-114 returns true unconditionally for an empty key — that is its by-contract opt-out path. So when req.socket.remoteAddress is missing (closed socket, edge proxy quirk), the limiter silently becomes a no-op and the request proceeds to readBody without a concurrency cap.

Fix

Detect missing remoteAddress, log a warning, and fall back to a constant non-empty key so the limiter still engages:

const remoteAddress = req.socket.remoteAddress;
if (!remoteAddress) {
  console.warn(`[voice-call] Webhook accepted with no remote address; using shared fallback in-flight key`);
}
const inFlightKey = remoteAddress || "__voice_call_no_remote__";

All requests with absent IP info share one in-flight bucket of size maxInFlightPerKey (default 8).

Why only voice-call

I initially added a googlechat companion fix in this PR, but @clawsweeper Codex review correctly pointed out that withResolvedWebhookRequestPipeline already supplies a non-empty fallback key at src/plugin-sdk/webhook-targets.ts:144 (${resolved.path}:${remoteAddress ?? "unknown"}), which is strictly better than what my fix was passing (it preserves the per-IP component). I reverted those two commits. Voice-call has its own pipeline that does not go through that helper, so the fail-open is real for voice-call specifically.

Sweep results

I checked all callers of createWebhookInFlightLimiter:

Plugin Status Why
voice-call ❌ Bug → fixed empty ?? "" fallback, direct call to tryAcquire
googlechat ✅ Safe uses shared helper's per-path:remote fallback
bluebubbles ✅ Safe path-prefixed key (always non-empty)
line ✅ Safe line:${normalizedPath} always non-empty
synology-chat ✅ Safe account-scoped key, comment explicitly documents the trap
webhooks (catch-all) ✅ Safe passes resolved client IP through helper

Test plan

  • pnpm test extensions/voice-call/src/webhook.test.ts — existing 8-concurrent in-flight test still asserts 429 on the 9th request (no regression on the happy path).
  • Manual: deploy voice-call behind a proxy that strips the source IP, observe the warning log and the 429 engagement.

No new automated test added: the private handleRequest path doesn't have a clean unit-test seam, the change is two-line auditable from the diff, and adding a focused test would require breaking encapsulation through as unknown as casts.

Related context (not blockers)

Same shape as the silent-failure cluster fixed earlier this week:

The shared SDK helper's empty-key bypass behavior at webhook-request-guards.ts:111-114 is preserved unchanged. Hardening that primitive (fail-closed by default) is a separate, larger-scope change worth its own PR.

@openclaw-barnacle openclaw-barnacle Bot added channel: googlechat Channel integration: googlechat channel: voice-call Channel integration: voice-call size: XS triage: blank-template Candidate: PR template appears mostly untouched. triage: refactor-only Candidate: refactor/cleanup-only PR without maintainer context. labels Apr 29, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 29, 2026

Greptile Summary

This PR fixes two places where the shared in-flight limiter was silently bypassed: the voice-call webhook was using an empty remoteAddress as the key (which tryAcquire treats as fail-open), and the Google Chat webhook handler was not passing any inFlightKey to the shared pipeline at all. Both fixes are correct in principle; one minor refinement is noted inline.

Confidence Score: 4/5

Safe to merge; the core bug fixes are correct and the only remaining concern is a minor key-granularity issue in the Google Chat handler.

Both changes correctly close the fail-open paths in the in-flight limiter. The only finding is P2: the Google Chat key embeds the full URL including query string rather than just the pathname, which could fragment rate-limit buckets under adversarial query-string variation. This does not break the limiter but weakens its effectiveness.

extensions/googlechat/src/monitor-webhook.ts — verify whether per-pathname or per-full-URL keying is intended.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/googlechat/src/monitor-webhook.ts
Line: 194

Comment:
**Key includes query string, not just path**

The comment says "per-path", but `req.url` in Node.js includes the full query string. Each unique query-string combination gets its own 8-slot in-flight bucket, so a caller sending requests with varying query parameters will never be capped by a single bucket. Stripping the query string — e.g. via `(req.url ?? "/").split("?")[0]` — would match the documented intent and keep the key space bounded to the number of actual webhook paths.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(googlechat): wire missing inFlightKe..." | Re-trigger Greptile

Comment thread extensions/googlechat/src/monitor-webhook.ts Outdated
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: passed for ClawSweeper automerge.

What this changes:

The PR changes the voice-call webhook pipeline to use a logged, constant non-empty fallback in-flight limiter key when socket remote-address metadata is missing, adds regression coverage, updates the changelog, and aligns one changed-test routing expectation.

Automerge follow-up:

No separate repair lane is appropriate because this active implementation PR is narrow, reviewed, and green; the remaining action is normal automerge or maintainer merge.

Security review:

Security review cleared: The diff narrows a pre-auth webhook limiter bypass and only changes TypeScript source/tests plus changelog/test-routing text; it does not add dependencies, workflows, lockfiles, secret handling, downloads, or command execution paths.

Review details

Best possible solution:

Land the focused voice-call fallback-key fix with its regression test and changelog entry, and keep any broader change to the shared SDK empty-key opt-out contract as a separate hardening discussion.

What I checked:

  • Current main fail-open path: Current main still derives the voice-call webhook in-flight limiter key as req.socket.remoteAddress ?? "", so missing socket metadata reaches the limiter as an empty string before body read. (extensions/voice-call/src/webhook.ts:619, 5a631e1ee9fa)
  • Limiter empty-key contract: createWebhookInFlightLimiter.tryAcquire returns true for falsy keys and release ignores falsy keys, confirming that an empty string is an intentional opt-out path. (src/plugin-sdk/webhook-request-guards.ts:111, 5a631e1ee9fa)
  • PR runtime fix: The latest patch adds MISSING_REMOTE_ADDRESS_IN_FLIGHT_KEY, warns when remoteAddress is missing, and passes the constant fallback key to tryAcquire. (extensions/voice-call/src/webhook.ts:617, f1367a72f7ea)
  • PR regression test: The PR adds a missing-remote-address regression test that holds eight webhook bodies in flight, verifies the ninth request is rejected with 429, and confirms only the first eight enter body read. (extensions/voice-call/src/webhook.test.ts:969, f1367a72f7ea)
  • Other caller sweep: The shared resolved webhook pipeline already falls back to a non-empty ${resolved.path}:unknown key when no remote address is available, so the earlier Google Chat concern is not present in the latest patch. (src/plugin-sdk/webhook-targets.ts:141, 5a631e1ee9fa)
  • Latest PR state and checks: The public PR API reports latest head f1367a72f7eac74b05319dd134865c51f02f744e, mergeable state clean, and the check-runs API showed all non-skipped checks completed successfully for that head. (f1367a72f7ea)

Likely related people:

  • steipete: GitHub path history ties steipete to the shared webhook guard SDK seam, repeated voice-call webhook/test maintenance, changed-test routing maintenance, and maintainer automerge involvement on this PR. (role: current-main maintainer and webhook guardrail owner; confidence: high; commits: 3a68c56264bb, 49f693d06a6c, f7caf83da49c; files: extensions/voice-call/src/webhook.ts, extensions/voice-call/src/webhook.test.ts, src/plugin-sdk/webhook-request-guards.ts)
  • eleqtrizit: Recent merged work hardened voice-call stream ingress and trusted-proxy/IP limiting behavior, which is adjacent to this source-address limiter path even though it is not the exact webhook file changed here. (role: adjacent voice-call ingress hardening contributor; confidence: medium; commits: 692438cbb22e; files: extensions/voice-call/src/media-stream.ts, extensions/voice-call/src/media-stream.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 5a631e1ee9fa.

@davidangularme davidangularme force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from dc66a74 to be55bfd Compare April 29, 2026 15:54
@openclaw-barnacle openclaw-barnacle Bot removed the channel: googlechat Channel integration: googlechat label Apr 29, 2026
@davidangularme davidangularme changed the title Fix/voice call webhook rate limit proxy fix(voice-call): close webhook in-flight limiter fail-open on empty remote address Apr 29, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: needs maintainer review before merge.

What this changes:

The PR updates the voice-call webhook pipeline to use a logged constant fallback in-flight limiter key when req.socket.remoteAddress is missing.

Maintainer follow-up before merge:

This is an active external implementation PR whose latest diff appears narrow and correct; the next action is maintainer review, CI, and a decision on whether to require a regression test, not a separate ClawSweeper repair branch.

Review details

Best possible solution:

Land the narrow voice-call fallback-key fix after maintainer validation, ideally with a focused missing-remote-address regression test if the test seam is acceptable. Keep the shared SDK empty-key opt-out contract and unrelated channel behavior out of this PR unless a separate API hardening decision is made.

Acceptance criteria:

  • pnpm test extensions/voice-call/src/webhook.test.ts
  • pnpm check:changed

What I checked:

  • Current-main fail-open path: runWebhookPipeline still derives inFlightKey from req.socket.remoteAddress ?? "", while createWebhookInFlightLimiter.tryAcquire returns true for empty keys, so a missing remote address bypasses the in-flight cap before readBody. (extensions/voice-call/src/webhook.ts:619, e8b82d1cf92f)
  • Latest PR scope: The latest head be55bfd4de38 modifies only extensions/voice-call/src/webhook.ts with 12 additions and 1 deletion; the earlier Google Chat change that triggered the prior bot review is no longer in the diff. (extensions/voice-call/src/webhook.ts:616, be55bfd4de38)
  • Release path remains paired: The existing finally block releases the same inFlightKey, so using a non-empty fallback key will also decrement the fallback bucket after request handling completes. (extensions/voice-call/src/webhook.ts:681, e8b82d1cf92f)
  • Existing test coverage: Current tests cover normal per-source-IP pre-auth in-flight limiting and 429 behavior, but they do not directly simulate a missing remoteAddress; no tests were run because this review was read-only. (extensions/voice-call/src/webhook.test.ts:914, e8b82d1cf92f)
  • Security-sensitive diff pass: The diff is limited to TypeScript webhook limiter keying/logging in one plugin file. It does not touch workflows, dependencies, lockfiles, package publishing metadata, secret handling, artifact downloads, or command execution paths. (extensions/voice-call/src/webhook.ts:616, be55bfd4de38)
  • Relevant prior discussion: The previous ClawSweeper comment found a blocking Google Chat limiter-key issue on an earlier head. The author then force-pushed and removed the Google Chat file from the PR, leaving only the voice-call fix under review. (be55bfd4de38)

Likely related people:

  • steipete: GitHub path history shows steipete introduced the voice-call pre-auth webhook guards and in-flight limiter in 651dc745, and also owns the shared webhook guardrail history in src/plugin-sdk/webhook-request-guards.ts. (role: current-main introducer / recent maintainer; confidence: high; commits: 651dc7450b68, 3a68c56264bb, 49f693d06a6c; files: extensions/voice-call/src/webhook.ts, extensions/voice-call/src/webhook.test.ts, src/plugin-sdk/webhook-request-guards.ts)
  • eleqtrizit: Recent merged work in 692438cb hardened voice-call stream ingress and trusted-proxy/IP limit behavior, which is adjacent to this webhook source-IP limiter concern. (role: adjacent voice-call ingress hardening contributor; confidence: medium; commits: 692438cbb22e; files: extensions/voice-call/src/media-stream.ts, extensions/voice-call/src/media-stream.test.ts)

Remaining risk / open question:

  • No regression test directly exercises req.socket.remoteAddress being absent; the existing in-flight test covers the normal source-IP path only.
  • Head checks were still queued or pending during inspection, so this review does not have completed CI proof for be55bfd4de38.

Codex review notes: model gpt-5.5, reasoning high; reviewed against e8b82d1cf92f.

@steipete
Copy link
Copy Markdown
Contributor

/clawsweeper automerge

@clawsweeper clawsweeper Bot added the clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge label Apr 29, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

ClawSweeper automerge is enabled for this PR.

I added clawsweeper:automerge and asked ClawSweeper to review this head. If ClawSweeper emits a repair marker or requests changes, I will repair/rebase the branch and ask for another review, up to the configured round limit.

A maintainer can pause this with /clawsweeper stop.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: needs maintainer review before merge.

What this changes:

The PR changes the voice-call webhook pipeline to log missing req.socket.remoteAddress and use a constant non-empty fallback key for the in-flight limiter before reading webhook bodies.

Maintainer follow-up before merge:

No separate repair lane is needed; this is an active implementation PR whose latest patch appears narrow and correct, so the remaining action is normal CI/automerge or maintainer validation.

Review details

Best possible solution:

Land the focused voice-call fallback-key fix after normal CI/automerge validation, ideally with a small regression test for missing remote-address requests if maintainers accept the private-method test seam. Keep the shared SDK empty-key opt-out contract and unrelated channels out of this PR.

What I checked:

Likely related people:

  • steipete: Current-main blame for the affected voice-call webhook limiter block points to Peter Steinberger, and prior ClawSweeper context ties steipete to the voice-call pre-auth guard and shared webhook guardrail history. (role: current-main introducer / recent maintainer; confidence: high; commits: 34d11d57579d, 651dc7450b68, 3a68c56264bb; files: extensions/voice-call/src/webhook.ts, extensions/voice-call/src/webhook.test.ts, src/plugin-sdk/webhook-request-guards.ts)
  • eleqtrizit: Prior ClawSweeper context identifies recent merged work hardening voice-call stream ingress and trusted-proxy/IP limit behavior, which is adjacent to this webhook source-IP limiter concern. (role: adjacent voice-call ingress hardening contributor; confidence: medium; commits: 692438cbb22e; files: extensions/voice-call/src/media-stream.ts, extensions/voice-call/src/media-stream.test.ts)

Remaining risk / open question:

  • No automated regression test in the PR directly exercises req.socket.remoteAddress being absent; existing coverage only exercises the normal per-source-IP limiter path.
  • This read-only pass did not execute the PR branch tests; normal CI or maintainer validation should still cover the latest head before merge.

Codex review notes: model gpt-5.5, reasoning high; reviewed against e46dccb35374.

@clawsweeper clawsweeper Bot added the clawsweeper:human-review Needs maintainer review before ClawSweeper can continue label Apr 29, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

ClawSweeper is pausing automerge for human review.

Source: clawsweeper[bot]
Reason: structured ClawSweeper verdict: needs-human (sha=be55bfd4de3801cc7a6e8b2f7d6fe28a44da490a)

I added clawsweeper:human-review and left the final call with a maintainer.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

Codex review: passed for ClawSweeper automerge.

What this changes:

The PR changes extensions/voice-call/src/webhook.ts to warn when req.socket.remoteAddress is missing and use a constant non-empty fallback key for the webhook in-flight limiter.

Automerge follow-up:

No separate repair lane is appropriate because this is an active implementation PR with maintainer automerge enabled and no blocking Codex finding on the latest narrow diff.

Review details

Best possible solution:

Land the focused voice-call fallback-key change after CI/automerge validation, ideally with a small missing-remote-address regression test if maintainers accept the private-method seam; leave the shared SDK empty-key opt-out contract and unrelated channels out of this PR.

What I checked:

  • Current main still has the fail-open key: runWebhookPipeline derives inFlightKey as req.socket.remoteAddress ?? "" before calling the limiter, so an absent remote address reaches the empty-string path. (extensions/voice-call/src/webhook.ts:619, df0074768c97)
  • Limiter contract treats empty keys as opt-out: createWebhookInFlightLimiter.tryAcquire returns true immediately for a falsy key, and release ignores falsy keys, matching the PR's root-cause claim. (src/plugin-sdk/webhook-request-guards.ts:111, df0074768c97)
  • Release path stays paired: The existing finally block releases the same inFlightKey variable acquired before body read, so the PR's fallback key would be decremented through the existing cleanup path. (extensions/voice-call/src/webhook.ts:681, df0074768c97)
  • Existing coverage covers only normal source-IP limiting: The current voice-call test suite asserts that eight concurrent pre-auth requests from one source IP are allowed and the ninth is rejected with 429, but it does not simulate a missing remote address. (extensions/voice-call/src/webhook.test.ts:914, df0074768c97)
  • Latest PR scope is narrow: The provided latest PR diff at head be55bfd4de38 modifies only extensions/voice-call/src/webhook.ts, replacing the empty fallback with a logged non-empty fallback key. Earlier Google Chat changes that triggered prior bot review are no longer in the latest diff. (extensions/voice-call/src/webhook.ts:616, be55bfd4de38)
  • Other in-flight limiter callers checked: A sweep of createWebhookInFlightLimiter callers shows voice-call is the direct empty-key case; shared target routing supplies ${resolved.path}:${remoteAddress ?? "unknown"}, and other channel/plugin callers pass path/account-prefixed non-empty keys. (src/plugin-sdk/webhook-targets.ts:141, df0074768c97)

Likely related people:

  • steipete: Current-main blame and history for the affected voice-call webhook pipeline and shared webhook limiter files point to Peter Steinberger, and the PR timeline also shows maintainer automerge involvement from steipete. (role: current-main introducer / recent maintainer; confidence: high; commits: 587b537b4745, be8c24633aaa; files: extensions/voice-call/src/webhook.ts, extensions/voice-call/src/webhook.test.ts, src/plugin-sdk/webhook-request-guards.ts)

Remaining risk / open question:

  • No automated regression test in the PR directly exercises req.socket.remoteAddress being absent; current coverage only exercises the normal per-source-IP limiter path.
  • This read-only review did not run the PR branch tests, so normal CI/automerge proof is still needed before merge.

Codex review notes: model gpt-5.5, reasoning high; reviewed against df0074768c97.

@clawsweeper clawsweeper Bot removed the clawsweeper:human-review Needs maintainer review before ClawSweeper can continue label Apr 29, 2026
@steipete
Copy link
Copy Markdown
Contributor

/clawsweeper re-review

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

🦞🦞
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.

@clawsweeper clawsweeper Bot added the clawsweeper:human-review Needs maintainer review before ClawSweeper can continue label Apr 29, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

🦞🦞
ClawSweeper is pausing automerge for human review.

Source: clawsweeper[bot]
Reason: structured ClawSweeper verdict: needs-human (sha=be55bfd4de3801cc7a6e8b2f7d6fe28a44da490a)

I added clawsweeper:human-review and left the final call with a maintainer.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 29, 2026

🦞🦞
Thanks, ClawSweeper. ClawSweeper saw the passing review, but did not merge yet.

Source: clawsweeper[bot]
Feedback: structured ClawSweeper verdict: pass (sha=be55bfd4de3801cc7a6e8b2f7d6fe28a44da490a)
Merge status: PR is paused for human review

I left the PR open for the remaining gate instead of bypassing it.

@clawsweeper clawsweeper Bot removed the clawsweeper:human-review Needs maintainer review before ClawSweeper can continue label Apr 29, 2026
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from be55bfd to db493ed Compare April 29, 2026 23:51
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from db493ed to 875bdcc Compare April 29, 2026 23:52
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from 875bdcc to 673a6fe Compare April 29, 2026 23:58
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

🦞🦞
Thanks, ClawSweeper. ClawSweeper saw the passing review, but did not merge yet.

Source: clawsweeper[bot]
Feedback: structured ClawSweeper verdict: pass (sha=673a6febf62626defe484b3715061da96c77efb9)
Merge status: mergeable state is CONFLICTING

I left the PR open for the remaining gate instead of bypassing it.

@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from 673a6fe to 8f46c21 Compare April 30, 2026 00:06
@openclaw-barnacle openclaw-barnacle Bot added the commands Command implementations label Apr 30, 2026
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from cd49b04 to 6132046 Compare April 30, 2026 00:20
@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations and removed commands Command implementations labels Apr 30, 2026
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from 32e9807 to a82a6bd Compare April 30, 2026 00:25
@openclaw-barnacle openclaw-barnacle Bot removed the commands Command implementations label Apr 30, 2026
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from a82a6bd to e4e8b26 Compare April 30, 2026 00:29
davidangularme and others added 3 commits April 30, 2026 01:34
…dress

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.
@steipete steipete force-pushed the fix/voice-call-webhook-rate-limit-proxy branch from e4e8b26 to f1367a7 Compare April 30, 2026 00:35
@steipete
Copy link
Copy Markdown
Contributor

@clawsweeper automerge

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

🦞🦞
ClawSweeper automerge is enabled for this PR.

I added clawsweeper:automerge and asked ClawSweeper to review this head. If ClawSweeper emits a repair marker or requests changes, I will repair/rebase the branch and ask for another review, up to the configured round limit.

Draft PRs stay fix-only until GitHub marks them ready for review. A maintainer can pause this with /clawsweeper stop.

@steipete
Copy link
Copy Markdown
Contributor

@clawsweeper automerge

@clawsweeper clawsweeper Bot merged commit 9b1bde2 into openclaw:main Apr 30, 2026
76 checks passed
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

🦞🦞
Thanks, ClawSweeper. ClawSweeper merged this PR after the passing review.

Source: clawsweeper[bot]
Feedback: structured ClawSweeper verdict: pass (sha=f1367a72f7eac74b05319dd134865c51f02f744e)
Merge status: merged by ClawSweeper automerge
Merged at: 2026-04-30T01:02:15Z

The automerge loop is complete.

sragss added a commit to Merit-Systems/openclaw that referenced this pull request May 1, 2026
* docs: refresh plugin sdk api baseline

* fix(google): support Vertex authorized_user ADC

* fix(cli): scope packaged compile cache

* test: align model list expectations

* changelog: Add inferred follow-up commitments for agents

Move commitment changelog entry to unreleased.

* test: avoid volatile model availability assertions

* feat: default active steering to batched delivery

* test(plugins): align release validation fixtures

* docs(config): document queue backlog alias

* refactor(channels): finish turn kernel migration

* fix(test): configure kitchen sink before enable

* fix(msteams): accept conversation id allowlists

* fix: changed explicit-path handling regression (#74672)

* fix: changed explicit-path handling regression

* fix: preserve unicode adc fallback paths

---------

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Shakker <[email protected]>

* test: cover sdk gateway integration

* docs: clarify app sdk documentation

* test: align sdk gateway event e2e

* fix(discord): cool down Cloudflare 429 responses

* fix(discord): bound application summary probes

* fix(discord): allow configured application ids

* docs(discord): clarify application id account scope

* docs(discord): tag Cloudflare cooldown changelog

* fix(docs): bound i18n codex prompt cleanup

* fix: compatibility gaps in the new Google Vertex ADC manifest evidence

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.

* test(sdk): remove redundant fake transport cast

* fix(status): resolve packaged channel setup loader

* docs: document shipped app sdk

* docs: update clawsweeper automerge workflow

* fix(voice-call): close webhook in-flight limiter fail-open on empty remote address (#74453)

* 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 <[email protected]>

* fix(models): unconditionally suppress stale openai-codex/gpt-5.4-mini inline entries (#74451) (#74655)

* 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 <[email protected]>

* fix(changelog): add missing reporter attribution for #74451 models suppression fix

* docs: credit codex mini suppression contributors

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Shakker <[email protected]>

* test(scripts): align changed sdk routing expectation

* docs: document clawsweeper changelog gate

* docs: explain security autofix boundary

* fix(docs): allow partial i18n doc batches

* fix: refresh Google Meet speech retry readiness

* refactor(channels): share turn dispatch results

* test(core): stabilize changed gate mocks

* fix: harden slack command menus

* fix(codex): flush pending steering on completion

* chore: ignore Google OAuth client secrets

* docs(changelog): backfill last-24h sidebar trigger entry

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(plugins): use tokenjuice published openclaw types

* chore(ci): add CodeQL PR security guard

Runs the narrow CodeQL critical-security matrix on non-draft pull requests for code and workflow security-boundary changes.

* fix: harden slack interactive blocks

* fix: mirror sqlite-vec for bundled memory runtime

* fix: bound slack interactive button urls

* fix(gateway): bound discovery advertise startup

* fix(gateway): refresh model catalog off request path

* fix(feishu): fallback to media resource download (#73986) (thanks @alex-xuweilong)

* test: cover app sdk gateway surfaces

* fix(amazon-bedrock): expose Opus 4.7 thinking profile

* fix(whatsapp): track provider-accepted auto-replies

* fix: bound slack command confirm text

* refactor(channels): move more turn policy into kernel

* fix(plugins): repair incomplete runtime-deps mirrors

* fix(bonjour): cap flapping advertiser restarts

* fix: bound slack approval metadata

* fix: interpolate heartbeat response prefix templates (#73996) (thanks @yweiii and @JunJD)

* fix(mattermost): add WebSocket ping/pong keepalive (#73979)

Adds Mattermost WebSocket ping/pong liveness checks so half-open sockets terminate and the existing reconnect loop recovers.

Fixes #41837.
Carries forward #57621.
Refs #50138, #44160, and #51104.
Thanks @JasonWang1124.

Co-authored-by: JasonWang1124 <[email protected]>

* fix(feishu): clean up bitable placeholder rows with empty defaults

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.

* fix(memory-lancedb): show full IDs in memory_forget candidate list

* fix(lint): resolve oxlint errors

* test(memory-lancedb): mock embedding transport in forget test

* docs(changelog): thank memory forget fix contributor

* fix: cap slack command menu blocks

* fix: drop overlong slack command values

* chore(ci): widen CodeQL PR guard

Runs the PR CodeQL security guard as high-confidence high/critical security coverage and adds the initial plugin/package-contract quality guard.

* refactor(channels): route inbound turns through kernel

* fix(macos): keep attach-only from stopping gateway launchd

* style(macos): order attach-only test modifiers

* docs: credit macos attach-only launchd fix

* fix(plugin-sdk): restore zalouser facade

* fix(plugins): repair configured runtime deps

* fix(plugins): resolve plugin paths from root

* fix(plugins): prefer require runtime aliases

* docs: update plugin runtime changelog

* docs: document high-confidence triage candidate filter

* fix(channels): preserve observe-only turn compatibility

* fix: warn before npm prefix redirection (#73890) (thanks @Sayeem3051)

* fix(sdk): stabilize run event chat projections (#74750) thanks @bitloi

Co-authored-by: bitloi <[email protected]>

* fix(gateway): align sessions abort wait semantics (#74751) thanks @BunsDev

Co-authored-by: Val Alexander <[email protected]>

* fix: stabilize full release validation

* fix(discord): avoid resolving tokens for read-only accessors

* docs: update changelog for Discord SecretRef accessor (#74737)

* fix(agents): drop metadata-only replay turns

Fixes #74745

* fix(file-transfer): require canonical node policy authorization (#74742)

* 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 <[email protected]>

* fix(clawsweeper): address review for automerge-openclaw-openclaw-74134 (1)

Co-authored-by: Omar Shahine <[email protected]>

* 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 <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
Co-authored-by: clawsweeper-repair <[email protected]>

* test: accept kitchen sink conformance diagnostics

* docs: update 2026.4.29 changelog

* docs(changelog): backfill 1f1f70a23f gateway sessions abort wait semantics

* fix(channels): keep status accessors config-only

* fix(doctor): suppress false-positive embedding warning when probe skipped

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

* fix(doctor): propagate gateway skipped-probe flag through adapter

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.

* fix(doctor): add skipped discriminator to distinguish probe skip from gateway timeout

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

* test(doctor): add skipped: false to gateway error and timeout test assertions

* docs: credit doctor memory probe fix (#74653) (thanks @hclsys)

* fix: keep slack message controls

* fix: cap slack block fallback text

* fix: cap slack edit fallback text

* fix: cap slack approval update text

* fix(active-memory): clarify modelFallbackPolicy deprecation warning text

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.

* fix(active-memory): clarify fallback config help (#74602) (thanks @jeffrey701)

* config: accept browser.tabCleanup keys in zod schema (#74577) (#74638)

* config: accept browser.tabCleanup keys in zod schema (#74577)

* docs: update config baseline hash

---------

Co-authored-by: Peter Steinberger <[email protected]>

* chore: refresh a2ui bundle hash

* fix: accept extensionless runtime dependency mains

* fix: harden Windows Parallels update smoke

* fix: stabilize Parallels update restart checks

* fix(acp): fall through to thread-bound resolution when token is unresolvable (#66299) (#74641)

* 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 <[email protected]>

* fix(feishu): skip empty-text messages with no media to prevent blank session turns (#74634) (#74661)

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(exec): preserve turnSourceChannel as messageProvider in approval followup runs (#74666)

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 <[email protected]>

* chore(ci): add gateway CodeQL PR quality guard

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.

* fix: reject invalid cron edits on disabled jobs (#74720)

* fix(cron): reject invalid disabled schedule updates

* docs: add cron validation changelog entry

---------

Co-authored-by: Peter Steinberger <[email protected]>

* chore(ci): add provider CodeQL PR quality guard

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.

* chore(deps): bump actions group

Bumps the actions group with 2 updates in the / directory: [useblacksmith/setup-docker-builder](https://github.com/useblacksmith/setup-docker-builder) and [useblacksmith/build-push-action](https://github.com/useblacksmith/build-push-action).


Updates `useblacksmith/setup-docker-builder` from 1.7.0 to 1.8.0
- [Release notes](https://github.com/useblacksmith/setup-docker-builder/releases)
- [Commits](https://github.com/useblacksmith/setup-docker-builder/compare/ac083cc84672d01c60d5e8561d0a939b697de542...722e97d12b1d06a961800dd6c05d79d951ad3c80)

Updates `useblacksmith/build-push-action` from 2.1.0 to 2.2.0
- [Release notes](https://github.com/useblacksmith/build-push-action/releases)
- [Commits](https://github.com/useblacksmith/build-push-action/compare/cbd1f60d194a98cb3be5523b15134501eaf0fbf3...fb9e3e6a9299c78462bfadd0d93352c316adc9b8)

---
updated-dependencies:
- dependency-name: useblacksmith/build-push-action
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: useblacksmith/setup-docker-builder
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump swift-testing

Bumps [github.com/apple/swift-testing](https://github.com/apple/swift-testing) from 0.99.0 to 6.3.1.
- [Release notes](https://github.com/apple/swift-testing/releases)
- [Commits](https://github.com/apple/swift-testing/compare/0.99.0...6.3.1)

---
updated-dependencies:
- dependency-name: github.com/apple/swift-testing
  dependency-version: 6.3.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(commands): require gateway memory probe skipped state

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(test): keep kitchen-sink conformance diagnostics clean

* fix: test-harness regression risk

* fix: keep kitchen-sink conformance diagnostics clean

---------

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper-repair <[email protected]>

* fix(gateway): avoid caching empty model catalogs

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* build(deps): bump debian docker base digest

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] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <[email protected]>

* chore(ci): add channel CodeQL PR quality guard

Adds the channel runtime quality shard to the PR CodeQL guard and keeps non-security quality analysis path-sharded by surface.

* docs(ci): rewrite for structure, deduplication, and findability

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.

* fix(slack): share edit fallback text truncation

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(slack): cap approval update fallback text

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(channels): suppress observe-only prepared dispatch

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* test(ci): guard install smoke docker cache removal

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(media): treat legacy Word docs as binary attachments

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>

* chore(ci): add process CodeQL PR quality guard

Adds the MCP/process runtime quality shard to the PR CodeQL guard and keeps non-security quality analysis path-sharded by surface.

* fix(slack): cap select option values

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(slack): preserve mixed interactive blocks

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(plugins): prefer require export conditions

* fix: fixed condition order prefers a top-level require export before a node condition, which...

* fix(clawsweeper): address review for clawsweeper-commit-openclaw-openclaw-6877360218c9 (1)

---------

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(bedrock): expose Opus 4.7 max thinking

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(google): accept Windows ADC manifest paths

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* chore(ci): add session CodeQL PR quality guard

Adds the session diagnostics quality shard to the PR CodeQL guard while keeping diagnostics and delivery queue analysis path-sharded by surface.

* fix(gateway): preserve rpc abort terminal snapshots

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(plugins): select runtime deps by configured models

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(sdk): emit replacement chat projection deltas

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(plugins): honor runtime deps fallback install option

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix: configs that used the previously documented WhatsApp exposeErrorText key now fail valida... (#74667)

* 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>

* fix: environment edge case launcher regression (#74696)

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* chore(ui): refresh fa control ui locale

* fix: Windows-specific reliability gap in the new timeout cleanup path (#74703)

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* chore(ci): add plugin SDK reply CodeQL PR guard

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(ui): refresh Persian locale copy

* chore(ci): add memory CodeQL PR quality guard

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.

* fix(control-ui): disable refresh during active runs

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.

* chore(ci): add auth CodeQL PR quality guard

Adds the core-auth-secrets quality shard to the PR CodeQL guard and documents the expanded ten-shard PR quality set.

* docs(changelog): backfill c34ed90822 control UI refresh-during-runs guard

* fix(slack): offset presentation controls after native blocks

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(plugins): keep disabled plugin runtime deps off

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* chore(ci): add config CodeQL PR quality guard

Adds the config-boundary quality shard to the PR CodeQL guard and documents the expanded eleven-shard PR quality set.

* fix(ci): disable install smoke Docker build cache

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* chore(ci): cover bundled channels in CodeQL PR guard

Extends the channel CodeQL quality shard to bundled channel plugin source directories and documents the scoped PR guard coverage.

* fix: enable native require fast path on Windows for bundled plugins (#74173)

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 <[email protected]>
Co-authored-by: Copilot <[email protected]>

* Fix CLI text command hangs (#74220)

* fix(cli): keep agents list off plugin preload

* docs(changelog): note cli text hang fix

* test(cli): update preaction agents list expectations

* test(channels): align module loader jiti fixture

* fix(skills): scan grouped skill directories

* 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 <[email protected]>
Co-authored-by: vincentkoc <[email protected]>
Co-authored-by: Otto Deng <[email protected]>

* chore(ci): add agent CodeQL PR quality guard

Promotes the existing agent-runtime quality shard to PR/manual selection and documents the expanded twelve-shard PR quality set.

* fix(skills): bound grouped skill directory scans

* fix(security): remediate CodeQL alerts

* fix: existing doctor-contract Windows loader test still expects Jiti to be called for contrac... (#74923)

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix: change disables bundled dependency repair when plugins.enabled: false, but the same fall... (#74916)

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <[email protected]>

* docs(changelog): note outbound CodeQL remediation

Adds the requested changelog attribution for CodeQL alert 228.

* docs(changelog): note secret comparison CodeQL remediation

Adds the requested changelog attribution for CodeQL alert 229.

* fix(sdk): treat terminal wait timeouts as timed out (#74697)

* fix: wait-status mapping sdk regression

* fix(sdk): treat terminal wait timeouts as timed out

---------

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper-repair <[email protected]>

* fix(ci): committed Plugin SDK API baseline hash is not reproducible from the committed source... (#74789)

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix: derive dynamic context-window guard thresholds

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: 9c80383639321871fe93e276c56459de096a2d70

* fix(security): sanitize QQBot debug log values

Sanitizes QQBot debug log values to remediate CodeQL alert 230.

* fix(pdf): resolve standard fonts from pdfjs package root (#70936)

* 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 <[email protected]>
Co-authored-by: vincentkoc <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>

* fix(cron): warn when --agent is not specified on cron add (#42245)

* 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 <[email protected]>

* fix(security): emit QQBot debug logs as sanitized lines

Emits QQBot debug logs as CRLF-neutralized lines to remediate CodeQL alert 231.

* fix: bounded directory scan actionable regression (#74942)

* fix: bounded directory scan actionable regression

* fix: current main remaining regression

* fix(skills): compose workspace scan caps

---------

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <[email protected]>

* fix(memory-lancedb): get memory records through ltm list command (#67952)

* fix(mem-lancedb): get memory records through ltm list command

* code review

---------

Co-authored-by: zhangyue19921010 <[email protected]>

* feat(channel) update yuanbao plugin github location (#74253)

* feat(channel) update yuanbao plugin version and github location

* feat(channel) update yuanbao plugin github location

* fix(channel): update yuanbao plugin GitHub location and add yuanbao alias (#74253) (thanks @loongfay)

---------

Co-authored-by: loongzhao <[email protected]>
Co-authored-by: sliverp <[email protected]>

* fix(security): align QQBot log sanitizer with CodeQL

Aligns QQBot debug log newline removal with the CodeQL js/log-injection sanitizer model to close alert 232.

* fix(github): skip maintainer-owned Barnacle targets

* docs(changelog): backfill 1e20babcf7 memory-lancedb ltm list

* fix(qqbot): unify slash command auth, c2cOnly gating, and file delivery (#73616)

* 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 <[email protected]>

* fix: warning text cli correctness issue (#74964)

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(agents): preserve string user content when merging turns

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <[email protected]>

* docs: cover qqbot /bot-me + c2cOnly admin gating (62fb87641e) and cron add --agent warning (dc0c54c7f1)

* test(gateway): avoid post-close auth rotation rpc

* ci: right-size OpenGrep PR scan

* ci: right-size opengrep pr scan

* ci: avoid opengrep rulepack self-scan

* ci: opt opengrep workflows into node24 actions

* ci: update opengrep workflow action majors

* chore(ci): tune stale policy and add backfill

* chore(ci): tune stale grace periods

* chore(ci): add stale closure backfill

* chore(ci): skip maintainer assignees in stale backfill

* ci: shallow checkout OpenGrep PR scan

* fix(channels): align Yuanbao catalog id

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(control-ui): wire slash menu accessibility

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.

* chore(barnacle): add false positive close label (#75014)

* fix(cli): avoid progress spinners in active TUI input (#75003)

Merged via squash.

Prepared head SHA: 129e23e71605c5158ac33ad30de088aca530f712
Co-authored-by: velvet-shark <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark

* fix(ci): bound manual stale closure backfill

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>

* fix(macos): repair stale gateway tls pins (#75038)

Merged via squash.

Prepared head SHA: 35196f8f714d4ab07ebb211802693d03385486ad
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman

* fix(macos): keep A2UI canvas content visible (#75039)

* fix(security): stop implicit tool grants from config sections (#47487) (#75055)

* fix(security): stop implicit tool grants from config sections (#47487)

Configured tool sections (tools.exec, tools.fs) no longer implicitly
widen restrictive profiles (messagin…
lxe pushed a commit to lxe/openclaw that referenced this pull request May 6, 2026
…emote address (openclaw#74453)

* 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 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: voice-call Channel integration: voice-call clawsweeper:automerge Maintainer opted this PR into bounded ClawSweeper-reviewed automerge size: S triage: blank-template Candidate: PR template appears mostly untouched. triage: refactor-only Candidate: refactor/cleanup-only PR without maintainer context.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants