Compare commits

..

2 Commits

Author SHA1 Message Date
Peter Steinberger
16e6789dd5 fix: preserve inline code copy fidelity in web ui (#32346) (thanks @hclsys) 2026-03-03 02:04:36 +00:00
HCL
ba99fda951 fix(webui): prevent inline code from breaking mid-token on copy/paste
The parent `.chat-text` applies `overflow-wrap: anywhere; word-break: break-word;`
which forces long tokens (UUIDs, hashes) inside inline `<code>` to break across
visual lines. When copied, the browser injects spaces at those break points,
corrupting the pasted value.

Override with `overflow-wrap: normal; word-break: keep-all;` on inline `<code>`
selectors so tokens stay intact.

Fixes #32230

Signed-off-by: HCL <chenglunhu@gmail.com>
2026-03-03 02:04:09 +00:00
1289 changed files with 12333 additions and 61739 deletions

View File

@@ -51,10 +51,6 @@ vendor/
# Keep the rest of apps/ and vendor/ excluded to avoid a large build context.
!apps/shared/
!apps/shared/OpenClawKit/
!apps/shared/OpenClawKit/Sources/
!apps/shared/OpenClawKit/Sources/OpenClawKit/
!apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/
!apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json
!apps/shared/OpenClawKit/Tools/
!apps/shared/OpenClawKit/Tools/CanvasA2UI/
!apps/shared/OpenClawKit/Tools/CanvasA2UI/**

View File

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

View File

@@ -1,7 +1,7 @@
name: Setup Node environment
description: >
Initialize submodules with retry, install Node 22, pnpm, optionally Bun,
and optionally run pnpm install. Requires actions/checkout to run first.
and run pnpm install. Requires actions/checkout to run first.
inputs:
node-version:
description: Node.js version to install.
@@ -15,14 +15,6 @@ inputs:
description: Whether to install Bun alongside Node.
required: false
default: "true"
use-sticky-disk:
description: Use Blacksmith sticky disks for pnpm store caching.
required: false
default: "false"
install-deps:
description: Whether to run pnpm install after environment setup.
required: false
default: "true"
frozen-lockfile:
description: Whether to use --frozen-lockfile for install.
required: false
@@ -48,20 +40,19 @@ runs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
check-latest: false
check-latest: true
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: ${{ inputs.pnpm-version }}
cache-key-suffix: "node22"
use-sticky-disk: ${{ inputs.use-sticky-disk }}
- name: Setup Bun
if: inputs.install-bun == 'true'
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.9"
bun-version: "1.3.9+cf6cdbbba"
- name: Runtime versions
shell: bash
@@ -72,12 +63,10 @@ runs:
if command -v bun &>/dev/null; then bun -v; fi
- name: Capture node path
if: inputs.install-deps == 'true'
shell: bash
run: echo "NODE_BIN=$(dirname "$(node -p "process.execPath")")" >> "$GITHUB_ENV"
- name: Install dependencies
if: inputs.install-deps == 'true'
shell: bash
env:
CI: "true"

View File

@@ -9,18 +9,6 @@ inputs:
description: Suffix appended to the cache key.
required: false
default: "node22"
use-sticky-disk:
description: Use Blacksmith sticky disks instead of actions/cache for pnpm store.
required: false
default: "false"
use-restore-keys:
description: Whether to use restore-keys fallback for actions/cache.
required: false
default: "true"
use-actions-cache:
description: Whether to restore/save pnpm store with actions/cache.
required: false
default: "true"
runs:
using: composite
steps:
@@ -50,22 +38,7 @@ runs:
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Mount pnpm store sticky disk
if: inputs.use-sticky-disk == 'true'
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-pnpm-store-${{ runner.os }}-${{ inputs.cache-key-suffix }}
path: ${{ steps.pnpm-store.outputs.path }}
- name: Restore pnpm store cache (exact key only)
if: inputs.use-actions-cache == 'true' && inputs.use-sticky-disk != 'true' && inputs.use-restore-keys != 'true'
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ inputs.cache-key-suffix }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Restore pnpm store cache (with fallback keys)
if: inputs.use-actions-cache == 'true' && inputs.use-sticky-disk != 'true' && inputs.use-restore-keys == 'true'
- name: Restore pnpm store cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}

View File

@@ -32,13 +32,12 @@ jobs:
# Push to main keeps broad coverage.
changed-scope:
needs: [docs-scope]
if: github.event_name == 'pull_request' && needs.docs-scope.outputs.docs_only != 'true'
if: needs.docs-scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
outputs:
run_node: ${{ steps.scope.outputs.run_node }}
run_macos: ${{ steps.scope.outputs.run_macos }}
run_android: ${{ steps.scope.outputs.run_android }}
run_windows: ${{ steps.scope.outputs.run_windows }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -58,11 +57,75 @@ jobs:
BASE="${{ github.event.pull_request.base.sha }}"
fi
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
CHANGED="$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")"
if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then
# Fail-safe: run broad checks if detection fails.
echo "run_node=true" >> "$GITHUB_OUTPUT"
echo "run_macos=true" >> "$GITHUB_OUTPUT"
echo "run_android=true" >> "$GITHUB_OUTPUT"
exit 0
fi
run_node=false
run_macos=false
run_android=false
has_non_docs=false
has_non_native_non_docs=false
while IFS= read -r path; do
[ -z "$path" ] && continue
case "$path" in
docs/*|*.md|*.mdx)
continue
;;
*)
has_non_docs=true
;;
esac
case "$path" in
# Generated protocol models are already covered by protocol:check and
# should not force the full native macOS lane.
apps/macos/Sources/OpenClawProtocol/*|apps/shared/OpenClawKit/Sources/OpenClawProtocol/*)
;;
apps/macos/*|apps/ios/*|apps/shared/*|Swabble/*)
run_macos=true
;;
esac
case "$path" in
apps/android/*|apps/shared/*)
run_android=true
;;
esac
case "$path" in
src/*|test/*|extensions/*|packages/*|scripts/*|ui/*|.github/*|openclaw.mjs|package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig*.json|vitest*.ts|tsdown.config.ts|.oxlintrc.json|.oxfmtrc.jsonc)
run_node=true
;;
esac
case "$path" in
apps/android/*|apps/ios/*|apps/macos/*|apps/shared/*|Swabble/*|appcast.xml)
;;
*)
has_non_native_non_docs=true
;;
esac
done <<< "$CHANGED"
# If there are non-doc files outside native app trees, keep Node checks enabled.
if [ "$run_node" = false ] && [ "$has_non_docs" = true ] && [ "$has_non_native_non_docs" = true ]; then
run_node=true
fi
echo "run_node=${run_node}" >> "$GITHUB_OUTPUT"
echo "run_macos=${run_macos}" >> "$GITHUB_OUTPUT"
echo "run_android=${run_android}" >> "$GITHUB_OUTPUT"
# Build dist once for Node-relevant changes and share it with downstream jobs.
build-artifacts:
needs: [docs-scope, changed-scope]
needs: [docs-scope, changed-scope, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
@@ -75,7 +138,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Build dist
run: pnpm build
@@ -102,7 +164,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Download dist artifact
uses: actions/download-artifact@v4
@@ -114,7 +175,7 @@ jobs:
run: pnpm release:check
checks:
needs: [docs-scope, changed-scope]
needs: [docs-scope, changed-scope, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
strategy:
@@ -146,7 +207,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "${{ matrix.runtime == 'bun' }}"
use-sticky-disk: "true"
- name: Configure Node test resources
if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node'
@@ -163,8 +223,8 @@ jobs:
# Types, lint, and format check.
check:
name: "check"
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
needs: [docs-scope]
if: needs.docs-scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
@@ -176,7 +236,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Check types and lint and oxfmt
run: pnpm check
@@ -216,7 +275,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Run ${{ matrix.tool }} dead-code scan
run: ${{ matrix.command }}
@@ -242,7 +300,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Check docs
run: pnpm check:docs
@@ -285,8 +342,6 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
install-deps: "false"
- name: Setup Python
uses: actions/setup-python@v5
@@ -330,15 +385,15 @@ jobs:
run: pre-commit run --all-files pnpm-audit-prod
checks-windows:
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_windows == 'true')
runs-on: blacksmith-32vcpu-windows-2025
needs: [docs-scope, changed-scope, build-artifacts, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-windows-2025
timeout-minutes: 45
env:
NODE_OPTIONS: --max-old-space-size=6144
# Keep total concurrency predictable on the 32 vCPU runner.
# Windows shard 2 has shown intermittent instability at 2 workers.
OPENCLAW_TEST_WORKERS: 1
NODE_OPTIONS: --max-old-space-size=4096
# Keep total concurrency predictable on the 16 vCPU runner:
# `scripts/test-parallel.mjs` runs some vitest suites in parallel processes.
OPENCLAW_TEST_WORKERS: 2
defaults:
run:
shell: bash
@@ -346,36 +401,26 @@ jobs:
fail-fast: false
matrix:
include:
- runtime: node
task: lint
shard_index: 0
shard_count: 1
command: pnpm lint
- runtime: node
task: test
shard_index: 1
shard_count: 6
command: pnpm test
shard_count: 2
command: pnpm canvas:a2ui:bundle && pnpm test
- runtime: node
task: test
shard_index: 2
shard_count: 6
command: pnpm test
shard_count: 2
command: pnpm canvas:a2ui:bundle && pnpm test
- runtime: node
task: test
shard_index: 3
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 4
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 5
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 6
shard_count: 6
command: pnpm test
task: protocol
shard_index: 0
shard_count: 1
command: pnpm protocol:check
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -401,22 +446,31 @@ jobs:
Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)"
}
- name: Download dist artifact (lint lane)
if: matrix.task == 'lint'
uses: actions/download-artifact@v4
with:
name: dist-build
path: dist/
- name: Verify dist artifact (lint lane)
if: matrix.task == 'lint'
run: |
set -euo pipefail
test -s dist/index.js
test -s dist/plugin-sdk/index.js
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
check-latest: false
check-latest: true
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "10.23.0"
cache-key-suffix: "node22"
# Sticky disk mount currently retries/fails on every shard and adds ~50s
# before install while still yielding zero pnpm store reuse.
use-sticky-disk: "false"
use-restore-keys: "false"
use-actions-cache: "false"
- name: Runtime versions
run: |
@@ -435,7 +489,7 @@ jobs:
which node
node -v
pnpm -v
pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
- name: Configure test shard (Windows)
if: matrix.task == 'test'
@@ -443,10 +497,6 @@ jobs:
echo "OPENCLAW_TEST_SHARDS=${{ matrix.shard_count }}" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=${{ matrix.shard_index }}" >> "$GITHUB_ENV"
- name: Build A2UI bundle (Windows)
if: matrix.task == 'test'
run: pnpm canvas:a2ui:bundle
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
run: ${{ matrix.command }}
@@ -688,7 +738,7 @@ jobs:
PY
android:
needs: [docs-scope, changed-scope]
needs: [docs-scope, changed-scope, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
strategy:

View File

@@ -34,8 +34,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -92,12 +92,14 @@ jobs:
- name: Build and push amd64 image
id: build
uses: useblacksmith/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64,mode=max
provenance: false
push: true
@@ -113,8 +115,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -171,12 +173,14 @@ jobs:
- name: Build and push arm64 image
id: build
uses: useblacksmith/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/arm64
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64,mode=max
provenance: false
push: true

View File

@@ -44,14 +44,10 @@ jobs:
with:
pnpm-version: "10.23.0"
cache-key-suffix: "node22"
use-sticky-disk: "true"
- name: Install pnpm deps (minimal)
run: pnpm install --ignore-scripts --frozen-lockfile
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Run root Dockerfile CLI smoke
run: |
docker build -t openclaw-dockerfile-smoke:local -f Dockerfile .

View File

@@ -26,9 +26,6 @@ jobs:
with:
submodules: false
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Build minimal sandbox base (USER sandbox)
shell: bash
run: |

View File

@@ -1,155 +0,0 @@
name: Stale
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
permissions: {}
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Mark stale issues and pull requests
uses: actions/stale@v9
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 7
days-before-issue-close: 5
days-before-pr-stale: 5
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale
operations-per-run: 10000
exempt-all-assignees: true
remove-stale-when-updated: true
stale-issue-message: |
This issue has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
stale-pr-message: |
This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
lock-closed-issues:
permissions:
issues: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Lock closed issues after 48h of no comments
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const lockAfterHours = 48;
const lockAfterMs = lockAfterHours * 60 * 60 * 1000;
const perPage = 100;
const cutoffMs = Date.now() - lockAfterMs;
const { owner, repo } = context.repo;
let locked = 0;
let inspected = 0;
let page = 1;
while (true) {
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "closed",
sort: "updated",
direction: "desc",
per_page: perPage,
page,
});
if (issues.length === 0) {
break;
}
for (const issue of issues) {
if (issue.pull_request) {
continue;
}
if (issue.locked) {
continue;
}
if (!issue.closed_at) {
continue;
}
inspected += 1;
const closedAtMs = Date.parse(issue.closed_at);
if (!Number.isFinite(closedAtMs)) {
continue;
}
if (closedAtMs > cutoffMs) {
continue;
}
let lastCommentMs = 0;
if (issue.comments > 0) {
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issue.number,
per_page: 1,
page: 1,
sort: "created",
direction: "desc",
});
if (comments.length > 0) {
lastCommentMs = Date.parse(comments[0].created_at);
}
}
const lastActivityMs = Math.max(closedAtMs, lastCommentMs || 0);
if (lastActivityMs > cutoffMs) {
continue;
}
await github.rest.issues.lock({
owner,
repo,
issue_number: issue.number,
lock_reason: "resolved",
});
locked += 1;
}
page += 1;
}
core.info(`Inspected ${inspected} closed issues; locked ${locked}.`);

1
.gitignore vendored
View File

@@ -27,7 +27,6 @@ mise.toml
apps/android/.gradle/
apps/android/app/build/
apps/android/.cxx/
apps/android/.kotlin/
# Bun build artifacts
*.bun-build

View File

@@ -5,7 +5,6 @@
- GitHub issues/comments/PR comments: use literal multiline strings or `-F - <<'EOF'` (or $'...') for real newlines; never embed "\\n".
- GitHub comment footgun: never use `gh issue/pr comment -b "..."` when body contains backticks or shell chars. Always use single-quoted heredoc (`-F - <<'EOF'`) so no command substitution/escaping corruption.
- GitHub linking footgun: dont wrap issue/PR refs like `#24643` in backticks when you want auto-linking. Use plain `#24643` (optionally add full URL).
- GitHub searching footgun: don't limit yourself to the first 500 issues or PRs when wanting to search all. Unless you're supposed to look at the most recent, keep going until you've reached the last page in the search
- Security advisory analysis: before triage/severity decisions, read `SECURITY.md` to align with OpenClaw's trust model and design boundaries.
## Project Structure & Module Organization
@@ -76,8 +75,6 @@
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
- Formatting/linting via Oxlint and Oxfmt; run `pnpm check` before commits.
- Never add `@ts-nocheck` and do not disable `no-explicit-any`; fix root causes and update Oxlint/Oxfmt config only when required.
- Dynamic import guardrail: do not mix `await import("x")` and static `import ... from "x"` for the same module in production code paths. If you need lazy loading, create a dedicated `*.runtime.ts` boundary (that re-exports from `x`) and dynamically import that boundary from lazy callers only.
- Dynamic import verification: after refactors that touch lazy-loading/module boundaries, run `pnpm build` and check for `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings before submitting.
- Never share class behavior via prototype mutation (`applyPrototypeMixins`, `Object.defineProperty` on `.prototype`, or exporting `Class.prototype` for merges). Use explicit inheritance/composition (`A extends B extends C`) or helper composition so TypeScript can typecheck.
- If this pattern is needed, stop and get explicit approval before shipping; default behavior is to split/refactor into an explicit class hierarchy and keep members strongly typed.
- In tests, prefer per-instance stubs over prototype mutation (`SomeClass.prototype.method = ...`) unless a test explicitly documents why prototype-level patching is required.

View File

@@ -2,331 +2,207 @@
Docs: https://docs.openclaw.ai
## 2026.3.3
## 2026.3.2 (Unreleased)
### Changes
- Web UI/i18n: add Spanish (`es`) locale support in the Control UI, including locale detection, lazy loading, and language picker labels across supported locales. (#35038) Thanks @DaoPromociones.
- Discord/allowBots mention gating: add `allowBots: "mentions"` to only accept bot-authored messages that mention the bot. Thanks @thewilloftheshadow.
- Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind.
- Tools/Web search: switch Perplexity provider to Search API with structured results plus new language/region/time filters. (#33822) Thanks @kesku.
- Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet.
- Agents/tool-result truncation: preserve important tail diagnostics by using head+tail truncation for oversized tool results while keeping configurable truncation options. (#20076) thanks @jlwestsr.
- Telegram/topic agent routing: support per-topic `agentId` overrides in forum groups and DM topics so topics can route to dedicated agents with isolated sessions. (#33647; based on #31513) Thanks @kesor and @Sid-Qin.
- Slack/DM typing feedback: add `channels.slack.typingReaction` so Socket Mode DMs can show reaction-based processing status even when Slack native assistant typing is unavailable. (#19816) Thanks @dalefrieswthat.
- Cron/job snapshot persistence: skip backup during normalization persistence in `ensureLoaded` so `jobs.json.bak` keeps the pre-edit snapshot for recovery, while preserving backup creation on explicit user-driven writes. (#35234) Thanks @0xsline.
### Fixes
- Agents/schema cleaning: detect Venice + Grok model IDs as xAI-proxied targets so unsupported JSON Schema keywords are stripped before requests, preventing Venice/Grok `Invalid arguments` failures. (openclaw#35355) thanks @Sid-Qin.
- Skills/native command deduplication: centralize skill command dedupe by canonical `skillName` in `listSkillCommandsForAgents` so duplicate suffixed variants (for example `_2`) are no longer surfaced across interfaces outside Discord. (#27521) thanks @shivama205.
- Agents/xAI tool-call argument decoding: decode HTML-entity encoded xAI/Grok tool-call argument values (`&amp;`, `&quot;`, `&lt;`, `&gt;`, numeric entities) before tool execution so commands with shell operators and quotes no longer fail with parse errors. (#35276) Thanks @Sid-Qin.
- Agents/thinking-tag promotion hardening: guard `promoteThinkingTagsToBlocks` against malformed assistant content entries (`null`/`undefined`) before `block.type` reads so malformed provider payloads no longer crash session processing while preserving pass-through behavior. (#35143) thanks @Sid-Qin.
- Gateway/Control UI version reporting: align runtime and browser client version metadata to avoid `dev` placeholders, wait for bootstrap version before first UI websocket connect, and only forward bootstrap `serverVersion` to same-origin gateway targets to prevent cross-target version leakage. (from #35230, #30928, #33928) Thanks @Sid-Qin, @joelnishanth, and @MoerAI.
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
- Cron/announce delivery robustness: bypass pending-descendant announce guards for cron completion sends, ensure named-agent announce routes have outbound session entries, and fall back to direct delivery only when an announce send was actually attempted and failed. (from #35185, #32443, #34987) Thanks @Sid-Qin, @scoootscooob, and @bmendonca3.
- Auto-reply/system events: restore runtime system events to the message timeline (`System:` lines), preserve think-hint parsing with prepended events, and carry events into deferred followup/collect/steer-backlog prompts to keep cache behavior stable without dropping queued metadata. (#34794) Thanks @anisoptera.
- Security/audit account handling: avoid prototype-chain account IDs in audit validation by using own-property checks for `accounts`. (#34982) Thanks @HOYALIM.
- Cron/restart catch-up semantics: replay interrupted recurring jobs and missed immediate cron slots on startup without replaying interrupted one-shot jobs, with guarded missed-slot probing to avoid malformed-schedule startup aborts and duplicate-trigger drift after restart. (from #34466, #34896, #34625, #33206) Thanks @dunamismax, @dsantoreis, @Octane0411, and @Sid-Qin.
- Agents/session usage tracking: preserve accumulated usage metadata on embedded Pi runner error exits so failed turns still update session `totalTokens` from real usage instead of stale prior values. (#34275) thanks @RealKai42.
- Nodes/system.run approval hardening: use explicit argv-mutation signaling when regenerating prepared `rawCommand`, and cover the `system.run.prepare -> system.run` handoff so direct PATH-based `nodes.run` commands no longer fail with `rawCommand does not match command`. (#33137) thanks @Sid-Qin.
- Models/custom provider headers: propagate `models.providers.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.
- Ollama/custom provider headers: forward resolved model headers into native Ollama stream requests so header-authenticated Ollama proxies receive configured request headers. (#24337) thanks @echoVic.
- Daemon/systemd install robustness: treat `systemctl --user is-enabled` exit-code-4 `not-found` responses as not-enabled by combining stderr/stdout detail parsing, so Ubuntu fresh installs no longer fail with `systemctl is-enabled unavailable`. (#33634) Thanks @Yuandiaodiaodiao.
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.
- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.
- TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc.
- Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant.
- Sessions/subagent attachments: remove `attachments[].content.maxLength` from `sessions_spawn` schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera.
- Runtime/tool-state stability: recover from dangling Anthropic `tool_use` after compaction, serialize long-running Discord handler runs without blocking new inbound events, and prevent stale busy snapshots from suppressing stuck-channel recovery. (from #33630, #33583) Thanks @kevinWangSheng and @theotarr.
- ACP/Discord startup hardening: clean up stuck ACP worker children on gateway restart, unbind stale ACP thread bindings during Discord startup reconciliation, and add per-thread listener watchdog timeouts so wedged turns cannot block later messages. (#33699) Thanks @dutifulbob.
- Extensions/media local-root propagation: consistently forward `mediaLocalRoots` through extension `sendMedia` adapters (Google Chat, Slack, iMessage, Signal, WhatsApp), preserving non-local media behavior while restoring local attachment resolution from configured roots. Synthesis of #33581, #33545, #33540, #33536, #33528. Thanks @bmendonca3.
- Feishu/video media send contract: keep mp4-like outbound payloads on `msg_type: "media"` (including reply and reply-in-thread paths) so videos render as media instead of degrading to file-link behavior, while preserving existing non-video file subtype handling. (from #33720, #33808, #33678) Thanks @polooooo, @dingjianrui, and @kevinWangSheng.
- Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan.
- Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy.
- Plugins/startup performance: reduce bursty plugin discovery/manifest overhead with short in-process caches, skip importing bundled memory plugins that are disabled by slot selection, and speed legacy root `openclaw/plugin-sdk` compatibility via runtime root-alias routing while preserving backward compatibility. Thanks @gumadeiras.
- Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras.
- Config/heartbeat legacy-path handling: auto-migrate top-level `heartbeat` into `agents.defaults.heartbeat` (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan.
- Plugins/SDK subpath parity: expand plugin SDK subpaths across bundled channels/extensions (Discord, Slack, Signal, iMessage, WhatsApp, LINE, and bundled companion plugins), with build/export/type/runtime wiring so scoped imports resolve consistently in source and dist while preserving compatibility. (#33737) thanks @gumadeiras.
- Plugins/bundled scoped-import migration: migrate bundled plugins from monolithic `openclaw/plugin-sdk` imports to scoped subpaths (or `openclaw/plugin-sdk/core`) across registration and startup-sensitive runtime files, add CI/release guardrails to prevent regressions, and keep root `openclaw/plugin-sdk` support for external/community plugins. Thanks @gumadeiras.
- Routing/session duplicate suppression synthesis: align shared session delivery-context inheritance, channel-paired route-field merges, and reply-surface target matching so dmScope=main turns avoid cross-surface duplicate replies while thread-aware forwarding keeps intended routing semantics. (from #33629, #26889, #17337, #33250) Thanks @Yuandiaodiaodiao, @kevinwildenradt, @Glucksberg, and @bmendonca3.
- Routing/legacy session route inheritance: preserve external route metadata inheritance for legacy channel session keys (`agent:<agent>:<channel>:<peer>` and `...:thread:<id>`) so `chat.send` does not incorrectly fall back to webchat when valid delivery context exists. Follow-up to #33786.
- Routing/legacy route guard tightening: require legacy session-key channel hints to match the saved delivery channel before inheriting external routing metadata, preventing custom namespaced keys like `agent:<agent>:work:<ticket>` from inheriting stale non-webchat routes.
- Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n.
- Auth/credential semantics: align profile eligibility + probe diagnostics with SecretRef/expiry rules and harden browser download atomic writes. (#33733) thanks @joshavant.
- Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown `gateway.nodes.denyCommands` entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot.
- Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan.
- Docs/security threat-model links: replace relative `.md` links with Mintlify-compatible root-relative routes in security docs to prevent broken internal navigation. (#27698) thanks @clawdoo.
- iOS/Voice timing safety: guard system speech start/finish callbacks to the active utterance to avoid misattributed start events during rapid stop/restart cycles. (#33304) thanks @mbelinky; original implementation direction by @ngutman.
- iOS/Talk incremental speech pacing: allow long punctuation-free assistant chunks to start speaking at safe whitespace boundaries so voice responses begin sooner instead of waiting for terminal punctuation. (#33305) thanks @mbelinky; original implementation by @ngutman.
- iOS/Watch reply reliability: make watch session activation waiters robust under concurrent requests so status/send calls no longer hang intermittently, and align delegate callbacks with Swift 6 actor safety. (#33306) thanks @mbelinky; original implementation by @Rocuts.
- Docs/tool-loop detection config keys: align `docs/tools/loop-detection.md` examples and field names with the current `tools.loopDetection` schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd.
- Gateway/session agent discovery: include disk-scanned agent IDs in `listConfiguredAgentIds` even when `agents.list` is configured, so disk-only/ACP agent sessions remain visible in gateway session aggregation and listings. (#32831) thanks @Sid-Qin.
- Discord/inbound debouncer: skip bot-own MESSAGE_CREATE events before they reach the debounce queue to avoid self-triggered slowdowns in busy servers. Thanks @thewilloftheshadow.
- Discord/Agent-scoped media roots: pass `mediaLocalRoots` through Discord monitor reply delivery (message + component interaction paths) so local media attachments honor per-agent workspace roots instead of falling back to default global roots. Thanks @thewilloftheshadow.
- Discord/slash command handling: intercept text-based slash commands in channels, register plugin commands as native, and send fallback acknowledgments for empty slash runs so interactions do not hang. Thanks @thewilloftheshadow.
- Discord/thread session lifecycle: reset thread-scoped sessions when a thread is archived so reopening a thread starts fresh without deleting transcript history. Thanks @thewilloftheshadow.
- Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow.
- Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow.
- Discord/config SecretRef typing: align Discord account token config typing with SecretInput so SecretRef tokens typecheck. (#32490) Thanks @scoootscooob.
- Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow.
- Discord/voice decoder fallback: drop the native Opus dependency and use opusscript for voice decoding to avoid native-opus installs. Thanks @thewilloftheshadow.
- Discord/auto presence health signal: add runtime availability-driven presence updates plus connected-state reporting to improve health monitoring and operator visibility. (#33277) Thanks @thewilloftheshadow.
- Telegram/draft-stream boundary stability: materialize DM draft previews at assistant-message/tool boundaries, serialize lane-boundary callbacks before final delivery, and scope preview cleanup to the active preview so multi-step Telegram streams no longer lose, overwrite, or leave stale preview bubbles. (#33842) Thanks @ngutman.
- Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.
- Telegram/DM draft final delivery: materialize text-only `sendMessageDraft` previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13.
- Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus.
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.
- Discord/chunk delivery reliability: preserve chunk ordering when using a REST client and retry chunk sends on 429/5xx using account retry settings. (#33226) Thanks @thewilloftheshadow.
- Discord/mention handling: add id-based mention formatting + cached rewrites, resolve inbound mentions to display names, and add optional ignoreOtherMentions gating (excluding @everyone/@here). (#33224) Thanks @thewilloftheshadow.
- Discord/media SSRF allowlist: allow Discord CDN hostnames (including wildcard domains) in inbound media SSRF policy to prevent proxy/VPN fake-ip blocks. (#33275) Thanks @thewilloftheshadow.
- Telegram/device pairing notifications: auto-arm one-shot notify on `/pair qr`, auto-ping on new pairing requests, and add manual fallback via `/pair approve latest` if the ping does not arrive. (#33299) thanks @mbelinky.
- Exec heartbeat routing: scope exec-triggered heartbeat wakes to agent session keys so unrelated agents are no longer awakened by exec events, while preserving legacy unscoped behavior for non-canonical session keys. (#32724) thanks @altaywtf
- macOS/Tailscale remote gateway discovery: add a Tailscale Serve fallback peer probe path (`wss://<peer>.ts.net`) when Bonjour and wide-area DNS-SD discovery return no gateways, and refresh both discovery paths from macOS onboarding. (#32860) Thanks @ngutman.
- iOS/Gateway keychain hardening: move gateway metadata and TLS fingerprints to device keychain storage with safer migration behavior and rollback-safe writes to reduce credential loss risk during upgrades. (#33029) thanks @mbelinky.
- iOS/Concurrency stability: replace risky shared-state access in camera and gateway connection paths with lock-protected access patterns to reduce crash risk under load. (#33241) thanks @mbelinky.
- iOS/Security guardrails: limit production API-key sourcing to app config and make deep-link confirmation prompts safer by coalescing queued requests instead of silently dropping them. (#33031) thanks @mbelinky.
- iOS/TTS playback fallback: keep voice playback resilient by switching from PCM to MP3 when provider format support is unavailable, while avoiding sticky fallback on generic local playback errors. (#33032) thanks @mbelinky.
- Plugin outbound/text-only adapter compatibility: allow direct-delivery channel plugins that only implement `sendText` (without `sendMedia`) to remain outbound-capable, gracefully fall back to text delivery for media payloads when `sendMedia` is absent, and fail explicitly for media-only payloads with no text fallback. (#32788) thanks @liuxiaopai-ai.
- Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add `openclaw doctor` warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin.
- Telegram/plugin outbound hook parity: run `message_sending` + `message_sent` in Telegram reply delivery, include reply-path hook metadata (`mediaUrls`, `threadId`), and report `message_sent.success=false` when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee.
- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. (#28610, #31149, #34055). Thanks @niceysam, @cryptomaltese and @vincentkoc.
- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. (#28786, #31338, #34055). Thanks @Sid-Qin and @vincentkoc.
- ACP/sessions_spawn parent stream visibility: add `streamTo: "parent"` for `runtime: "acp"` to forward initial child-run progress/no-output/completion updates back into the requester session as system events (instead of direct child delivery), and emit a tail-able session-scoped relay log (`<sessionId>.acp-stream.jsonl`, returned as `streamLogPath` when available), improving orchestrator visibility for blocked or long-running harness turns. (#34310, #29909; reopened from #34055). Thanks @vincentkoc.
- Agents/bootstrap truncation warning handling: unify bootstrap budget/truncation analysis across embedded + CLI runtime, `/context`, and `openclaw doctor`; add `agents.defaults.bootstrapPromptTruncationWarning` (`off|once|always`, default `once`) and persist warning-signature metadata so truncation warnings are consistent and deduped across turns. (#32769) Thanks @gumadeiras.
- Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras.
- Agents/Session startup date grounding: substitute `YYYY-MM-DD` placeholders in startup/post-compaction AGENTS context and append runtime current-time lines for `/new` and `/reset` prompts so daily-memory references resolve correctly. (#32381) Thanks @chengzhichao-xydt.
- Agents/Compaction template heading alignment: update AGENTS template section names to `Session Startup`/`Red Lines` and keep legacy `Every Session`/`Safety` fallback extraction so post-compaction context remains intact across template versions. (#25098) thanks @echoVic.
- Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.
- Agents/Compaction safeguard structure hardening: require exact fallback summary headings, sanitize untrusted compaction instruction text before prompt embedding, and keep structured sections when preserving all turns. (#25555) thanks @rodrigouroz.
- Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai.
- Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind.
- Memory/local embedding initialization hardening: add regression coverage for transient initialization retry and mixed `embedQuery` + `embedBatch` concurrent startup to lock single-flight initialization behavior. (#15639) thanks @SubtleSpark.
- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. Related to #28261. Landed from contributor PRs #28610 and #31149. Thanks @niceysam, @cryptomaltese and @vincentkoc.
- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. Related to #28786. Landed from contributor PR #31338. Thanks @Sid-Qin and @vincentkoc.
- LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman.
- LINE/media download synthesis: fix file-media download handling and M4A audio classification across overlapping LINE regressions. (from #26386, #27761, #27787, #29509, #29755, #29776, #29785, #32240) Thanks @kevinWangSheng, @loiie45e, @carrotRakko, @Sid-Qin, @codeafridi, and @bmendonca3.
- LINE/context and routing synthesis: fix group/room peer routing and command-authorization context propagation, and keep processing later events in mixed-success webhook batches. (from #21955, #24475, #27035, #28286) Thanks @lailoo, @mcaxtr, @jervyclaw, @Glucksberg, and @Takhoffman.
- LINE/status/config/webhook synthesis: fix status false positives from snapshot/config state and accept LINE webhook HEAD probes for compatibility. (from #10487, #25726, #27537, #27908, #31387) Thanks @BlueBirdBack, @stakeswky, @loiie45e, @puritysb, and @mcaxtr.
- LINE cleanup/test follow-ups: fold cleanup/test learnings into the synthesis review path while keeping runtime changes focused on regression fixes. (from #17630, #17289) Thanks @Clawborn and @davidahmann.
## 2026.3.2
### Changes
- Secrets/SecretRef coverage: expand SecretRef support across the full supported user-supplied credential surface (64 targets total), including runtime collectors, `openclaw secrets` planning/apply/audit flows, onboarding SecretInput UX, and related docs; unresolved refs now fail fast on active surfaces while inactive surfaces report non-blocking diagnostics. (#29580) Thanks @joshavant.
- Tools/PDF analysis: add a first-class `pdf` tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (`agents.defaults.pdfModel`, `pdfMaxBytesMb`, `pdfMaxPages`), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.
- Outbound adapters/plugins: add shared `sendPayload` support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.
- Models/MiniMax: add first-class `MiniMax-M2.5-highspeed` support across built-in provider catalogs, onboarding flows, and MiniMax OAuth plugin defaults, while keeping legacy `MiniMax-M2.5-Lightning` compatibility for existing configs.
- Docs/Models: refresh MiniMax, Moonshot (Kimi), GLM/Z.AI model docs to align with latest defaults (`MiniMax-M2.5`, `MiniMax-M2.5-highspeed`, `moonshot/kimi-k2.5`, `zai/glm-5`) and keep Moonshot model lists synced from shared source data.
- Memory/Ollama embeddings: add `memorySearch.provider = "ollama"` and `memorySearch.fallback = "ollama"` support, honor `models.providers.ollama` settings for memory embedding requests, and document Ollama embedding usage. (#26349) Thanks @nico-hoff.
- Outbound adapters/plugins: add shared `sendPayload` support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.
- Media understanding/audio echo: add optional `tools.media.audio.echoTranscript` + `echoFormat` to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.
- Plugin runtime/STT: add `api.runtime.stt.transcribeAudioFile(...)` so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.
- Plugin SDK/channel extensibility: expose `channelRuntime` on `ChannelGatewayContext` so external channel plugins can access shared runtime helpers (reply/routing/session/text/media/commands) without internal imports. (#25462) Thanks @guxiaobo.
- Plugin runtime/events: expose `runtime.events.onAgentEvent` and `runtime.events.onSessionTranscriptUpdate` for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.
- Plugin runtime/system: expose `runtime.system.requestHeartbeatNow(...)` so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral.
- Plugin hooks/session lifecycle: include `sessionKey` in `session_start`/`session_end` hook events and contexts so plugins can correlate lifecycle callbacks with routing identity. (#26394) Thanks @tempeste.
- Sessions/Attachments: add inline file attachment support for `sessions_spawn` (subagent runtime only) with base64/utf8 encoding, transcript content redaction, lifecycle cleanup, and configurable limits via `tools.sessions_spawn.attachments`. (#16761) Thanks @napetrov.
- Telegram/Streaming defaults: default `channels.telegram.streaming` to `partial` (from `off`) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.
- Tools/PDF analysis: add a first-class `pdf` tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (`agents.defaults.pdfModel`, `pdfMaxBytesMb`, `pdfMaxPages`), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.
- Zalo Personal plugin (`@openclaw/zalouser`): rebuilt channel runtime to use native `zca-js` integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.
- Telegram/DM streaming: use `sendMessageDraft` for private preview streaming, keep reasoning/answer preview lanes separated in DM reasoning-stream mode. (#31824) Thanks @obviyus.
- Telegram/voice mention gating: add optional `disableAudioPreflight` on group/topic config to skip mention-detection preflight transcription for inbound voice notes where operators want text-only mention checks. (#23067) Thanks @yangnim21029.
- CLI/Config validation: add `openclaw config validate` (with `--json`) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.
- Tools/Diffs: add PDF file output support and rendering quality customization controls (`fileQuality`, `fileScale`, `fileMaxWidth`) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.
- Memory/Ollama embeddings: add `memorySearch.provider = "ollama"` and `memorySearch.fallback = "ollama"` support, honor `models.providers.ollama` settings for memory embedding requests, and document Ollama embedding usage. (#26349) Thanks @nico-hoff.
- Zalo Personal plugin (`@openclaw/zalouser`): rebuilt channel runtime to use native `zca-js` integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.
- Plugin SDK/channel extensibility: expose `channelRuntime` on `ChannelGatewayContext` so external channel plugins can access shared runtime helpers (reply/routing/session/text/media/commands) without internal imports. (#25462) Thanks @guxiaobo.
- Plugin runtime/STT: add `api.runtime.stt.transcribeAudioFile(...)` so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.
- Plugin hooks/session lifecycle: include `sessionKey` in `session_start`/`session_end` hook events and contexts so plugins can correlate lifecycle callbacks with routing identity. (#26394) Thanks @tempeste.
- Hooks/message lifecycle: add internal hook events `message:transcribed` and `message:preprocessed`, plus richer outbound `message:sent` context (`isGroup`, `groupId`) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon.
- Media understanding/audio echo: add optional `tools.media.audio.echoTranscript` + `echoFormat` to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.
- Plugin runtime/system: expose `runtime.system.requestHeartbeatNow(...)` so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral.
- Plugin runtime/events: expose `runtime.events.onAgentEvent` and `runtime.events.onSessionTranscriptUpdate` for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.
- Telegram/Streaming defaults: default `channels.telegram.streaming` to `partial` (from `off`) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.
- CLI/Config validation: add `openclaw config validate` (with `--json`) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.
- CLI/Banner taglines: add `cli.banner.taglineMode` (`random` | `default` | `off`) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.
- Tools/Diffs: add PDF file output support and rendering quality customization controls (`fileQuality`, `fileScale`, `fileMaxWidth`) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.
- README/Contributors: rank contributor avatars by composite score (commits + merged PRs + code LOC), excluding docs-only LOC to prevent bulk-generated files from inflating rankings. (#23970) Thanks @tyler6204.
### Breaking
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
### Fixes
- Feishu/Outbound render mode: respect Feishu account `renderMode` in outbound sends so card mode (and auto-detected markdown tables/code blocks) uses markdown card delivery instead of always sending plain text. (#31562) Thanks @arkyu2077.
- Plugin command/runtime hardening: validate and normalize plugin command name/description at registration boundaries, and guard Telegram native menu normalization paths so malformed plugin command specs cannot crash startup (`trim` on undefined). (#31997) Fixes #31944. Thanks @liuxiaopai-ai.
- Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing `token.trim()` crashes during status/start flows. (#31973) Thanks @ningding97.
- Discord/lifecycle startup status: push an immediate `connected` status snapshot when the gateway is already connected before lifecycle debug listeners attach, with abort-guarding to avoid contradictory status flips during pre-aborted startup. (#32336) Thanks @mitchmcalister.
- Feishu/inbound mention normalization: preserve all inbound mention semantics by normalizing Feishu mention placeholders into explicit `<at user_id=\"...\">name</at>` tags (instead of stripping them), improving multi-mention context fidelity in agent prompts while retaining bot/self mention disambiguation. (#30252) Thanks @Lanfei.
- Feishu/multi-app mention routing: guard mention detection in multi-bot groups by validating mention display name alongside bot `open_id`, preventing false-positive self-mentions from Feishu WebSocket remapping so only the actually mentioned bot responds under `requireMention`. (#30315) Thanks @teaguexiao.
- Feishu/session-memory hook parity: trigger the shared `before_reset` session-memory hook path when Feishu `/new` and `/reset` commands execute so reset flows preserve memory behavior consistent with other channels. (#31437) Thanks @Linux2010.
- Feishu/LINE group system prompts: forward per-group `systemPrompt` config into inbound context `GroupSystemPrompt` for Feishu and LINE group/room events so configured group-specific behavior actually applies at dispatch time. (#31713) Thanks @whiskyboy.
- Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.
- Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older `openclaw/plugin-sdk` builds omit webhook default constants. (#31606)
- Feishu/group broadcast dispatch: add configurable multi-agent group broadcast dispatch with observer-session isolation, cross-account dedupe safeguards, and non-mention history buffering rules that avoid duplicate replay in broadcast/topic workflows. (#29575) Thanks @ohmyskyhigh.
- Gateway/Subagent TLS pairing: allow authenticated local `gateway-client` backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring `sessions_spawn` with `gateway.tls.enabled=true` in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.
- Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.
- Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.
- Feishu/Dedup restart resilience: warm persistent dedup state into memory on monitor startup so retry events after gateway restart stay suppressed without requiring initial on-disk probe misses. (#31605)
- Voice-call/runtime lifecycle: prevent `EADDRINUSE` loops by resetting failed runtime promises, making webhook `start()` idempotent with the actual bound port, and fully cleaning up webhook/tunnel/tailscale resources after startup failures. (#32395) Thanks @scoootscooob.
- Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example `(a|aa)+`), and bound large regex-evaluation inputs for session-filter and log-redaction paths.
- Gateway/Plugin HTTP hardening: require explicit `auth` for plugin route registration, add route ownership guards for duplicate `path+match` registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.
- Browser/Profile defaults: prefer `openclaw` profile over `chrome` in headless/no-sandbox environments unless an explicit `defaultProfile` is configured. (#14944) Thanks @BenediktSchackenberg.
- Gateway/WS security: keep plaintext `ws://` loopback-only by default, with explicit break-glass private-network opt-in via `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.
- OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit `doctor --deep`) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.
- Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.
- CLI/Config validation and routing hardening: dedupe `openclaw config validate` failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including `--json` fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed `config get/unset` with split root options). Thanks @gumadeiras.
- Browser/Extension relay reconnect tolerance: keep `/json/version` and `/cdp` reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.
- CLI/Browser start timeout: honor `openclaw browser --timeout <ms> start` and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.
- Synology Chat/gateway lifecycle: keep `startAccount` pending until abort for inactive and active account paths to prevent webhook route restart loops under gateway supervision. (#23074) Thanks @druide67.
- Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like `/usr/bin/g++` and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
- Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.
- Slack/Bolt startup compatibility: remove invalid `message.channels` and `message.groups` event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified `message` handler (`channel_type`). (#32033) Thanks @mahopan.
- Sessions/idle reset correctness: preserve existing `updatedAt` during inbound metadata-only writes so idle-reset boundaries are not unintentionally refreshed before actual user turns. (#32379) Thanks @romeodiaz.
- Slack/socket auth failure handling: fail fast on non-recoverable auth errors (`account_inactive`, `invalid_auth`, etc.) during startup and reconnect instead of retry-looping indefinitely, including `unable_to_socket_mode_start` error payload propagation. (#32377) Thanks @scoootscooob.
- Gateway/macOS LaunchAgent hardening: write `Umask=077` in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.
- macOS/LaunchAgent security defaults: write `Umask=63` (octal `077`) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system `022`. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.
- Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from `HTTPS_PROXY`/`HTTP_PROXY` env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.
- Sandbox/workspace mount permissions: make primary `/workspace` bind mounts read-only whenever `workspaceAccess` is not `rw` (including `none`) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.
- Tools/fsPolicy propagation: honor `tools.fs.workspaceOnly` for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.
- Daemon/Homebrew runtime pinning: resolve Homebrew Cellar Node paths to stable Homebrew-managed symlinks (including versioned formulas like `node@22`) so gateway installs keep the intended runtime across brew upgrades. (#32185) Thanks @scoootscooob.
- Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.
- Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded `/api/channels/*` variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.
- Browser/Gateway hardening: preserve env credentials for `OPENCLAW_GATEWAY_URL` / `CLAWDBOT_GATEWAY_URL` while treating explicit `--url` as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
- Gateway/Webchat streaming finalization: flush throttled trailing assistant text before `final` chat events so streaming consumers do not miss tail content, while preserving duplicate suppression and heartbeat/silent lead-fragment guards. (#24856) Thanks @visionik and @vincentkoc.
- Control UI/Legacy browser compatibility: replace `toSorted`-dependent cron suggestion sorting in `app-render` with a compatibility helper so older browsers without `Array.prototype.toSorted` no longer white-screen. (#31775) Thanks @liuxiaopai-ai.
- macOS/PeekabooBridge: add compatibility socket symlinks for legacy `clawdbot`, `clawdis`, and `moltbot` Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.
- CLI/installer Node preflight: enforce Node.js `v22.12+` consistently in both `openclaw.mjs` runtime bootstrap and installer active-shell checks, with actionable nvm recovery guidance for mismatched shell PATH/defaults. (#32356) Thanks @jasonhargrove.
- Web UI/inline code copy fidelity: disable forced mid-token wraps on inline `<code>` spans so copied UUID/hash/token strings preserve exact content instead of inserting line-break spaces. (#32346) Thanks @hclsys.
- Gateway/message tool reliability: avoid false `Unknown channel` failures when `message.*` actions receive platform-specific channel ids by falling back to `toolContext.currentChannelProvider`, and prevent health-monitor restart thrash for channels that just (re)started by adding a per-channel startup-connect grace window. (from #32367) Thanks @MunemHashmi.
- Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for `.cmd` shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.
- Security/ACP sandbox inheritance: enforce fail-closed runtime guardrails for `sessions_spawn` with `runtime="acp"` by rejecting ACP spawns from sandboxed requester sessions and rejecting `sandbox="require"` for ACP runtime, preventing sandbox-boundary bypass via host-side ACP initialization. (#32254) Thanks @tdjackey for reporting, and @dutifulbob for the fix.
- Security/Web tools SSRF guard: keep DNS pinning for untrusted `web_fetch` and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.
- Discord/lifecycle startup status: push an immediate `connected` status snapshot when the gateway is already connected before lifecycle debug listeners attach, with abort-guarding to avoid contradictory status flips during pre-aborted startup. (#32336) Thanks @mitchmcalister.
- Cron/isolated delivery target fallback: remove early unresolved-target return so cron delivery can flow through shared outbound target resolution (including per-channel `resolveDefaultTo` fallback) when `delivery.to` is omitted. (#32364) Thanks @hclsys.
- WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack.
- Feishu/default account resolution: always honor explicit `channels.feishu.defaultAccount` during outbound account selection (including top-level-credential setups where the preferred id is not present in `accounts`), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3.
- Gemini schema sanitization: coerce malformed JSON Schema `properties` values (`null`, arrays, primitives) to `{}` before provider validation, preventing downstream strict-validator crashes on invalid plugin/tool schemas. (#32332) Thanks @webdevtodayjason.
- Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.
- Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.
- Browser/Extension relay stale tabs: evict stale cached targets from `/json/list` when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.
- Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up `PortInUseError` races after `browser start`/`open`. (#29538) Thanks @AaronWander.
- Models/openai-completions developer-role compatibility: force `supportsDeveloperRole=false` for non-native endpoints, treat unparseable `baseUrl` values as non-native, and add regression coverage for empty/malformed baseUrl plus explicit-true override behavior. (#29479) thanks @akramcodez.
- OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty `function_call_output.call_id` payloads in the WS conversion path to avoid OpenAI 400 errors (`Invalid 'input[n].call_id': empty string`), with regression coverage for both inbound stream normalization and outbound payload guards.
- Security/Nodes camera URL downloads: bind node `camera.snap`/`camera.clip` URL payload downloads to the resolved node host, enforce fail-closed behavior when node `remoteIp` is unavailable, and use SSRF-guarded fetch with redirect host/protocol checks to prevent off-node fetch pivots. Thanks @tdjackey for reporting.
- Config/backups hardening: enforce owner-only (`0600`) permissions on rotated config backups and clean orphan `.bak.*` files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.
- Telegram/inbound media filenames: preserve original `file_name` metadata for document/audio/video/animation downloads (with fetch/path fallbacks), so saved inbound attachments keep sender-provided names instead of opaque Telegram file paths. (#31837) Thanks @Kay-051.
- Gateway/OpenAI chat completions: honor `x-openclaw-message-channel` when building `agentCommand` input for `/v1/chat/completions`, preserving caller channel identity instead of forcing `webchat`. (#30462) Thanks @bmendonca3.
- Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @bmendonca3.
- Media/MIME normalization: normalize parameterized/case-variant MIME strings in `kindFromMime` (for example `Audio/Ogg; codecs=opus`) so WhatsApp voice notes are classified as audio and routed through transcription correctly. (#32280) Thanks @Lucenx9.
- Discord/audio preflight mentions: detect audio attachments via Discord `content_type` and gate preflight transcription on typed text (not media placeholders), so guild voice-note mentions are transcribed and matched correctly. (#32136) Thanks @jnMetaCode.
- Discord/acp inline actions: prefer autocomplete for `/acp` action inline values and ignore bound-thread bot system messages to prevent ACP loops. (#33136) Thanks @thewilloftheshadow.
- Feishu/topic session routing: use `thread_id` as topic session scope fallback when `root_id` is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun.
- Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of `NO_REPLY` and keep final-message buffering in sync, preventing partial `NO` leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.
- Telegram/models picker callbacks: keep long model buttons selectable by falling back to compact callback payloads and resolving provider ids on selection (with provider re-prompt on ambiguity), avoiding Telegram 64-byte callback truncation failures. (#31857) Thanks @bmendonca3.
- Context-window metadata warmup: add exponential config-load retry backoff (1s -> 2s -> 4s, capped at 60s) so transient startup failures recover automatically without hot-loop retries.
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
- CLI/Config validation and routing hardening: dedupe `openclaw config validate` failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including `--json` fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed `config get/unset` with split root options). Thanks @gumadeiras.
- Models/config env propagation: apply `config.env.vars` before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.
- Hooks/runtime stability: keep the internal hook handler registry on a `globalThis` singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.
- Hooks/plugin context parity: ensure `llm_input` hooks in embedded attempts receive the same `trigger` and `channelId`-aware `hookCtx` used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.
- Restart sentinel formatting: avoid duplicate `Reason:` lines when restart message text already matches `stats.reason`, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2.
- Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.
- Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg.
- Voice-call/Twilio external outbound: auto-register webhook-first `outbound-api` calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.
- Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.
- Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.
- Feishu/topic session routing: use `thread_id` as topic session scope fallback when `root_id` is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun.
- Feishu/topic root replies: prefer `root_id` as outbound `replyTargetMessageId` when present, and parse millisecond `message_create_time` values correctly so topic replies anchor to the root message in grouped thread flows. (#29968) Thanks @bmendonca3.
- Feishu/DM pairing reply target: send pairing challenge replies to `chat:<chat_id>` instead of `user:<sender_open_id>` so Lark/Feishu private chats with user-id-only sender payloads receive pairing messages reliably. (#31403) Thanks @stakeswky.
- Feishu/Lark private DM routing: treat inbound `chat_type: "private"` as direct-message context for pairing/mention-forward/reaction synthetic handling so Lark private chats behave like Feishu p2p DMs. (#31400) Thanks @stakeswky.
- Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (`contact:contact.base:readonly`) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761)
- Sandbox/workspace mount permissions: make primary `/workspace` bind mounts read-only whenever `workspaceAccess` is not `rw` (including `none`) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.
- Security audit/skills workspace hardening: add `skills.workspace.symlink_escape` warning in `openclaw security audit` when workspace `skills/**/SKILL.md` resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary.
- Signal/message actions: allow `react` to fall back to `toolContext.currentMessageId` when `messageId` is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax.
- Discord/message actions: allow `react` to fall back to `toolContext.currentMessageId` when `messageId` is omitted, matching Telegram/Signal reaction ergonomics in inbound turns.
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.
- Slack/thread context payloads: only inject thread starter/history text on first thread turn for new sessions while preserving thread metadata, reducing repeated context-token bloat on long-lived thread sessions. (#32133) Thanks @sourman.
- Slack/session routing: keep top-level channel messages in one shared session when `replyToMode=off`, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193) Thanks @bmendonca3.
- Voice-call/webhook routing: require exact webhook path matches (instead of prefix matches) so lookalike paths cannot reach provider verification/dispatch logic. (#31930) Thanks @afurm.
- Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.
- Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (`monitor.account-scope.test.ts`) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.
- Feishu/Send target prefixes: normalize explicit `group:`/`dm:` send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai.
- Webchat/Feishu session continuation: preserve routable `OriginatingChannel`/`OriginatingTo` metadata from session delivery context in `chat.send`, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)
- Telegram/implicit mention forum handling: exclude Telegram forum system service messages (`forum_topic_*`, `general_forum_topic_*`) from reply-chain implicit mention detection so `requireMention` does not get bypassed inside bot-created topic lifecycle events. (#32262) Thanks @scoootscooob.
- Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.
- Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)
- Webchat/silent token leak: filter assistant `NO_REPLY`-only transcript entries from `chat.history` responses and add client-side defense-in-depth guards in the chat controller so internal silent tokens never render as visible chat bubbles. (#32015) Consolidates overlap from #32183, #32082, #32045, #32052, #32172, and #32112. Thanks @ademczuk, @liuxiaopai-ai, @ningding97, @bmendonca3, and @x4v13r1120.
- Doctor/local memory provider checks: stop false-positive local-provider warnings when `provider=local` and no explicit `modelPath` is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.
- Gateway/OpenAI chat completions: honor `x-openclaw-message-channel` when building `agentCommand` input for `/v1/chat/completions`, preserving caller channel identity instead of forcing `webchat`. (#30462) Thanks @bmendonca3.
- Secrets/exec resolver timeout defaults: use provider `timeoutMs` as the default inactivity (`noOutputTimeoutMs`) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.
- Feishu/File upload filenames: percent-encode non-ASCII/special-character `file_name` values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.
- Auto-reply/inline command cleanup: preserve newline structure when stripping inline `/status` and extracting inline slash commands by collapsing only horizontal whitespace, preventing paragraph flattening in multi-line replies. (#32224) Thanks @scoootscooob.
- macOS/LaunchAgent security defaults: write `Umask=63` (octal `077`) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system `022`. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.
- Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @Glucksberg.
- Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from `HTTPS_PROXY`/`HTTP_PROXY` env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.
- Media/MIME normalization: normalize parameterized/case-variant MIME strings in `kindFromMime` (for example `Audio/Ogg; codecs=opus`) so WhatsApp voice notes are classified as audio and routed through transcription correctly. (#32280) Thanks @Lucenx9.
- Media/MIME channel parity: route Telegram/Signal/iMessage media-kind checks through normalized `kindFromMime` so mixed-case/parameterized MIME values classify consistently across message channels.
- Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.
- Media understanding/parakeet CLI output parsing: read `parakeet-mlx` transcripts from `--output-dir/<media-basename>.txt` when txt output is requested (or default), with stdout fallback for non-txt formats. (#9177) Thanks @mac-110.
- Media understanding/audio transcription guard: skip tiny/empty audio files (<1024 bytes) before provider/CLI transcription to avoid noisy invalid-audio failures and preserve clean fallback behavior. (#8388) Thanks @Glucksberg.
- Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.
- Gateway/Node browser proxy routing: honor `profile` from `browser.request` JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.
- Gateway/Control UI basePath POST handling: return 405 for `POST` on exact basePath routes (for example `/openclaw`) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.
- Browser/default profile selection: default `browser.defaultProfile` behavior now prefers `openclaw` (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the `chrome` relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.
- Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.
- Models/config env propagation: apply `config.env.vars` before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.
- Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so `openclaw models status` no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.
- Gateway/Heartbeat model reload: treat `models.*` and `agents.defaults.model` config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.
- Memory/LanceDB embeddings: forward configured `embedding.dimensions` into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.
- Browser/CDP status accuracy: require a successful `Browser.getVersion` response over the CDP websocket (not just socket-open) before reporting `cdpReady`, so stale idle command channels are surfaced as unhealthy. (#23427) Thanks @vincentkoc.
- Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.
- Security/Node exec approvals: revalidate approval-bound `cwd` identity immediately before execution/forwarding and fail closed with an explicit denial when `cwd` drifts after approval hardening.
- Security audit/skills workspace hardening: add `skills.workspace.symlink_escape` warning in `openclaw security audit` when workspace `skills/**/SKILL.md` resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary.
- Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example `env sh -c ...`) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.
- Security/fs-safe write hardening: make `writeFileWithinRoot` use same-directory temp writes plus atomic rename, add post-write inode/hardlink revalidation with security warnings on boundary drift, and avoid truncating existing targets when final rename fails.
- Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.
- Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like `[System Message]` and line-leading `System:` in untrusted message content. (#30448)
- Sandbox/Docker setup command parsing: accept `agents.*.sandbox.docker.setupCommand` as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.
- Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction `AGENTS.md` context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.
- Agents/Sandbox workdir mapping: map container workdir paths (for example `/workspace`) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.
- Docker/Sandbox bootstrap hardening: make `OPENCLAW_SANDBOX` opt-in parsing explicit (`1|true|yes|on`), support custom Docker socket paths via `OPENCLAW_DOCKER_SOCKET`, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to `off` when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.
- Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @AIflow-Labs.
- Feishu/Run channel fallback: prefer `Provider` over `Surface` when inferring queued run `messageProvider` fallback (when `OriginatingChannel` is missing), preventing Feishu turns from being mislabeled as `webchat` in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.
- Skills/sherpa-onnx-tts: run the `sherpa-onnx-tts` bin under ESM (replace CommonJS `require` imports) and add regression coverage to prevent `require is not defined in ES module scope` startup crashes. (#31965) Thanks @bmendonca3.
- Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.
- Slack/Channel message subscriptions: register explicit `message.channels` and `message.groups` monitor handlers (alongside generic `message`) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674.
- Hooks/session-scoped memory context: expose ephemeral `sessionId` in embedded plugin tool contexts and `before_tool_call`/`after_tool_call` hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across `/new` and `/reset`. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex.
- Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.
- Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.
- Feishu/File upload filenames: percent-encode non-ASCII/special-character `file_name` values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.
- Media/MIME channel parity: route Telegram/Signal/iMessage media-kind checks through normalized `kindFromMime` so mixed-case/parameterized MIME values classify consistently across message channels.
- WhatsApp/inbound self-message context: propagate inbound `fromMe` through the web inbox pipeline and annotate direct self messages as `(self)` in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.
- Webchat/stream finalization: persist streamed assistant text when final events omit `message`, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.
- Feishu/Inbound ordering: serialize message handling per chat while preserving cross-chat concurrency to avoid same-chat race drops under bursty inbound traffic. (#31807)
- Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580)
- Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970)
- Feishu/Streaming block fallback: preserve markdown block stream text as final streaming-card content when final payload text is missing, while still suppressing non-card internal block chunk delivery. (#30663)
- Feishu/Bitable API errors: unify Feishu Bitable tool error handling with structured `LarkApiError` responses and consistent API/context attribution across wiki/base metadata, field, and record operations. (#31450)
- Feishu/Missing-scope grant URL fix: rewrite known invalid scope aliases (`contact:contact.base:readonly`) to valid scope names in permission grant links, so remediation URLs open with correct Feishu consent scopes. (#31943)
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
- WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack.
- Feishu/default account resolution: always honor explicit `channels.feishu.defaultAccount` during outbound account selection (including top-level-credential setups where the preferred id is not present in `accounts`), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3.
- Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (`contact:contact.base:readonly`) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761)
- Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.
- Feishu/Inbound debounce: debounce rapid same-chat sender bursts into one ordered dispatch turn, skip already-processed retries when composing merged text, and preserve bot-mention intent across merged entries to reduce duplicate or late inbound handling. (#31548)
- Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.
- Browser/Extension re-announce reliability: keep relay state in `connecting` when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.
- Browser/Act request compatibility: accept legacy flattened `action="act"` params (`kind/ref/text/...`) in addition to `request={...}` so browser act calls no longer fail with `request required`. (#15120) Thanks @vincentkoc.
- OpenRouter/x-ai compatibility: skip `reasoning.effort` injection for `x-ai/*` models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.
- Models/openai-completions developer-role compatibility: force `supportsDeveloperRole=false` for non-native endpoints, treat unparseable `baseUrl` values as non-native, and add regression coverage for empty/malformed baseUrl plus explicit-true override behavior. (#29479) thanks @akramcodez.
- Browser/Profile attach-only override: support `browser.profiles.<name>.attachOnly` (fallback to global `browser.attachOnly`) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.
- Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file `starttime` with `/proc/<pid>/stat` starttime, so stale `.jsonl.lock` files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.
- Cron/isolated delivery target fallback: remove early unresolved-target return so cron delivery can flow through shared outbound target resolution (including per-channel `resolveDefaultTo` fallback) when `delivery.to` is omitted. (#32364) Thanks @hclsys.
- OpenAI media capabilities: include `audio` in the OpenAI provider capability list so audio transcription models are eligible in media-understanding provider selection. (#12717) Thanks @openjay.
- Browser/Managed tab cap: limit loopback managed `openclaw` page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.
- Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.
- Gateway/Node dangerous-command parity: include `sms.send` in default onboarding node `denyCommands`, share onboarding deny defaults with the gateway dangerous-command source of truth, and include `sms.send` in phone-control `/phone arm writes` handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.
- Browser/CDP proxy bypass: force direct loopback agent paths and scoped `NO_PROXY` expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.
- Sessions/idle reset correctness: preserve existing `updatedAt` during inbound metadata-only writes so idle-reset boundaries are not unintentionally refreshed before actual user turns. (#32379) Thanks @romeodiaz.
- Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example `env sh -c ...`) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.
- Security/Node exec approvals: revalidate approval-bound `cwd` identity immediately before execution/forwarding and fail closed with an explicit denial when `cwd` drifts after approval hardening.
- Security/ACP sandbox inheritance: enforce fail-closed runtime guardrails for `sessions_spawn` with `runtime="acp"` by rejecting ACP spawns from sandboxed requester sessions and rejecting `sandbox="require"` for ACP runtime, preventing sandbox-boundary bypass via host-side ACP initialization. (#32254) Thanks @tdjackey for reporting, and @dutifulbob for the fix.
- Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.
- Security/fs-safe write hardening: make `writeFileWithinRoot` use same-directory temp writes plus atomic rename, add post-write inode/hardlink revalidation with security warnings on boundary drift, and avoid truncating existing targets when final rename fails.
- Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.
- Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example `(a|aa)+`), and bound large regex-evaluation inputs for session-filter and log-redaction paths.
- Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.
- Security/Web tools SSRF guard: keep DNS pinning for untrusted `web_fetch` and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.
- Security/Nodes camera URL downloads: bind node `camera.snap`/`camera.clip` URL payload downloads to the resolved node host, enforce fail-closed behavior when node `remoteIp` is unavailable, and use SSRF-guarded fetch with redirect host/protocol checks to prevent off-node fetch pivots. Thanks @tdjackey for reporting.
- Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded `/api/channels/*` variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.
- Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like `[System Message]` and line-leading `System:` in untrusted message content. (#30448)
- Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.
- Auto-reply/reminder guard note suppression: when a turn makes reminder-like commitments but schedules no new cron jobs, suppress the unscheduled-reminder warning note only if an enabled cron already exists for the same session; keep warnings for unrelated sessions, disabled jobs, or unreadable cron store paths. (#32255) Thanks @scoootscooob.
- Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so `HEARTBEAT_OK` noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.
- Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing `HEARTBEAT_OK` from being delivered to users. (#32131) Thanks @adhishthite.
- Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing `starttime` when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.
- Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (`mtimeMs` + `sizeBytes`), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.
- Agents/Subagents `sessions_spawn`: reject malformed `agentId` inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.
- CLI/installer Node preflight: enforce Node.js `v22.12+` consistently in both `openclaw.mjs` runtime bootstrap and installer active-shell checks, with actionable nvm recovery guidance for mismatched shell PATH/defaults. (#32356) Thanks @jasonhargrove.
- Web UI/config form: support SecretInput string-or-secret-ref unions in map `additionalProperties`, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.
- Auto-reply/inline command cleanup: preserve newline structure when stripping inline `/status` and extracting inline slash commands by collapsing only horizontal whitespace, preventing paragraph flattening in multi-line replies. (#32224) Thanks @scoootscooob.
- Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like `source`/`provider`), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.
- Hooks/runtime stability: keep the internal hook handler registry on a `globalThis` singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.
- Hooks/after_tool_call: include embedded session context (`sessionKey`, `agentId`) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc.
- Hooks/tool-call correlation: include `runId` and `toolCallId` in plugin tool hook payloads/context and scope tool start/adjusted-param tracking by run to prevent cross-run collisions in `before_tool_call` and `after_tool_call`. (#32360) Thanks @vincentkoc.
- Plugins/install diagnostics: reject legacy plugin package shapes without `openclaw.extensions` and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.
- Hooks/plugin context parity: ensure `llm_input` hooks in embedded attempts receive the same `trigger` and `channelId`-aware `hookCtx` used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.
- Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (`pnpm`, `bun`) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.
- Cron/session reaper reliability: move cron session reaper sweeps into `onTimer` `finally` and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.
- Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so `HEARTBEAT_OK` noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.
- Authentication: classify `permission_error` as `auth_permanent` for profile fallback. (#31324) Thanks @Sid-Qin.
- Agents/host edit reliability: treat host edit-tool throws as success only when on-disk post-check confirms replacement likely happened (`newText` present and `oldText` absent), preventing false failure reports while avoiding pre-write false positives. (#32383) Thanks @polooooo.
- Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example `diffs` -> bundled `@openclaw/diffs`), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.
- Web UI/inline code copy fidelity: disable forced mid-token wraps on inline `<code>` spans so copied UUID/hash/token strings preserve exact content instead of inserting line-break spaces. (#32346) Thanks @hclsys.
- Restart sentinel formatting: avoid duplicate `Reason:` lines when restart message text already matches `stats.reason`, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2.
- Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.
- Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.
- Failover/error classification: treat HTTP `529` (provider overloaded, common with Anthropic-compatible APIs) as `rate_limit` so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.
- Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- Secrets/exec resolver timeout defaults: use provider `timeoutMs` as the default inactivity (`noOutputTimeoutMs`) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.
- Auto-reply/reminder guard note suppression: when a turn makes reminder-like commitments but schedules no new cron jobs, suppress the unscheduled-reminder warning note only if an enabled cron already exists for the same session; keep warnings for unrelated sessions, disabled jobs, or unreadable cron store paths. (#32255) Thanks @scoootscooob.
- Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing `HEARTBEAT_OK` from being delivered to users. (#32131) Thanks @adhishthite.
- Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.
- Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so `openclaw models status` no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.
- Config/backups hardening: enforce owner-only (`0600`) permissions on rotated config backups and clean orphan `.bak.*` files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.
- Tests/Windows backup rotation: skip chmod-only backup permission assertions on Windows while retaining compose/rotation/prune coverage across platforms to avoid false CI failures from Windows non-POSIX mode semantics. (#32286) Thanks @jalehman.
- WhatsApp/inbound self-message context: propagate inbound `fromMe` through the web inbox pipeline and annotate direct self messages as `(self)` in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.
- Webchat/silent token leak: filter assistant `NO_REPLY`-only transcript entries from `chat.history` responses and add client-side defense-in-depth guards in the chat controller so internal silent tokens never render as visible chat bubbles. (#32015) Consolidates overlap from #32183, #32082, #32045, #32052, #32172, and #32112. Thanks @ademczuk, @liuxiaopai-ai, @ningding97, @bmendonca3, and @x4v13r1120.
- Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like `/usr/bin/g++` and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.
- Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.
- Hooks/after_tool_call: include embedded session context (`sessionKey`, `agentId`) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc.
- Hooks/session-scoped memory context: expose ephemeral `sessionId` in embedded plugin tool contexts and `before_tool_call`/`after_tool_call` hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across `/new` and `/reset`. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex.
- Hooks/tool-call correlation: include `runId` and `toolCallId` in plugin tool hook payloads/context and scope tool start/adjusted-param tracking by run to prevent cross-run collisions in `before_tool_call` and `after_tool_call`. (#32360) Thanks @vincentkoc.
- Webchat/stream finalization: persist streamed assistant text when final events omit `message`, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.
- Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.
- Gateway/Heartbeat model reload: treat `models.*` and `agents.defaults.model` config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.
- Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of `NO_REPLY` and keep final-message buffering in sync, preventing partial `NO` leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.
- Tools/fsPolicy propagation: honor `tools.fs.workspaceOnly` for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.
- Daemon/Homebrew runtime pinning: resolve Homebrew Cellar Node paths to stable Homebrew-managed symlinks (including versioned formulas like `node@22`) so gateway installs keep the intended runtime across brew upgrades. (#32185) Thanks @scoootscooob.
- Discord/audio preflight mentions: detect audio attachments via Discord `content_type` and gate preflight transcription on typed text (not media placeholders), so guild voice-note mentions are transcribed and matched correctly. (#32136) Thanks @jnMetaCode.
- Memory/LanceDB embeddings: forward configured `embedding.dimensions` into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.
- Failover/error classification: treat HTTP `529` (provider overloaded, common with Anthropic-compatible APIs) as `rate_limit` so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.
- Plugin command/runtime hardening: validate and normalize plugin command name/description at registration boundaries, and guard Telegram native menu normalization paths so malformed plugin command specs cannot crash startup (`trim` on undefined). (#31997) Fixes #31944. Thanks @liuxiaopai-ai.
- Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (`pnpm`, `bun`) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.
- Web UI/config form: support SecretInput string-or-secret-ref unions in map `additionalProperties`, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.
- Plugins/install diagnostics: reject legacy plugin package shapes without `openclaw.extensions` and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.
- Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example `diffs` -> bundled `@openclaw/diffs`), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.
- Browser/default profile selection: default `browser.defaultProfile` behavior now prefers `openclaw` (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the `chrome` relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.
- Doctor/local memory provider checks: stop false-positive local-provider warnings when `provider=local` and no explicit `modelPath` is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.
- Cron/session reaper reliability: move cron session reaper sweeps into `onTimer` `finally` and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.
- Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.
- Sandbox/Docker setup command parsing: accept `agents.*.sandbox.docker.setupCommand` as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.
- Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.
- Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction `AGENTS.md` context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.
- Gateway/Node dangerous-command parity: include `sms.send` in default onboarding node `denyCommands`, share onboarding deny defaults with the gateway dangerous-command source of truth, and include `sms.send` in phone-control `/phone arm writes` handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.
- Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.
- Gateway/Plugin HTTP hardening: require explicit `auth` for plugin route registration, add route ownership guards for duplicate `path+match` registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.
- Agents/Subagents `sessions_spawn`: reject malformed `agentId` inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.
- Webchat/Feishu session continuation: preserve routable `OriginatingChannel`/`OriginatingTo` metadata from session delivery context in `chat.send`, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Control UI/Legacy browser compatibility: replace `toSorted`-dependent cron suggestion sorting in `app-render` with a compatibility helper so older browsers without `Array.prototype.toSorted` no longer white-screen. (#31775) Thanks @liuxiaopai-ai.
- Agents/Sandbox workdir mapping: map container workdir paths (for example `/workspace`) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.
- Gateway/Control UI basePath POST handling: return 405 for `POST` on exact basePath routes (for example `/openclaw`) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.
- Authentication: classify `permission_error` as `auth_permanent` for profile fallback. (#31324) Thanks @Sid-Qin.
- Gateway/Node browser proxy routing: honor `profile` from `browser.request` JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.
- Browser/Extension relay reconnect tolerance: keep `/json/version` and `/cdp` reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.
- Browser/Extension re-announce reliability: keep relay state in `connecting` when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.
- Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.
- Browser/Profile defaults: prefer `openclaw` profile over `chrome` in headless/no-sandbox environments unless an explicit `defaultProfile` is configured. (#14944) Thanks @BenediktSchackenberg.
- Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.
- Browser/Profile attach-only override: support `browser.profiles.<name>.attachOnly` (fallback to global `browser.attachOnly`) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.
- Browser/Act request compatibility: accept legacy flattened `action="act"` params (`kind/ref/text/...`) in addition to `request={...}` so browser act calls no longer fail with `request required`. (#15120) Thanks @vincentkoc.
- Browser/Extension relay stale tabs: evict stale cached targets from `/json/list` when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.
- CLI/Browser start timeout: honor `openclaw browser --timeout <ms> start` and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.
- Browser/CDP status accuracy: require a successful `Browser.getVersion` response over the CDP websocket (not just socket-open) before reporting `cdpReady`, so stale idle command channels are surfaced as unhealthy. (#23427) Thanks @vincentkoc.
- Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.
- Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up `PortInUseError` races after `browser start`/`open`. (#29538) Thanks @AaronWander.
- Browser/Managed tab cap: limit loopback managed `openclaw` page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.
- Browser/CDP proxy bypass: force direct loopback agent paths and scoped `NO_PROXY` expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.
- Browser/Gateway hardening: preserve env credentials for `OPENCLAW_GATEWAY_URL` / `CLAWDBOT_GATEWAY_URL` while treating explicit `--url` as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.
- Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for `.cmd` shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.
- Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.
- Gateway/WS security: keep plaintext `ws://` loopback-only by default, with explicit break-glass private-network opt-in via `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.
- Gateway/Subagent TLS pairing: allow authenticated local `gateway-client` backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring `sessions_spawn` with `gateway.tls.enabled=true` in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.
- Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file `starttime` with `/proc/<pid>/stat` starttime, so stale `.jsonl.lock` files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.
- Gateway/macOS LaunchAgent hardening: write `Umask=077` in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
- OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit `doctor --deep`) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.
- Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.
- Synology Chat/gateway lifecycle: keep `startAccount` pending until abort for inactive and active account paths to prevent webhook route restart loops under gateway supervision. (#23074) Thanks @druide67.
- Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.
- Telegram/inbound media filenames: preserve original `file_name` metadata for document/audio/video/animation downloads (with fetch/path fallbacks), so saved inbound attachments keep sender-provided names instead of opaque Telegram file paths. (#31837) Thanks @Kay-051.
- Telegram/implicit mention forum handling: exclude Telegram forum system service messages (`forum_topic_*`, `general_forum_topic_*`) from reply-chain implicit mention detection so `requireMention` does not get bypassed inside bot-created topic lifecycle events. (#32262) Thanks @scoootscooob.
- Telegram/models picker callbacks: keep long model buttons selectable by falling back to compact callback payloads and resolving provider ids on selection (with provider re-prompt on ambiguity), avoiding Telegram 64-byte callback truncation failures. (#31857) Thanks @bmendonca3.
- WhatsApp/inbound self-message context: propagate inbound `fromMe` through the web inbox pipeline and annotate direct self messages as `(self)` in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.
- Slack/thread context payloads: only inject thread starter/history text on first thread turn for new sessions while preserving thread metadata, reducing repeated context-token bloat on long-lived thread sessions. (#32133) Thanks @sourman.
- Slack/session routing: keep top-level channel messages in one shared session when `replyToMode=off`, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193) Thanks @bmendonca3.
- Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.
- OpenRouter/x-ai compatibility: skip `reasoning.effort` injection for `x-ai/*` models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.
- Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.
- Voice-call/webhook routing: require exact webhook path matches (instead of prefix matches) so lookalike paths cannot reach provider verification/dispatch logic. (#31930) Thanks @afurm.
- Slack/Bolt startup compatibility: remove invalid `message.channels` and `message.groups` event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified `message` handler (`channel_type`). (#32033) Thanks @mahopan.
- Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing `token.trim()` crashes during status/start flows. (#31973) Thanks @ningding97.
- Skills/sherpa-onnx-tts: run the `sherpa-onnx-tts` bin under ESM (replace CommonJS `require` imports) and add regression coverage to prevent `require is not defined in ES module scope` startup crashes. (#31965) Thanks @bmendonca3.
- Feishu/Run channel fallback: prefer `Provider` over `Surface` when inferring queued run `messageProvider` fallback (when `OriginatingChannel` is missing), preventing Feishu turns from being mislabeled as `webchat` in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.
- Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.
- Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (`monitor.account-scope.test.ts`) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.
- macOS/PeekabooBridge: add compatibility socket symlinks for legacy `clawdbot`, `clawdis`, and `moltbot` Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.
- Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)
- Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older `openclaw/plugin-sdk` builds omit webhook default constants. (#31606)
- Feishu/Inbound debounce: debounce rapid same-chat sender bursts into one ordered dispatch turn, skip already-processed retries when composing merged text, and preserve bot-mention intent across merged entries to reduce duplicate or late inbound handling. (#31548)
- Feishu/Inbound ordering: serialize message handling per chat while preserving cross-chat concurrency to avoid same-chat race drops under bursty inbound traffic. (#31807)
- Feishu/Dedup restart resilience: warm persistent dedup state into memory on monitor startup so retry events after gateway restart stay suppressed without requiring initial on-disk probe misses. (#31605)
- Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580)
- Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970)
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
- Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.
- Docker/Sandbox bootstrap hardening: make `OPENCLAW_SANDBOX` opt-in parsing explicit (`1|true|yes|on`), support custom Docker socket paths via `OPENCLAW_DOCKER_SOCKET`, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to `off` when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.
- Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.
- Feishu/Send target prefixes: normalize explicit `group:`/`dm:` send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai.
- Slack/Channel message subscriptions: register explicit `message.channels` and `message.groups` monitor handlers (alongside generic `message`) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674.
- Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.
- Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.
## 2026.3.1
@@ -363,7 +239,6 @@ Docs: https://docs.openclaw.ai
### Fixes
- Feishu/Streaming card text fidelity: merge throttled/fragmented partial updates without dropping content and avoid newline injection when stitching chunk-style deltas so card-stream output matches final reply text. (#29616) Thanks @HaoHuaqing.
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
- Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.
- Web tools/RFC2544 fake-IP compatibility: allow RFC2544 benchmark range (`198.18.0.0/15`) for trusted web-tool fetch endpoints so proxy fake-IP networking modes do not trigger false SSRF blocks. Landed from contributor PR #31176 by @sunkinux. Thanks @sunkinux.
@@ -374,7 +249,7 @@ Docs: https://docs.openclaw.ai
- CLI/Cron: clarify `cron list` output by renaming `Agent` to `Agent ID` and adding a `Model` column for isolated agent-turn jobs. (#26259) Thanks @openperf.
- Gateway/Control UI origins: honor `gateway.controlUi.allowedOrigins: ["*"]` wildcard entries (including trimmed values) and lock behavior with regression tests. Landed from contributor PR #31058 by @byungsker. Thanks @byungsker.
- Agents/Sessions list transcript paths: handle missing/non-string/relative `sessions.list.path` values and per-agent `{agentId}` templates when deriving `transcriptPath`, so cross-agent session listings resolve to concrete agent session files instead of workspace-relative paths. (#24775) Thanks @martinfrancois.
- Gateway/Control UI CSP: allow required Google Fonts origins in Control UI CSP. (#29279) Thanks @vincentkoc.
- Gateway/Control UI CSP: allow required Google Fonts origins in Control UI CSP. (#29279) Thanks @Glucksberg and @vincentkoc.
- CLI/Install: add an npm-link fallback to fix CLI startup `Permission denied` failures (`exit 127`) on affected installs. (#17151) Thanks @sskyu and @vincentkoc.
- Plugins/NPM spec install: fix npm-spec plugin installs when `npm pack` output is empty by detecting newly created `.tgz` archives in the pack directory. (#21039) Thanks @graysurf and @vincentkoc.
- Plugins/Install: clear stale install errors when an npm package is not found so follow-up install attempts report current state correctly. (#25073) Thanks @dalefrieswthat.
@@ -400,12 +275,12 @@ Docs: https://docs.openclaw.ai
- Android/Voice screen TTS: stream assistant speech via ElevenLabs WebSocket in Talk Mode, stop cleanly on speaker mute/barge-in, and ignore stale out-of-order stream events. (#29521) Thanks @gregmousseau.
- Android/Photos permissions: declare Android 14+ selected-photo access permission (`READ_MEDIA_VISUAL_USER_SELECTED`) and align Android permission/settings paths with current minSdk behavior for more reliable permission state handling.
- Feishu/Reply media attachments: send Feishu reply `mediaUrl`/`mediaUrls` payloads as attachments alongside text/streamed replies in the reply dispatcher, including legacy fallback when `mediaUrls` is empty. (#28959) Thanks @icesword0760.
- Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (`SLACK_USER_TOKEN` env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @chilu18.
- Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (`SLACK_USER_TOKEN` env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @Glucksberg.
- Feishu/Outbound session routing: stop assuming bare `oc_` identifiers are always group chats, honor explicit `dm:`/`group:` prefixes for `oc_` chat IDs, and default ambiguous bare `oc_` targets to direct routing to avoid DM session misclassification. (#10407) Thanks @Bermudarat.
- Feishu/Group session routing: add configurable group session scopes (`group`, `group_sender`, `group_topic`, `group_topic_sender`) with legacy `topicSessionMode=enabled` compatibility so Feishu group conversations can isolate sessions by sender/topic as configured. (#17798) Thanks @yfge.
- Feishu/Reply-in-thread routing: add `replyInThread` config (`disabled|enabled`) for group replies, propagate `reply_in_thread` across text/card/media/streaming sends, and align topic-scoped session routing so newly created reply threads stay on the same session root. (#27325) Thanks @kcinzgg.
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @hou-rong.
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @PinoHouse.
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg.
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg.
- Feishu/Mobile video media type: treat inbound `message_type: "media"` as video-equivalent for media key extraction, placeholder inference, and media download resolution so mobile-app video sends ingest correctly. (#25502) Thanks @4ier.
- Feishu/Inbound sender fallback: fall back to `sender_id.user_id` when `sender_id.open_id` is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl.
- Feishu/Reply context metadata: include inbound `parent_id` and `root_id` as `ReplyToId`/`RootMessageId` in inbound context, and parse interactive-card quote bodies into readable text when fetching replied messages. (#18529) Thanks @qiangu.
@@ -426,8 +301,6 @@ Docs: https://docs.openclaw.ai
- Android/Gateway canvas capability refresh: send `node.canvas.capability.refresh` with object `params` (`{}`) from Android node runtime so gateway object-schema validation accepts refresh retries and A2UI host recovery works after scoped capability expiry. (#28413) Thanks @obviyus.
- Onboarding/Custom providers: improve verification reliability for slower local endpoints (for example Ollama) during setup. (#27380) Thanks @Sid-Qin.
- Daemon/macOS TLS certs: default LaunchAgent service env `NODE_EXTRA_CA_CERTS` to `/etc/ssl/cert.pem` (while preserving explicit overrides) so HTTPS clients no longer fail with local-issuer errors under launchd. (#27915) Thanks @Lukavyi.
- Daemon/Linux systemd user-bus fallback: when `systemctl --user` cannot reach the user bus due missing session env, fall back to `systemctl --machine <user>@ --user` so daemon checks/install continue in headless SSH/server sessions. (#34884) Thanks @vincentkoc.
- Gateway/Linux restart health: reduce false `openclaw gateway restart` timeouts by falling back to `ss -ltnp` when `lsof` is missing, confirming ambiguous busy-port cases via local gateway probe, and targeting the original `SUDO_USER` systemd user scope for restart commands. (#34874) Thanks @vincentkoc.
- Discord/Components wildcard handlers: use distinct internal registration sentinel IDs and parse those sentinels as wildcard keys so select/user/role/channel/mentionable/modal interactions are not dropped by raw customId dedupe paths. Landed from contributor PR #29459 by @Sid-Qin. Thanks @Sid-Qin.
- Feishu/Reaction notifications: add `channels.feishu.reactionNotifications` (`off | own | all`, default `own`) so operators can disable reaction ingress or allow all verified reaction events (not only bot-authored message reactions). (#28529) Thanks @cowboy129.
- Feishu/Typing backoff: re-throw Feishu typing add/remove rate-limit and quota errors (`429`, `99991400`, `99991403`) and detect SDK non-throwing backoff responses so the typing keepalive circuit breaker can stop retries instead of looping indefinitely. (#28494) Thanks @guoqunabc.
@@ -448,7 +321,6 @@ Docs: https://docs.openclaw.ai
- fix(model): preserve reasoning in provider fallback resolution. (#29285) Fixes #25636. Thanks @vincentkoc.
- Docker/Image permissions: normalize `/app/extensions`, `/app/.agent`, and `/app/.agents` to directory mode `755` and file mode `644` during image build so plugin discovery does not block inherited world-writable paths. (#30191) Fixes #30139. Thanks @edincampara.
- OpenAI Responses/Compaction: rewrite and unify the OpenAI Responses store patches to treat empty `baseUrl` as non-direct, honor `compat.supportsStore=false`, and auto-inject server-side compaction `context_management` for compatible direct OpenAI models (with per-model opt-out/threshold overrides). Landed from contributor PRs #16930 (@OiPunk), #22441 (@EdwardWu7), and #25088 (@MoerAI). Thanks @OiPunk, @EdwardWu7, and @MoerAI.
- Agents/Compaction safeguard: preserve recent turns verbatim with stable user/assistant pairing, keep multimodal and tool-result hints in preserved tails, and avoid empty-history fallback text in compacted output. (#25554) thanks @rodrigouroz.
- Usage normalization: clamp negative prompt/input token values to zero (including `prompt_tokens` alias inputs) so `/usage` and TUI usage displays cannot show nonsensical negative counts. Landed from contributor PR #31211 by @scoootscooob. Thanks @scoootscooob.
- Secrets/Auth profiles: normalize inline SecretRef `token`/`key` values to canonical `tokenRef`/`keyRef` before persistence, and keep explicit `keyRef` precedence when inline refs are also present. Landed from contributor PR #31047 by @minupla. Thanks @minupla.
- Codex/Usage window: label weekly usage window as `Week` instead of `Day`. (#26267) Thanks @Sid-Qin.
@@ -457,13 +329,8 @@ Docs: https://docs.openclaw.ai
## Unreleased
### Changes
- Docs/Contributing: require before/after screenshots for UI or visual PRs in the pre-PR checklist. (#32206) Thanks @hydro13.
### Fixes
- Logging/Subsystem console timestamps: route subsystem console timestamp rendering through `formatConsoleTimestamp(...)` so `pretty` and timestamp-prefix output use local timezone formatting consistently instead of inline UTC `toISOString()` paths. (#25970) Thanks @openperf.
- Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, prevent inbound preview text from leaking into prompt system events, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #31209, #29610, #30432, #30331, and #29501. Thanks @stakeswky, @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.
- Feishu/Target routing + replies + dedupe: normalize provider-prefixed targets (`feishu:`/`lark:`), prefer configured `channels.feishu.defaultAccount` for tool execution, honor Feishu outbound `renderMode` in adapter text/caption sends, fall back to normal send when reply targets are withdrawn/deleted, and add synchronous in-memory dedupe guard for concurrent duplicate inbound events. Landed from contributor PRs #30428, #30438, #29958, #30444, and #29463. Thanks @bmendonca3 and @Yaxuan42.
- Channels/Multi-account default routing: add optional `channels.<channel>.defaultAccount` default-selection support across message channels so omitted `accountId` routes to an explicit configured account instead of relying on implicit first-entry ordering (fallback behavior unchanged when unset).
@@ -474,7 +341,7 @@ Docs: https://docs.openclaw.ai
- Matrix/Directory room IDs: preserve original room-ID casing for direct `!roomId` group lookups (without `:server`) so allowlist checks do not fail on case-sensitive IDs. Landed from contributor PR #31201 by @williamos-dev. Thanks @williamos-dev.
- Discord/Inbound media fallback: preserve attachment and sticker metadata when Discord CDN fetch/save fails by keeping URL-based media entries in context, with regression coverage for save failures and mixed success/failure ordering. Landed from contributor PR #28906 by @Sid-Qin. Thanks @Sid-Qin.
- Auto-reply/Block reply timeout path: normalize `onBlockReply(...)` execution through `Promise.resolve(...)` before timeout wrapping so mixed sync/async callbacks keep deterministic timeout behavior across strict TypeScript build paths. (#19779) Thanks @dalefrieswthat and @vincentkoc.
- Cron/One-shot reschedule re-arm: allow completed `at` jobs to run again when rescheduled to a later time than `lastRunAtMs`, while keeping completed non-rescheduled one-shot jobs inactive. (#28915) Thanks @arosstale.
- Cron/One-shot reschedule re-arm: allow completed `at` jobs to run again when rescheduled to a later time than `lastRunAtMs`, while keeping completed non-rescheduled one-shot jobs inactive. (#28915) Thanks @Glucksberg.
- Docs/Docker images: clarify the official GHCR image source and tag guidance (`main`, `latest`, `<version>`), and document that `OPENCLAW_IMAGE` skips local image builds but still uses the repo-local compose/setup flow. (#27214, #31180) Fixes #15655. Thanks @ipl31.
- Docs/Gateway Docker bind guidance: clarify bridge-network loopback behavior and require bind mode values (`auto`/`loopback`/`lan`/`tailnet`/`custom`) instead of host aliases in `gateway.bind`. (#28001) Thanks @Anandesh-Sharma and @vincentkoc.
- Docker/Image base annotations: add OCI labels for base image plus source/documentation/license metadata, include revision/version/created labels in Docker release builds, and document annotation keys/release context in install docs. Fixes #27945. Thanks @vincentkoc.
@@ -486,7 +353,7 @@ Docs: https://docs.openclaw.ai
- Discord/Ack reactions: add Discord-account-level `ackReactionScope` override and support explicit `off`/`none` values in shared config schemas to disable ack reactions per account. Landed from contributor PR #30400 by @BlueBirdBack. Thanks @BlueBirdBack.
- Discord/Forum thread tags: support `appliedTags` on Discord thread-create actions and map to `applied_tags` for forum/media starter posts, with targeted thread-creation regression coverage. Landed from contributor PR #30358 by @pushkarsingh32. Thanks @pushkarsingh32.
- Discord/Application ID fallback: parse bot application IDs from token prefixes without numeric precision loss and use token fallback only on transport/timeout failures when probing `/oauth2/applications/@me`. Landed from contributor PR #29695 by @dhananjai1729. Thanks @dhananjai1729.
- Discord/EventQueue timeout config: expose per-account `channels.discord.accounts.<id>.eventQueue.listenerTimeout` (and related queue options) so long-running handlers can avoid Carbon listener timeout drops. Landed from contributor PR #24270 by @pdd-cli. Thanks @pdd-cli.
- Discord/EventQueue timeout config: expose per-account `channels.discord.accounts.<id>.eventQueue.listenerTimeout` (and related queue options) so long-running handlers can avoid Carbon listener timeout drops. Landed from contributor PR #28945 by @Glucksberg. Thanks @Glucksberg.
- CLI/Cron run exit code: return exit code `0` only when `cron run` reports `{ ok: true, ran: true }`, and `1` for non-run/error outcomes so scripting/debugging reflects actual execution status. Landed from contributor PR #31121 by @Sid-Qin. Thanks @Sid-Qin.
- Cron/Failure delivery routing: add `failureAlert.mode` (`announce|webhook`) and `failureAlert.accountId` support, plus `cron.failureDestination` and per-job `delivery.failureDestination` routing with duplicate-target suppression, best-effort skip behavior, and global+job merge semantics. Landed from contributor PR #31059 by @kesor. Thanks @kesor.
- CLI/JSON preflight output: keep `--json` command stdout machine-readable by suppressing doctor preflight note output while still running legacy migration/config doctor flow. (#24368) Thanks @altaywtf.
@@ -568,7 +435,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Control UI API routing: when `gateway.controlUi.basePath` is unset (default), stop serving Control UI SPA HTML for `/api` and `/api/*` so API paths fall through to normal gateway handlers/404 responses instead of `index.html`. (#30333) Fixes #30295. thanks @Sid-Qin.
- Cron/One-shot reliability: retry transient one-shot failures with bounded backoff and configurable retry policy before disabling. (#24435) Thanks @hugenshen.
- Gateway/Cron auditability: add gateway info logs for successful cron create, update, and remove operations. (#25090) Thanks @MoerAI.
- Gateway/Tailscale onboarding origin allowlist: auto-add the detected Tailnet HTTPS origin during interactive configure/onboarding flows (including IPv6-safe origin formatting and binary-path reuse), so Tailscale serve/funnel Control UI access works without manual `allowedOrigins` edits. Landed from contributor PR #26157 by @stakeswky. Thanks @stakeswky.
- Gateway/Tailscale onboarding origin allowlist: auto-add the detected Tailnet HTTPS origin during interactive configure/onboarding flows (including IPv6-safe origin formatting and binary-path reuse), so Tailscale serve/funnel Control UI access works without manual `allowedOrigins` edits. Landed from contributor PR #28960 by @Glucksberg. Thanks @Glucksberg.
- Gateway/Upgrade migration for Control UI origins: seed `gateway.controlUi.allowedOrigins` on startup for legacy non-loopback configs (`lan`/`tailnet`/`custom`) when origins are missing or blank, preventing post-upgrade crash loops while preserving explicit existing policy. Landed from contributor PR #29394 by @synchronic1. Thanks @synchronic1.
- Gateway/Plugin HTTP auth hardening: require gateway auth for protected plugin paths and explicit `registerHttpRoute` paths (while preserving wildcard-handler behavior for signature-auth webhooks), and run plugin handlers after built-in handlers for deterministic route precedence. Landed from contributor PR #29198 by @Mariana-Codebase. Thanks @Mariana-Codebase.
- Gateway/Config patch guard: reject `config.patch` updates that set non-loopback `gateway.bind` while `gateway.tailscale.mode` is `serve`/`funnel`, preventing restart crash loops from invalid bind/tailscale combinations. Landed from contributor PR #30910 by @liuxiaopai-ai. Thanks @liuxiaopai-ai.
@@ -579,11 +446,10 @@ Docs: https://docs.openclaw.ai
- Slack/Transient request errors: classify Slack request-error messages like `Client network socket disconnected before secure TLS connection was established` as transient in unhandled-rejection fatal detection, preventing temporary network drops from crash-looping the gateway. (#23169) Thanks @graysurf.
- Slack/Usage footer formatting: wrap session keys in inline code in full response-usage footers so Slack does not parse colon-delimited session segments as emoji shortcodes. (#30258) Thanks @pushkarsingh32.
- Slack/Thread session isolation: route channel/group top-level messages into thread-scoped sessions (`:thread:<ts>`) and read inbound `previousTimestamp` from the resolved thread session key, preventing cross-thread context bleed and stale timestamp lookups. (#10686) Thanks @pablohrcarvalho.
- Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715) Thanks @AIflow-Labs.
- Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715) Thanks @Glucksberg.
- Slack/Legacy streaming config: map boolean `channels.slack.streaming=false` to unified streaming mode `off` (with `nativeStreaming=false`) so legacy configs correctly disable draft preview/native streaming instead of defaulting to `partial`. (#25990) Thanks @chilu18.
- Slack/Socket reconnect reliability: reconnect Socket Mode after disconnect/start failures using bounded exponential backoff with abort-aware waits, while preserving clean shutdown behavior and adding disconnect/error helper tests. (#27232) Thanks @pandego.
- Memory/QMD update+embed output cap: discard captured stdout for `qmd update` and `qmd embed` runs (while keeping stderr diagnostics) so large index progress output no longer fails sync with `produced too much output` during boot/refresh. (#28900; landed from contributor PR #23311 by @haitao-sjsu) Thanks @haitao-sjsu.
- Feishu/Onboarding SecretRef guards: avoid direct `.trim()` calls on object-form `appId`/`appSecret` in onboarding credential checks, keep status semantics strict when an account explicitly sets empty `appId` (no fallback to top-level `appId`), recognize env SecretRef `appId`/`appSecret` as configured so readiness is accurate, and preserve unresolved SecretRef errors in default account resolution for actionable diagnostics. (#30903) Thanks @LiaoyuanNing.
- Memory/QMD update+embed output cap: discard captured stdout for `qmd update` and `qmd embed` runs (while keeping stderr diagnostics) so large index progress output no longer fails sync with `produced too much output` during boot/refresh. (#28900) Thanks @Glucksberg.
- Onboarding/Custom providers: raise default custom-provider model context window to the runtime hard minimum (16k) and auto-heal existing custom model entries below that threshold during reconfiguration, preventing immediate `Model context window too small (4096 tokens)` failures. (#21653) Thanks @r4jiv007.
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
@@ -825,7 +691,7 @@ Docs: https://docs.openclaw.ai
- WhatsApp/Web reconnect: treat close status `440` as non-retryable (including string-form status values), stop reconnect loops immediately, and emit operator guidance to relink after resolving session conflicts. (#25858) Thanks @markmusson.
- WhatsApp/Reasoning safety: suppress outbound payloads marked as reasoning and hard-drop text payloads that begin with `Reasoning:` before WhatsApp delivery, preventing hidden thinking blocks from leaking to end users through final-message paths. (#25804, #25214, #24328)
- Matrix/Read receipts: send read receipts as soon as Matrix messages arrive (before handler pipeline work), so clients no longer show long-lived unread/sent states while replies are processing. (#25841, #25840) Thanks @joshjhall.
- Telegram/Replies: when markdown formatting renders to empty HTML (for example syntax-only chunks in threaded replies), retry delivery with plain text, and fail loud when both formatted and plain payloads are empty to avoid false delivered states. (#25096, #25091) Thanks @ArsalanShakil.
- Telegram/Replies: when markdown formatting renders to empty HTML (for example syntax-only chunks in threaded replies), retry delivery with plain text, and fail loud when both formatted and plain payloads are empty to avoid false delivered states. (#25096, #25091) Thanks @Glucksberg.
- Telegram/Media fetch: prioritize IPv4 before IPv6 in SSRF pinned DNS address ordering so media downloads still work on hosts with broken IPv6 routing. (#24295, #23975) Thanks @Glucksberg.
- Telegram/Outbound API: replace Node 22's global undici dispatcher when applying Telegram `autoSelectFamily` decisions so outbound `fetch` calls inherit IPv4 fallback instead of staying pinned to stale dispatcher settings. (#25682, #25676) Thanks @lairtonlelis.
- Onboarding/Telegram: keep core-channel onboarding available when plugin registry population is missing by falling back to built-in adapters and continuing wizard setup with actionable recovery guidance. (#25803) Thanks @Suko.
@@ -916,11 +782,11 @@ Docs: https://docs.openclaw.ai
- Agents/Reasoning: when model-default thinking is active (for example `thinking=low`), keep auto-reasoning disabled unless explicitly enabled, preventing `Reasoning:` thinking-block leakage in channel replies. (#24335, #24290) thanks @Kay-051.
- Agents/Reasoning: avoid classifying provider reasoning-required errors as context overflows so these failures no longer trigger compaction-style overflow recovery. (#24593) Thanks @vincentkoc.
- Agents/Models: codify `agents.defaults.model` / `agents.defaults.imageModel` config-boundary input as `string | {primary,fallbacks}`, split explicit vs effective model resolution, and fix `models status --agent` source attribution so defaults-inherited agents are labeled as `defaults` while runtime selection still honors defaults fallback. (#24210) thanks @bianbiandashen.
- Agents/Compaction: pass `agentDir` into manual `/compact` command runs so compaction auth/profile resolution stays scoped to the active agent. (#24133) thanks @miloudbelarebia.
- Agents/Compaction: pass `agentDir` into manual `/compact` command runs so compaction auth/profile resolution stays scoped to the active agent. (#24133) thanks @Glucksberg.
- Agents/Compaction: pass model metadata through the embedded runtime so safeguard summarization can run when `ctx.model` is unavailable, avoiding repeated `"Summary unavailable due to context limits"` fallback summaries. (#3479) Thanks @battman21, @hanxiao and @vincentkoc.
- Agents/Compaction: cancel safeguard compaction when summary generation cannot run (missing model/API key or summarization failure), preserving history instead of truncating to fallback `"Summary unavailable"` text. (#10711) Thanks @DukeDeSouth and @vincentkoc.
- Agents/Tools: make `session_status` read transcript-derived usage mid-turn and tail-read session logs for cache-aware context reporting without full-log scans. (#22387) Thanks @1ucian.
- Agents/Overflow: detect additional provider context-overflow error shapes (including `input length` + `max_tokens` exceed-context variants) so failures route through compaction/recovery paths instead of leaking raw provider errors to users. (#9951) Thanks @echoVic.
- Agents/Overflow: detect additional provider context-overflow error shapes (including `input length` + `max_tokens` exceed-context variants) so failures route through compaction/recovery paths instead of leaking raw provider errors to users. (#9951) Thanks @echoVic and @Glucksberg.
- Agents/Overflow: add Chinese context-overflow pattern detection in `isContextOverflowError` so localized provider errors route through overflow recovery paths. (#22855) Thanks @Clawborn.
- Agents/Failover: treat HTTP 502/503/504 errors as failover-eligible transient timeouts so fallback chains can switch providers/models during upstream outages instead of retrying the same failing target. (#20999) Thanks @taw0002 and @vincentkoc.
- Auto-reply/Inbound metadata: hide direct-chat `message_id`/`message_id_full` and sender metadata only from normalized chat type (not sender-id sentinels), preserving group metadata visibility and preventing sender-id spoofed direct-mode classification. (#24373) thanks @jd316.
@@ -989,7 +855,7 @@ Docs: https://docs.openclaw.ai
- Agents/Kimi: classify Moonshot `Your request exceeded model token limit` failures as context overflows so auto-compaction and user-facing overflow recovery trigger correctly instead of surfacing raw invalid-request errors. (#9562) Thanks @danilofalcao.
- Providers/Moonshot: mark Kimi K2.5 as image-capable in implicit + onboarding model definitions, and refresh stale explicit provider capability fields (`input`/`reasoning`/context limits) from implicit catalogs so existing configs pick up Moonshot vision support without manual model rewrites. (#13135, #4459) Thanks @manikv12.
- Agents/Transcript: enable consecutive-user turn merging for strict non-OpenAI `openai-completions` providers (for example Moonshot/Kimi), reducing `roles must alternate` ordering failures on OpenAI-compatible endpoints while preserving current OpenRouter/Opencode behavior. (#7693) Thanks @steipete.
- Install/Discord Voice: make the native Opus decoder optional so `openclaw` install/update no longer hard-fails when native builds fail, while keeping `opusscript` as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) Thanks @jeadland, @Sheetaa, and @Breakyman.
- Install/Discord Voice: make `@discordjs/opus` an optional dependency so `openclaw` install/update no longer hard-fails when native Opus builds fail, while keeping `opusscript` as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) Thanks @jeadland, @Sheetaa, and @Breakyman.
- Docker/Setup: precreate `$OPENCLAW_CONFIG_DIR/identity` during `docker-setup.sh` so CLI commands that need device identity (for example `devices list`) avoid `EACCES ... /home/node/.openclaw/identity` failures on restrictive bind mounts. (#23948) Thanks @ackson-beep.
- Exec/Background: stop applying the default exec timeout to background sessions (`background: true` or explicit `yieldMs`) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303) Thanks @steipete.
- Slack/Threading: sessions: keep parent-session forking and thread-history context active beyond first turn by removing first-turn-only gates in session init, thread-history fetch, and reply prompt context injection. (#23843, #23090) Thanks @vincentkoc and @Taskle.
@@ -1066,7 +932,7 @@ Docs: https://docs.openclaw.ai
- Config/Memory: allow `"mistral"` in `agents.defaults.memorySearch.provider` and `agents.defaults.memorySearch.fallback` schema validation. (#14934) Thanks @ThomsenDrake.
- Feishu/Commands: in group chats, command authorization now falls back to top-level `channels.feishu.allowFrom` when per-group `allowFrom` is not set, so `/command` no longer gets blocked by an unintended empty allowlist. (#23756) Thanks @steipete.
- Dev tooling: prevent `CLAUDE.md` symlink target regressions by excluding CLAUDE symlink sentinels from `oxfmt` and marking them `-text` in `.gitattributes`, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc.
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349; landed from contributor PR #5005 by @Diaspar4u) Thanks @Diaspar4u.
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633) Thanks @steipete.
- Hooks/Loader: avoid redundant hook-module recompilation on gateway restart by skipping cache-busting for bundled hooks and using stable file metadata keys (`mtime+size`) for mutable workspace/managed/plugin hook imports. (#16953) Thanks @mudrii.
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
@@ -1357,8 +1223,6 @@ Docs: https://docs.openclaw.ai
- iOS/Watch: add an Apple Watch companion MVP with watch inbox UI, watch notification relay handling, and gateway command surfaces for watch status/send flows. (#20054) Thanks @mbelinky.
- iOS/Gateway: wake disconnected iOS nodes via APNs before `nodes.invoke` and auto-reconnect gateway sessions on silent push wake to reduce invoke failures while the app is backgrounded. (#20332) Thanks @mbelinky.
- Gateway/CLI: add paired-device hygiene flows with `device.pair.remove`, plus `openclaw devices remove` and guarded `openclaw devices clear --yes [--pending]` commands for removing paired entries and optionally rejecting pending requests. (#20057) Thanks @mbelinky.
- Mattermost: add opt-in native slash command support with registration lifecycle, callback route/token validation, multi-account token routing, and callback URL/path configuration (`channels.mattermost.commands.*`). (#16515) Thanks @echo931.
- Mattermost: harden native slash callback auth-bypass behavior for configurable callback paths, add callback validation coverage, and clarify callback reachability/allowlist docs. (#32467) Thanks @mukhtharcm and @echo931.
- iOS/APNs: add push registration and notification-signing configuration for node delivery. (#20308) Thanks @mbelinky.
- Gateway/APNs: add a push-test pipeline for APNs delivery validation in gateway flows. (#20307) Thanks @mbelinky.
- Security/Audit: add `gateway.http.no_auth` findings when `gateway.auth.mode="none"` leaves Gateway HTTP APIs reachable, with loopback warning and remote-exposure critical severity, plus regression coverage and docs updates.
@@ -1758,7 +1622,7 @@ Docs: https://docs.openclaw.ai
- Agents: treat `read` tool `file_path` arguments as valid in tool-start diagnostics to avoid false “read tool called without path” warnings when alias parameters are used. (#16717) Thanks @Stache73.
- Agents/Transcript: drop malformed tool-call blocks with blank required fields (`id`/`name` or missing `input`/`arguments`) during session transcript repair to prevent persistent tool-call corruption on future turns. (#15485) Thanks @mike-zachariades.
- Tools/Write/Edit: normalize structured text-block arguments for `content`/`oldText`/`newText` before filesystem edits, preventing JSON-like file corruption and false “exact text not found” misses from block-form params. (#16778) Thanks @danielpipernz.
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @briancolinger.
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg.
- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
- Agents/Process: supervise PTY/child process lifecycles with explicit ownership, cancellation, timeouts, and deterministic cleanup, preventing Codex/Pi PTY sessions from dying or stalling on resume. (#14257) Thanks @onutc.
- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard.
@@ -2081,7 +1945,7 @@ Docs: https://docs.openclaw.ai
- Cron: prevent one-shot `at` jobs from re-firing on gateway restart when previously skipped or errored. (#13845)
- Discord: add exec approval cleanup option to delete DMs after approval/denial/timeout. (#13205) Thanks @thewilloftheshadow.
- Sessions: prune stale entries, cap session store size, rotate large stores, accept duration/size thresholds, default to warn-only maintenance, and prune cron run sessions after retention windows. (#13083) Thanks @skyfallsin, @gumadeiras.
- Sessions: prune stale entries, cap session store size, rotate large stores, accept duration/size thresholds, default to warn-only maintenance, and prune cron run sessions after retention windows. (#13083) Thanks @skyfallsin, @Glucksberg, @gumadeiras.
- CI: Implement pipeline and workflow order. Thanks @quotentiroler.
- WhatsApp: preserve original filenames for inbound documents. (#12691) Thanks @akramcodez.
- Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov.
@@ -2231,7 +2095,7 @@ Docs: https://docs.openclaw.ai
- Cron: accept epoch timestamps and 0ms durations in CLI `--at` parsing.
- Cron: reload store data when the store file is recreated or mtime changes.
- Cron: deliver announce runs directly, honor delivery mode, and respect wakeMode for summaries. (#8540) Thanks @tyler6204.
- Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @sleontenko.
- Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @Glucksberg.
- macOS: fix cron payload summary rendering and ISO 8601 formatter concurrency safety.
- Discord: enforce DM allowlists for agent components (buttons/select menus), honoring pairing store approvals and tag matches. (#11254) Thanks @thedudeabidesai.
@@ -2561,7 +2425,7 @@ Docs: https://docs.openclaw.ai
- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.openclaw.ai/tools/web
- UI: refresh Control UI dashboard design system (colors, icons, typography). (#1745, #1786) Thanks @EnzeD, @mousberg.
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.openclaw.ai/tools/exec-approvals https://docs.openclaw.ai/tools/slash-commands
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @steipete.
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.openclaw.ai/diagnostics/flags
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
- Docs: add verbose installer troubleshooting guidance.
@@ -2574,7 +2438,7 @@ Docs: https://docs.openclaw.ai
- Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @steipete.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
- Web UI: hide internal `message_id` hints in chat bubbles.
- Gateway: allow Control UI token-only auth to skip device pairing even when device identity is present (`gateway.controlUi.allowInsecureAuth`). (#1679) Thanks @steipete.
- Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47.

View File

@@ -74,7 +74,6 @@ Welcome to the lobster tank! 🦞
- Ensure CI checks pass
- Keep PRs focused (one thing per PR; do not mix unrelated concerns)
- Describe what & why
- **Include screenshots** — one showing the problem/before, one showing the fix/after (for UI or visual changes)
## Control UI Decorators

View File

@@ -72,7 +72,7 @@ RUN if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \
# Update OPENCLAW_DOCKER_GPG_FINGERPRINT when Docker rotates release keys.
curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \
expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "fpr" { print toupper($10); exit }')" && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == \"fpr\" { print toupper($10); exit }')" && \
if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-<empty>})" >&2; \
exit 1; \

View File

@@ -2,225 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.3.2</title>
<pubDate>Tue, 03 Mar 2026 04:30:29 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026030290</sparkle:version>
<sparkle:shortVersionString>2026.3.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.2</h2>
<h3>Changes</h3>
<ul>
<li>Secrets/SecretRef coverage: expand SecretRef support across the full supported user-supplied credential surface (64 targets total), including runtime collectors, <code>openclaw secrets</code> planning/apply/audit flows, onboarding SecretInput UX, and related docs; unresolved refs now fail fast on active surfaces while inactive surfaces report non-blocking diagnostics. (#29580) Thanks @joshavant.</li>
<li>Tools/PDF analysis: add a first-class <code>pdf</code> tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (<code>agents.defaults.pdfModel</code>, <code>pdfMaxBytesMb</code>, <code>pdfMaxPages</code>), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.</li>
<li>Outbound adapters/plugins: add shared <code>sendPayload</code> support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.</li>
<li>Models/MiniMax: add first-class <code>MiniMax-M2.5-highspeed</code> support across built-in provider catalogs, onboarding flows, and MiniMax OAuth plugin defaults, while keeping legacy <code>MiniMax-M2.5-Lightning</code> compatibility for existing configs.</li>
<li>Sessions/Attachments: add inline file attachment support for <code>sessions_spawn</code> (subagent runtime only) with base64/utf8 encoding, transcript content redaction, lifecycle cleanup, and configurable limits via <code>tools.sessions_spawn.attachments</code>. (#16761) Thanks @napetrov.</li>
<li>Telegram/Streaming defaults: default <code>channels.telegram.streaming</code> to <code>partial</code> (from <code>off</code>) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.</li>
<li>Telegram/DM streaming: use <code>sendMessageDraft</code> for private preview streaming, keep reasoning/answer preview lanes separated in DM reasoning-stream mode. (#31824) Thanks @obviyus.</li>
<li>Telegram/voice mention gating: add optional <code>disableAudioPreflight</code> on group/topic config to skip mention-detection preflight transcription for inbound voice notes where operators want text-only mention checks. (#23067) Thanks @yangnim21029.</li>
<li>CLI/Config validation: add <code>openclaw config validate</code> (with <code>--json</code>) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.</li>
<li>Tools/Diffs: add PDF file output support and rendering quality customization controls (<code>fileQuality</code>, <code>fileScale</code>, <code>fileMaxWidth</code>) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.</li>
<li>Memory/Ollama embeddings: add <code>memorySearch.provider = "ollama"</code> and <code>memorySearch.fallback = "ollama"</code> support, honor <code>models.providers.ollama</code> settings for memory embedding requests, and document Ollama embedding usage. (#26349) Thanks @nico-hoff.</li>
<li>Zalo Personal plugin (<code>@openclaw/zalouser</code>): rebuilt channel runtime to use native <code>zca-js</code> integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.</li>
<li>Plugin SDK/channel extensibility: expose <code>channelRuntime</code> on <code>ChannelGatewayContext</code> so external channel plugins can access shared runtime helpers (reply/routing/session/text/media/commands) without internal imports. (#25462) Thanks @guxiaobo.</li>
<li>Plugin runtime/STT: add <code>api.runtime.stt.transcribeAudioFile(...)</code> so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.</li>
<li>Plugin hooks/session lifecycle: include <code>sessionKey</code> in <code>session_start</code>/<code>session_end</code> hook events and contexts so plugins can correlate lifecycle callbacks with routing identity. (#26394) Thanks @tempeste.</li>
<li>Hooks/message lifecycle: add internal hook events <code>message:transcribed</code> and <code>message:preprocessed</code>, plus richer outbound <code>message:sent</code> context (<code>isGroup</code>, <code>groupId</code>) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon.</li>
<li>Media understanding/audio echo: add optional <code>tools.media.audio.echoTranscript</code> + <code>echoFormat</code> to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.</li>
<li>Plugin runtime/system: expose <code>runtime.system.requestHeartbeatNow(...)</code> so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral.</li>
<li>Plugin runtime/events: expose <code>runtime.events.onAgentEvent</code> and <code>runtime.events.onSessionTranscriptUpdate</code> for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.</li>
<li>CLI/Banner taglines: add <code>cli.banner.taglineMode</code> (<code>random</code> | <code>default</code> | <code>off</code>) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> Onboarding now defaults <code>tools.profile</code> to <code>messaging</code> for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.</li>
<li><strong>BREAKING:</strong> ACP dispatch now defaults to enabled unless explicitly disabled (<code>acp.dispatch.enabled=false</code>). If you need to pause ACP turn routing while keeping <code>/acp</code> controls, set <code>acp.dispatch.enabled=false</code>. Docs: https://docs.openclaw.ai/tools/acp-agents</li>
<li><strong>BREAKING:</strong> Plugin SDK removed <code>api.registerHttpHandler(...)</code>. Plugins must register explicit HTTP routes via <code>api.registerHttpRoute({ path, auth, match, handler })</code>, and dynamic webhook lifecycles should use <code>registerPluginHttpRoute(...)</code>.</li>
<li><strong>BREAKING:</strong> Zalo Personal plugin (<code>@openclaw/zalouser</code>) no longer depends on external <code>zca</code>-compatible CLI binaries (<code>openzca</code>, <code>zca-cli</code>) for runtime send/listen/login; operators should use <code>openclaw channels login --channel zalouser</code> after upgrade to refresh sessions in the new JS-native path.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Plugin command/runtime hardening: validate and normalize plugin command name/description at registration boundaries, and guard Telegram native menu normalization paths so malformed plugin command specs cannot crash startup (<code>trim</code> on undefined). (#31997) Fixes #31944. Thanks @liuxiaopai-ai.</li>
<li>Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing <code>token.trim()</code> crashes during status/start flows. (#31973) Thanks @ningding97.</li>
<li>Discord/lifecycle startup status: push an immediate <code>connected</code> status snapshot when the gateway is already connected before lifecycle debug listeners attach, with abort-guarding to avoid contradictory status flips during pre-aborted startup. (#32336) Thanks @mitchmcalister.</li>
<li>Feishu/LINE group system prompts: forward per-group <code>systemPrompt</code> config into inbound context <code>GroupSystemPrompt</code> for Feishu and LINE group/room events so configured group-specific behavior actually applies at dispatch time. (#31713) Thanks @whiskyboy.</li>
<li>Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.</li>
<li>Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older <code>openclaw/plugin-sdk</code> builds omit webhook default constants. (#31606)</li>
<li>Feishu/group broadcast dispatch: add configurable multi-agent group broadcast dispatch with observer-session isolation, cross-account dedupe safeguards, and non-mention history buffering rules that avoid duplicate replay in broadcast/topic workflows. (#29575) Thanks @ohmyskyhigh.</li>
<li>Gateway/Subagent TLS pairing: allow authenticated local <code>gateway-client</code> backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring <code>sessions_spawn</code> with <code>gateway.tls.enabled=true</code> in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.</li>
<li>Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.</li>
<li>Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.</li>
<li>Feishu/Dedup restart resilience: warm persistent dedup state into memory on monitor startup so retry events after gateway restart stay suppressed without requiring initial on-disk probe misses. (#31605)</li>
<li>Voice-call/runtime lifecycle: prevent <code>EADDRINUSE</code> loops by resetting failed runtime promises, making webhook <code>start()</code> idempotent with the actual bound port, and fully cleaning up webhook/tunnel/tailscale resources after startup failures. (#32395) Thanks @scoootscooob.</li>
<li>Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when <code>gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback</code> accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example <code>(a|aa)+</code>), and bound large regex-evaluation inputs for session-filter and log-redaction paths.</li>
<li>Gateway/Plugin HTTP hardening: require explicit <code>auth</code> for plugin route registration, add route ownership guards for duplicate <code>path+match</code> registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.</li>
<li>Browser/Profile defaults: prefer <code>openclaw</code> profile over <code>chrome</code> in headless/no-sandbox environments unless an explicit <code>defaultProfile</code> is configured. (#14944) Thanks @BenediktSchackenberg.</li>
<li>Gateway/WS security: keep plaintext <code>ws://</code> loopback-only by default, with explicit break-glass private-network opt-in via <code>OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1</code>; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.</li>
<li>OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit <code>doctor --deep</code>) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.</li>
<li>Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.</li>
<li>CLI/Config validation and routing hardening: dedupe <code>openclaw config validate</code> failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including <code>--json</code> fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed <code>config get/unset</code> with split root options). Thanks @gumadeiras.</li>
<li>Browser/Extension relay reconnect tolerance: keep <code>/json/version</code> and <code>/cdp</code> reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.</li>
<li>CLI/Browser start timeout: honor <code>openclaw browser --timeout <ms> start</code> and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.</li>
<li>Synology Chat/gateway lifecycle: keep <code>startAccount</code> pending until abort for inactive and active account paths to prevent webhook route restart loops under gateway supervision. (#23074) Thanks @druide67.</li>
<li>Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like <code>/usr/bin/g++</code> and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.</li>
<li>Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with <code>204</code> to avoid persistent <code>Processing...</code> states in Synology Chat clients. (#26635) Thanks @memphislee09-source.</li>
<li>Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.</li>
<li>Slack/Bolt startup compatibility: remove invalid <code>message.channels</code> and <code>message.groups</code> event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified <code>message</code> handler (<code>channel_type</code>). (#32033) Thanks @mahopan.</li>
<li>Slack/socket auth failure handling: fail fast on non-recoverable auth errors (<code>account_inactive</code>, <code>invalid_auth</code>, etc.) during startup and reconnect instead of retry-looping indefinitely, including <code>unable_to_socket_mode_start</code> error payload propagation. (#32377) Thanks @scoootscooob.</li>
<li>Gateway/macOS LaunchAgent hardening: write <code>Umask=077</code> in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.</li>
<li>macOS/LaunchAgent security defaults: write <code>Umask=63</code> (octal <code>077</code>) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system <code>022</code>. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.</li>
<li>Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from <code>HTTPS_PROXY</code>/<code>HTTP_PROXY</code> env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.</li>
<li>Sandbox/workspace mount permissions: make primary <code>/workspace</code> bind mounts read-only whenever <code>workspaceAccess</code> is not <code>rw</code> (including <code>none</code>) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.</li>
<li>Tools/fsPolicy propagation: honor <code>tools.fs.workspaceOnly</code> for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.</li>
<li>Daemon/Homebrew runtime pinning: resolve Homebrew Cellar Node paths to stable Homebrew-managed symlinks (including versioned formulas like <code>node@22</code>) so gateway installs keep the intended runtime across brew upgrades. (#32185) Thanks @scoootscooob.</li>
<li>Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.</li>
<li>Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded <code>/api/channels/*</code> variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.</li>
<li>Browser/Gateway hardening: preserve env credentials for <code>OPENCLAW_GATEWAY_URL</code> / <code>CLAWDBOT_GATEWAY_URL</code> while treating explicit <code>--url</code> as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.</li>
<li>Gateway/Control UI basePath webhook passthrough: let non-read methods under configured <code>controlUiBasePath</code> fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.</li>
<li>Control UI/Legacy browser compatibility: replace <code>toSorted</code>-dependent cron suggestion sorting in <code>app-render</code> with a compatibility helper so older browsers without <code>Array.prototype.toSorted</code> no longer white-screen. (#31775) Thanks @liuxiaopai-ai.</li>
<li>macOS/PeekabooBridge: add compatibility socket symlinks for legacy <code>clawdbot</code>, <code>clawdis</code>, and <code>moltbot</code> Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.</li>
<li>Gateway/message tool reliability: avoid false <code>Unknown channel</code> failures when <code>message.*</code> actions receive platform-specific channel ids by falling back to <code>toolContext.currentChannelProvider</code>, and prevent health-monitor restart thrash for channels that just (re)started by adding a per-channel startup-connect grace window. (from #32367) Thanks @MunemHashmi.</li>
<li>Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for <code>.cmd</code> shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.</li>
<li>Security/ACP sandbox inheritance: enforce fail-closed runtime guardrails for <code>sessions_spawn</code> with <code>runtime="acp"</code> by rejecting ACP spawns from sandboxed requester sessions and rejecting <code>sandbox="require"</code> for ACP runtime, preventing sandbox-boundary bypass via host-side ACP initialization. (#32254) Thanks @tdjackey for reporting, and @dutifulbob for the fix.</li>
<li>Security/Web tools SSRF guard: keep DNS pinning for untrusted <code>web_fetch</code> and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.</li>
<li>Gemini schema sanitization: coerce malformed JSON Schema <code>properties</code> values (<code>null</code>, arrays, primitives) to <code>{}</code> before provider validation, preventing downstream strict-validator crashes on invalid plugin/tool schemas. (#32332) Thanks @webdevtodayjason.</li>
<li>Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.</li>
<li>Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.</li>
<li>Browser/Extension relay stale tabs: evict stale cached targets from <code>/json/list</code> when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.</li>
<li>Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up <code>PortInUseError</code> races after <code>browser start</code>/<code>open</code>. (#29538) Thanks @AaronWander.</li>
<li>OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty <code>function_call_output.call_id</code> payloads in the WS conversion path to avoid OpenAI 400 errors (<code>Invalid 'input[n].call_id': empty string</code>), with regression coverage for both inbound stream normalization and outbound payload guards.</li>
<li>Security/Nodes camera URL downloads: bind node <code>camera.snap</code>/<code>camera.clip</code> URL payload downloads to the resolved node host, enforce fail-closed behavior when node <code>remoteIp</code> is unavailable, and use SSRF-guarded fetch with redirect host/protocol checks to prevent off-node fetch pivots. Thanks @tdjackey for reporting.</li>
<li>Config/backups hardening: enforce owner-only (<code>0600</code>) permissions on rotated config backups and clean orphan <code>.bak.*</code> files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.</li>
<li>Telegram/inbound media filenames: preserve original <code>file_name</code> metadata for document/audio/video/animation downloads (with fetch/path fallbacks), so saved inbound attachments keep sender-provided names instead of opaque Telegram file paths. (#31837) Thanks @Kay-051.</li>
<li>Gateway/OpenAI chat completions: honor <code>x-openclaw-message-channel</code> when building <code>agentCommand</code> input for <code>/v1/chat/completions</code>, preserving caller channel identity instead of forcing <code>webchat</code>. (#30462) Thanks @bmendonca3.</li>
<li>Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @Glucksberg.</li>
<li>Media/MIME normalization: normalize parameterized/case-variant MIME strings in <code>kindFromMime</code> (for example <code>Audio/Ogg; codecs=opus</code>) so WhatsApp voice notes are classified as audio and routed through transcription correctly. (#32280) Thanks @Lucenx9.</li>
<li>Discord/audio preflight mentions: detect audio attachments via Discord <code>content_type</code> and gate preflight transcription on typed text (not media placeholders), so guild voice-note mentions are transcribed and matched correctly. (#32136) Thanks @jnMetaCode.</li>
<li>Feishu/topic session routing: use <code>thread_id</code> as topic session scope fallback when <code>root_id</code> is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun.</li>
<li>Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of <code>NO_REPLY</code> and keep final-message buffering in sync, preventing partial <code>NO</code> leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.</li>
<li>Telegram/models picker callbacks: keep long model buttons selectable by falling back to compact callback payloads and resolving provider ids on selection (with provider re-prompt on ambiguity), avoiding Telegram 64-byte callback truncation failures. (#31857) Thanks @bmendonca3.</li>
<li>Context-window metadata warmup: add exponential config-load retry backoff (1s -> 2s -> 4s, capped at 60s) so transient startup failures recover automatically without hot-loop retries.</li>
<li>Voice-call/Twilio external outbound: auto-register webhook-first <code>outbound-api</code> calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.</li>
<li>Feishu/topic root replies: prefer <code>root_id</code> as outbound <code>replyTargetMessageId</code> when present, and parse millisecond <code>message_create_time</code> values correctly so topic replies anchor to the root message in grouped thread flows. (#29968) Thanks @bmendonca3.</li>
<li>Feishu/DM pairing reply target: send pairing challenge replies to <code>chat:<chat_id></code> instead of <code>user:<sender_open_id></code> so Lark/Feishu private chats with user-id-only sender payloads receive pairing messages reliably. (#31403) Thanks @stakeswky.</li>
<li>Feishu/Lark private DM routing: treat inbound <code>chat_type: "private"</code> as direct-message context for pairing/mention-forward/reaction synthetic handling so Lark private chats behave like Feishu p2p DMs. (#31400) Thanks @stakeswky.</li>
<li>Signal/message actions: allow <code>react</code> to fall back to <code>toolContext.currentMessageId</code> when <code>messageId</code> is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax.</li>
<li>Discord/message actions: allow <code>react</code> to fall back to <code>toolContext.currentMessageId</code> when <code>messageId</code> is omitted, matching Telegram/Signal reaction ergonomics in inbound turns.</li>
<li>Synology Chat/reply delivery: resolve webhook usernames to Chat API <code>user_id</code> values for outbound chatbot replies, avoiding mismatches between webhook user IDs and <code>method=chatbot</code> recipient IDs in multi-account setups. (#23709) Thanks @druide67.</li>
<li>Slack/thread context payloads: only inject thread starter/history text on first thread turn for new sessions while preserving thread metadata, reducing repeated context-token bloat on long-lived thread sessions. (#32133) Thanks @sourman.</li>
<li>Slack/session routing: keep top-level channel messages in one shared session when <code>replyToMode=off</code>, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193) Thanks @bmendonca3.</li>
<li>Voice-call/webhook routing: require exact webhook path matches (instead of prefix matches) so lookalike paths cannot reach provider verification/dispatch logic. (#31930) Thanks @afurm.</li>
<li>Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.</li>
<li>Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (<code>monitor.account-scope.test.ts</code>) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.</li>
<li>Feishu/Send target prefixes: normalize explicit <code>group:</code>/<code>dm:</code> send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai.</li>
<li>Webchat/Feishu session continuation: preserve routable <code>OriginatingChannel</code>/<code>OriginatingTo</code> metadata from session delivery context in <code>chat.send</code>, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)</li>
<li>Telegram/implicit mention forum handling: exclude Telegram forum system service messages (<code>forum_topic_*</code>, <code>general_forum_topic_*</code>) from reply-chain implicit mention detection so <code>requireMention</code> does not get bypassed inside bot-created topic lifecycle events. (#32262) Thanks @scoootscooob.</li>
<li>Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.</li>
<li>Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (<code>provider: "message"</code>) and normalize <code>lark</code>/<code>feishu</code> provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)</li>
<li>Webchat/silent token leak: filter assistant <code>NO_REPLY</code>-only transcript entries from <code>chat.history</code> responses and add client-side defense-in-depth guards in the chat controller so internal silent tokens never render as visible chat bubbles. (#32015) Consolidates overlap from #32183, #32082, #32045, #32052, #32172, and #32112. Thanks @ademczuk, @liuxiaopai-ai, @ningding97, @bmendonca3, and @x4v13r1120.</li>
<li>Doctor/local memory provider checks: stop false-positive local-provider warnings when <code>provider=local</code> and no explicit <code>modelPath</code> is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.</li>
<li>Media understanding/parakeet CLI output parsing: read <code>parakeet-mlx</code> transcripts from <code>--output-dir/<media-basename>.txt</code> when txt output is requested (or default), with stdout fallback for non-txt formats. (#9177) Thanks @mac-110.</li>
<li>Media understanding/audio transcription guard: skip tiny/empty audio files (<1024 bytes) before provider/CLI transcription to avoid noisy invalid-audio failures and preserve clean fallback behavior. (#8388) Thanks @Glucksberg.</li>
<li>Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.</li>
<li>Gateway/Node browser proxy routing: honor <code>profile</code> from <code>browser.request</code> JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.</li>
<li>Gateway/Control UI basePath POST handling: return 405 for <code>POST</code> on exact basePath routes (for example <code>/openclaw</code>) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.</li>
<li>Browser/default profile selection: default <code>browser.defaultProfile</code> behavior now prefers <code>openclaw</code> (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the <code>chrome</code> relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.</li>
<li>Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.</li>
<li>Models/config env propagation: apply <code>config.env.vars</code> before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.</li>
<li>Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so <code>openclaw models status</code> no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.</li>
<li>Gateway/Heartbeat model reload: treat <code>models.*</code> and <code>agents.defaults.model</code> config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.</li>
<li>Memory/LanceDB embeddings: forward configured <code>embedding.dimensions</code> into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.</li>
<li>Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.</li>
<li>Browser/CDP status accuracy: require a successful <code>Browser.getVersion</code> response over the CDP websocket (not just socket-open) before reporting <code>cdpReady</code>, so stale idle command channels are surfaced as unhealthy. (#23427) Thanks @vincentkoc.</li>
<li>Daemon/systemd checks in containers: treat missing <code>systemctl</code> invocations (including <code>spawn systemctl ENOENT</code>/<code>EACCES</code>) as unavailable service state during <code>is-enabled</code> checks, preventing container flows from failing with <code>Gateway service check failed</code> before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.</li>
<li>Security/Node exec approvals: revalidate approval-bound <code>cwd</code> identity immediately before execution/forwarding and fail closed with an explicit denial when <code>cwd</code> drifts after approval hardening.</li>
<li>Security audit/skills workspace hardening: add <code>skills.workspace.symlink_escape</code> warning in <code>openclaw security audit</code> when workspace <code>skills/**/SKILL.md</code> resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary.</li>
<li>Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example <code>env sh -c ...</code>) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.</li>
<li>Security/fs-safe write hardening: make <code>writeFileWithinRoot</code> use same-directory temp writes plus atomic rename, add post-write inode/hardlink revalidation with security warnings on boundary drift, and avoid truncating existing targets when final rename fails.</li>
<li>Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.</li>
<li>Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like <code>[System Message]</code> and line-leading <code>System:</code> in untrusted message content. (#30448)</li>
<li>Sandbox/Docker setup command parsing: accept <code>agents.*.sandbox.docker.setupCommand</code> as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.</li>
<li>Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction <code>AGENTS.md</code> context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.</li>
<li>Agents/Sandbox workdir mapping: map container workdir paths (for example <code>/workspace</code>) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.</li>
<li>Docker/Sandbox bootstrap hardening: make <code>OPENCLAW_SANDBOX</code> opt-in parsing explicit (<code>1|true|yes|on</code>), support custom Docker socket paths via <code>OPENCLAW_DOCKER_SOCKET</code>, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to <code>off</code> when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.</li>
<li>Hooks/webhook ACK compatibility: return <code>200</code> (instead of <code>202</code>) for successful <code>/hooks/agent</code> requests so providers that require <code>200</code> (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg.</li>
<li>Feishu/Run channel fallback: prefer <code>Provider</code> over <code>Surface</code> when inferring queued run <code>messageProvider</code> fallback (when <code>OriginatingChannel</code> is missing), preventing Feishu turns from being mislabeled as <code>webchat</code> in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.</li>
<li>Skills/sherpa-onnx-tts: run the <code>sherpa-onnx-tts</code> bin under ESM (replace CommonJS <code>require</code> imports) and add regression coverage to prevent <code>require is not defined in ES module scope</code> startup crashes. (#31965) Thanks @bmendonca3.</li>
<li>Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.</li>
<li>Slack/Channel message subscriptions: register explicit <code>message.channels</code> and <code>message.groups</code> monitor handlers (alongside generic <code>message</code>) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674.</li>
<li>Hooks/session-scoped memory context: expose ephemeral <code>sessionId</code> in embedded plugin tool contexts and <code>before_tool_call</code>/<code>after_tool_call</code> hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across <code>/new</code> and <code>/reset</code>. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex.</li>
<li>Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.</li>
<li>Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.</li>
<li>Feishu/File upload filenames: percent-encode non-ASCII/special-character <code>file_name</code> values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.</li>
<li>Media/MIME channel parity: route Telegram/Signal/iMessage media-kind checks through normalized <code>kindFromMime</code> so mixed-case/parameterized MIME values classify consistently across message channels.</li>
<li>WhatsApp/inbound self-message context: propagate inbound <code>fromMe</code> through the web inbox pipeline and annotate direct self messages as <code>(self)</code> in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.</li>
<li>Webchat/stream finalization: persist streamed assistant text when final events omit <code>message</code>, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.</li>
<li>Feishu/Inbound ordering: serialize message handling per chat while preserving cross-chat concurrency to avoid same-chat race drops under bursty inbound traffic. (#31807)</li>
<li>Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580)</li>
<li>Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970)</li>
<li>Feishu/Streaming block fallback: preserve markdown block stream text as final streaming-card content when final payload text is missing, while still suppressing non-card internal block chunk delivery. (#30663)</li>
<li>Feishu/Bitable API errors: unify Feishu Bitable tool error handling with structured <code>LarkApiError</code> responses and consistent API/context attribution across wiki/base metadata, field, and record operations. (#31450)</li>
<li>Feishu/Missing-scope grant URL fix: rewrite known invalid scope aliases (<code>contact:contact.base:readonly</code>) to valid scope names in permission grant links, so remediation URLs open with correct Feishu consent scopes. (#31943)</li>
<li>BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound <code>message_id</code> selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.</li>
<li>WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack.</li>
<li>Feishu/default account resolution: always honor explicit <code>channels.feishu.defaultAccount</code> during outbound account selection (including top-level-credential setups where the preferred id is not present in <code>accounts</code>), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3.</li>
<li>Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (<code>contact:contact.base:readonly</code>) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761)</li>
<li>Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.</li>
<li>Feishu/Inbound debounce: debounce rapid same-chat sender bursts into one ordered dispatch turn, skip already-processed retries when composing merged text, and preserve bot-mention intent across merged entries to reduce duplicate or late inbound handling. (#31548)</li>
<li>Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.</li>
<li>Browser/Extension re-announce reliability: keep relay state in <code>connecting</code> when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.</li>
<li>Browser/Act request compatibility: accept legacy flattened <code>action="act"</code> params (<code>kind/ref/text/...</code>) in addition to <code>request={...}</code> so browser act calls no longer fail with <code>request required</code>. (#15120) Thanks @vincentkoc.</li>
<li>OpenRouter/x-ai compatibility: skip <code>reasoning.effort</code> injection for <code>x-ai/*</code> models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.</li>
<li>Models/openai-completions developer-role compatibility: force <code>supportsDeveloperRole=false</code> for non-native endpoints, treat unparseable <code>baseUrl</code> values as non-native, and add regression coverage for empty/malformed baseUrl plus explicit-true override behavior. (#29479) thanks @akramcodez.</li>
<li>Browser/Profile attach-only override: support <code>browser.profiles.<name>.attachOnly</code> (fallback to global <code>browser.attachOnly</code>) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.</li>
<li>Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file <code>starttime</code> with <code>/proc/<pid>/stat</code> starttime, so stale <code>.jsonl.lock</code> files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.</li>
<li>Cron/isolated delivery target fallback: remove early unresolved-target return so cron delivery can flow through shared outbound target resolution (including per-channel <code>resolveDefaultTo</code> fallback) when <code>delivery.to</code> is omitted. (#32364) Thanks @hclsys.</li>
<li>OpenAI media capabilities: include <code>audio</code> in the OpenAI provider capability list so audio transcription models are eligible in media-understanding provider selection. (#12717) Thanks @openjay.</li>
<li>Browser/Managed tab cap: limit loopback managed <code>openclaw</code> page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.</li>
<li>Docker/Image health checks: add Dockerfile <code>HEALTHCHECK</code> that probes gateway <code>GET /healthz</code> so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.</li>
<li>Gateway/Node dangerous-command parity: include <code>sms.send</code> in default onboarding node <code>denyCommands</code>, share onboarding deny defaults with the gateway dangerous-command source of truth, and include <code>sms.send</code> in phone-control <code>/phone arm writes</code> handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.</li>
<li>Pairing/AllowFrom account fallback: handle omitted <code>accountId</code> values in <code>readChannelAllowFromStore</code> and <code>readChannelAllowFromStoreSync</code> as <code>default</code>, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.</li>
<li>Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.</li>
<li>Browser/CDP proxy bypass: force direct loopback agent paths and scoped <code>NO_PROXY</code> expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.</li>
<li>Sessions/idle reset correctness: preserve existing <code>updatedAt</code> during inbound metadata-only writes so idle-reset boundaries are not unintentionally refreshed before actual user turns. (#32379) Thanks @romeodiaz.</li>
<li>Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing <code>starttime</code> when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.</li>
<li>Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (<code>mtimeMs</code> + <code>sizeBytes</code>), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.</li>
<li>Agents/Subagents <code>sessions_spawn</code>: reject malformed <code>agentId</code> inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.</li>
<li>CLI/installer Node preflight: enforce Node.js <code>v22.12+</code> consistently in both <code>openclaw.mjs</code> runtime bootstrap and installer active-shell checks, with actionable nvm recovery guidance for mismatched shell PATH/defaults. (#32356) Thanks @jasonhargrove.</li>
<li>Web UI/config form: support SecretInput string-or-secret-ref unions in map <code>additionalProperties</code>, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.</li>
<li>Auto-reply/inline command cleanup: preserve newline structure when stripping inline <code>/status</code> and extracting inline slash commands by collapsing only horizontal whitespace, preventing paragraph flattening in multi-line replies. (#32224) Thanks @scoootscooob.</li>
<li>Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like <code>source</code>/<code>provider</code>), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.</li>
<li>Hooks/runtime stability: keep the internal hook handler registry on a <code>globalThis</code> singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.</li>
<li>Hooks/after_tool_call: include embedded session context (<code>sessionKey</code>, <code>agentId</code>) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc.</li>
<li>Hooks/tool-call correlation: include <code>runId</code> and <code>toolCallId</code> in plugin tool hook payloads/context and scope tool start/adjusted-param tracking by run to prevent cross-run collisions in <code>before_tool_call</code> and <code>after_tool_call</code>. (#32360) Thanks @vincentkoc.</li>
<li>Plugins/install diagnostics: reject legacy plugin package shapes without <code>openclaw.extensions</code> and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.</li>
<li>Hooks/plugin context parity: ensure <code>llm_input</code> hooks in embedded attempts receive the same <code>trigger</code> and <code>channelId</code>-aware <code>hookCtx</code> used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.</li>
<li>Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (<code>pnpm</code>, <code>bun</code>) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.</li>
<li>Cron/session reaper reliability: move cron session reaper sweeps into <code>onTimer</code> <code>finally</code> and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.</li>
<li>Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so <code>HEARTBEAT_OK</code> noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.</li>
<li>Authentication: classify <code>permission_error</code> as <code>auth_permanent</code> for profile fallback. (#31324) Thanks @Sid-Qin.</li>
<li>Agents/host edit reliability: treat host edit-tool throws as success only when on-disk post-check confirms replacement likely happened (<code>newText</code> present and <code>oldText</code> absent), preventing false failure reports while avoiding pre-write false positives. (#32383) Thanks @polooooo.</li>
<li>Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example <code>diffs</code> -> bundled <code>@openclaw/diffs</code>), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.</li>
<li>Web UI/inline code copy fidelity: disable forced mid-token wraps on inline <code><code></code> spans so copied UUID/hash/token strings preserve exact content instead of inserting line-break spaces. (#32346) Thanks @hclsys.</li>
<li>Restart sentinel formatting: avoid duplicate <code>Reason:</code> lines when restart message text already matches <code>stats.reason</code>, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2.</li>
<li>Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.</li>
<li>Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.</li>
<li>Failover/error classification: treat HTTP <code>529</code> (provider overloaded, common with Anthropic-compatible APIs) as <code>rate_limit</code> so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.</li>
<li>Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.</li>
<li>Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.</li>
<li>Secrets/exec resolver timeout defaults: use provider <code>timeoutMs</code> as the default inactivity (<code>noOutputTimeoutMs</code>) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.</li>
<li>Auto-reply/reminder guard note suppression: when a turn makes reminder-like commitments but schedules no new cron jobs, suppress the unscheduled-reminder warning note only if an enabled cron already exists for the same session; keep warnings for unrelated sessions, disabled jobs, or unreadable cron store paths. (#32255) Thanks @scoootscooob.</li>
<li>Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing <code>HEARTBEAT_OK</code> from being delivered to users. (#32131) Thanks @adhishthite.</li>
<li>Cron/store migration: normalize legacy cron jobs with string <code>schedule</code> and top-level <code>command</code>/<code>timeout</code> fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.</li>
<li>Tests/Windows backup rotation: skip chmod-only backup permission assertions on Windows while retaining compose/rotation/prune coverage across platforms to avoid false CI failures from Windows non-POSIX mode semantics. (#32286) Thanks @jalehman.</li>
<li>Tests/Subagent announce: set <code>OPENCLAW_TEST_FAST=1</code> before importing <code>subagent-announce</code> format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.2/OpenClaw-2026.3.2.zip" length="23181513" type="application/octet-stream" sparkle:edSignature="THMgkcoMgz2vv5zse3Po3K7l3Or2RhBKurXZIi8iYVXN76yJy1YXAY6kXi6ovD+dbYn68JKYDIKA1Ya78bO7BQ=="/>
</item>
<item>
<title>2026.3.1</title>
<pubDate>Mon, 02 Mar 2026 04:40:59 +0000</pubDate>
@@ -359,5 +140,175 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.1/OpenClaw-2026.3.1.zip" length="12804155" type="application/octet-stream" sparkle:edSignature="TF1otD4Vk3pG0iViX7mvix5DQEgAsk4JkSFvH7opjf9aawV16f29SUa2wRmiCFU6HEgyNgnGI/078O+A27eXCA=="/>
</item>
<item>
<title>2026.2.15</title>
<pubDate>Mon, 16 Feb 2026 05:04:34 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>202602150</sparkle:version>
<sparkle:shortVersionString>2026.2.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.15</h2>
<h3>Changes</h3>
<ul>
<li>Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.</li>
<li>Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.</li>
<li>Plugins: expose <code>llm_input</code> and <code>llm_output</code> hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.</li>
<li>Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set <code>agents.defaults.subagents.maxSpawnDepth: 2</code> to allow sub-agents to spawn their own children. Includes <code>maxChildrenPerAgent</code> limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.</li>
<li>Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.</li>
<li>Cron/Gateway: add finished-run webhook delivery toggle (<code>notify</code>) and dedicated webhook auth token support (<code>cron.webhookToken</code>) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.</li>
<li>Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.</li>
<li>Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.</li>
<li>Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.</li>
<li>Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.</li>
<li>Gateway/Security: redact sensitive session/path details from <code>status</code> responses for non-admin clients; full details remain available to <code>operator.admin</code>. (#8590) Thanks @fr33d3m0n.</li>
<li>Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (<code>allowInsecureAuth</code> / <code>dangerouslyDisableDeviceAuth</code>) when device identity is unavailable, preventing false <code>missing scope</code> failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.</li>
<li>LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.</li>
<li>Skills/Security: restrict <code>download</code> installer <code>targetDir</code> to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.</li>
<li>Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.</li>
<li>Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.</li>
<li>Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving <code>passwordFile</code> path exemptions, preventing accidental redaction of non-secret config values like <code>maxTokens</code> and IRC password-file paths. (#16042) Thanks @akramcodez.</li>
<li>Dev tooling: harden git <code>pre-commit</code> hook against option injection from malicious filenames (for example <code>--force</code>), preventing accidental staging of ignored files. Thanks @mrthankyou.</li>
<li>Gateway/Agent: reject malformed <code>agent:</code>-prefixed session keys (for example, <code>agent:main</code>) in <code>agent</code> and <code>agent.identity.get</code> instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.</li>
<li>Gateway/Chat: harden <code>chat.send</code> inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.</li>
<li>Gateway/Send: return an actionable error when <code>send</code> targets internal-only <code>webchat</code>, guiding callers to use <code>chat.send</code> or a deliverable channel. (#15703) Thanks @rodrigouroz.</li>
<li>Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing <code>script-src 'self'</code>. Thanks @Adam55A-code.</li>
<li>Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.</li>
<li>Agents/Sandbox: clarify system prompt path guidance so sandbox <code>bash/exec</code> uses container paths (for example <code>/workspace</code>) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.</li>
<li>Agents/Context: apply configured model <code>contextWindow</code> overrides after provider discovery so <code>lookupContextTokens()</code> honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.</li>
<li>Agents/Context: derive <code>lookupContextTokens()</code> from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.</li>
<li>Agents/OpenAI: force <code>store=true</code> for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.</li>
<li>Memory/FTS: make <code>buildFtsQuery</code> Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.</li>
<li>Auto-reply/Compaction: resolve <code>memory/YYYY-MM-DD.md</code> placeholders with timezone-aware runtime dates and append a <code>Current time:</code> line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.</li>
<li>Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.</li>
<li>Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.</li>
<li>Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.</li>
<li>Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.</li>
<li>Subagents/Models: preserve <code>agents.defaults.model.fallbacks</code> when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.</li>
<li>Telegram: omit <code>message_thread_id</code> for DM sends/draft previews and keep forum-topic handling (<code>id=1</code> general omitted, non-general kept), preventing DM failures with <code>400 Bad Request: message thread not found</code>. (#10942) Thanks @garnetlyx.</li>
<li>Telegram: replace inbound <code><media:audio></code> placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.</li>
<li>Telegram: retry inbound media <code>getFile</code> calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.</li>
<li>Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.</li>
<li>Discord: preserve channel session continuity when runtime payloads omit <code>message.channelId</code> by falling back to event/raw <code>channel_id</code> values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as <code>sessionKey=unknown</code>. (#17622) Thanks @shakkernerd.</li>
<li>Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with <code>_2</code> suffixes. (#17365) Thanks @seewhyme.</li>
<li>Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.</li>
<li>Web UI/Agents: hide <code>BOOTSTRAP.md</code> in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.</li>
<li>Auto-reply/WhatsApp/TUI/Web: when a final assistant message is <code>NO_REPLY</code> and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show <code>NO_REPLY</code> placeholders. (#7010) Thanks @Morrowind-Xie.</li>
<li>Cron: infer <code>payload.kind="agentTurn"</code> for model-only <code>cron.update</code> payload patches, so partial agent-turn updates do not fail validation when <code>kind</code> is omitted. (#15664) Thanks @rodrigouroz.</li>
<li>TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.</li>
<li>TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.</li>
<li>TUI: suppress false <code>(no output)</code> placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.</li>
<li>TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.</li>
<li>CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.15/OpenClaw-2026.2.15.zip" length="22896513" type="application/octet-stream" sparkle:edSignature="MLGsd2NeHXFRH1Or0bFQnAjqfuuJDuhl1mvKFIqTQcRvwbeyvOyyLXrqSbmaOgJR3wBQBKLs6jYQ9dQ/3R8RCg=="/>
</item>
<item>
<title>2026.2.26</title>
<pubDate>Thu, 26 Feb 2026 23:37:15 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>202602260</sparkle:version>
<sparkle:shortVersionString>2026.2.26</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.26</h2>
<h3>Changes</h3>
<ul>
<li>Highlight: External Secrets Management introduces a full <code>openclaw secrets</code> workflow (<code>audit</code>, <code>configure</code>, <code>apply</code>, <code>reload</code>) with runtime snapshot activation, strict <code>secrets apply</code> target-path validation, safer migration scrubbing, ref-only auth-profile support, and dedicated docs. (#26155) Thanks @joshavant.</li>
<li>ACP/Thread-bound agents: make ACP agents first-class runtimes for thread sessions with <code>acp</code> spawn/send dispatch integration, acpx backend bridging, lifecycle controls, startup reconciliation, runtime cleanup, and coalesced thread replies. (#23580) thanks @osolmaz.</li>
<li>Agents/Routing CLI: add <code>openclaw agents bindings</code>, <code>openclaw agents bind</code>, and <code>openclaw agents unbind</code> for account-scoped route management, including channel-only to account-scoped binding upgrades, role-aware binding identity handling, plugin-resolved binding account IDs, and optional account-binding prompts in <code>openclaw channels add</code>. (#27195) thanks @gumadeiras.</li>
<li>Codex/WebSocket transport: make <code>openai-codex</code> WebSocket-first by default (<code>transport: "auto"</code> with SSE fallback), keep explicit per-model/runtime transport overrides, and add regression coverage + docs for transport selection.</li>
<li>Onboarding/Plugins: let channel plugins own interactive onboarding flows with optional <code>configureInteractive</code> and <code>configureWhenConfigured</code> hooks while preserving the generic fallback path. (#27191) thanks @gumadeiras.</li>
<li>Android/Nodes: add Android <code>device</code> capability plus <code>device.status</code> and <code>device.info</code> node commands, including runtime handler wiring and protocol/registry coverage for device status/info payloads. (#27664) Thanks @obviyus.</li>
<li>Android/Nodes: add <code>notifications.list</code> support on Android nodes and expose <code>nodes notifications_list</code> in agent tooling for listing active device notifications. (#27344) thanks @obviyus.</li>
<li>Docs/Contributing: add Nimrod Gutman to the maintainer roster in <code>CONTRIBUTING.md</code>. (#27840) Thanks @ngutman.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Telegram/DM allowlist runtime inheritance: enforce <code>dmPolicy: "allowlist"</code> <code>allowFrom</code> requirements using effective account-plus-parent config across account-capable channels (Telegram, Discord, Slack, Signal, iMessage, IRC, BlueBubbles, WhatsApp), and align <code>openclaw doctor</code> checks to the same inheritance logic so DM traffic is not silently dropped after upgrades. (#27936) Thanks @widingmarcus-cyber.</li>
<li>Delivery queue/recovery backoff: prevent retry starvation by persisting <code>lastAttemptAt</code> on failed sends and deferring recovery retries until each entry's <code>lastAttemptAt + backoff</code> window is eligible, while continuing to recover ready entries behind deferred ones. Landed from contributor PR #27710 by @Jimmy-xuzimo. Thanks @Jimmy-xuzimo.</li>
<li>Google Chat/Lifecycle: keep Google Chat <code>startAccount</code> pending until abort in webhook mode so startup is no longer interpreted as immediate exit, preventing auto-restart loops and webhook-target churn. (#27384) thanks @junsuwhy.</li>
<li>Temp dirs/Linux umask: force <code>0700</code> permissions after temp-dir creation and self-heal existing writable temp dirs before trust checks so <code>umask 0002</code> installs no longer crash-loop on startup. Landed from contributor PR #27860 by @stakeswky. (#27853) Thanks @stakeswky.</li>
<li>Nextcloud Talk/Lifecycle: keep <code>startAccount</code> pending until abort and stop the webhook monitor on shutdown, preventing <code>EADDRINUSE</code> restart loops when the gateway manages account lifecycle. (#27897)</li>
<li>Microsoft Teams/File uploads: acknowledge <code>fileConsent/invoke</code> immediately (<code>invokeResponse</code> before upload + file card send) so Teams no longer shows false "Something went wrong" timeout banners while upload completion continues asynchronously; includes updated async regression coverage. Landed from contributor PR #27641 by @scz2011.</li>
<li>Queue/Drain/Cron reliability: harden lane draining with guaranteed <code>draining</code> flag reset on synchronous pump failures, reject new queue enqueues during gateway restart drain windows (instead of silently killing accepted tasks), add <code>/stop</code> queued-backlog cutoff metadata with stale-message skipping (while avoiding cross-session native-stop cutoff bleed), and raise isolated cron <code>agentTurn</code> outer safety timeout to avoid false 10-minute timeout races against longer agent session timeouts. (#27407, #27332, #27427)</li>
<li>Typing/Main reply pipeline: always mark dispatch idle in <code>agent-runner</code> finalization so typing cleanup runs even when dispatcher <code>onIdle</code> does not fire, preventing stuck typing indicators after run completion. (#27250) Thanks @Sid-Qin.</li>
<li>Typing/TTL safety net: add max-duration guardrails to shared typing callbacks so stuck lifecycle edges auto-stop typing indicators even when explicit idle/cleanup signals are missed. (#27428) Thanks @Crpdim.</li>
<li>Typing/Cross-channel leakage: unify run-scoped typing suppression for cross-channel/internal-webchat routes, preserve current inbound origin as embedded run message channel context, harden shared typing keepalive with consecutive-failure circuit breaker edge-case handling, and enforce dispatcher completion/idle waits in extension dispatcher callsites (Feishu, Matrix, Mattermost, MSTeams) so typing indicators always clean up on success/error paths. Related: #27647, #27493, #27598. Supersedes/replaces draft PRs: #27640, #27593, #27540.</li>
<li>Telegram/sendChatAction 401 handling: add bounded exponential backoff + temporary local typing suppression after repeated unauthorized failures to stop unbounded <code>sendChatAction</code> retry loops that can trigger Telegram abuse enforcement and bot deletion. (#27415) Thanks @widingmarcus-cyber.</li>
<li>Telegram/Webhook startup: clarify webhook config guidance, allow <code>channels.telegram.webhookPort: 0</code> for ephemeral listener binding, and log both the local listener URL and Telegram-advertised webhook URL with the bound port. (#25732) thanks @huntharo.</li>
<li>Browser/Chrome extension handshake: bind relay WS message handling before <code>onopen</code> and add non-blocking <code>connect.challenge</code> response handling for gateway-style handshake frames, avoiding stuck <code>…</code> badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553)</li>
<li>Browser/Extension relay init: dedupe concurrent same-port relay startup with shared in-flight initialization promises so callers await one startup lifecycle and receive consistent success/failure results. Landed from contributor PR #21277 by @HOYALIM. (Related #20688)</li>
<li>Browser/Fill relay + CLI parity: accept <code>act.fill</code> fields without explicit <code>type</code> by defaulting missing/empty <code>type</code> to <code>text</code> in both browser relay route parsing and <code>openclaw browser fill</code> CLI field parsing, so relay calls no longer fail when the model omits field type metadata. Landed from contributor PR #27662 by @Uface11. (#27296) Thanks @Uface11.</li>
<li>Feishu/Permission error dispatch: merge sender-name permission notices into the main inbound dispatch so one user message produces one agent turn/reply (instead of a duplicate permission-notice turn), with regression coverage. (#27381) thanks @byungsker.</li>
<li>Agents/Canvas default node resolution: when multiple connected canvas-capable nodes exist and no single <code>mac-*</code> candidate is selected, default to the first connected candidate instead of failing with <code>node required</code> for implicit-node canvas tool calls. Landed from contributor PR #27444 by @carbaj03. Thanks @carbaj03.</li>
<li>TUI/stream assembly: preserve streamed text across real tool-boundary drops without keeping stale streamed text when non-text blocks appear only in the final payload. Landed from contributor PR #27711 by @scz2011. (#27674)</li>
<li>Hooks/Internal <code>message:sent</code>: forward <code>sessionKey</code> on outbound sends from agent delivery, cron isolated delivery, gateway receipt acks, heartbeat sends, session-maintenance warnings, and restart-sentinel recovery so internal <code>message:sent</code> hooks consistently dispatch with session context, including <code>openclaw agent --deliver</code> runs resumed via <code>--session-id</code> (without explicit <code>--session-key</code>). Landed from contributor PR #27584 by @qualiobra. Thanks @qualiobra.</li>
<li>Pi image-token usage: stop re-injecting history image blocks each turn, process image references from the current prompt only, and prune already-answered user-image blocks in stored history to prevent runaway token growth. (#27602)</li>
<li>BlueBubbles/SSRF: auto-allowlist the configured <code>serverUrl</code> hostname for attachment fetches so localhost/private-IP BlueBubbles setups are no longer false-blocked by default SSRF checks. Landed from contributor PR #27648 by @lailoo. (#27599) Thanks @taylorhou for reporting.</li>
<li>Agents/Compaction + onboarding safety: prevent destructive double-compaction by stripping stale assistant usage around compaction boundaries, skipping post-compaction custom metadata writes in the same attempt, and cancelling safeguard compaction when there are no real conversation messages to summarize; harden workspace/bootstrap detection for memory-backed workspaces; and change <code>openclaw onboard --reset</code> default scope to <code>config+creds+sessions</code> (workspace deletion now requires <code>--reset-scope full</code>). (#26458, #27314) Thanks @jaden-clovervnd, @Sid-Qin, and @widingmarcus-cyber for fix direction in #26502, #26529, and #27492.</li>
<li>NO_REPLY suppression: suppress <code>NO_REPLY</code> before Slack API send and in sub-agent announce completion flow so sentinel text no longer leaks into user channels. Landed from contributor PRs #27529 (by @Sid-Qin) and #27535 (rewritten minimal landing by maintainers). (#27387, #27531)</li>
<li>Matrix/Group sender identity: preserve sender labels in Matrix group inbound prompt text (<code>BodyForAgent</code>) for both channel and threaded messages, and align group envelopes with shared inbound sender-prefix formatting so first-person requests resolve against the current sender. (#27401) thanks @koushikxd.</li>
<li>Auto-reply/Streaming: suppress only exact <code>NO_REPLY</code> final replies while still filtering streaming partial sentinel fragments (<code>NO_</code>, <code>NO_RE</code>, <code>HEARTBEAT_...</code>) so substantive replies ending with <code>NO_REPLY</code> are delivered and partial silent tokens do not leak during streaming. (#19576) Thanks @aldoeliacim.</li>
<li>Auto-reply/Inbound metadata: add a readable <code>timestamp</code> field to conversation info and ignore invalid/out-of-range timestamp values so prompt assembly never crashes on malformed timestamp inputs. (#17017) thanks @liuy.</li>
<li>Typing/Run completion race: prevent post-run keepalive ticks from re-triggering typing callbacks by guarding <code>triggerTyping()</code> with <code>runComplete</code>, with regression coverage for no-restart behavior during run-complete/dispatch-idle boundaries. (#27413) Thanks @widingmarcus-cyber.</li>
<li>Typing/Dispatch idle: force typing cleanup when <code>markDispatchIdle</code> never arrives after run completion, avoiding leaked typing keepalive loops in cron/announce edges. Landed from contributor PR #27541 by @Sid-Qin. (#27493)</li>
<li>Telegram/Inline buttons: allow callback-query button handling in groups (including <code>/models</code> follow-up buttons) when group policy authorizes the sender, by removing the redundant callback allowlist gate that blocked open-policy groups. (#27343) Thanks @GodsBoy.</li>
<li>Telegram/Streaming preview: when finalizing without an existing preview message, prime pending preview text with final answer before stop-flush so users do not briefly see stale 1-2 word fragments (for example <code>no</code> before <code>no problem</code>). (#27449) Thanks @emanuelst for the original fix direction in #19673.</li>
<li>Browser/Extension relay CORS: handle <code>/json*</code> <code>OPTIONS</code> preflight before auth checks, allow Chrome extension origins, and return extension-origin CORS headers on relay HTTP responses so extension token validation no longer fails cross-origin. Landed from contributor PR #23962 by @miloudbelarebia. (#23842)</li>
<li>Browser/Extension relay auth: allow <code>?token=</code> query-param auth on relay <code>/json*</code> endpoints (consistent with relay WebSocket auth) so curl/devtools-style <code>/json/version</code> and <code>/json/list</code> probes work without requiring custom headers. Landed from contributor PR #26015 by @Sid-Qin. (#25928)</li>
<li>Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay <code>stop()</code> before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng.</li>
<li>Browser/Extension relay reconnect resilience: keep CDP clients alive across brief MV3 extension disconnect windows, wait briefly for extension reconnect before failing in-flight CDP commands, and only tear down relay target/client state after reconnect grace expires. Landed from contributor PR #27617 by @davidemanuelDEV.</li>
<li>Browser/Route decode hardening: guard malformed percent-encoding in relay target action routes and browser route-param decoding so crafted <code>%</code> paths return <code>400</code> instead of crashing/unhandled URI decode failures. Landed from contributor PR #11880 by @Yida-Dev.</li>
<li>Feishu/Inbound message metadata: include inbound <code>message_id</code> in <code>BodyForAgent</code> on a dedicated metadata line so agents can reliably correlate and act on media/message operations that require message IDs, with regression coverage. (#27253) thanks @xss925175263.</li>
<li>Feishu/Doc tools: route <code>feishu_doc</code> and <code>feishu_app_scopes</code> through the active agent account context (with explicit <code>accountId</code> override support) so multi-account agents no longer default to the first configured app, with regression coverage for context routing and explicit override behavior. (#27338) thanks @AaronL725.</li>
<li>LINE/Inline directives auth: gate directive parsing (<code>/model</code>, <code>/think</code>, <code>/verbose</code>, <code>/reasoning</code>, <code>/queue</code>) on resolved authorization (<code>command.isAuthorizedSender</code>) so <code>commands.allowFrom</code>-authorized LINE senders are not silently stripped when raw <code>CommandAuthorized</code> is unset. Landed from contributor PR #27248 by @kevinWangSheng. (#27240)</li>
<li>Onboarding/Gateway: seed default Control UI <code>allowedOrigins</code> for non-loopback binds during onboarding (<code>localhost</code>/<code>127.0.0.1</code> plus custom bind host) so fresh non-loopback setups do not fail startup due to missing origin policy. (#26157) thanks @stakeswky.</li>
<li>Docker/GCP onboarding: reduce first-build OOM risk by capping Node heap during <code>pnpm install</code>, reuse existing gateway token during <code>docker-setup.sh</code> reruns so <code>.env</code> stays aligned with config, auto-bootstrap Control UI allowed origins for non-loopback Docker binds, and add GCP docs guidance for tokenized dashboard links + pairing recovery commands. (#26253) Thanks @pandego.</li>
<li>CLI/Gateway <code>--force</code> in non-root Docker: recover from <code>lsof</code> permission failures (<code>EACCES</code>/<code>EPERM</code>) by falling back to <code>fuser</code> kill + probe-based port checks, so <code>openclaw gateway --force</code> works for default container <code>node</code> user flows. (#27941)</li>
<li>Gateway/Bind visibility: emit a startup warning when binding to non-loopback addresses so operators get explicit exposure guidance in runtime logs. (#25397) thanks @let5sne.</li>
<li>Sessions cleanup/Doctor: add <code>openclaw sessions cleanup --fix-missing</code> to prune store entries whose transcript files are missing, including doctor guidance and CLI coverage. Landed from contributor PR #27508 by @Sid-Qin. (#27422)</li>
<li>Doctor/State integrity: ignore metadata-only slash routing sessions when checking recent missing transcripts so <code>openclaw doctor</code> no longer reports false-positive transcript-missing warnings for <code>*:slash:*</code> keys. (#27375) thanks @gumadeiras.</li>
<li>CLI/Gateway status: force local <code>gateway status</code> probe host to <code>127.0.0.1</code> for <code>bind=lan</code> so co-located probes do not trip non-loopback plaintext WebSocket checks. (#26997) thanks @chikko80.</li>
<li>CLI/Gateway auth: align <code>gateway run --auth</code> parsing/help text with supported gateway auth modes by accepting <code>none</code> and <code>trusted-proxy</code> (in addition to <code>token</code>/<code>password</code>) for CLI overrides. (#27469) thanks @s1korrrr.</li>
<li>CLI/Daemon status TLS probe: use <code>wss://</code> and forward local TLS certificate fingerprint for TLS-enabled gateway daemon probes so <code>openclaw daemon status</code> works with <code>gateway.bind=lan</code> + <code>gateway.tls.enabled=true</code>. (#24234) thanks @liuy.</li>
<li>Podman/Default bind: change <code>run-openclaw-podman.sh</code> default gateway bind from <code>lan</code> to <code>loopback</code> and document explicit LAN opt-in with Control UI origin configuration. (#27491) thanks @robbyczgw-cla.</li>
<li>Daemon/macOS launchd: forward proxy env vars into supervised service environments, keep LaunchAgent <code>KeepAlive=true</code> semantics, and harden restart sequencing to <code>print -> bootout -> wait old pid exit -> bootstrap -> kickstart</code>. (#27276) thanks @frankekn.</li>
<li>Gateway/macOS restart-loop hardening: detect OpenClaw-managed supervisor markers during SIGUSR1 restart handoff, clean stale gateway PIDs before <code>/restart</code> launchctl/systemctl triggers, and set LaunchAgent <code>ThrottleInterval=60</code> to bound launchd retry storms during lock-release races. Landed from contributor PRs #27655 (@taw0002), #27448 (@Sid-Qin), and #27650 (@kevinWangSheng). (#27605, #27590, #26904, #26736)</li>
<li>Models/MiniMax auth header defaults: set <code>authHeader: true</code> for both onboarding-generated MiniMax API providers and implicit built-in MiniMax (<code>minimax</code>, <code>minimax-portal</code>) provider templates so first requests no longer fail with MiniMax <code>401 authentication_error</code> due to missing <code>Authorization</code> header. Landed from contributor PRs #27622 by @riccoyuanft and #27631 by @kevinWangSheng. (#27600, #15303)</li>
<li>Auth/Auth profiles: normalize <code>auth-profiles.json</code> alias fields (<code>mode -> type</code>, <code>apiKey -> key</code>) before credential validation so entries copied from <code>openclaw.json</code> auth examples are no longer silently dropped. (#26950) thanks @byungsker.</li>
<li>Models/Profile suffix parsing: centralize trailing <code>@profile</code> parsing and only treat <code>@</code> as a profile separator when it appears after the final <code>/</code>, preserving model IDs like <code>openai/@cf/...</code> and <code>openrouter/@preset/...</code> across <code>/model</code> directive parsing and allowlist model resolution, with regression coverage.</li>
<li>Models/OpenAI Codex config schema parity: accept <code>openai-codex-responses</code> in the config model API schema and TypeScript <code>ModelApi</code> union, with regression coverage for config validation. Landed from contributor PR #27501 by @AytuncYildizli. Thanks @AytuncYildizli.</li>
<li>Agents/Models config: preserve agent-level provider <code>apiKey</code> and <code>baseUrl</code> during merge-mode <code>models.json</code> updates when agent values are present. (#27293) thanks @Sid-Qin.</li>
<li>Azure OpenAI Responses: force <code>store=true</code> for <code>azure-openai-responses</code> direct responses API calls to avoid multi-turn 400 failures. Landed from contributor PR #27499 by @polarbear-Yang. (#27497)</li>
<li>Security/Node exec approvals: require structured <code>commandArgv</code> approvals for <code>host=node</code>, enforce versioned <code>systemRunBindingV1</code> matching for argv/cwd/session/agent/env context with fail-closed behavior on missing/mismatched bindings, and add <code>GIT_EXTERNAL_DIFF</code> to blocked host env keys. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Plugin channel HTTP auth: normalize protected <code>/api/channels</code> path checks against canonicalized request paths (case + percent-decoding + slash normalization), resolve encoded dot-segment traversal variants, and fail closed on malformed <code>%</code>-encoded channel prefixes so alternate-path variants cannot bypass gateway auth. This ships in the next npm release (<code>2026.2.26</code>). Thanks @zpbrent for reporting.</li>
<li>Security/Gateway node pairing: pin paired-device <code>platform</code>/<code>deviceFamily</code> metadata across reconnects and bind those fields into device-auth signatures, so reconnect metadata spoofing cannot expand node command allowlists without explicit repair pairing. This ships in the next npm release (<code>2026.2.26</code>). Thanks @76embiid21 for reporting.</li>
<li>Security/Sandbox path alias guard: reject broken symlink targets by resolving through existing ancestors and failing closed on out-of-root targets, preventing workspace-only <code>apply_patch</code> writes from escaping sandbox/workspace boundaries via dangling symlinks. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Workspace FS boundary aliases: harden canonical boundary resolution for non-existent-leaf symlink aliases while preserving valid in-root aliases, preventing first-write workspace escapes via out-of-root symlink targets. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Config includes: harden <code>$include</code> file loading with verified-open reads, reject hardlinked include aliases, and enforce include file-size guardrails so config include resolution remains bounded to trusted in-root files. This ships in the next npm release (<code>2026.2.26</code>). Thanks @zpbrent for reporting.</li>
<li>Security/Node exec approvals hardening: freeze immutable approval-time execution plans (<code>argv</code>/<code>cwd</code>/<code>agentId</code>/<code>sessionKey</code>) via <code>system.run.prepare</code>, enforce those canonical plan values during approval forwarding/execution, and reject mutable parent-symlink cwd paths during approval-plan building to prevent approval bypass via symlink rebind. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Microsoft Teams media fetch: route Graph message/hosted-content/attachment fetches and auth-scope fallback attachment downloads through shared SSRF-guarded fetch paths, and centralize hostname-suffix allowlist policy helpers in the plugin SDK to remove channel/plugin drift. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Voice Call (Twilio): bind webhook replay + manager dedupe identity to authenticated request material, remove unsigned <code>i-twilio-idempotency-token</code> trust from replay/dedupe keys, and thread verified request identity through provider parse flow to harden cross-provider event dedupe. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Exec approvals forwarding: prefer turn-source channel/account/thread metadata when resolving approval delivery targets so stale session routes do not misroute approval prompts.</li>
<li>Security/Pairing multi-account isolation: enforce account-scoped pairing allowlists and pending-request storage across core + extension message channels while preserving channel-scoped defaults for the default account. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting and @gumadeiras for implementation.</li>
<li>Config/Plugins entries: treat unknown <code>plugins.entries.*</code> ids as startup warnings (ignored stale keys) instead of hard validation failures that can crash-loop gateway boot. Landed from contributor PR #27506 by @Sid-Qin. (#27455)</li>
<li>Telegram native commands: degrade command registration on <code>BOT_COMMANDS_TOO_MUCH</code> by retrying with fewer commands instead of crash-looping startup sync. Landed from contributor PR #27512 by @Sid-Qin. (#27456)</li>
<li>Web tools/Proxy: route <code>web_search</code> provider HTTP calls (Brave, Perplexity, xAI, Gemini, Kimi), redirect resolution, and <code>web_fetch</code> through a shared proxy-aware SSRF guard path so gateway installs behind <code>HTTP_PROXY</code>/<code>HTTPS_PROXY</code>/<code>ALL_PROXY</code> no longer fail with transport <code>fetch failed</code> errors. (#27430) thanks @kevinWangSheng.</li>
<li>Android/Node invoke: remove native gateway WebSocket <code>Origin</code> header to avoid false origin rejections, unify invoke command registry/policy/error parsing paths, and keep command availability checks centralized to reduce dispatcher/advertisement drift. (#27257) Thanks @obviyus.</li>
<li>Gateway shared-auth scopes: preserve requested operator scopes for shared-token clients when device identity is unavailable, instead of clearing scopes during auth handling. Landed from contributor PR #27498 by @kevinWangSheng. (#27494)</li>
<li>Cron/Hooks isolated routing: preserve canonical <code>agent:*</code> session keys in isolated runs so already-qualified keys are not double-prefixed (for example <code>agent:main:main</code> no longer becomes <code>agent:main:agent:main:main</code>). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282)</li>
<li>Channels/Multi-account config: when adding a non-default channel account to a single-account top-level channel setup, move existing account-scoped top-level single-account values into <code>channels.<channel>.accounts.default</code> before writing the new account so the original account keeps working without duplicated account values at channel root; <code>openclaw doctor --fix</code> now repairs previously mixed channel account shapes the same way. (#27334) thanks @gumadeiras.</li>
<li>iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman.</li>
<li>CI/Windows: shard the Windows <code>checks-windows</code> test lane into two matrix jobs and honor explicit shard index overrides in <code>scripts/test-parallel.mjs</code> to reduce CI critical-path wall time. (#27234) Thanks @joshavant.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.26/OpenClaw-2026.2.26.zip" length="12796628" type="application/octet-stream" sparkle:edSignature="qqVJfkQS9Q4LCTlGtOyXzORWZWWnOkWyiJ6DVX27oPF8aeUlUyfHrmb51sFiNjSuCJC2xmJW1Mi1CAHl/I1pCw=="/>
</item>
</channel>
</rss>

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>OpenClaw Activity</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.2</string>
<key>CFBundleVersion</key>
<string>20260301</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
<key>NSSupportsLiveActivities</key>
<true/>
</dict>
</plist>

View File

@@ -1,9 +0,0 @@
import SwiftUI
import WidgetKit
@main
struct OpenClawActivityWidgetBundle: WidgetBundle {
var body: some Widget {
OpenClawLiveActivity()
}
}

View File

@@ -1,84 +0,0 @@
import ActivityKit
import SwiftUI
import WidgetKit
struct OpenClawLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: OpenClawActivityAttributes.self) { context in
lockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
statusDot(state: context.state)
}
DynamicIslandExpandedRegion(.center) {
Text(context.state.statusText)
.font(.subheadline)
.lineLimit(1)
}
DynamicIslandExpandedRegion(.trailing) {
trailingView(state: context.state)
}
} compactLeading: {
statusDot(state: context.state)
} compactTrailing: {
Text(context.state.statusText)
.font(.caption2)
.lineLimit(1)
.frame(maxWidth: 64)
} minimal: {
statusDot(state: context.state)
}
}
}
@ViewBuilder
private func lockScreenView(context: ActivityViewContext<OpenClawActivityAttributes>) -> some View {
HStack(spacing: 8) {
statusDot(state: context.state)
.frame(width: 10, height: 10)
VStack(alignment: .leading, spacing: 2) {
Text("OpenClaw")
.font(.subheadline.bold())
Text(context.state.statusText)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
trailingView(state: context.state)
}
.padding(.vertical, 4)
}
@ViewBuilder
private func trailingView(state: OpenClawActivityAttributes.ContentState) -> some View {
if state.isConnecting {
ProgressView().controlSize(.small)
} else if state.isDisconnected {
Image(systemName: "wifi.slash")
.foregroundStyle(.red)
} else if state.isIdle {
Image(systemName: "antenna.radiowaves.left.and.right")
.foregroundStyle(.green)
} else {
Text(state.startedAt, style: .timer)
.font(.caption)
.monospacedDigit()
.foregroundStyle(.secondary)
}
}
@ViewBuilder
private func statusDot(state: OpenClawActivityAttributes.ContentState) -> some View {
Circle()
.fill(dotColor(state: state))
.frame(width: 6, height: 6)
}
private func dotColor(state: OpenClawActivityAttributes.ContentState) -> Color {
if state.isDisconnected { return .red }
if state.isConnecting { return .gray }
if state.isIdle { return .green }
return .blue
}
}

View File

@@ -4,7 +4,6 @@ OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.watchkitapp
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.watchkitapp.extension
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.ios.activitywidget
// Local contributors can override this by running scripts/ios-configure-signing.sh.
// Keep include after defaults: xcconfig is evaluated top-to-bottom.

View File

@@ -1,7 +1,6 @@
import AVFoundation
import OpenClawKit
import Foundation
import os
actor CameraController {
struct CameraDeviceInfo: Codable, Sendable {
@@ -121,8 +120,7 @@ actor CameraController {
Self.pickCamera(facing: preferFrontCamera ? .front : .back, deviceId: deviceId)
},
cameraUnavailableError: CameraError.cameraUnavailable,
mapSetupError: Self.mapMovieSetupError,
operation: { output in
mapSetupError: Self.mapMovieSetupError) { output in
var delegate: MovieFileDelegate?
let recordedURL: URL = try await withCheckedThrowingContinuation { cont in
let d = MovieFileDelegate(cont)
@@ -133,7 +131,7 @@ actor CameraController {
// Transcode .mov -> .mp4 for easier downstream handling.
try await Self.exportToMP4(inputURL: recordedURL, outputURL: mp4URL)
return try Data(contentsOf: mp4URL)
})
}
return (
format: format.rawValue,
base64: data.base64EncodedString(),
@@ -261,7 +259,7 @@ actor CameraController {
private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
private let continuation: CheckedContinuation<Data, Error>
private let resumed = OSAllocatedUnfairLock(initialState: false)
private var didResume = false
init(_ continuation: CheckedContinuation<Data, Error>) {
self.continuation = continuation
@@ -272,12 +270,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
let alreadyResumed = self.resumed.withLock { old in
let was = old
old = true
return was
}
guard !alreadyResumed else { return }
guard !self.didResume else { return }
self.didResume = true
if let error {
self.continuation.resume(throwing: error)
@@ -306,19 +300,15 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
error: Error?
) {
guard let error else { return }
let alreadyResumed = self.resumed.withLock { old in
let was = old
old = true
return was
}
guard !alreadyResumed else { return }
guard !self.didResume else { return }
self.didResume = true
self.continuation.resume(throwing: error)
}
}
private final class MovieFileDelegate: NSObject, AVCaptureFileOutputRecordingDelegate {
private let continuation: CheckedContinuation<URL, Error>
private let resumed = OSAllocatedUnfairLock(initialState: false)
private var didResume = false
init(_ continuation: CheckedContinuation<URL, Error>) {
self.continuation = continuation
@@ -330,12 +320,8 @@ private final class MovieFileDelegate: NSObject, AVCaptureFileOutputRecordingDel
from connections: [AVCaptureConnection],
error: Error?)
{
let alreadyResumed = self.resumed.withLock { old in
let was = old
old = true
return was
}
guard !alreadyResumed else { return }
guard !self.didResume else { return }
self.didResume = true
if let error {
let ns = error as NSError

View File

@@ -9,7 +9,6 @@ import Darwin
import OpenClawKit
import Network
import Observation
import os
import Photos
import ReplayKit
import Security
@@ -991,16 +990,12 @@ extension GatewayConnectionController {
#endif
private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @unchecked Sendable {
private struct ProbeState {
var didFinish = false
var session: URLSession?
var task: URLSessionWebSocketTask?
}
private let url: URL
private let timeoutSeconds: Double
private let onComplete: (String?) -> Void
private let state = OSAllocatedUnfairLock(initialState: ProbeState())
private var didFinish = false
private var session: URLSession?
private var task: URLSessionWebSocketTask?
init(url: URL, timeoutSeconds: Double, onComplete: @escaping (String?) -> Void) {
self.url = url
@@ -1013,11 +1008,9 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u
config.timeoutIntervalForRequest = self.timeoutSeconds
config.timeoutIntervalForResource = self.timeoutSeconds
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
self.session = session
let task = session.webSocketTask(with: self.url)
self.state.withLock { s in
s.session = session
s.task = task
}
self.task = task
task.resume()
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + self.timeoutSeconds) { [weak self] in
@@ -1043,18 +1036,12 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u
}
private func finish(_ fingerprint: String?) {
let (shouldComplete, taskToCancel, sessionToInvalidate) = self.state.withLock { s -> (Bool, URLSessionWebSocketTask?, URLSession?) in
guard !s.didFinish else { return (false, nil, nil) }
s.didFinish = true
let task = s.task
let session = s.session
s.task = nil
s.session = nil
return (true, task, session)
}
guard shouldComplete else { return }
taskToCancel?.cancel(with: .goingAway, reason: nil)
sessionToInvalidate?.invalidateAndCancel()
objc_sync_enter(self)
defer { objc_sync_exit(self) }
guard !self.didFinish else { return }
self.didFinish = true
self.task?.cancel(with: .goingAway, reason: nil)
self.session?.invalidateAndCancel()
self.onComplete(fingerprint)
}

View File

@@ -25,7 +25,6 @@ enum GatewaySettingsStore {
private static let instanceIdAccount = "instanceId"
private static let preferredGatewayStableIDAccount = "preferredStableID"
private static let lastDiscoveredGatewayStableIDAccount = "lastDiscoveredStableID"
private static let lastGatewayConnectionAccount = "lastConnection"
private static let talkProviderApiKeyAccountPrefix = "provider.apiKey."
static func bootstrapPersistence() {
@@ -141,20 +140,11 @@ enum GatewaySettingsStore {
}
}
private enum LastGatewayKind: String, Codable {
private enum LastGatewayKind: String {
case manual
case discovered
}
/// JSON-serializable envelope stored as a single Keychain entry.
private struct LastGatewayConnectionData: Codable {
var kind: LastGatewayKind
var stableID: String
var useTLS: Bool
var host: String?
var port: Int?
}
static func loadTalkProviderApiKey(provider: String) -> String? {
guard let providerId = self.normalizedTalkProviderID(provider) else { return nil }
let account = self.talkProviderApiKeyAccount(providerId: providerId)
@@ -178,93 +168,47 @@ enum GatewaySettingsStore {
}
static func saveLastGatewayConnectionManual(host: String, port: Int, useTLS: Bool, stableID: String) {
let payload = LastGatewayConnectionData(
kind: .manual, stableID: stableID, useTLS: useTLS, host: host, port: port)
self.saveLastGatewayConnectionData(payload)
let defaults = UserDefaults.standard
defaults.set(LastGatewayKind.manual.rawValue, forKey: self.lastGatewayKindDefaultsKey)
defaults.set(host, forKey: self.lastGatewayHostDefaultsKey)
defaults.set(port, forKey: self.lastGatewayPortDefaultsKey)
defaults.set(useTLS, forKey: self.lastGatewayTlsDefaultsKey)
defaults.set(stableID, forKey: self.lastGatewayStableIDDefaultsKey)
}
static func saveLastGatewayConnectionDiscovered(stableID: String, useTLS: Bool) {
let payload = LastGatewayConnectionData(
kind: .discovered, stableID: stableID, useTLS: useTLS)
self.saveLastGatewayConnectionData(payload)
let defaults = UserDefaults.standard
defaults.set(LastGatewayKind.discovered.rawValue, forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.set(useTLS, forKey: self.lastGatewayTlsDefaultsKey)
defaults.set(stableID, forKey: self.lastGatewayStableIDDefaultsKey)
}
static func loadLastGatewayConnection() -> LastGatewayConnection? {
// Migrate legacy UserDefaults entries on first access.
self.migrateLastGatewayFromUserDefaultsIfNeeded()
guard let json = KeychainStore.loadString(
service: self.gatewayService, account: self.lastGatewayConnectionAccount),
let data = json.data(using: .utf8),
let stored = try? JSONDecoder().decode(LastGatewayConnectionData.self, from: data)
else { return nil }
let stableID = stored.stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !stableID.isEmpty else { return nil }
if stored.kind == .discovered {
return .discovered(stableID: stableID, useTLS: stored.useTLS)
}
let host = (stored.host ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let port = stored.port ?? 0
guard !host.isEmpty, port > 0, port <= 65535 else { return nil }
return .manual(host: host, port: port, useTLS: stored.useTLS, stableID: stableID)
}
static func clearLastGatewayConnection(defaults: UserDefaults = .standard) {
_ = KeychainStore.delete(
service: self.gatewayService, account: self.lastGatewayConnectionAccount)
// Clean up any legacy UserDefaults entries.
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey)
}
@discardableResult
private static func saveLastGatewayConnectionData(_ payload: LastGatewayConnectionData) -> Bool {
guard let data = try? JSONEncoder().encode(payload),
let json = String(data: data, encoding: .utf8)
else { return false }
return KeychainStore.saveString(
json, service: self.gatewayService, account: self.lastGatewayConnectionAccount)
}
/// Migrate legacy UserDefaults gateway.last.* keys into a single Keychain entry.
private static func migrateLastGatewayFromUserDefaultsIfNeeded() {
let defaults = UserDefaults.standard
let stableID = defaults.string(forKey: self.lastGatewayStableIDDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !stableID.isEmpty else { return }
// Already migrated if Keychain entry exists.
if KeychainStore.loadString(
service: self.gatewayService, account: self.lastGatewayConnectionAccount) != nil
{
// Clean up legacy keys.
self.removeLastGatewayDefaults(defaults)
return
}
guard !stableID.isEmpty else { return nil }
let useTLS = defaults.bool(forKey: self.lastGatewayTlsDefaultsKey)
let kindRaw = defaults.string(forKey: self.lastGatewayKindDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let kind = LastGatewayKind(rawValue: kindRaw) ?? .manual
let host = defaults.string(forKey: self.lastGatewayHostDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let port = defaults.object(forKey: self.lastGatewayPortDefaultsKey) as? Int
let payload = LastGatewayConnectionData(
kind: kind, stableID: stableID, useTLS: useTLS,
host: kind == .manual ? host : nil,
port: kind == .manual ? port : nil)
guard self.saveLastGatewayConnectionData(payload) else { return }
self.removeLastGatewayDefaults(defaults)
if kind == .discovered {
return .discovered(stableID: stableID, useTLS: useTLS)
}
let host = defaults.string(forKey: self.lastGatewayHostDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let port = defaults.integer(forKey: self.lastGatewayPortDefaultsKey)
// Back-compat: older builds persisted manual-style host/port without a kind marker.
guard !host.isEmpty, port > 0, port <= 65535 else { return nil }
return .manual(host: host, port: port, useTLS: useTLS, stableID: stableID)
}
private static func removeLastGatewayDefaults(_ defaults: UserDefaults) {
static func clearLastGatewayConnection(defaults: UserDefaults = .standard) {
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
@@ -411,15 +355,9 @@ enum GatewayDiagnostics {
private static let maxLogBytes: Int64 = 512 * 1024
private static let keepLogBytes: Int64 = 256 * 1024
private static let logSizeCheckEveryWrites = 50
private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0)
private static let isoFormatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
nonisolated(unsafe) private static var logWritesSinceCheck = 0
private static var fileURL: URL? {
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?
.appendingPathComponent("openclaw-gateway.log")
}
@@ -466,41 +404,32 @@ enum GatewayDiagnostics {
}
}
private static func applyFileProtection(url: URL) {
try? FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
ofItemAtPath: url.path)
}
static func bootstrap() {
guard let url = fileURL else { return }
queue.async {
self.truncateLogIfNeeded(url: url)
let timestamp = self.isoFormatter.string(from: Date())
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let timestamp = formatter.string(from: Date())
let line = "[\(timestamp)] gateway diagnostics started\n"
if let data = line.data(using: .utf8) {
self.appendToLog(url: url, data: data)
self.applyFileProtection(url: url)
}
}
}
static func log(_ message: String) {
let timestamp = self.isoFormatter.string(from: Date())
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let timestamp = formatter.string(from: Date())
let line = "[\(timestamp)] \(message)"
logger.info("\(line, privacy: .public)")
guard let url = fileURL else { return }
queue.async {
let shouldTruncate = self.logWritesSinceCheck.withLock { count in
count += 1
if count >= self.logSizeCheckEveryWrites {
count = 0
return true
}
return false
}
if shouldTruncate {
self.logWritesSinceCheck += 1
if self.logWritesSinceCheck >= self.logSizeCheckEveryWrites {
self.logWritesSinceCheck = 0
self.truncateLogIfNeeded(url: url)
}
let entry = line + "\n"

View File

@@ -1,16 +1,48 @@
import Foundation
import OpenClawKit
import Security
enum KeychainStore {
static func loadString(service: String, account: String) -> String? {
GenericPasswordKeychainStore.loadString(service: service, account: account)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
static func saveString(_ value: String, service: String, account: String) -> Bool {
GenericPasswordKeychainStore.saveString(value, service: service, account: account)
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
let update: [String: Any] = [kSecValueData as String: data]
let status = SecItemUpdate(query as CFDictionary, update as CFDictionary)
if status == errSecSuccess { return true }
if status != errSecItemNotFound { return false }
var insert = query
insert[kSecValueData as String] = data
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
return SecItemAdd(insert as CFDictionary, nil) == errSecSuccess
}
static func delete(service: String, account: String) -> Bool {
GenericPasswordKeychainStore.delete(service: service, account: account)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
}

View File

@@ -54,8 +54,6 @@
<string>OpenClaw needs microphone access for voice wake.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@@ -1,125 +0,0 @@
import ActivityKit
import Foundation
import os
/// Minimal Live Activity lifecycle focused on connection health + stale cleanup.
@MainActor
final class LiveActivityManager {
static let shared = LiveActivityManager()
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "LiveActivity")
private var currentActivity: Activity<OpenClawActivityAttributes>?
private var activityStartDate: Date = .now
private init() {
self.hydrateCurrentAndPruneDuplicates()
}
var isActive: Bool {
guard let activity = self.currentActivity else { return false }
guard activity.activityState == .active else {
self.currentActivity = nil
return false
}
return true
}
func startActivity(agentName: String, sessionKey: String) {
self.hydrateCurrentAndPruneDuplicates()
if self.currentActivity != nil {
self.handleConnecting()
return
}
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
self.logger.info("Live Activities disabled; skipping start")
return
}
self.activityStartDate = .now
let attributes = OpenClawActivityAttributes(agentName: agentName, sessionKey: sessionKey)
do {
let activity = try Activity.request(
attributes: attributes,
content: ActivityContent(state: self.connectingState(), staleDate: nil),
pushType: nil)
self.currentActivity = activity
self.logger.info("started live activity id=\(activity.id, privacy: .public)")
} catch {
self.logger.error("failed to start live activity: \(error.localizedDescription, privacy: .public)")
}
}
func handleConnecting() {
self.updateCurrent(state: self.connectingState())
}
func handleReconnect() {
self.updateCurrent(state: self.idleState())
}
func handleDisconnect() {
self.updateCurrent(state: self.disconnectedState())
}
private func hydrateCurrentAndPruneDuplicates() {
let active = Activity<OpenClawActivityAttributes>.activities
guard !active.isEmpty else {
self.currentActivity = nil
return
}
let keeper = active.max { lhs, rhs in
lhs.content.state.startedAt < rhs.content.state.startedAt
} ?? active[0]
self.currentActivity = keeper
self.activityStartDate = keeper.content.state.startedAt
let stale = active.filter { $0.id != keeper.id }
for activity in stale {
Task {
await activity.end(
ActivityContent(state: self.disconnectedState(), staleDate: nil),
dismissalPolicy: .immediate)
}
}
}
private func updateCurrent(state: OpenClawActivityAttributes.ContentState) {
guard let activity = self.currentActivity else { return }
Task {
await activity.update(ActivityContent(state: state, staleDate: nil))
}
}
private func connectingState() -> OpenClawActivityAttributes.ContentState {
OpenClawActivityAttributes.ContentState(
statusText: "Connecting...",
isIdle: false,
isDisconnected: false,
isConnecting: true,
startedAt: self.activityStartDate)
}
private func idleState() -> OpenClawActivityAttributes.ContentState {
OpenClawActivityAttributes.ContentState(
statusText: "Idle",
isIdle: true,
isDisconnected: false,
isConnecting: false,
startedAt: self.activityStartDate)
}
private func disconnectedState() -> OpenClawActivityAttributes.ContentState {
OpenClawActivityAttributes.ContentState(
statusText: "Disconnected",
isIdle: false,
isDisconnected: true,
isConnecting: false,
startedAt: self.activityStartDate)
}
}

View File

@@ -1,45 +0,0 @@
import ActivityKit
import Foundation
/// Shared schema used by iOS app + Live Activity widget extension.
struct OpenClawActivityAttributes: ActivityAttributes {
var agentName: String
var sessionKey: String
struct ContentState: Codable, Hashable {
var statusText: String
var isIdle: Bool
var isDisconnected: Bool
var isConnecting: Bool
var startedAt: Date
}
}
#if DEBUG
extension OpenClawActivityAttributes {
static let preview = OpenClawActivityAttributes(agentName: "main", sessionKey: "main")
}
extension OpenClawActivityAttributes.ContentState {
static let connecting = OpenClawActivityAttributes.ContentState(
statusText: "Connecting...",
isIdle: false,
isDisconnected: false,
isConnecting: true,
startedAt: .now)
static let idle = OpenClawActivityAttributes.ContentState(
statusText: "Idle",
isIdle: true,
isDisconnected: false,
isConnecting: false,
startedAt: .now)
static let disconnected = OpenClawActivityAttributes.ContentState(
statusText: "Disconnected",
isIdle: false,
isDisconnected: true,
isConnecting: false,
startedAt: .now)
}
#endif

View File

@@ -21,7 +21,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
self.manager
}
var locationRequestContinuation: CheckedContinuation<CLLocation, Swift.Error>? {
var locationRequestContinuation: CheckedContinuation<CLLocation, Error>? {
get { self.locationContinuation }
set { self.locationContinuation = newValue }
}
@@ -65,10 +65,9 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
desiredAccuracy: desiredAccuracy,
maxAgeMs: maxAgeMs,
timeoutMs: timeoutMs,
request: { try await self.requestLocationOnce() },
withTimeout: { timeoutMs, operation in
request: { try await self.requestLocationOnce() }) { timeoutMs, operation in
try await self.withTimeout(timeoutMs: timeoutMs, operation: operation)
})
}
}
private func awaitAuthorizationChange() async -> CLAuthorizationStatus {

View File

@@ -90,9 +90,7 @@ final class NodeAppModel {
var lastShareEventText: String = "No share events yet."
var openChatRequestID: Int = 0
private(set) var pendingAgentDeepLinkPrompt: AgentDeepLinkPrompt?
private var queuedAgentDeepLinkPrompt: AgentDeepLinkPrompt?
private var lastAgentDeepLinkPromptAt: Date = .distantPast
@ObservationIgnored private var queuedAgentDeepLinkPromptTask: Task<Void, Never>?
// Primary "node" connection: used for device capabilities and node.invoke requests.
private let nodeGateway = GatewayNodeSession()
@@ -1695,7 +1693,6 @@ extension NodeAppModel {
self.operatorGatewayTask = nil
self.voiceWakeSyncTask?.cancel()
self.voiceWakeSyncTask = nil
LiveActivityManager.shared.handleDisconnect()
self.gatewayHealthMonitor.stop()
Task {
await self.operatorGateway.disconnect()
@@ -1732,7 +1729,6 @@ private extension NodeAppModel {
self.operatorConnected = false
self.voiceWakeSyncTask?.cancel()
self.voiceWakeSyncTask = nil
LiveActivityManager.shared.handleDisconnect()
self.gatewayDefaultAgentId = nil
self.gatewayAgents = []
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
@@ -1813,7 +1809,6 @@ private extension NodeAppModel {
await self.refreshAgentsFromGateway()
await self.refreshShareRouteFromGateway()
await self.startVoiceWakeSync()
await MainActor.run { LiveActivityManager.shared.handleReconnect() }
await MainActor.run { self.startGatewayHealthMonitor() }
},
onDisconnected: { [weak self] reason in
@@ -1821,7 +1816,6 @@ private extension NodeAppModel {
await MainActor.run {
self.operatorConnected = false
self.talkMode.updateGatewayConnected(false)
LiveActivityManager.shared.handleDisconnect()
}
GatewayDiagnostics.log("operator gateway disconnected reason=\(reason)")
await MainActor.run { self.stopGatewayHealthMonitor() }
@@ -1886,14 +1880,6 @@ private extension NodeAppModel {
self.gatewayStatusText = (attempt == 0) ? "Connecting…" : "Reconnecting…"
self.gatewayServerName = nil
self.gatewayRemoteAddress = nil
let liveActivity = LiveActivityManager.shared
if liveActivity.isActive {
liveActivity.handleConnecting()
} else {
liveActivity.startActivity(
agentName: self.selectedAgentId ?? "main",
sessionKey: self.mainSessionKey)
}
}
do {
@@ -2605,31 +2591,19 @@ extension NodeAppModel {
"agent deep link rejected: unkeyed message too long chars=\(message.count, privacy: .public)")
return
}
if Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt) < 1.0 {
self.deepLinkLogger.debug("agent deep link prompt throttled")
return
}
self.lastAgentDeepLinkPromptAt = Date()
let urlText = originalURL.absoluteString
let prompt = AgentDeepLinkPrompt(
id: UUID().uuidString,
messagePreview: message,
urlPreview: urlText.count > 500 ? "\(urlText.prefix(500))" : urlText,
request: self.effectiveAgentDeepLinkForPrompt(link))
let promptIntervalSeconds = 5.0
let elapsed = Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt)
if elapsed < promptIntervalSeconds {
if self.pendingAgentDeepLinkPrompt != nil {
self.pendingAgentDeepLinkPrompt = prompt
self.recordShareEvent("Updated local confirmation request (\(message.count) chars).")
self.deepLinkLogger.debug("agent deep link prompt coalesced into active confirmation")
return
}
let remaining = max(0, promptIntervalSeconds - elapsed)
self.queueAgentDeepLinkPrompt(prompt, initialDelaySeconds: remaining)
self.recordShareEvent("Queued local confirmation (\(message.count) chars).")
self.deepLinkLogger.debug("agent deep link prompt queued due to rate limit")
return
}
self.presentAgentDeepLinkPrompt(prompt)
self.pendingAgentDeepLinkPrompt = prompt
self.recordShareEvent("Awaiting local confirmation (\(message.count) chars).")
self.deepLinkLogger.info("agent deep link requires local confirmation")
return
@@ -2698,60 +2672,6 @@ extension NodeAppModel {
self.deepLinkLogger.info("agent deep link cancelled by local user")
}
private func presentAgentDeepLinkPrompt(_ prompt: AgentDeepLinkPrompt) {
self.lastAgentDeepLinkPromptAt = Date()
self.pendingAgentDeepLinkPrompt = prompt
}
private func queueAgentDeepLinkPrompt(_ prompt: AgentDeepLinkPrompt, initialDelaySeconds: TimeInterval) {
self.queuedAgentDeepLinkPrompt = prompt
guard self.queuedAgentDeepLinkPromptTask == nil else { return }
self.queuedAgentDeepLinkPromptTask = Task { [weak self] in
guard let self else { return }
let delayNs = UInt64(max(0, initialDelaySeconds) * 1_000_000_000)
if delayNs > 0 {
do {
try await Task.sleep(nanoseconds: delayNs)
} catch {
return
}
}
await self.deliverQueuedAgentDeepLinkPrompt()
}
}
private func deliverQueuedAgentDeepLinkPrompt() async {
defer { self.queuedAgentDeepLinkPromptTask = nil }
let promptIntervalSeconds = 5.0
while let prompt = self.queuedAgentDeepLinkPrompt {
if self.pendingAgentDeepLinkPrompt != nil {
do {
try await Task.sleep(nanoseconds: 200_000_000)
} catch {
return
}
continue
}
let elapsed = Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt)
if elapsed < promptIntervalSeconds {
let remaining = max(0, promptIntervalSeconds - elapsed)
do {
try await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
} catch {
return
}
continue
}
self.queuedAgentDeepLinkPrompt = nil
self.presentAgentDeepLinkPrompt(prompt)
self.recordShareEvent("Awaiting local confirmation (\(prompt.messagePreview.count) chars).")
self.deepLinkLogger.info("agent deep link queued prompt delivered")
}
}
private func submitAgentDeepLink(_ link: AgentDeepLink, messageCharCount: Int) async {
do {
try await self.sendAgentRequest(link: link)

View File

@@ -302,7 +302,6 @@ private struct ManualEntryStep: View {
// (GatewaySetupCode) decode raw setup codes.
}
@MainActor
private func gatewayConnectionStatusLines(
appModel: NodeAppModel,
gatewayController: GatewayConnectionController) -> [String]
@@ -310,7 +309,6 @@ private func gatewayConnectionStatusLines(
ConnectionStatusBox.defaultLines(appModel: appModel, gatewayController: gatewayController)
}
@MainActor
private func resetGatewayConnectionState(
appModel: NodeAppModel,
connectStatusText: inout String?,
@@ -321,7 +319,6 @@ private func resetGatewayConnectionState(
connectingGatewayID = nil
}
@MainActor
@ViewBuilder
private func gatewayConnectionStatusSection(
appModel: NodeAppModel,

View File

@@ -1,5 +1,4 @@
import AVFoundation
import OpenClawKit
import ReplayKit
final class ScreenRecordService: @unchecked Sendable {

View File

@@ -20,11 +20,10 @@ enum WatchMessagingError: LocalizedError {
}
}
@MainActor
final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServicing {
nonisolated private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging")
final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked Sendable {
private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging")
private let session: WCSession?
private var pendingActivationContinuations: [CheckedContinuation<Void, Never>] = []
private let replyHandlerLock = NSLock()
private var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
override init() {
@@ -40,11 +39,11 @@ final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServi
}
}
nonisolated static func isSupportedOnDevice() -> Bool {
static func isSupportedOnDevice() -> Bool {
WCSession.isSupported()
}
nonisolated static func currentStatusSnapshot() -> WatchMessagingStatus {
static func currentStatusSnapshot() -> WatchMessagingStatus {
guard WCSession.isSupported() else {
return WatchMessagingStatus(
supported: false,
@@ -71,7 +70,9 @@ final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServi
}
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
self.replyHandlerLock.lock()
self.replyHandler = handler
self.replyHandlerLock.unlock()
}
func sendNotification(
@@ -160,15 +161,19 @@ final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServi
}
private func emitReply(_ event: WatchQuickReplyEvent) {
self.replyHandler?(event)
let handler: ((WatchQuickReplyEvent) -> Void)?
self.replyHandlerLock.lock()
handler = self.replyHandler
self.replyHandlerLock.unlock()
handler?(event)
}
nonisolated private static func nonEmpty(_ value: String?) -> String? {
private static func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
nonisolated private static func parseQuickReplyPayload(
private static func parseQuickReplyPayload(
_ payload: [String: Any],
transport: String) -> WatchQuickReplyEvent?
{
@@ -200,12 +205,13 @@ final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServi
guard let session = self.session else { return }
if session.activationState == .activated { return }
session.activate()
await withCheckedContinuation { continuation in
self.pendingActivationContinuations.append(continuation)
for _ in 0..<8 {
if session.activationState == .activated { return }
try? await Task.sleep(nanoseconds: 100_000_000)
}
}
nonisolated private static func status(for session: WCSession) -> WatchMessagingStatus {
private static func status(for session: WCSession) -> WatchMessagingStatus {
WatchMessagingStatus(
supported: true,
paired: session.isPaired,
@@ -214,7 +220,7 @@ final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServi
activationState: activationStateLabel(session.activationState))
}
nonisolated private static func activationStateLabel(_ state: WCSessionActivationState) -> String {
private static func activationStateLabel(_ state: WCSessionActivationState) -> String {
switch state {
case .notActivated:
"notActivated"
@@ -229,42 +235,32 @@ final class WatchMessagingService: NSObject, @preconcurrency WatchMessagingServi
}
extension WatchMessagingService: WCSessionDelegate {
nonisolated func session(
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: (any Error)?)
{
if let error {
Self.logger.error("watch activation failed: \(error.localizedDescription, privacy: .public)")
} else {
Self.logger.debug("watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)")
}
// Always resume all waiters so callers never hang, even on error.
Task { @MainActor in
let waiters = self.pendingActivationContinuations
self.pendingActivationContinuations.removeAll()
for continuation in waiters {
continuation.resume()
}
return
}
Self.logger.debug("watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)")
}
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidBecomeInactive(_ session: WCSession) {}
nonisolated func sessionDidDeactivate(_ session: WCSession) {
func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
nonisolated func session(_: WCSession, didReceiveMessage message: [String: Any]) {
func session(_: WCSession, didReceiveMessage message: [String: Any]) {
guard let event = Self.parseQuickReplyPayload(message, transport: "sendMessage") else {
return
}
Task { @MainActor in
self.emitReply(event)
}
self.emitReply(event)
}
nonisolated func session(
func session(
_: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void)
@@ -274,19 +270,15 @@ extension WatchMessagingService: WCSessionDelegate {
return
}
replyHandler(["ok": true])
Task { @MainActor in
self.emitReply(event)
}
self.emitReply(event)
}
nonisolated func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
guard let event = Self.parseQuickReplyPayload(userInfo, transport: "transferUserInfo") else {
return
}
Task { @MainActor in
self.emitReply(event)
}
self.emitReply(event)
}
nonisolated func sessionReachabilityDidChange(_ session: WCSession) {}
func sessionReachabilityDidChange(_ session: WCSession) {}
}

View File

@@ -7,23 +7,6 @@ import Observation
import OSLog
import Speech
private final class StreamFailureBox: @unchecked Sendable {
private let lock = NSLock()
private var valueInternal: Error?
func set(_ error: Error) {
self.lock.lock()
self.valueInternal = error
self.lock.unlock()
}
var value: Error? {
self.lock.lock()
defer { self.lock.unlock() }
return self.valueInternal
}
}
// This file intentionally centralizes talk mode state + behavior.
// It's large, and splitting would force `private` -> `fileprivate` across many members.
// We'll refactor into smaller files when the surface stabilizes.
@@ -89,9 +72,6 @@ final class TalkModeManager: NSObject {
private var mainSessionKey: String = "main"
private var fallbackVoiceId: String?
private var lastPlaybackWasPCM: Bool = false
/// Set when the ElevenLabs API rejects PCM format (e.g. 403 subscription_required).
/// Once set, all subsequent requests in this session use MP3 instead of re-trying PCM.
private var pcmFormatUnavailable: Bool = false
var pcmPlayer: PCMStreamingAudioPlaying = PCMStreamingAudioPlayer.shared
var mp3Player: StreamingAudioPlaying = StreamingAudioPlayer.shared
@@ -1007,12 +987,9 @@ final class TalkModeManager: NSObject {
self.logger.warning("unknown voice alias \(requestedVoice ?? "?", privacy: .public)")
}
let configuredKey = self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil
#if DEBUG
let resolvedKey = configuredKey ?? ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
#else
let resolvedKey = configuredKey
#endif
let resolvedKey =
(self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil) ??
ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
let apiKey = resolvedKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let preferredVoice = resolvedVoice ?? self.currentVoiceId ?? self.defaultVoiceId
let voiceId: String? = if let apiKey, !apiKey.isEmpty {
@@ -1027,8 +1004,7 @@ final class TalkModeManager: NSObject {
let desiredOutputFormat = (directive?.outputFormat ?? self.defaultOutputFormat)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let requestedOutputFormat = (desiredOutputFormat?.isEmpty == false) ? desiredOutputFormat : nil
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(
requestedOutputFormat ?? self.effectiveDefaultOutputFormat)
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(requestedOutputFormat ?? "pcm_44100")
if outputFormat == nil, let requestedOutputFormat {
self.logger.warning(
"talk output_format unsupported for local playback: \(requestedOutputFormat, privacy: .public)")
@@ -1057,7 +1033,7 @@ final class TalkModeManager: NSObject {
let request = makeRequest(outputFormat: outputFormat)
let client = ElevenLabsTTSClient(apiKey: apiKey)
let rawStream = client.streamSynthesize(voiceId: voiceId, request: request)
let stream = client.streamSynthesize(voiceId: voiceId, request: request)
if self.interruptOnSpeech {
do {
@@ -1072,16 +1048,11 @@ final class TalkModeManager: NSObject {
let sampleRate = TalkTTSValidation.pcmSampleRate(from: outputFormat)
let result: StreamingPlaybackResult
if let sampleRate {
let streamFailure = StreamFailureBox()
let stream = Self.monitorStreamFailures(rawStream, failureBox: streamFailure)
self.lastPlaybackWasPCM = true
var playback = await self.pcmPlayer.play(stream: stream, sampleRate: sampleRate)
if !playback.finished, playback.interruptedAt == nil {
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100_128")
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
self.logger.warning("pcm playback failed; retrying mp3")
if Self.isPCMFormatRejectedByAPI(streamFailure.value) {
self.pcmFormatUnavailable = true
}
self.lastPlaybackWasPCM = false
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
@@ -1091,7 +1062,7 @@ final class TalkModeManager: NSObject {
result = playback
} else {
self.lastPlaybackWasPCM = false
result = await self.mp3Player.play(stream: rawStream)
result = await self.mp3Player.play(stream: stream)
}
let duration = Date().timeIntervalSince(started)
self.logger.info("elevenlabs stream finished=\(result.finished, privacy: .public) dur=\(duration, privacy: .public)s")
@@ -1417,7 +1388,7 @@ final class TalkModeManager: NSObject {
private func resolveIncrementalPrefetchOutputFormat(context: IncrementalSpeechContext) -> String? {
if TalkTTSValidation.pcmSampleRate(from: context.outputFormat) != nil {
return ElevenLabsTTSClient.validatedOutputFormat("mp3_44100_128")
return ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
}
return context.outputFormat
}
@@ -1506,19 +1477,15 @@ final class TalkModeManager: NSObject {
let desiredOutputFormat = (directive?.outputFormat ?? self.defaultOutputFormat)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let requestedOutputFormat = (desiredOutputFormat?.isEmpty == false) ? desiredOutputFormat : nil
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(
requestedOutputFormat ?? self.effectiveDefaultOutputFormat)
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(requestedOutputFormat ?? "pcm_44100")
if outputFormat == nil, let requestedOutputFormat {
self.logger.warning(
"talk output_format unsupported for local playback: \(requestedOutputFormat, privacy: .public)")
}
let configuredKey = self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil
#if DEBUG
let resolvedKey = configuredKey ?? ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
#else
let resolvedKey = configuredKey
#endif
let resolvedKey =
(self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil) ??
ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
let apiKey = resolvedKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let voiceId: String? = if let apiKey, !apiKey.isEmpty {
await self.resolveVoiceId(preferred: preferredVoice, apiKey: apiKey)
@@ -1561,44 +1528,6 @@ final class TalkModeManager: NSObject {
latencyTier: TalkTTSValidation.validatedLatencyTier(context.directive?.latencyTier))
}
/// Returns `mp3_44100_128` when the API has already rejected PCM, otherwise `pcm_44100`.
private var effectiveDefaultOutputFormat: String {
self.pcmFormatUnavailable ? "mp3_44100_128" : "pcm_44100"
}
private static func monitorStreamFailures(
_ stream: AsyncThrowingStream<Data, Error>,
failureBox: StreamFailureBox
) -> AsyncThrowingStream<Data, Error>
{
AsyncThrowingStream { continuation in
let task = Task {
do {
for try await chunk in stream {
continuation.yield(chunk)
}
continuation.finish()
} catch {
failureBox.set(error)
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
private static func isPCMFormatRejectedByAPI(_ error: Error?) -> Bool {
guard let error = error as NSError? else { return false }
guard error.domain == "ElevenLabsTTS", error.code >= 400 else { return false }
let message = (error.userInfo[NSLocalizedDescriptionKey] as? String ?? error.localizedDescription).lowercased()
return message.contains("output_format")
|| message.contains("pcm_")
|| message.contains("pcm ")
|| message.contains("subscription_required")
}
private static func makeBufferedAudioStream(chunks: [Data]) -> AsyncThrowingStream<Data, Error> {
AsyncThrowingStream { continuation in
for chunk in chunks {
@@ -1640,27 +1569,22 @@ final class TalkModeManager: NSObject {
text: text,
context: context,
outputFormat: context.outputFormat)
let rawStream: AsyncThrowingStream<Data, Error>
let stream: AsyncThrowingStream<Data, Error>
if let prefetchedAudio, !prefetchedAudio.chunks.isEmpty {
rawStream = Self.makeBufferedAudioStream(chunks: prefetchedAudio.chunks)
stream = Self.makeBufferedAudioStream(chunks: prefetchedAudio.chunks)
} else {
rawStream = client.streamSynthesize(voiceId: voiceId, request: request)
stream = client.streamSynthesize(voiceId: voiceId, request: request)
}
let playbackFormat = prefetchedAudio?.outputFormat ?? context.outputFormat
let sampleRate = TalkTTSValidation.pcmSampleRate(from: playbackFormat)
let result: StreamingPlaybackResult
if let sampleRate {
let streamFailure = StreamFailureBox()
let stream = Self.monitorStreamFailures(rawStream, failureBox: streamFailure)
self.lastPlaybackWasPCM = true
var playback = await self.pcmPlayer.play(stream: stream, sampleRate: sampleRate)
if !playback.finished, playback.interruptedAt == nil {
self.logger.warning("pcm playback failed; retrying mp3")
if Self.isPCMFormatRejectedByAPI(streamFailure.value) {
self.pcmFormatUnavailable = true
}
self.lastPlaybackWasPCM = false
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100_128")
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
request: self.makeIncrementalTTSRequest(
@@ -1672,7 +1596,7 @@ final class TalkModeManager: NSObject {
result = playback
} else {
self.lastPlaybackWasPCM = false
result = await self.mp3Player.play(stream: rawStream)
result = await self.mp3Player.play(stream: stream)
}
if !result.finished, let interruptedAt = result.interruptedAt {
self.lastInterruptedAtSeconds = interruptedAt
@@ -1682,8 +1606,6 @@ final class TalkModeManager: NSObject {
}
private struct IncrementalSpeechBuffer {
private static let softBoundaryMinChars = 72
private(set) var latestText: String = ""
private(set) var directive: TalkDirective?
private var spokenOffset: Int = 0
@@ -1776,9 +1698,8 @@ private struct IncrementalSpeechBuffer {
}
if !inCodeBlock {
let currentChar = chars[idx]
buffer.append(currentChar)
if Self.isBoundary(currentChar) || Self.isSoftBoundary(currentChar, bufferedChars: buffer.count) {
buffer.append(chars[idx])
if Self.isBoundary(chars[idx]) {
lastBoundary = idx + 1
bufferAtBoundary = buffer
inCodeBlockAtBoundary = inCodeBlock
@@ -1805,10 +1726,6 @@ private struct IncrementalSpeechBuffer {
private static func isBoundary(_ ch: Character) -> Bool {
ch == "." || ch == "!" || ch == "?" || ch == "\n"
}
private static func isSoftBoundary(_ ch: Character, bufferedChars: Int) -> Bool {
bufferedChars >= Self.softBoundaryMinChars && ch.isWhitespace
}
}
extension TalkModeManager {
@@ -2003,7 +1920,6 @@ extension TalkModeManager {
func reloadConfig() async {
guard let gateway else { return }
self.pcmFormatUnavailable = false
do {
let res = try await gateway.request(
method: "talk.config",
@@ -2183,10 +2099,6 @@ private final class AudioTapDiagnostics: @unchecked Sendable {
#if DEBUG
extension TalkModeManager {
static func _test_isPCMFormatRejectedByAPI(_ error: Error?) -> Bool {
self.isPCMFormatRejectedByAPI(error)
}
func _test_seedTranscript(_ transcript: String) {
self.lastTranscript = transcript
self.lastHeard = Date()

View File

@@ -441,6 +441,13 @@ final class VoiceWakeManager: NSObject {
}
}
private static func permissionMessage(
kind: String,
status: AVAudioSession.RecordPermission) -> String
{
self.deniedByDefaultPermissionMessage(kind: kind, isUndetermined: status == .undetermined)
}
private static func permissionMessage(
kind: String,
status: SFSpeechRecognizerAuthorizationStatus) -> String
@@ -459,7 +466,7 @@ final class VoiceWakeManager: NSObject {
}
}
private nonisolated static func deniedByDefaultPermissionMessage(kind: String, isUndetermined: Bool) -> String {
private static func deniedByDefaultPermissionMessage(kind: String, isUndetermined: Bool) -> String {
if isUndetermined {
return "\(kind) permission not granted"
}

View File

@@ -62,7 +62,3 @@ Sources/Voice/VoiceWakePreferences.swift
../../Swabble/Sources/SwabbleKit/WakeWordGate.swift
Sources/Voice/TalkModeManager.swift
Sources/Voice/TalkOrbOverlay.swift
Sources/LiveActivity/OpenClawActivityAttributes.swift
Sources/LiveActivity/LiveActivityManager.swift
ActivityWidget/OpenClawActivityWidgetBundle.swift
ActivityWidget/OpenClawLiveActivity.swift

View File

@@ -71,37 +71,18 @@ import UIKit
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {
if let prior {
_ = KeychainStore.saveString(prior, service: "ai.openclaw.gateway", account: "lastConnection")
} else {
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
}
withUserDefaults([:]) {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "gateway.example.com",
port: 443,
useTLS: true,
stableID: "manual|gateway.example.com|443")
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "gateway.example.com", port: 443, useTLS: true, stableID: "manual|gateway.example.com|443"))
}
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "gateway.example.com",
port: 443,
useTLS: true,
stableID: "manual|gateway.example.com|443")
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "gateway.example.com", port: 443, useTLS: true, stableID: "manual|gateway.example.com|443"))
}
@Test @MainActor func loadLastConnectionReturnsNilForInvalidData() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {
if let prior {
_ = KeychainStore.saveString(prior, service: "ai.openclaw.gateway", account: "lastConnection")
} else {
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
}
}
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
// Plant legacy UserDefaults with invalid host/port to exercise migration + validation.
withUserDefaults([
"gateway.last.kind": "manual",
"gateway.last.host": "",

View File

@@ -27,7 +27,6 @@ private let lastGatewayDefaultsKeys = [
"gateway.last.tls",
"gateway.last.stableID",
]
private let lastGatewayKeychainEntry = KeychainEntry(service: gatewayService, account: "lastConnection")
private func snapshotDefaults(_ keys: [String]) -> [String: Any?] {
let defaults = UserDefaults.standard
@@ -85,13 +84,9 @@ private func withBootstrapSnapshots(_ body: () -> Void) {
body()
}
private func withLastGatewaySnapshot(_ body: () -> Void) {
let defaultsSnapshot = snapshotDefaults(lastGatewayDefaultsKeys)
let keychainSnapshot = snapshotKeychain([lastGatewayKeychainEntry])
defer {
restoreDefaults(defaultsSnapshot)
restoreKeychain(keychainSnapshot)
}
private func withLastGatewayDefaultsSnapshot(_ body: () -> Void) {
let snapshot = snapshotDefaults(lastGatewayDefaultsKeys)
defer { restoreDefaults(snapshot) }
body()
}
@@ -140,7 +135,7 @@ private func withLastGatewaySnapshot(_ body: () -> Void) {
}
@Test func lastGateway_manualRoundTrip() {
withLastGatewaySnapshot {
withLastGatewayDefaultsSnapshot {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "example.com",
port: 443,
@@ -152,24 +147,28 @@ private func withLastGatewaySnapshot(_ body: () -> Void) {
}
}
@Test func lastGateway_discoveredOverwritesManual() {
withLastGatewaySnapshot {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "10.0.0.99",
port: 18789,
useTLS: true,
stableID: "manual|10.0.0.99|18789")
@Test func lastGateway_discoveredDoesNotPersistResolvedHostPort() {
withLastGatewayDefaultsSnapshot {
// Simulate a prior manual record that included host/port.
applyDefaults([
"gateway.last.host": "10.0.0.99",
"gateway.last.port": 18789,
"gateway.last.tls": true,
"gateway.last.stableID": "manual|10.0.0.99|18789",
"gateway.last.kind": "manual",
])
GatewaySettingsStore.saveLastGatewayConnectionDiscovered(stableID: "gw|abc", useTLS: true)
let defaults = UserDefaults.standard
#expect(defaults.object(forKey: "gateway.last.host") == nil)
#expect(defaults.object(forKey: "gateway.last.port") == nil)
#expect(GatewaySettingsStore.loadLastGatewayConnection() == .discovered(stableID: "gw|abc", useTLS: true))
}
}
@Test func lastGateway_migratesFromUserDefaults() {
withLastGatewaySnapshot {
// Clear Keychain entry and plant legacy UserDefaults values.
applyKeychain([lastGatewayKeychainEntry: nil])
@Test func lastGateway_backCompat_manualLoadsWhenKindMissing() {
withLastGatewayDefaultsSnapshot {
applyDefaults([
"gateway.last.kind": nil,
"gateway.last.host": "example.org",
@@ -180,11 +179,6 @@ private func withLastGatewaySnapshot(_ body: () -> Void) {
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "example.org", port: 18789, useTLS: false, stableID: "manual|example.org|18789"))
// Legacy keys should be cleaned up after migration.
let defaults = UserDefaults.standard
#expect(defaults.object(forKey: "gateway.last.stableID") == nil)
#expect(defaults.object(forKey: "gateway.last.host") == nil)
}
}

View File

@@ -416,20 +416,6 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
#expect(appModel.openChatRequestID == 1)
}
@Test @MainActor func handleDeepLinkCoalescesPromptWhenRateLimited() async throws {
let appModel = NodeAppModel()
appModel._test_setGatewayConnected(true)
await appModel.handleDeepLink(url: makeAgentDeepLinkURL(message: "first prompt"))
let firstPrompt = try #require(appModel.pendingAgentDeepLinkPrompt)
await appModel.handleDeepLink(url: makeAgentDeepLinkURL(message: "second prompt"))
let coalescedPrompt = try #require(appModel.pendingAgentDeepLinkPrompt)
#expect(coalescedPrompt.id != firstPrompt.id)
#expect(coalescedPrompt.messagePreview.contains("second prompt"))
}
@Test @MainActor func handleDeepLinkStripsDeliveryFieldsWhenUnkeyed() async throws {
let appModel = NodeAppModel()
appModel._test_setGatewayConnected(true)

View File

@@ -1,4 +1,3 @@
import Foundation
import Testing
@testable import OpenClaw
@@ -29,22 +28,4 @@ import Testing
let selection = TalkModeManager.selectTalkProviderConfig(talk)
#expect(selection == nil)
}
@Test func detectsPCMFormatRejectionFromElevenLabsError() {
let error = NSError(
domain: "ElevenLabsTTS",
code: 403,
userInfo: [
NSLocalizedDescriptionKey: "ElevenLabs failed: 403 subscription_required output_format=pcm_44100",
])
#expect(TalkModeManager._test_isPCMFormatRejectedByAPI(error))
}
@Test func ignoresGenericPlaybackFailuresForPCMFormatRejection() {
let error = NSError(
domain: "StreamingAudio",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "queue enqueue failed"])
#expect(TalkModeManager._test_isPCMFormatRejectedByAPI(error) == false)
}
}

View File

@@ -1,28 +0,0 @@
import Testing
@testable import OpenClaw
@MainActor
@Suite struct TalkModeIncrementalSpeechBufferTests {
@Test func emitsSoftBoundaryBeforeTerminalPunctuation() {
let manager = TalkModeManager(allowSimulatorCapture: true)
manager._test_incrementalReset()
let partial =
"We start speaking earlier by splitting this long stream chunk at a whitespace boundary before punctuation arrives"
let segments = manager._test_incrementalIngest(partial, isFinal: false)
#expect(segments.count == 1)
#expect(segments[0].count >= 72)
#expect(segments[0].count < partial.count)
}
@Test func keepsShortChunkBufferedWithoutPunctuation() {
let manager = TalkModeManager(allowSimulatorCapture: true)
manager._test_incrementalReset()
let short = "short chunk without punctuation"
let segments = manager._test_incrementalIngest(short, isFinal: false)
#expect(segments.isEmpty)
}
}

View File

@@ -38,8 +38,6 @@ targets:
dependencies:
- target: OpenClawShareExtension
embed: true
- target: OpenClawActivityWidget
embed: true
- target: OpenClawWatchApp
- package: OpenClawKit
- package: OpenClawKit
@@ -86,7 +84,6 @@ targets:
TARGETED_DEVICE_FAMILY: "1"
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
SUPPORTS_LIVE_ACTIVITIES: YES
ENABLE_APPINTENTS_METADATA: NO
ENABLE_APP_INTENTS_METADATA_GENERATION: NO
info:
@@ -118,7 +115,6 @@ targets:
NSLocationAlwaysAndWhenInUseUsageDescription: OpenClaw can share your location in the background when you enable Always.
NSMicrophoneUsageDescription: OpenClaw needs microphone access for voice wake.
NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for voice wake.
NSSupportsLiveActivities: true
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
@@ -168,37 +164,6 @@ targets:
NSExtensionActivationSupportsImageWithMaxCount: 10
NSExtensionActivationSupportsMovieWithMaxCount: 1
OpenClawActivityWidget:
type: app-extension
platform: iOS
configFiles:
Debug: Signing.xcconfig
Release: Signing.xcconfig
sources:
- path: ActivityWidget
- path: Sources/LiveActivity/OpenClawActivityAttributes.swift
dependencies:
- sdk: WidgetKit.framework
- sdk: ActivityKit.framework
settings:
base:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID)"
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
SUPPORTS_LIVE_ACTIVITIES: YES
info:
path: ActivityWidget/Info.plist
properties:
CFBundleDisplayName: OpenClaw Activity
CFBundleShortVersionString: "2026.3.2"
CFBundleVersion: "20260301"
NSSupportsLiveActivities: true
NSExtension:
NSExtensionPointIdentifier: com.apple.widgetkit-extension
OpenClawWatchApp:
type: application.watchapp2
platform: watchOS

View File

@@ -164,7 +164,7 @@ actor CameraCaptureService {
}
private func ensureAccess(for mediaType: AVMediaType) async throws {
if await !(CameraAuthorization.isAuthorized(for: mediaType)) {
if !(await CameraAuthorization.isAuthorized(for: mediaType)) {
throw CameraError.permissionDenied(kind: mediaType == .video ? "Camera" : "Microphone")
}
}

View File

@@ -9,4 +9,5 @@ final class CanvasFileWatcher: @unchecked Sendable, SimpleFileWatcherOwner {
queueLabel: "ai.openclaw.canvaswatcher",
onChange: onChange))
}
}

View File

@@ -25,22 +25,11 @@ extension CanvasWindowController {
}
static func _testParseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count == 4 else { return nil }
let bytes: [UInt8] = parts.compactMap { UInt8($0) }
guard bytes.count == 4 else { return nil }
return (bytes[0], bytes[1], bytes[2], bytes[3])
LoopbackHost.parseIPv4(host)
}
static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
let (a, b, _, _) = ip
if a == 10 { return true }
if a == 172, (16...31).contains(Int(b)) { return true }
if a == 192, b == 168 { return true }
if a == 127 { return true }
if a == 169, b == 254 { return true }
if a == 100, (64...127).contains(Int(b)) { return true }
return false
LoopbackHost.isLocalNetworkIPv4(ip)
}
static func _testIsLocalNetworkCanvasURL(_ url: URL) -> Bool {

View File

@@ -30,4 +30,5 @@ final class ConfigFileWatcher: @unchecked Sendable, SimpleFileWatcherOwner {
},
onChange: onChange))
}
}

View File

@@ -124,12 +124,12 @@ enum ExecSystemRunCommandValidator {
let lower = token.lowercased()
let flag = lower.split(separator: "=", maxSplits: 1).first.map(String.init) ?? lower
if ExecEnvOptions.flagOnly.contains(flag) {
if ExecEnvOptions.flagOnly.contains(flag) {
usesModifiers = true
idx += 1
continue
}
if ExecEnvOptions.withValue.contains(flag) {
if ExecEnvOptions.withValue.contains(flag) {
usesModifiers = true
if !lower.contains("=") {
expectsOptionValue = true

View File

@@ -39,12 +39,11 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate, Locatio
desiredAccuracy: desiredAccuracy,
maxAgeMs: maxAgeMs,
timeoutMs: timeoutMs,
request: { try await self.requestLocationOnce() },
withTimeout: { timeoutMs, operation in
request: { try await self.requestLocationOnce() }) { timeoutMs, operation in
try await self.withTimeout(timeoutMs: timeoutMs) {
try await operation()
}
})
}
}
private func withTimeout<T: Sendable>(

View File

@@ -184,9 +184,9 @@ struct NodeMenuRowView: View {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(NodeMenuEntryFormatter.primaryName(self.entry))
.font(.callout.weight(NodeMenuEntryFormatter.isConnected(self.entry) ? .semibold : .regular))
.foregroundStyle(self.palette.primary)
Text(NodeMenuEntryFormatter.primaryName(self.entry))
.font(.callout.weight(NodeMenuEntryFormatter.isConnected(self.entry) ? .semibold : .regular))
.foregroundStyle(self.palette.primary)
.lineLimit(1)
.truncationMode(.middle)
.layoutPriority(1)
@@ -195,9 +195,9 @@ struct NodeMenuRowView: View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
if let right = NodeMenuEntryFormatter.headlineRight(self.entry) {
Text(right)
.font(.caption.monospacedDigit())
.foregroundStyle(self.palette.secondary)
Text(right)
.font(.caption.monospacedDigit())
.foregroundStyle(self.palette.secondary)
.lineLimit(1)
.truncationMode(.middle)
.layoutPriority(2)

View File

@@ -64,10 +64,9 @@ final class NotifyOverlayController {
OverlayPanelFactory.present(
window: self.window,
isVisible: &self.model.isVisible,
target: target)
{ window in
self.updateWindowFrame(animate: true)
window.orderFrontRegardless()
target: target) { window in
self.updateWindowFrame(animate: true)
window.orderFrontRegardless()
}
}

View File

@@ -134,10 +134,10 @@ extension OnboardingView {
if self.gatewayDiscovery.gateways.isEmpty {
ProgressView().controlSize(.small)
Button("Refresh") {
self.gatewayDiscovery.refreshRemoteFallbackNow(timeoutSeconds: 5.0)
self.gatewayDiscovery.refreshWideAreaFallbackNow(timeoutSeconds: 5.0)
}
.buttonStyle(.link)
.help("Retry remote discovery (Tailscale DNS-SD + Serve probe).")
.help("Retry Tailscale discovery (DNS-SD).")
}
Spacer(minLength: 0)
}
@@ -311,7 +311,7 @@ extension OnboardingView {
.font(.caption.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
.truncationMode(.middle)
}
}
Spacer(minLength: 0)

View File

@@ -134,8 +134,7 @@ enum PairingAlertSupport {
messageText: String,
informativeText: String,
alertHostWindow: inout NSWindow?,
completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) -> NSAlert
{
completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) -> NSAlert {
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
@@ -170,22 +169,49 @@ enum PairingAlertSupport {
requestId: String,
messageText: String,
informativeText: String,
state: PairingAlertState,
activeAlert: inout NSAlert?,
activeRequestId: inout String?,
alertHostWindow: inout NSWindow?,
clearActive: @escaping @MainActor (NSWindow) -> Void,
onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void)
{
self.presentPairingAlert(
requestId: requestId,
messageText: messageText,
informativeText: informativeText,
activeAlert: &activeAlert,
activeRequestId: &activeRequestId,
alertHostWindow: &alertHostWindow)
{ response, hostWindow in
Task { @MainActor in
clearActive(hostWindow)
await onResponse(response, request)
}
}
}
static func presentPairingAlert<Request>(
request: Request,
requestId: String,
messageText: String,
informativeText: String,
state: PairingAlertState,
onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void)
{
self.presentPairingAlert(
request: request,
requestId: requestId,
messageText: messageText,
informativeText: informativeText,
activeAlert: &state.activeAlert,
activeRequestId: &state.activeRequestId,
alertHostWindow: &state.alertHostWindow,
completion: { response, hostWindow in
Task { @MainActor in
self.clearActivePairingAlert(state: state, hostWindow: hostWindow)
await onResponse(response, request)
}
})
alertHostWindow: &state.alertHostWindow)
{ response, hostWindow in
Task { @MainActor in
self.clearActivePairingAlert(state: state, hostWindow: hostWindow)
await onResponse(response, request)
}
}
}
static func clearActivePairingAlert(
@@ -205,12 +231,12 @@ enum PairingAlertSupport {
hostWindow: hostWindow)
}
static func stopPairingPrompter(
static func stopPairingPrompter<Request>(
isStopping: inout Bool,
activeAlert: inout NSAlert?,
activeRequestId: inout String?,
task: inout Task<Void, Never>?,
queue: inout [some Any],
queue: inout [Request],
isPresenting: inout Bool,
alertHostWindow: inout NSWindow?)
{
@@ -226,10 +252,10 @@ enum PairingAlertSupport {
alertHostWindow = nil
}
static func stopPairingPrompter(
static func stopPairingPrompter<Request>(
isStopping: inout Bool,
task: inout Task<Void, Never>?,
queue: inout [some Any],
queue: inout [Request],
isPresenting: inout Bool,
state: PairingAlertState)
{

View File

@@ -36,7 +36,6 @@ final class PeekabooBridgeHostCoordinator {
?? fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support")
return Self.legacySocketDirectoryNames.map { Self.makeSocketPath(for: $0, in: base) }
}
func setEnabled(_ enabled: Bool) async {
if enabled {
await self.startIfNeeded()
@@ -86,7 +85,7 @@ final class PeekabooBridgeHostCoordinator {
}
private func ensureLegacySocketSymlinks() {
for legacyPath in Self.legacySocketPaths {
Self.legacySocketPaths.forEach { legacyPath in
self.ensureLegacySocketSymlink(at: legacyPath)
}
}
@@ -117,9 +116,7 @@ final class PeekabooBridgeHostCoordinator {
}
try fileManager.createSymbolicLink(atPath: legacyPath, withDestinationPath: Self.openclawSocketPath)
} catch {
let message = "Failed to create legacy PeekabooBridge socket symlink: \(error.localizedDescription)"
self.logger
.debug("\(message, privacy: .public)")
self.logger.debug("Failed to create legacy PeekabooBridge socket symlink: \(error.localizedDescription, privacy: .public)")
}
}

View File

@@ -33,10 +33,9 @@ final class TalkOverlayController {
OverlayPanelFactory.present(
window: self.window,
isVisible: &self.model.isVisible,
target: target)
{ window in
window.setFrame(target, display: true)
window.orderFrontRegardless()
target: target) { window in
window.setFrame(target, display: true)
window.orderFrontRegardless()
}
}

View File

@@ -77,14 +77,14 @@ struct GatewayCostUsageDay: Codable {
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.date = try c.decode(String.self, forKey: .date)
self.totals = try GatewayCostUsageTotals(
input: c.decode(Int.self, forKey: .input),
output: c.decode(Int.self, forKey: .output),
cacheRead: c.decode(Int.self, forKey: .cacheRead),
cacheWrite: c.decode(Int.self, forKey: .cacheWrite),
totalTokens: c.decode(Int.self, forKey: .totalTokens),
totalCost: c.decode(Double.self, forKey: .totalCost),
missingCostEntries: c.decode(Int.self, forKey: .missingCostEntries))
self.totals = GatewayCostUsageTotals(
input: try c.decode(Int.self, forKey: .input),
output: try c.decode(Int.self, forKey: .output),
cacheRead: try c.decode(Int.self, forKey: .cacheRead),
cacheWrite: try c.decode(Int.self, forKey: .cacheWrite),
totalTokens: try c.decode(Int.self, forKey: .totalTokens),
totalCost: try c.decode(Double.self, forKey: .totalCost),
missingCostEntries: try c.decode(Int.self, forKey: .missingCostEntries))
}
func encode(to encoder: Encoder) throws {

View File

@@ -172,9 +172,9 @@ actor VoicePushToTalk {
let adoptedPrefix = self.adoptedPrefix
let adoptedAttributed: NSAttributedString? = adoptedPrefix.isEmpty ? nil : VoiceOverlayTextFormatting
.makeAttributed(
committed: adoptedPrefix,
volatile: "",
isFinal: false)
committed: adoptedPrefix,
volatile: "",
isFinal: false)
self.overlayToken = await MainActor.run {
VoiceSessionCoordinator.shared.startSession(
source: .pushToTalk,
@@ -406,4 +406,5 @@ actor VoicePushToTalk {
if suffix.isEmpty { return prefix }
return "\(prefix) \(suffix)"
}
}

View File

@@ -23,11 +23,10 @@ extension VoiceWakeOverlayController {
"overlay present windowShown textLen=\(self.model.text.count, privacy: .public)")
// Keep the status item in listening mode until we explicitly dismiss the overlay.
AppStateStore.shared.triggerVoiceEars(ttl: nil)
},
onAlreadyVisible: { window in
}) { window in
self.updateWindowFrame(animate: true)
window.orderFrontRegardless()
})
}
}
private func ensureWindow() {

View File

@@ -773,4 +773,5 @@ actor VoiceWakeRuntime {
}
#endif
}

View File

@@ -76,8 +76,6 @@ public final class GatewayDiscoveryModel {
private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:]
private var wideAreaFallbackTask: Task<Void, Never>?
private var wideAreaFallbackGateways: [DiscoveredGateway] = []
private var tailscaleServeFallbackTask: Task<Void, Never>?
private var tailscaleServeFallbackGateways: [DiscoveredGateway] = []
private let logger = Logger(subsystem: "ai.openclaw", category: "gateway-discovery")
public init(
@@ -113,7 +111,6 @@ public final class GatewayDiscoveryModel {
}
self.scheduleWideAreaFallback()
self.scheduleTailscaleServeFallback()
}
public func refreshWideAreaFallbackNow(timeoutSeconds: TimeInterval = 5.0) {
@@ -129,23 +126,6 @@ public final class GatewayDiscoveryModel {
}
}
public func refreshTailscaleServeFallbackNow(timeoutSeconds: TimeInterval = 5.0) {
Task.detached(priority: .utility) { [weak self] in
guard let self else { return }
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: timeoutSeconds)
await MainActor.run { [weak self] in
guard let self else { return }
self.tailscaleServeFallbackGateways = self.mapTailscaleServeBeacons(beacons)
self.recomputeGateways()
}
}
}
public func refreshRemoteFallbackNow(timeoutSeconds: TimeInterval = 5.0) {
self.refreshWideAreaFallbackNow(timeoutSeconds: timeoutSeconds)
self.refreshTailscaleServeFallbackNow(timeoutSeconds: timeoutSeconds)
}
public func stop() {
for browser in self.browsers.values {
browser.cancel()
@@ -160,9 +140,6 @@ public final class GatewayDiscoveryModel {
self.wideAreaFallbackTask?.cancel()
self.wideAreaFallbackTask = nil
self.wideAreaFallbackGateways = []
self.tailscaleServeFallbackTask?.cancel()
self.tailscaleServeFallbackTask = nil
self.tailscaleServeFallbackGateways = []
self.gateways = []
self.statusText = "Stopped"
}
@@ -191,45 +168,22 @@ public final class GatewayDiscoveryModel {
}
}
private func mapTailscaleServeBeacons(
_ beacons: [TailscaleServeGatewayBeacon]) -> [DiscoveredGateway]
{
beacons.map { beacon in
let stableID = "tailscale-serve|\(beacon.tailnetDns.lowercased())"
let isLocal = Self.isLocalGateway(
lanHost: nil,
tailnetDns: beacon.tailnetDns,
displayName: beacon.displayName,
serviceName: nil,
local: self.localIdentity)
return DiscoveredGateway(
displayName: beacon.displayName,
serviceHost: beacon.host,
servicePort: beacon.port,
lanHost: nil,
tailnetDns: beacon.tailnetDns,
sshPort: 22,
gatewayPort: beacon.port,
cliPath: nil,
stableID: stableID,
debugID: "\(beacon.host):\(beacon.port)",
isLocal: isLocal)
}
}
private func recomputeGateways() {
let primary = self.sortedDeduped(gateways: self.gatewaysByDomain.values.flatMap(\.self))
let primaryFiltered = self.filterLocalGateways ? primary.filter { !$0.isLocal } : primary
// Bonjour can return only "local" results for the wide-area domain (or no results at all),
// and cross-network setups may rely on Tailscale Serve without DNS-SD.
let fallback = self.wideAreaFallbackGateways + self.tailscaleServeFallbackGateways
guard !fallback.isEmpty else {
if !primaryFiltered.isEmpty {
self.gateways = primaryFiltered
return
}
let combined = self.sortedDeduped(gateways: primary + fallback)
// Bonjour can return only "local" results for the wide-area domain (or no results at all),
// which makes onboarding look empty even though Tailscale DNS-SD can already see gateways.
guard !self.wideAreaFallbackGateways.isEmpty else {
self.gateways = primaryFiltered
return
}
let combined = self.sortedDeduped(gateways: primary + self.wideAreaFallbackGateways)
self.gateways = self.filterLocalGateways ? combined.filter { !$0.isLocal } : combined
}
@@ -330,39 +284,6 @@ public final class GatewayDiscoveryModel {
}
}
private func scheduleTailscaleServeFallback() {
if Self.isRunningTests { return }
guard self.tailscaleServeFallbackTask == nil else { return }
self.tailscaleServeFallbackTask = Task.detached(priority: .utility) { [weak self] in
guard let self else { return }
var attempt = 0
let startedAt = Date()
while !Task.isCancelled, Date().timeIntervalSince(startedAt) < 35.0 {
let hasResults = await MainActor.run {
if self.filterLocalGateways {
return !self.gateways.isEmpty
}
return self.gateways.contains(where: { !$0.isLocal })
}
if hasResults { return }
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: 2.4)
if !beacons.isEmpty {
await MainActor.run { [weak self] in
guard let self else { return }
self.tailscaleServeFallbackGateways = self.mapTailscaleServeBeacons(beacons)
self.recomputeGateways()
}
return
}
attempt += 1
let backoff = min(8.0, 0.8 + (Double(attempt) * 0.8))
try? await Task.sleep(nanoseconds: UInt64(backoff * 1_000_000_000))
}
}
}
private var hasUsableWideAreaResults: Bool {
guard let domain = OpenClawBonjour.wideAreaGatewayServiceDomain else { return false }
guard let gateways = self.gatewaysByDomain[domain], !gateways.isEmpty else { return false }
@@ -370,25 +291,11 @@ public final class GatewayDiscoveryModel {
return gateways.contains(where: { !$0.isLocal })
}
static func dedupeKey(for gateway: DiscoveredGateway) -> String {
if let host = gateway.serviceHost?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!host.isEmpty,
let port = gateway.servicePort,
port > 0
{
return "endpoint|\(host):\(port)"
}
return "stable|\(gateway.stableID)"
}
private func sortedDeduped(gateways: [DiscoveredGateway]) -> [DiscoveredGateway] {
var seen = Set<String>()
let deduped = gateways.filter { gateway in
let key = Self.dedupeKey(for: gateway)
if seen.contains(key) { return false }
seen.insert(key)
if seen.contains(gateway.stableID) { return false }
seen.insert(gateway.stableID)
return true
}
return deduped.sorted {

View File

@@ -13,8 +13,8 @@ public enum TailscaleNetwork {
}
public static func detectTailnetIPv4() -> String? {
for entry in NetworkInterfaceIPv4.addresses() where self.isTailnetIPv4(entry.ip) {
return entry.ip
for entry in NetworkInterfaceIPv4.addresses() {
if self.isTailnetIPv4(entry.ip) { return entry.ip }
}
return nil
}

View File

@@ -1,315 +0,0 @@
import Foundation
import OpenClawKit
struct TailscaleServeGatewayBeacon: Sendable, Equatable {
var displayName: String
var tailnetDns: String
var host: String
var port: Int
}
enum TailscaleServeGatewayDiscovery {
private static let maxCandidates = 32
private static let probeConcurrency = 6
private static let defaultProbeTimeoutSeconds: TimeInterval = 1.6
struct DiscoveryContext: Sendable {
var tailscaleStatus: @Sendable () async -> String?
var probeHost: @Sendable (_ host: String, _ timeout: TimeInterval) async -> Bool
static let live = DiscoveryContext(
tailscaleStatus: { await readTailscaleStatus() },
probeHost: { host, timeout in
await probeHostForGatewayChallenge(host: host, timeout: timeout)
})
}
static func discover(
timeoutSeconds: TimeInterval = 3.0,
context: DiscoveryContext = .live) async -> [TailscaleServeGatewayBeacon]
{
guard timeoutSeconds > 0 else { return [] }
guard let statusJson = await context.tailscaleStatus(),
let status = parseStatus(statusJson)
else {
return []
}
let candidates = self.collectCandidates(status: status)
if candidates.isEmpty { return [] }
let deadline = Date().addingTimeInterval(timeoutSeconds)
let perProbeTimeout = min(self.defaultProbeTimeoutSeconds, max(0.5, timeoutSeconds * 0.45))
var byHost: [String: TailscaleServeGatewayBeacon] = [:]
await withTaskGroup(of: TailscaleServeGatewayBeacon?.self) { group in
var index = 0
let workerCount = min(self.probeConcurrency, candidates.count)
func submitOne() {
guard index < candidates.count else { return }
let candidate = candidates[index]
index += 1
group.addTask {
let remaining = deadline.timeIntervalSinceNow
if remaining <= 0 {
return nil
}
let timeout = min(perProbeTimeout, remaining)
let reachable = await context.probeHost(candidate.dnsName, timeout)
if !reachable {
return nil
}
return TailscaleServeGatewayBeacon(
displayName: candidate.displayName,
tailnetDns: candidate.dnsName,
host: candidate.dnsName,
port: 443)
}
}
for _ in 0..<workerCount {
submitOne()
}
while let beacon = await group.next() {
if let beacon {
byHost[beacon.host.lowercased()] = beacon
}
submitOne()
}
}
return byHost.values.sorted {
$0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
}
}
private struct Candidate: Sendable {
var dnsName: String
var displayName: String
}
private static func collectCandidates(status: TailscaleStatus) -> [Candidate] {
let selfDns = normalizeDnsName(status.selfNode?.dnsName)
var out: [Candidate] = []
var seen = Set<String>()
for node in status.peer.values {
if node.online == false {
continue
}
guard let dnsName = normalizeDnsName(node.dnsName) else {
continue
}
if dnsName == selfDns {
continue
}
if seen.contains(dnsName) {
continue
}
seen.insert(dnsName)
out.append(Candidate(
dnsName: dnsName,
displayName: displayName(hostName: node.hostName, dnsName: dnsName)))
if out.count >= self.maxCandidates {
break
}
}
return out
}
private static func displayName(hostName: String?, dnsName: String) -> String {
if let hostName {
let trimmed = hostName.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
return trimmed
}
}
return dnsName
.split(separator: ".")
.first
.map(String.init) ?? dnsName
}
private static func normalizeDnsName(_ raw: String?) -> String? {
guard let raw else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return nil }
let withoutDot = trimmed.hasSuffix(".") ? String(trimmed.dropLast()) : trimmed
let lower = withoutDot.lowercased()
return lower.isEmpty ? nil : lower
}
private static func readTailscaleStatus() async -> String? {
let candidates = [
"/usr/local/bin/tailscale",
"/opt/homebrew/bin/tailscale",
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
"tailscale",
]
for candidate in candidates {
guard let executable = self.resolveExecutablePath(candidate) else { continue }
if let stdout = await self.run(path: executable, args: ["status", "--json"], timeout: 1.0) {
return stdout
}
}
return nil
}
static func resolveExecutablePath(
_ candidate: String,
env: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
let trimmed = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let fileManager = FileManager.default
let hasPathSeparator = trimmed.contains("/")
if hasPathSeparator {
return fileManager.isExecutableFile(atPath: trimmed) ? trimmed : nil
}
let pathRaw = env["PATH"] ?? ""
let entries = pathRaw.split(separator: ":").map(String.init)
for entry in entries {
let dir = entry.trimmingCharacters(in: .whitespacesAndNewlines)
if dir.isEmpty { continue }
let fullPath = URL(fileURLWithPath: dir)
.appendingPathComponent(trimmed)
.path
if fileManager.isExecutableFile(atPath: fullPath) {
return fullPath
}
}
return nil
}
private static func run(path: String, args: [String], timeout: TimeInterval) async -> String? {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .utility).async {
continuation.resume(returning: self.runBlocking(path: path, args: args, timeout: timeout))
}
}
}
private static func runBlocking(path: String, args: [String], timeout: TimeInterval) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: path)
process.arguments = args
let outPipe = Pipe()
process.standardOutput = outPipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
} catch {
return nil
}
let deadline = Date().addingTimeInterval(timeout)
while process.isRunning, Date() < deadline {
Thread.sleep(forTimeInterval: 0.02)
}
if process.isRunning {
process.terminate()
}
process.waitUntilExit()
let data = (try? outPipe.fileHandleForReading.readToEnd()) ?? Data()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
return output?.isEmpty == false ? output : nil
}
private static func parseStatus(_ raw: String) -> TailscaleStatus? {
guard let data = raw.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(TailscaleStatus.self, from: data)
}
private static func probeHostForGatewayChallenge(host: String, timeout: TimeInterval) async -> Bool {
var components = URLComponents()
components.scheme = "wss"
components.host = host
guard let url = components.url else { return false }
let config = URLSessionConfiguration.ephemeral
config.timeoutIntervalForRequest = max(0.5, timeout)
config.timeoutIntervalForResource = max(0.5, timeout)
let session = URLSession(configuration: config)
let task = session.webSocketTask(with: url)
task.resume()
defer {
task.cancel(with: .goingAway, reason: nil)
session.invalidateAndCancel()
}
do {
return try await AsyncTimeout.withTimeout(
seconds: timeout,
onTimeout: { NSError(domain: "TailscaleServeDiscovery", code: 1, userInfo: nil) },
operation: {
while true {
let message = try await task.receive()
if isConnectChallenge(message: message) {
return true
}
}
})
} catch {
return false
}
}
private static func isConnectChallenge(message: URLSessionWebSocketTask.Message) -> Bool {
let data: Data
switch message {
case let .data(value):
data = value
case let .string(value):
guard let encoded = value.data(using: .utf8) else { return false }
data = encoded
@unknown default:
return false
}
guard let object = try? JSONSerialization.jsonObject(with: data),
let dict = object as? [String: Any],
let type = dict["type"] as? String,
type == "event",
let event = dict["event"] as? String
else {
return false
}
return event == "connect.challenge"
}
}
private struct TailscaleStatus: Decodable {
struct Node: Decodable {
let dnsName: String?
let hostName: String?
let online: Bool?
private enum CodingKeys: String, CodingKey {
case dnsName = "DNSName"
case hostName = "HostName"
case online = "Online"
}
}
let selfNode: Node?
let peer: [String: Node]
private enum CodingKeys: String, CodingKey {
case selfNode = "Self"
case peer = "Peer"
}
}

View File

@@ -1030,74 +1030,6 @@ public struct PushTestResult: Codable, Sendable {
}
}
public struct SecretsReloadParams: Codable, Sendable {}
public struct SecretsResolveParams: Codable, Sendable {
public let commandname: String
public let targetids: [String]
public init(
commandname: String,
targetids: [String])
{
self.commandname = commandname
self.targetids = targetids
}
private enum CodingKeys: String, CodingKey {
case commandname = "commandName"
case targetids = "targetIds"
}
}
public struct SecretsResolveAssignment: Codable, Sendable {
public let path: String?
public let pathsegments: [String]
public let value: AnyCodable
public init(
path: String?,
pathsegments: [String],
value: AnyCodable)
{
self.path = path
self.pathsegments = pathsegments
self.value = value
}
private enum CodingKeys: String, CodingKey {
case path
case pathsegments = "pathSegments"
case value
}
}
public struct SecretsResolveResult: Codable, Sendable {
public let ok: Bool?
public let assignments: [SecretsResolveAssignment]?
public let diagnostics: [String]?
public let inactiverefpaths: [String]?
public init(
ok: Bool?,
assignments: [SecretsResolveAssignment]?,
diagnostics: [String]?,
inactiverefpaths: [String]?)
{
self.ok = ok
self.assignments = assignments
self.diagnostics = diagnostics
self.inactiverefpaths = inactiverefpaths
}
private enum CodingKeys: String, CodingKey {
case ok
case assignments
case diagnostics
case inactiverefpaths = "inactiveRefPaths"
}
}
public struct SessionsListParams: Codable, Sendable {
public let limit: Int?
public let activeminutes: Int?

View File

@@ -1,4 +1,4 @@
@testable import OpenClawDiscovery
import OpenClawDiscovery
import Testing
@Suite
@@ -121,50 +121,4 @@ struct GatewayDiscoveryModelTests {
host: "studio.local",
port: 2201) == "peter@studio.local:2201")
}
@Test func dedupeKeyPrefersResolvedEndpointAcrossSources() {
let wideArea = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: "gateway-host.tailnet-example.ts.net",
servicePort: 443,
lanHost: nil,
tailnetDns: "gateway-host.tailnet-example.ts.net",
sshPort: 22,
gatewayPort: 443,
cliPath: nil,
stableID: "wide-area|openclaw.internal.|gateway-host",
debugID: "wide-area",
isLocal: false)
let serve = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: "gateway-host.tailnet-example.ts.net",
servicePort: 443,
lanHost: nil,
tailnetDns: "gateway-host.tailnet-example.ts.net",
sshPort: 22,
gatewayPort: 443,
cliPath: nil,
stableID: "tailscale-serve|gateway-host.tailnet-example.ts.net",
debugID: "serve",
isLocal: false)
#expect(GatewayDiscoveryModel.dedupeKey(for: wideArea) == GatewayDiscoveryModel.dedupeKey(for: serve))
}
@Test func dedupeKeyFallsBackToStableIDWithoutEndpoint() {
let unresolved = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: nil,
servicePort: nil,
lanHost: nil,
tailnetDns: "gateway-host.tailnet-example.ts.net",
sshPort: 22,
gatewayPort: nil,
cliPath: nil,
stableID: "tailscale-serve|gateway-host.tailnet-example.ts.net",
debugID: "serve",
isLocal: false)
#expect(GatewayDiscoveryModel.dedupeKey(for: unresolved) == "stable|tailscale-serve|gateway-host.tailnet-example.ts.net")
}
}

View File

@@ -1,77 +0,0 @@
import Foundation
import Testing
@testable import OpenClawDiscovery
@Suite
struct TailscaleServeGatewayDiscoveryTests {
@Test func discoversServeGatewayFromTailnetPeers() async {
let statusJson = """
{
"Self": {
"DNSName": "local-mac.tailnet-example.ts.net.",
"HostName": "local-mac",
"Online": true
},
"Peer": {
"peer-1": {
"DNSName": "gateway-host.tailnet-example.ts.net.",
"HostName": "gateway-host",
"Online": true
},
"peer-2": {
"DNSName": "offline.tailnet-example.ts.net.",
"HostName": "offline-box",
"Online": false
},
"peer-3": {
"DNSName": "local-mac.tailnet-example.ts.net.",
"HostName": "local-mac",
"Online": true
}
}
}
"""
let context = TailscaleServeGatewayDiscovery.DiscoveryContext(
tailscaleStatus: { statusJson },
probeHost: { host, _ in
host == "gateway-host.tailnet-example.ts.net"
})
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: 2.0, context: context)
#expect(beacons.count == 1)
#expect(beacons.first?.displayName == "gateway-host")
#expect(beacons.first?.tailnetDns == "gateway-host.tailnet-example.ts.net")
#expect(beacons.first?.host == "gateway-host.tailnet-example.ts.net")
#expect(beacons.first?.port == 443)
}
@Test func returnsEmptyWhenStatusUnavailable() async {
let context = TailscaleServeGatewayDiscovery.DiscoveryContext(
tailscaleStatus: { nil },
probeHost: { _, _ in true })
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: 2.0, context: context)
#expect(beacons.isEmpty)
}
@Test func resolvesBareExecutableFromPATH() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let executable = tempDir.appendingPathComponent("tailscale")
try "#!/bin/sh\necho ok\n".write(to: executable, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
let env: [String: String] = ["PATH": tempDir.path]
let resolved = TailscaleServeGatewayDiscovery.resolveExecutablePath("tailscale", env: env)
#expect(resolved == executable.path)
}
@Test func rejectsMissingExecutableCandidate() {
#expect(TailscaleServeGatewayDiscovery.resolveExecutablePath("", env: [:]) == nil)
#expect(TailscaleServeGatewayDiscovery.resolveExecutablePath("definitely-not-here", env: ["PATH": "/tmp"]) == nil)
}
}

View File

@@ -17,41 +17,23 @@ public struct GatewayTLSParams: Sendable {
}
public enum GatewayTLSStore {
private static let keychainService = "ai.openclaw.tls-pinning"
private static let suiteName = "ai.openclaw.shared"
private static let keyPrefix = "gateway.tls."
// Legacy UserDefaults location used before Keychain migration.
private static let legacySuiteName = "ai.openclaw.shared"
private static let legacyKeyPrefix = "gateway.tls."
private static var defaults: UserDefaults {
UserDefaults(suiteName: suiteName) ?? .standard
}
public static func loadFingerprint(stableID: String) -> String? {
self.migrateFromUserDefaultsIfNeeded(stableID: stableID)
let raw = GenericPasswordKeychainStore.loadString(service: self.keychainService, account: stableID)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let key = self.keyPrefix + stableID
let raw = self.defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines)
if raw?.isEmpty == false { return raw }
return nil
}
public static func saveFingerprint(_ value: String, stableID: String) {
_ = GenericPasswordKeychainStore.saveString(value, service: self.keychainService, account: stableID)
}
// MARK: - Migration
/// On first Keychain read for a given stableID, move any legacy UserDefaults
/// fingerprint into Keychain and remove the old entry.
private static func migrateFromUserDefaultsIfNeeded(stableID: String) {
guard let defaults = UserDefaults(suiteName: self.legacySuiteName) else { return }
let legacyKey = self.legacyKeyPrefix + stableID
guard let existing = defaults.string(forKey: legacyKey)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!existing.isEmpty
else { return }
if GenericPasswordKeychainStore.loadString(service: self.keychainService, account: stableID) == nil {
guard GenericPasswordKeychainStore.saveString(existing, service: self.keychainService, account: stableID) else {
return
}
}
defaults.removeObject(forKey: legacyKey)
let key = self.keyPrefix + stableID
self.defaults.set(value, forKey: key)
}
}

View File

@@ -1,77 +0,0 @@
import Foundation
import Security
public enum GenericPasswordKeychainStore {
public static func loadString(service: String, account: String) -> String? {
guard let data = self.loadData(service: service, account: account) else { return nil }
return String(data: data, encoding: .utf8)
}
@discardableResult
public static func saveString(
_ value: String,
service: String,
account: String,
accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
) -> Bool {
self.saveData(Data(value.utf8), service: service, account: account, accessible: accessible)
}
@discardableResult
public static func delete(service: String, account: String) -> Bool {
let query = self.baseQuery(service: service, account: account)
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
private static func loadData(service: String, account: String) -> Data? {
var query = self.baseQuery(service: service, account: account)
query[kSecReturnData as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitOne
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return data
}
@discardableResult
private static func saveData(
_ data: Data,
service: String,
account: String,
accessible: CFString
) -> Bool {
let query = self.baseQuery(service: service, account: account)
let previousData = self.loadData(service: service, account: account)
let deleteStatus = SecItemDelete(query as CFDictionary)
guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else {
return false
}
var insert = query
insert[kSecValueData as String] = data
insert[kSecAttrAccessible as String] = accessible
if SecItemAdd(insert as CFDictionary, nil) == errSecSuccess {
return true
}
// Best-effort rollback: preserve prior value if replacement fails.
guard let previousData else { return false }
var rollback = query
rollback[kSecValueData as String] = previousData
rollback[kSecAttrAccessible as String] = accessible
_ = SecItemDelete(query as CFDictionary)
_ = SecItemAdd(rollback as CFDictionary, nil)
return false
}
private static func baseQuery(service: String, account: String) -> [String: Any] {
[
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
]
}
}

View File

@@ -12,7 +12,6 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
private let synth = AVSpeechSynthesizer()
private var speakContinuation: CheckedContinuation<Void, Error>?
private var currentUtterance: AVSpeechUtterance?
private var didStartCallback: (() -> Void)?
private var currentToken = UUID()
private var watchdog: Task<Void, Never>?
@@ -27,23 +26,17 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
self.currentToken = UUID()
self.watchdog?.cancel()
self.watchdog = nil
self.didStartCallback = nil
self.synth.stopSpeaking(at: .immediate)
self.finishCurrent(with: SpeakError.canceled)
}
public func speak(
text: String,
language: String? = nil,
onStart: (() -> Void)? = nil
) async throws {
public func speak(text: String, language: String? = nil) async throws {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.stop()
let token = UUID()
self.currentToken = token
self.didStartCallback = onStart
let utterance = AVSpeechUtterance(string: trimmed)
if let language, let voice = AVSpeechSynthesisVoice(language: language) {
@@ -83,13 +76,8 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
}
}
private func matchesCurrentUtterance(_ utteranceID: ObjectIdentifier) -> Bool {
guard let currentUtterance = self.currentUtterance else { return false }
return ObjectIdentifier(currentUtterance) == utteranceID
}
private func handleFinish(utteranceID: ObjectIdentifier, error: Error?) {
guard self.matchesCurrentUtterance(utteranceID) else { return }
private func handleFinish(error: Error?) {
guard self.currentUtterance != nil else { return }
self.watchdog?.cancel()
self.watchdog = nil
self.finishCurrent(with: error)
@@ -97,7 +85,6 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
private func finishCurrent(with error: Error?) {
self.currentUtterance = nil
self.didStartCallback = nil
let cont = self.speakContinuation
self.speakContinuation = nil
if let error {
@@ -109,26 +96,12 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
}
extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate {
public nonisolated func speechSynthesizer(
_ synthesizer: AVSpeechSynthesizer,
didStart utterance: AVSpeechUtterance)
{
let utteranceID = ObjectIdentifier(utterance)
Task { @MainActor in
guard self.matchesCurrentUtterance(utteranceID) else { return }
let callback = self.didStartCallback
self.didStartCallback = nil
callback?()
}
}
public nonisolated func speechSynthesizer(
_ synthesizer: AVSpeechSynthesizer,
didFinish utterance: AVSpeechUtterance)
{
let utteranceID = ObjectIdentifier(utterance)
Task { @MainActor in
self.handleFinish(utteranceID: utteranceID, error: nil)
self.handleFinish(error: nil)
}
}
@@ -136,9 +109,8 @@ extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate {
_ synthesizer: AVSpeechSynthesizer,
didCancel utterance: AVSpeechUtterance)
{
let utteranceID = ObjectIdentifier(utterance)
Task { @MainActor in
self.handleFinish(utteranceID: utteranceID, error: SpeakError.canceled)
self.handleFinish(error: SpeakError.canceled)
}
}
}

View File

@@ -1030,74 +1030,6 @@ public struct PushTestResult: Codable, Sendable {
}
}
public struct SecretsReloadParams: Codable, Sendable {}
public struct SecretsResolveParams: Codable, Sendable {
public let commandname: String
public let targetids: [String]
public init(
commandname: String,
targetids: [String])
{
self.commandname = commandname
self.targetids = targetids
}
private enum CodingKeys: String, CodingKey {
case commandname = "commandName"
case targetids = "targetIds"
}
}
public struct SecretsResolveAssignment: Codable, Sendable {
public let path: String?
public let pathsegments: [String]
public let value: AnyCodable
public init(
path: String?,
pathsegments: [String],
value: AnyCodable)
{
self.path = path
self.pathsegments = pathsegments
self.value = value
}
private enum CodingKeys: String, CodingKey {
case path
case pathsegments = "pathSegments"
case value
}
}
public struct SecretsResolveResult: Codable, Sendable {
public let ok: Bool?
public let assignments: [SecretsResolveAssignment]?
public let diagnostics: [String]?
public let inactiverefpaths: [String]?
public init(
ok: Bool?,
assignments: [SecretsResolveAssignment]?,
diagnostics: [String]?,
inactiverefpaths: [String]?)
{
self.ok = ok
self.assignments = assignments
self.diagnostics = diagnostics
self.inactiverefpaths = inactiverefpaths
}
private enum CodingKeys: String, CodingKey {
case ok
case assignments
case diagnostics
case inactiverefpaths = "inactiveRefPaths"
}
}
public struct SessionsListParams: Codable, Sendable {
public let limit: Int?
public let activeminutes: Int?

View File

@@ -0,0 +1,13 @@
# Changelog Fragments
Use this directory when a PR should not edit `CHANGELOG.md` directly.
- One fragment file per PR.
- File name recommendation: `pr-<number>.md`.
- Include at least one line with both `#<pr>` and `thanks @<contributor>`.
Example:
```md
- Fix LINE monitor lifecycle wait ownership (#27001) (thanks @alice)
```

View File

@@ -0,0 +1 @@
- Tlon plugin: sync upstream account/settings workflows, restore SSRF-safe media + SSE fetch paths, and improve invite/approval handling reliability. (#21208) (thanks @arthyn)

View File

@@ -0,0 +1 @@
- Clarify block reply pipeline seen-check parameter naming for maintainability (#5080) (thanks @yassine20011)

View File

@@ -0,0 +1 @@
- Memory flush: fix usage-threshold gating and transcript fallback paths so flushes run reliably when expected (#5343) (thanks @jarvis-medmatic)

View File

@@ -1 +0,0 @@
- Feishu reply routing now uses one canonical reply-target path across inbound and outbound flows: normal groups reply to the triggering message while topic-mode groups stay on topic roots, outbound sends preserve `replyToId`/`threadId`, withdrawn reply targets fall back to direct sends, and cron duplicate suppression normalizes Feishu/Lark target IDs consistently (#32980, #32958, #33572, #33526; #33789, #33575, #33515, #33161). Thanks @guoqunabc, @bmendonca3, @MunemHashmi, and @Jimmy-xuzimo.

View File

@@ -1,45 +0,0 @@
# Auth Credential Semantics
This document defines the canonical credential eligibility and resolution semantics used across:
- `resolveAuthProfileOrder`
- `resolveApiKeyForProfile`
- `models status --probe`
- `doctor-auth`
The goal is to keep selection-time and runtime behavior aligned.
## Stable Reason Codes
- `ok`
- `missing_credential`
- `invalid_expires`
- `expired`
- `unresolved_ref`
## Token Credentials
Token credentials (`type: "token"`) support inline `token` and/or `tokenRef`.
### Eligibility rules
1. A token profile is ineligible when both `token` and `tokenRef` are absent.
2. `expires` is optional.
3. If `expires` is present, it must be a finite number greater than `0`.
4. If `expires` is invalid (`NaN`, `0`, negative, non-finite, or wrong type), the profile is ineligible with `invalid_expires`.
5. If `expires` is in the past, the profile is ineligible with `expired`.
6. `tokenRef` does not bypass `expires` validation.
### Resolution rules
1. Resolver semantics match eligibility semantics for `expires`.
2. For eligible profiles, token material may be resolved from inline value or `tokenRef`.
3. Unresolvable refs produce `unresolved_ref` in `models status --probe` output.
## Legacy-Compatible Messaging
For script compatibility, probe errors keep this first line unchanged:
`Auth profile credentials are missing or expired.`
Human-friendly detail and stable reason codes may be added on subsequent lines.

View File

@@ -8,7 +8,7 @@ title: "Brave Search"
# Brave Search API
OpenClaw supports Brave Search as a web search provider for `web_search`.
OpenClaw uses Brave Search as the default provider for `web_search`.
## Get an API key
@@ -33,48 +33,9 @@ OpenClaw supports Brave Search as a web search provider for `web_search`.
}
```
## Tool parameters
| Parameter | Description |
| ------------- | ------------------------------------------------------------------- |
| `query` | Search query (required) |
| `count` | Number of results to return (1-10, default: 5) |
| `country` | 2-letter ISO country code (e.g., "US", "DE") |
| `language` | ISO 639-1 language code for search results (e.g., "en", "de", "fr") |
| `ui_lang` | ISO language code for UI elements |
| `freshness` | Time filter: `day` (24h), `week`, `month`, or `year` |
| `date_after` | Only results published after this date (YYYY-MM-DD) |
| `date_before` | Only results published before this date (YYYY-MM-DD) |
**Examples:**
```javascript
// Country and language-specific search
await web_search({
query: "renewable energy",
country: "DE",
language: "de",
});
// Recent results (past week)
await web_search({
query: "AI news",
freshness: "week",
});
// Date range search
await web_search({
query: "AI developments",
date_after: "2024-01-01",
date_before: "2024-06-30",
});
```
## Notes
- The Data for AI plan is **not** compatible with `web_search`.
- Brave provides paid plans; check the Brave API portal for current limits.
- Brave Terms include restrictions on some AI-related uses of Search Results. Review the Brave Terms of Service and confirm your intended use is compliant. For legal questions, consult your counsel.
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`).
- Brave provides a free tier plus paid plans; check the Brave API portal for current limits.
See [Web tools](/tools/web) for the full web_search configuration.

View File

@@ -17,7 +17,6 @@ host configuration.
- **AccountId**: perchannel account instance (when supported).
- Optional channel default account: `channels.<channel>.defaultAccount` chooses
which account is used when an outbound path does not specify `accountId`.
- In multi-account setups, set an explicit default (`defaultAccount` or `accounts.default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID.
- **AgentId**: an isolated workspace + session store (“brain”).
- **SessionKey**: the bucket key used to store context and control concurrency.

View File

@@ -133,8 +133,6 @@ openclaw gateway
DISCORD_BOT_TOKEN=...
```
SecretRef values are also supported for `channels.discord.token` (env/file/exec providers). See [Secrets Management](/gateway/secrets).
</Tab>
</Tabs>
@@ -421,7 +419,6 @@ Example:
guilds: {
"123456789012345678": {
requireMention: true,
ignoreOtherMentions: true,
users: ["987654321098765432"],
roles: ["123456789012345678"],
channels: {
@@ -449,7 +446,6 @@ Example:
- implicit reply-to-bot behavior in supported cases
`requireMention` is configured per guild/channel (`channels.discord.guilds...`).
`ignoreOtherMentions` optionally drops messages that mention another user/role but not the bot (excluding @everyone/@here).
Group DMs:
@@ -790,7 +786,7 @@ Default slash command settings:
</Accordion>
<Accordion title="Presence configuration">
Presence updates are applied when you set a status or activity field, or when you enable auto presence.
Presence updates are applied only when you set a status or activity field.
Status only example:
@@ -840,29 +836,6 @@ Default slash command settings:
- 4: Custom (uses the activity text as the status state; emoji is optional)
- 5: Competing
Auto presence example (runtime health signal):
```json5
{
channels: {
discord: {
autoPresence: {
enabled: true,
intervalMs: 30000,
minUpdateIntervalMs: 15000,
exhaustedText: "token exhausted",
},
},
},
}
```
Auto presence maps runtime availability to Discord status: healthy => online, degraded or unknown => idle, exhausted or unavailable => dnd. Optional text overrides:
- `autoPresence.healthyText`
- `autoPresence.degradedText`
- `autoPresence.exhaustedText` (supports `{reason}` placeholder)
</Accordion>
<Accordion title="Exec approvals in Discord">
@@ -1084,7 +1057,6 @@ openclaw logs --follow
By default bot-authored messages are ignored.
If you set `channels.discord.allowBots=true`, use strict mention and allowlist rules to avoid loop behavior.
Prefer `channels.discord.allowBots="mentions"` to only accept bot messages that mention the bot.
</Accordion>

View File

@@ -197,17 +197,6 @@ Edit `~/.openclaw/openclaw.json`:
If you use `connectionMode: "webhook"`, set `verificationToken`. The Feishu webhook server binds to `127.0.0.1` by default; set `webhookHost` only if you intentionally need a different bind address.
#### Verification Token (webhook mode)
When using webhook mode, set `channels.feishu.verificationToken` in your config. To get the value:
1. In Feishu Open Platform, open your app
2. Go to **Development****Events & Callbacks** (开发配置 → 事件与回调)
3. Open the **Encryption** tab (加密策略)
4. Copy **Verification Token**
![Verification Token location](../images/feishu-verification-token.png)
### Configure via environment variables
```bash
@@ -370,9 +359,9 @@ After approval, you can chat normally.
}
```
### Restrict which senders can message in a group (sender allowlist)
### Allow specific users to run control commands in a group (e.g. /reset, /new)
In addition to allowing the group itself, **all messages** in that group are gated by the sender open_id: only users listed in `groups.<chat_id>.allowFrom` have their messages processed; messages from other members are ignored (this is full sender-level gating, not only for control commands like /reset or /new).
In addition to allowing the group itself, control commands are gated by the **sender** open_id.
```json5
{

View File

@@ -55,45 +55,6 @@ Minimal config:
}
```
## Native slash commands
Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via
the Mattermost API and receives callback POSTs on the gateway HTTP server.
```json5
{
channels: {
mattermost: {
commands: {
native: true,
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Use when Mattermost cannot reach the gateway directly (reverse proxy/public URL).
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
},
},
}
```
Notes:
- `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable.
- If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`.
- For multi-account setups, `commands` can be set at the top level or under
`channels.mattermost.accounts.<id>.commands` (account values override top-level fields).
- Command callbacks are validated with per-command tokens and fail closed when token checks fail.
- Reachability requirement: the callback endpoint must be reachable from the Mattermost server.
- Do not set `callbackUrl` to `localhost` unless Mattermost runs on the same host/network namespace as OpenClaw.
- Do not set `callbackUrl` to your Mattermost base URL unless that URL reverse-proxies `/api/channels/mattermost/command` to OpenClaw.
- A quick check is `curl https://<gateway-host>/api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`.
- Mattermost egress allowlist requirement:
- If your callback targets private/tailnet/internal addresses, set Mattermost
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
- Use host/domain entries, not full URLs.
- Good: `gateway.tailnet-name.ts.net`
- Bad: `https://gateway.tailnet-name.ts.net`
## Environment variables (default account)
Set these on the gateway host if you prefer env vars:

View File

@@ -445,29 +445,6 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- typing actions still include `message_thread_id`
Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`).
`agentId` is topic-only and does not inherit from group defaults.
**Per-topic agent routing**: Each topic can route to a different agent by setting `agentId` in the topic config. This gives each topic its own isolated workspace, memory, and session. Example:
```json5
{
channels: {
telegram: {
groups: {
"-1001234567890": {
topics: {
"1": { agentId: "main" }, // General topic → main agent
"3": { agentId: "zu" }, // Dev topic → zu agent
"5": { agentId: "coder" } // Code review → coder agent
}
}
}
}
}
}
```
Each topic then has its own session key: `agent:zu:telegram:group:-1001234567890:topic:3`
Template context includes:
@@ -762,8 +739,6 @@ Primary reference:
- `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `channels.telegram.groupAllowFrom`: group sender allowlist (numeric Telegram user IDs). `openclaw doctor --fix` can resolve legacy `@username` entries to IDs. Non-numeric entries are ignored at auth time. Group auth does not use DM pairing-store fallback (`2026.2.25+`).
- Multi-account precedence:
- When two or more account IDs are configured, set `channels.telegram.defaultAccount` (or include `channels.telegram.accounts.default`) to make default routing explicit.
- If neither is set, OpenClaw falls back to the first normalized account ID and `openclaw doctor` warns.
- `channels.telegram.accounts.default.allowFrom` and `channels.telegram.accounts.default.groupAllowFrom` apply only to the `default` account.
- Named accounts inherit `channels.telegram.allowFrom` and `channels.telegram.groupAllowFrom` when account-level values are unset.
- Named accounts do not inherit `channels.telegram.accounts.default.allowFrom` / `groupAllowFrom`.
@@ -774,11 +749,9 @@ Primary reference:
- `channels.telegram.groups.<id>.allowFrom`: per-group sender allowlist override.
- `channels.telegram.groups.<id>.systemPrompt`: extra system prompt for the group.
- `channels.telegram.groups.<id>.enabled`: disable the group when `false`.
- `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (group fields + topic-only `agentId`).
- `channels.telegram.groups.<id>.topics.<threadId>.agentId`: route this topic to a specific agent (overrides group-level and binding routing).
- `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
- `channels.telegram.groups.<id>.topics.<threadId>.groupPolicy`: per-topic override for groupPolicy (`open | allowlist | disabled`).
- `channels.telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
- `channels.telegram.direct.<id>.topics.<threadId>.agentId`: route DM topics to a specific agent (same behavior as forum topics).
- `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist).
- `channels.telegram.accounts.<account>.capabilities.inlineButtons`: per-account override.
- `channels.telegram.commands.nativeSkills`: enable/disable Telegram native skills commands.

View File

@@ -13,20 +13,20 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
## Job Overview
| Job | Purpose | When it runs |
| ----------------- | ------------------------------------------------------- | ------------------------------------------------- |
| `docs-scope` | Detect docs-only changes | Always |
| `changed-scope` | Detect which areas changed (node/macos/android/windows) | Non-docs PRs |
| `check` | TypeScript types, lint, format | Push to `main`, or PRs with Node-relevant changes |
| `check-docs` | Markdown lint + broken link check | Docs changed |
| `code-analysis` | LOC threshold check (1000 lines) | PRs only |
| `secrets` | Detect leaked secrets | Always |
| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes |
| `release-check` | Validate npm pack contents | After build |
| `checks` | Node/Bun tests + protocol check | Non-docs, node changes |
| `checks-windows` | Windows-specific tests | Non-docs, windows-relevant changes |
| `macos` | Swift lint/build/test + TS tests | PRs with macos changes |
| `android` | Gradle build + tests | Non-docs, android changes |
| Job | Purpose | When it runs |
| ----------------- | ----------------------------------------------- | ------------------------- |
| `docs-scope` | Detect docs-only changes | Always |
| `changed-scope` | Detect which areas changed (node/macos/android) | Non-docs PRs |
| `check` | TypeScript types, lint, format | Non-docs changes |
| `check-docs` | Markdown lint + broken link check | Docs changed |
| `code-analysis` | LOC threshold check (1000 lines) | PRs only |
| `secrets` | Detect leaked secrets | Always |
| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes |
| `release-check` | Validate npm pack contents | After build |
| `checks` | Node/Bun tests + protocol check | Non-docs, node changes |
| `checks-windows` | Windows-specific tests | Non-docs, node changes |
| `macos` | Swift lint/build/test + TS tests | PRs with macos changes |
| `android` | Gradle build + tests | Non-docs, android changes |
## Fail-Fast Order
@@ -36,14 +36,12 @@ Jobs are ordered so cheap checks fail before expensive ones run:
2. `build-artifacts` (blocked on above)
3. `checks`, `checks-windows`, `macos`, `android` (blocked on build)
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
## Runners
| Runner | Jobs |
| -------------------------------- | ------------------------------------------ |
| `blacksmith-16vcpu-ubuntu-2404` | Most Linux jobs, including scope detection |
| `blacksmith-32vcpu-windows-2025` | `checks-windows` |
| `blacksmith-16vcpu-windows-2025` | `checks-windows` |
| `macos-latest` | `macos`, `ios` |
## Local Equivalents

View File

@@ -50,5 +50,3 @@ Notes:
- `memory status --deep --index` runs a reindex if the store is dirty.
- `memory index --verbose` prints per-phase details (provider, model, sources, batch activity).
- `memory status` includes any extra paths configured via `memorySearch.extraPaths`.
- If effectively active memory remote API key fields are configured as SecretRefs, the command resolves those values from the active gateway snapshot. If gateway is unavailable, the command fails fast.
- Gateway version skew note: this command path requires a gateway that supports `secrets.resolve`; older gateways return an unknown-method error.

View File

@@ -34,9 +34,6 @@ openclaw qr --url wss://gateway.example/ws --token '<token>'
## Notes
- `--token` and `--password` are mutually exclusive.
- With `--remote`, if effectively active remote credentials are configured as SecretRefs and you do not pass `--token` or `--password`, the command resolves them from the active gateway snapshot. If gateway is unavailable, the command fails fast.
- Without `--remote`, local `gateway.auth.password` SecretRefs are resolved when password auth can win (explicit `gateway.auth.mode="password"` or inferred password mode with no winning token from auth/env), and no CLI auth override is passed.
- Gateway version skew note: this command path requires a gateway that supports `secrets.resolve`; older gateways return an unknown-method error.
- After scanning, approve device pairing with:
- `openclaw devices list`
- `openclaw devices approve <requestId>`

View File

@@ -9,14 +9,14 @@ title: "secrets"
# `openclaw secrets`
Use `openclaw secrets` to manage SecretRefs and keep the active runtime snapshot healthy.
Use `openclaw secrets` to migrate credentials from plaintext to SecretRefs and keep the active secrets runtime healthy.
Command roles:
- `reload`: gateway RPC (`secrets.reload`) that re-resolves refs and swaps runtime snapshot only on full success (no config writes).
- `audit`: read-only scan of configuration/auth stores and legacy residues for plaintext, unresolved refs, and precedence drift.
- `configure`: interactive planner for provider setup, target mapping, and preflight (TTY required).
- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub targeted plaintext residues.
- `audit`: read-only scan of config + auth stores + legacy residues (`.env`, `auth.json`) for plaintext, unresolved refs, and precedence drift.
- `configure`: interactive planner for provider setup + target mapping + preflight (TTY required).
- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub migrated plaintext residues.
Recommended operator loop:
@@ -31,13 +31,11 @@ openclaw secrets reload
Exit code note for CI/gates:
- `audit --check` returns `1` on findings.
- unresolved refs return `2`.
- `audit --check` returns `1` on findings, `2` when refs are unresolved.
Related:
- Secrets guide: [Secrets Management](/gateway/secrets)
- Credential surface: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- Security guide: [Security](/gateway/security)
## Reload runtime snapshot
@@ -61,8 +59,8 @@ Scan OpenClaw state for:
- plaintext secret storage
- unresolved refs
- precedence drift (`auth-profiles.json` credentials shadowing `openclaw.json` refs)
- legacy residues (legacy auth store entries, OAuth reminders)
- precedence drift (`auth-profiles` shadowing config refs)
- legacy residues (`auth.json`, OAuth out-of-scope notes)
```bash
openclaw secrets audit
@@ -73,7 +71,7 @@ openclaw secrets audit --json
Exit behavior:
- `--check` exits non-zero on findings.
- unresolved refs exit with higher-priority non-zero code.
- unresolved refs exit with a higher-priority non-zero code.
Report shape highlights:
@@ -87,7 +85,7 @@ Report shape highlights:
## Configure (interactive helper)
Build provider and SecretRef changes interactively, run preflight, and optionally apply:
Build provider + SecretRef changes interactively, run preflight, and optionally apply:
```bash
openclaw secrets configure
@@ -95,7 +93,6 @@ openclaw secrets configure --plan-out /tmp/openclaw-secrets-plan.json
openclaw secrets configure --apply --yes
openclaw secrets configure --providers-only
openclaw secrets configure --skip-provider-setup
openclaw secrets configure --agent ops
openclaw secrets configure --json
```
@@ -109,26 +106,23 @@ Flags:
- `--providers-only`: configure `secrets.providers` only, skip credential mapping.
- `--skip-provider-setup`: skip provider setup and map credentials to existing providers.
- `--agent <id>`: scope `auth-profiles.json` target discovery and writes to one agent store.
Notes:
- Requires an interactive TTY.
- You cannot combine `--providers-only` with `--skip-provider-setup`.
- `configure` targets secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for the selected agent scope.
- `configure` supports creating new `auth-profiles.json` mappings directly in the picker flow.
- Canonical supported surface: [SecretRef Credential Surface](/reference/secretref-credential-surface).
- `configure` targets secret-bearing fields in `openclaw.json`.
- Include all secret-bearing fields you intend to migrate (for example both `models.providers.*.apiKey` and `skills.entries.*.apiKey`) so audit can reach a clean state.
- It performs preflight resolution before apply.
- Generated plans default to scrub options (`scrubEnv`, `scrubAuthProfilesForProviderTargets`, `scrubLegacyAuthJson` all enabled).
- Apply path is one-way for scrubbed plaintext values.
- Apply path is one-way for migrated plaintext values.
- Without `--apply`, CLI still prompts `Apply this plan now?` after preflight.
- With `--apply` (and no `--yes`), CLI prompts an extra irreversible confirmation.
- With `--apply` (and no `--yes`), CLI prompts an extra irreversible-migration confirmation.
Exec provider safety note:
- Homebrew installs often expose symlinked binaries under `/opt/homebrew/bin/*`.
- Set `allowSymlinkCommand: true` only when needed for trusted package-manager paths, and pair it with `trustedDirs` (for example `["/opt/homebrew"]`).
- On Windows, if ACL verification is unavailable for a provider path, OpenClaw fails closed. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
## Apply a saved plan
@@ -160,9 +154,10 @@ Safety comes from strict preflight + atomic-ish apply with best-effort in-memory
## Example
```bash
# Audit first, then configure, then confirm clean:
openclaw secrets audit --check
openclaw secrets configure
openclaw secrets audit --check
```
If `audit --check` still reports plaintext findings, update the remaining reported target paths and rerun audit.
If `audit --check` still reports plaintext findings after a partial migration, verify you also migrated skill keys (`skills.entries.*.apiKey`) and any other reported target paths.

View File

@@ -114,8 +114,6 @@ By default, OpenClaw injects a fixed set of workspace files (if present):
Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `150000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened.
When truncation occurs, the runtime can inject an in-prompt warning block under Project Context. Configure this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default `once`).
## Skills: whats injected vs loaded on-demand
The system prompt includes a compact **skills list** (name + description + location). This list has real overhead.

View File

@@ -73,10 +73,7 @@ compaction.
Large files are truncated with a marker. The max per-file size is controlled by
`agents.defaults.bootstrapMaxChars` (default: 20000). Total injected bootstrap
content across files is capped by `agents.defaults.bootstrapTotalMaxChars`
(default: 150000). Missing files inject a short missing-file marker. When truncation
occurs, OpenClaw can inject a warning block in Project Context; control this with
`agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`;
default: `once`).
(default: 150000). Missing files inject a short missing-file marker.
Sub-agent sessions only inject `AGENTS.md` and `TOOLS.md` (other bootstrap files
are filtered out to keep the sub-agent context small).

View File

@@ -994,7 +994,6 @@
"brave-search",
"perplexity",
"tools/diffs",
"tools/pdf",
"tools/elevated",
"tools/exec",
"tools/exec-approvals",
@@ -1182,7 +1181,6 @@
"gateway/configuration-reference",
"gateway/configuration-examples",
"gateway/authentication",
"auth-credential-semantics",
"gateway/secrets",
"gateway/secrets-plan-contract",
"gateway/trusted-proxy-auth",
@@ -1323,7 +1321,6 @@
"pages": [
"reference/wizard",
"reference/token-use",
"reference/secretref-credential-surface",
"reference/prompt-caching",
"reference/api-usage-costs",
"reference/transcript-hygiene",

View File

@@ -15,8 +15,6 @@ flows are also supported when they match your provider account model.
See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage
layout.
For SecretRef-based auth (`env`/`file`/`exec` providers), see [Secrets Management](/gateway/secrets).
For credential eligibility/reason-code rules used by `models status --probe`, see
[Auth Credential Semantics](/auth-credential-semantics).
## Recommended setup (API key, any provider)

View File

@@ -185,8 +185,8 @@ Input modes:
OpenClaw ships a default for `claude-cli`:
- `command: "claude"`
- `args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"]`
- `resumeArgs: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions", "--resume", "{sessionId}"]`
- `args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"]`
- `resumeArgs: ["-p", "--output-format", "json", "--dangerously-skip-permissions", "--resume", "{sessionId}"]`
- `modelArg: "--model"`
- `systemPromptArg: "--append-system-prompt"`
- `sessionArg: "--session-id"`

View File

@@ -205,7 +205,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
- Bot token: `channels.telegram.botToken` or `channels.telegram.tokenFile`, with `TELEGRAM_BOT_TOKEN` as fallback for the default account.
- Optional `channels.telegram.defaultAccount` overrides default account selection when it matches a configured account id.
- In multi-account setups (2+ account ids), set an explicit default (`channels.telegram.defaultAccount` or `channels.telegram.accounts.default`) to avoid fallback routing; `openclaw doctor` warns when this is missing or invalid.
- `configWrites: false` blocks Telegram-initiated config writes (supergroup ID migrations, `/config set|unset`).
- Telegram stream previews use `sendMessage` + `editMessageText` (works in direct and group chats).
- Retry policy: see [Retry policy](/concepts/retry).
@@ -245,7 +244,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
"123456789012345678": {
slug: "friends-of-openclaw",
requireMention: false,
ignoreOtherMentions: true,
reactionNotifications: "own",
users: ["987654321098765432"],
channels: {
@@ -306,8 +304,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
- Optional `channels.discord.defaultAccount` overrides default account selection when it matches a configured account id.
- Use `user:<id>` (DM) or `channel:<id>` (guild channel) for delivery targets; bare numeric IDs are rejected.
- Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged name (no `#`). Prefer guild IDs.
- Bot-authored messages are ignored by default. `allowBots: true` enables them; use `allowBots: "mentions"` to only accept bot messages that mention the bot (own messages still filtered).
- `channels.discord.guilds.<id>.ignoreOtherMentions` (and channel overrides) drops messages that mention another user or role but not the bot (excluding @everyone/@here).
- Bot-authored messages are ignored by default. `allowBots: true` enables them (own messages still filtered).
- `maxLinesPerMessage` (default 17) splits tall messages even when under 2000 chars.
- `channels.discord.threadBindings` controls Discord thread-bound routing:
- `enabled`: Discord override for thread-bound session features (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`, and bound delivery/routing)
@@ -319,7 +316,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
- `channels.discord.voice.daveEncryption` and `channels.discord.voice.decryptionFailureTolerance` pass through to `@discordjs/voice` DAVE options (`true` and `24` by default).
- OpenClaw additionally attempts voice receive recovery by leaving/rejoining a voice session after repeated decrypt failures.
- `channels.discord.streaming` is the canonical stream mode key. Legacy `streamMode` and boolean `streaming` values are auto-migrated.
- `channels.discord.autoPresence` maps runtime availability to bot presence (healthy => online, degraded => idle, exhausted => dnd) and allows optional status text overrides.
- `channels.discord.dangerouslyAllowNameMatching` re-enables mutable name/tag matching (break-glass compatibility mode).
**Reaction notification modes:** `off` (none), `own` (bot's messages, default), `all` (all messages), `allowlist` (from `guilds.<id>.users` on all messages).
@@ -447,13 +443,6 @@ Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
commands: {
native: true, // opt-in
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Optional explicit URL for reverse-proxy/public deployments
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
textChunkLimit: 4000,
chunkMode: "length",
},
@@ -463,13 +452,6 @@ Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message), `onchar` (messages starting with trigger prefix).
When Mattermost native commands are enabled:
- `commands.callbackPath` must be a path (for example `/api/channels/mattermost/command`), not a full URL.
- `commands.callbackUrl` must resolve to the OpenClaw gateway endpoint and be reachable from the Mattermost server.
- For private/tailnet/internal callback hosts, Mattermost may require
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
Use host/domain values, not full URLs.
- `channels.mattermost.configWrites`: allow or deny Mattermost-initiated config writes.
- `channels.mattermost.requireMention`: require `@mention` before replying in channels.
- Optional `channels.mattermost.defaultAccount` overrides default account selection when it matches a configured account id.
@@ -801,21 +783,6 @@ Max total characters injected across all workspace bootstrap files. Default: `15
}
```
### `agents.defaults.bootstrapPromptTruncationWarning`
Controls agent-visible warning text when bootstrap context is truncated.
Default: `"once"`.
- `"off"`: never inject warning text into the system prompt.
- `"once"`: inject warning once per unique truncation signature (recommended).
- `"always"`: inject warning on every run when truncation exists.
```json5
{
agents: { defaults: { bootstrapPromptTruncationWarning: "once" } }, // off | once | always
}
```
### `agents.defaults.imageMaxDimensionPx`
Max pixel size for the longest image side in transcript/tool image blocks before provider calls.
@@ -1203,8 +1170,8 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway
**`docker.binds`** mounts additional host directories; global and per-agent binds are merged.
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in `openclaw.json`.
noVNC observer access uses VNC auth by default and OpenClaw emits a short-lived token URL (instead of exposing the password in the shared URL).
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in main config.
noVNC observer access uses VNC auth by default and OpenClaw emits a short-lived token URL that serves a local bootstrap page; noVNC password is passed via URL fragment (instead of URL query).
- `allowHostControl: false` (default) blocks sandboxed sessions from targeting the host browser.
- `network` defaults to `openclaw-sandbox-browser` (dedicated bridge network). Set to `bridge` only when you explicitly want global bridge connectivity.
@@ -1638,8 +1605,7 @@ Defaults for Talk mode (macOS/iOS/Android).
```
- Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID`.
- `apiKey` and `providers.*.apiKey` accept plaintext strings or SecretRef objects.
- `ELEVENLABS_API_KEY` fallback applies only when no Talk API key is configured.
- `apiKey` falls back to `ELEVENLABS_API_KEY`.
- `voiceAliases` lets Talk directives use friendly names.
---
@@ -1838,7 +1804,7 @@ Configures inbound media understanding (image/audio/video):
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc.)
- `model`: model id override
- `profile` / `preferredProfile`: `auth-profiles.json` profile selection
- `profile` / `preferredProfile`: auth profile selection
**CLI entry** (`type: "cli"`):
@@ -1851,7 +1817,7 @@ Configures inbound media understanding (image/audio/video):
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
- Failures fall back to the next entry.
Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`.
Provider auth follows standard order: auth profiles → env vars → `models.providers.*.apiKey`.
</Accordion>
@@ -2672,11 +2638,14 @@ Validation:
- `source: "file"` id: absolute JSON pointer (for example `"/providers/openai/apiKey"`)
- `source: "exec"` id pattern: `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
### Supported credential surface
### Supported fields in config
- Canonical matrix: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- `secrets apply` targets supported `openclaw.json` credential paths.
- `auth-profiles.json` refs are included in runtime resolution and audit coverage.
- `models.providers.<provider>.apiKey`
- `skills.entries.<skillKey>.apiKey`
- `channels.googlechat.serviceAccount`
- `channels.googlechat.serviceAccountRef`
- `channels.googlechat.accounts.<accountId>.serviceAccount`
- `channels.googlechat.accounts.<accountId>.serviceAccountRef`
### Secret providers config
@@ -2714,7 +2683,6 @@ Notes:
- If `trustedDirs` is configured, the trusted-dir check applies to the resolved target path.
- `exec` child environment is minimal by default; pass required variables explicitly with `passEnv`.
- Secret refs are resolved at activation time into an in-memory snapshot, then request paths read the snapshot only.
- Active-surface filtering applies during activation: unresolved refs on enabled surfaces fail startup/reload, while inactive surfaces are skipped with diagnostics.
---
@@ -2734,8 +2702,8 @@ Notes:
}
```
- Per-agent profiles are stored at `<agentDir>/auth-profiles.json`.
- `auth-profiles.json` supports value-level refs (`keyRef` for `api_key`, `tokenRef` for `token`).
- Per-agent auth profiles stored at `<agentDir>/auth-profiles.json`.
- Auth profiles support value-level refs (`keyRef` for `api_key`, `tokenRef` for `token`).
- Static runtime credentials come from in-memory resolved snapshots; legacy static `auth.json` entries are scrubbed when discovered.
- Legacy OAuth imports from `~/.openclaw/credentials/oauth.json`.
- See [OAuth](/concepts/oauth).
@@ -2932,7 +2900,7 @@ Split config into multiple files:
- Array of files: deep-merged in order (later overrides earlier).
- Sibling keys: merged after includes (override included values).
- Nested includes: up to 10 levels deep.
- Paths: resolved relative to the including file, but must stay inside the top-level config directory (`dirname` of `openclaw.json`). Absolute/`../` forms are allowed only when they still resolve inside that boundary.
- Paths: resolved relative to the including file, but must stay inside the top-level config directory (`dirname` of the main config file). Absolute/`../` forms are allowed only when they still resolve inside that boundary.
- Errors: clear messages for missing files, parse errors, and circular includes.
---

View File

@@ -532,7 +532,6 @@ Rules:
```
SecretRef details (including `secrets.providers` for `env`/`file`/`exec`) are in [Secrets Management](/gateway/secrets).
Supported credential paths are listed in [SecretRef Credential Surface](/reference/secretref-credential-surface).
</Accordion>
See [Environment](/help/environment) for full precedence and sources.

View File

@@ -128,11 +128,6 @@ Current migrations:
`agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
- `browser.ssrfPolicy.allowPrivateNetwork``browser.ssrfPolicy.dangerouslyAllowPrivateNetwork`
Doctor warnings also include account-default guidance for multi-account channels:
- If two or more `channels.<channel>.accounts` entries are configured without `channels.<channel>.defaultAccount` or `accounts.default`, doctor warns that fallback routing can pick an unexpected account.
- If `channels.<channel>.defaultAccount` is set to an unknown account ID, doctor warns and lists configured account IDs.
### 2b) OpenCode Zen provider overrides
If youve added `models.providers.opencode` (or `opencode-zen`) manually, it

View File

@@ -1,9 +1,9 @@
---
summary: "Contract for `secrets apply` plans: target validation, path matching, and `auth-profiles.json` target scope"
summary: "Contract for `secrets apply` plans: allowed target paths, validation, and ref-only auth-profile behavior"
read_when:
- Generating or reviewing `openclaw secrets apply` plans
- Generating or reviewing `openclaw secrets apply` plan files
- Debugging `Invalid plan target path` errors
- Understanding target type and path validation behavior
- Understanding how `keyRef` and `tokenRef` influence implicit provider discovery
title: "Secrets Apply Plan Contract"
---
@@ -11,7 +11,7 @@ title: "Secrets Apply Plan Contract"
This page defines the strict contract enforced by `openclaw secrets apply`.
If a target does not match these rules, apply fails before mutating configuration.
If a target does not match these rules, apply fails before mutating config.
## Plan file shape
@@ -29,47 +29,29 @@ If a target does not match these rules, apply fails before mutating configuratio
providerId: "openai",
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
{
type: "auth-profiles.api_key.key",
path: "profiles.openai:default.key",
pathSegments: ["profiles", "openai:default", "key"],
agentId: "main",
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
],
}
```
## Supported target scope
## Allowed target types and paths
Plan targets are accepted for supported credential paths in:
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
## Target type behavior
General rule:
- `target.type` must be recognized and must match the normalized `target.path` shape.
Compatibility aliases remain accepted for existing plans:
- `models.providers.apiKey`
- `skills.entries.apiKey`
- `channels.googlechat.serviceAccount`
| `target.type` | Allowed `target.path` shape | Optional id match rule |
| ------------------------------------ | --------------------------------------------------------- | --------------------------------------------------- |
| `models.providers.apiKey` | `models.providers.<providerId>.apiKey` | `providerId` must match `<providerId>` when present |
| `skills.entries.apiKey` | `skills.entries.<skillKey>.apiKey` | n/a |
| `channels.googlechat.serviceAccount` | `channels.googlechat.serviceAccount` | `accountId` must be empty/omitted |
| `channels.googlechat.serviceAccount` | `channels.googlechat.accounts.<accountId>.serviceAccount` | `accountId` must match `<accountId>` when present |
## Path validation rules
Each target is validated with all of the following:
- `type` must be a recognized target type.
- `type` must be one of the allowed target types above.
- `path` must be a non-empty dot path.
- `pathSegments` can be omitted. If provided, it must normalize to exactly the same path as `path`.
- Forbidden segments are rejected: `__proto__`, `prototype`, `constructor`.
- The normalized path must match the registered path shape for the target type.
- If `providerId` or `accountId` is set, it must match the id encoded in the path.
- `auth-profiles.json` targets require `agentId`.
- When creating a new `auth-profiles.json` mapping, include `authProfileProvider`.
- The normalized path must match one of the allowed path shapes for the target type.
- If `providerId` / `accountId` is set, it must match the id encoded in the path.
## Failure behavior
@@ -79,12 +61,19 @@ If a target fails validation, apply exits with an error like:
Invalid plan target path for models.providers.apiKey: models.providers.openai.baseUrl
```
No writes are committed for an invalid plan.
No partial mutation is committed for that invalid target path.
## Runtime and audit scope notes
## Ref-only auth profiles and implicit providers
- Ref-only `auth-profiles.json` entries (`keyRef`/`tokenRef`) are included in runtime resolution and audit coverage.
- `secrets apply` writes supported `openclaw.json` targets, supported `auth-profiles.json` targets, and optional scrub targets.
Implicit provider discovery also considers auth profiles that store refs instead of plaintext credentials:
- `type: "api_key"` profiles can use `keyRef` (for example env-backed refs).
- `type: "token"` profiles can use `tokenRef`.
Behavior:
- For API-key providers (for example `volcengine`, `byteplus`), ref-only profiles can still activate implicit provider entries.
- For `github-copilot`, if the profile has no plaintext token, discovery will try `tokenRef` env resolution before token exchange.
## Operator checks
@@ -96,11 +85,10 @@ openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
```
If apply fails with an invalid target path message, regenerate the plan with `openclaw secrets configure` or fix the target path to a supported shape above.
If apply fails with an invalid target path message, regenerate the plan with `openclaw secrets configure` or fix the target path to one of the allowed shapes above.
## Related docs
- [Secrets Management](/gateway/secrets)
- [CLI `secrets`](/cli/secrets)
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
- [Configuration Reference](/gateway/configuration-reference)

View File

@@ -1,70 +1,35 @@
---
summary: "Secrets management: SecretRef contract, runtime snapshot behavior, and safe one-way scrubbing"
read_when:
- Configuring SecretRefs for provider credentials and `auth-profiles.json` refs
- Operating secrets reload, audit, configure, and apply safely in production
- Understanding startup fail-fast, inactive-surface filtering, and last-known-good behavior
- Configuring SecretRefs for providers, auth profiles, skills, or Google Chat
- Operating secrets reload/audit/configure/apply safely in production
- Understanding fail-fast and last-known-good behavior
title: "Secrets Management"
---
# Secrets management
OpenClaw supports additive SecretRefs so supported credentials do not need to be stored as plaintext in configuration.
OpenClaw supports additive secret references so credentials do not need to be stored as plaintext in config files.
Plaintext still works. SecretRefs are opt-in per credential.
Plaintext still works. Secret refs are optional.
## Goals and runtime model
Secrets are resolved into an in-memory runtime snapshot.
- Resolution is eager during activation, not lazy on request paths.
- Startup fails fast when an effectively active SecretRef cannot be resolved.
- Reload uses atomic swap: full success, or keep the last-known-good snapshot.
- Runtime requests read from the active in-memory snapshot only.
- Startup fails fast if any referenced credential cannot be resolved.
- Reload uses atomic swap: full success or keep last-known-good.
- Runtime requests read from the active in-memory snapshot.
This keeps secret-provider outages off hot request paths.
## Active-surface filtering
SecretRefs are validated only on effectively active surfaces.
- Enabled surfaces: unresolved refs block startup/reload.
- Inactive surfaces: unresolved refs do not block startup/reload.
- Inactive refs emit non-fatal diagnostics with code `SECRETS_REF_IGNORED_INACTIVE_SURFACE`.
Examples of inactive surfaces:
- Disabled channel/account entries.
- Top-level channel credentials that no enabled account inherits.
- Disabled tool/feature surfaces.
- Web search provider-specific keys that are not selected by `tools.web.search.provider`.
In auto mode (provider unset), provider-specific keys are also active for provider auto-detection.
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active (when `gateway.remote.enabled` is not `false`) if one of these is true:
- `gateway.mode=remote`
- `gateway.remote.url` is configured
- `gateway.tailscale.mode` is `serve` or `funnel`
In local mode without those remote surfaces:
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
## Gateway auth surface diagnostics
When a SecretRef is configured on `gateway.auth.password`, `gateway.remote.token`, or
`gateway.remote.password`, gateway startup/reload logs the surface state explicitly:
- `active`: the SecretRef is part of the effective auth surface and must resolve.
- `inactive`: the SecretRef is ignored for this runtime because another auth surface wins, or
because remote auth is disabled/not active.
These entries are logged with `SECRETS_GATEWAY_AUTH_SURFACE` and include the reason used by the
active-surface policy, so you can see why a credential was treated as active or inactive.
This keeps secret-provider outages off the hot request path.
## Onboarding reference preflight
When onboarding runs in interactive mode and you choose SecretRef storage, OpenClaw runs preflight validation before saving:
When onboarding runs in interactive mode and you choose secret reference storage, OpenClaw performs a fast preflight check before saving:
- Env refs: validates env var name and confirms a non-empty value is visible during onboarding.
- Provider refs (`file` or `exec`): validates provider selection, resolves `id`, and checks resolved value type.
- Provider refs (`file` or `exec`): validates the selected provider, resolves the provided `id`, and checks value type.
If validation fails, onboarding shows the error and lets you retry.
@@ -157,24 +122,22 @@ Define providers under `secrets.providers`:
- `mode: "json"` expects JSON object payload and resolves `id` as pointer.
- `mode: "singleValue"` expects ref id `"value"` and returns file contents.
- Path must pass ownership/permission checks.
- Windows fail-closed note: if ACL verification is unavailable for a path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
### Exec provider
- Runs configured absolute binary path, no shell.
- By default, `command` must point to a regular file (not a symlink).
- Set `allowSymlinkCommand: true` to allow symlink command paths (for example Homebrew shims). OpenClaw validates the resolved target path.
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
- Enable `allowSymlinkCommand` only when required for trusted package-manager paths, and pair it with `trustedDirs` (for example `["/opt/homebrew"]`).
- When `trustedDirs` is set, checks apply to the resolved target path.
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
Request payload (stdin):
- Request payload (stdin):
```json
{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }
```
Response payload (stdout):
- Response payload (stdout):
```json
{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "sk-..." } }
@@ -279,33 +242,37 @@ Optional per-id errors:
}
```
## Supported credential surface
## In-scope fields (v1)
Canonical supported and unsupported credentials are listed in:
### `~/.openclaw/openclaw.json`
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
- `models.providers.<provider>.apiKey`
- `skills.entries.<skillKey>.apiKey`
- `channels.googlechat.serviceAccount`
- `channels.googlechat.serviceAccountRef`
- `channels.googlechat.accounts.<accountId>.serviceAccount`
- `channels.googlechat.accounts.<accountId>.serviceAccountRef`
Runtime-minted or rotating credentials and OAuth refresh material are intentionally excluded from read-only SecretRef resolution.
### `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`
- `profiles.<profileId>.keyRef` for `type: "api_key"`
- `profiles.<profileId>.tokenRef` for `type: "token"`
OAuth credential storage changes are out of scope.
## Required behavior and precedence
- Field without a ref: unchanged.
- Field with a ref: required on active surfaces during activation.
- If both plaintext and ref are present, ref takes precedence on supported precedence paths.
- Field without ref: unchanged.
- Field with ref: required at activation time.
- If plaintext and ref both exist, ref wins at runtime and plaintext is ignored.
Warning and audit signals:
Warning code:
- `SECRETS_REF_OVERRIDES_PLAINTEXT` (runtime warning)
- `REF_SHADOWED` (audit finding when `auth-profiles.json` credentials take precedence over `openclaw.json` refs)
Google Chat compatibility behavior:
- `serviceAccountRef` takes precedence over plaintext `serviceAccount`.
- Plaintext value is ignored when sibling ref is set.
- `SECRETS_REF_OVERRIDES_PLAINTEXT`
## Activation triggers
Secret activation runs on:
Secret activation is attempted on:
- Startup (preflight plus final activation)
- Config reload hot-apply path
@@ -316,9 +283,9 @@ Activation contract:
- Success swaps the snapshot atomically.
- Startup failure aborts gateway startup.
- Runtime reload failure keeps the last-known-good snapshot.
- Runtime reload failure keeps last-known-good snapshot.
## Degraded and recovered signals
## Degraded and recovered operator signals
When reload-time activation fails after a healthy state, OpenClaw enters degraded secrets state.
@@ -330,22 +297,13 @@ One-shot system event and log codes:
Behavior:
- Degraded: runtime keeps last-known-good snapshot.
- Recovered: emitted once after the next successful activation.
- Recovered: emitted once after a successful activation.
- Repeated failures while already degraded log warnings but do not spam events.
- Startup fail-fast does not emit degraded events because runtime never became active.
## Command-path resolution
Credential-sensitive command paths that opt in (for example `openclaw memory` remote-memory paths and `openclaw qr --remote`) can resolve supported SecretRefs via gateway snapshot RPC.
- When gateway is running, those command paths read from the active snapshot.
- If a configured SecretRef is required and gateway is unavailable, command resolution fails fast with actionable diagnostics.
- Snapshot refresh after backend secret rotation is handled by `openclaw secrets reload`.
- Gateway RPC method used by these command paths: `secrets.resolve`.
- Startup fail-fast does not emit degraded events because no runtime snapshot exists yet.
## Audit and configure workflow
Default operator flow:
Use this default operator flow:
```bash
openclaw secrets audit --check
@@ -353,22 +311,26 @@ openclaw secrets configure
openclaw secrets audit --check
```
Migration completeness:
- Include `skills.entries.<skillKey>.apiKey` targets when those skills use API keys.
- If `audit --check` still reports plaintext findings after a partial migration, migrate the remaining reported paths and rerun audit.
### `secrets audit`
Findings include:
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`)
- unresolved refs
- precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs)
- legacy residues (`auth.json`, OAuth reminders)
- precedence shadowing (`auth-profiles` taking priority over config refs)
- legacy residues (`auth.json`, OAuth out-of-scope reminders)
### `secrets configure`
Interactive helper that:
- configures `secrets.providers` first (`env`/`file`/`exec`, add/edit/remove)
- lets you select supported secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for one agent scope
- can create a new `auth-profiles.json` mapping directly in the target picker
- lets you select secret-bearing fields in `openclaw.json`
- captures SecretRef details (`source`, `provider`, `id`)
- runs preflight resolution
- can apply immediately
@@ -377,11 +339,10 @@ Helpful modes:
- `openclaw secrets configure --providers-only`
- `openclaw secrets configure --skip-provider-setup`
- `openclaw secrets configure --agent <id>`
`configure` apply defaults:
`configure` apply defaults to:
- scrub matching static credentials from `auth-profiles.json` for targeted providers
- scrub matching static creds from `auth-profiles.json` for targeted providers
- scrub legacy static `api_key` entries from `auth.json`
- scrub matching known secret lines from `<config-dir>/.env`
@@ -400,31 +361,26 @@ For strict target/path contract details and exact rejection rules, see:
## One-way safety policy
OpenClaw intentionally does not write rollback backups containing historical plaintext secret values.
OpenClaw intentionally does **not** write rollback backups that contain pre-migration plaintext secret values.
Safety model:
- preflight must succeed before write mode
- runtime activation is validated before commit
- apply updates files using atomic file replacement and best-effort restore on failure
- apply updates files using atomic file replacement and best-effort in-memory restore on failure
## Legacy auth compatibility notes
## `auth.json` compatibility notes
For static credentials, runtime no longer depends on plaintext legacy auth storage.
For static credentials, OpenClaw runtime no longer depends on plaintext `auth.json`.
- Runtime credential source is the resolved in-memory snapshot.
- Legacy static `api_key` entries are scrubbed when discovered.
- OAuth-related compatibility behavior remains separate.
## Web UI note
Some SecretInput unions are easier to configure in raw editor mode than in form mode.
- Legacy `auth.json` static `api_key` entries are scrubbed when discovered.
- OAuth-related legacy compatibility behavior remains separate.
## Related docs
- CLI commands: [secrets](/cli/secrets)
- Plan contract details: [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
- Credential surface: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- Auth setup: [Authentication](/gateway/authentication)
- Security posture: [Security](/gateway/security)
- Environment precedence: [Environment Variables](/help/environment)

View File

@@ -200,7 +200,7 @@ Use this when auditing access or deciding what to back up:
- **WhatsApp**: `~/.openclaw/credentials/whatsapp/<accountId>/creds.json`
- **Telegram bot token**: config/env or `channels.telegram.tokenFile`
- **Discord bot token**: config/env or SecretRef (env/file/exec providers)
- **Discord bot token**: config/env (token file not yet supported)
- **Slack tokens**: config/env (`channels.slack.*`)
- **Pairing allowlists**:
- `~/.openclaw/credentials/<channel>-allowFrom.json` (default account)
@@ -630,56 +630,7 @@ Rules of thumb:
- If you must bind to LAN, firewall the port to a tight allowlist of source IPs; do not port-forward it broadly.
- Never expose the Gateway unauthenticated on `0.0.0.0`.
### 0.4.1) Docker port publishing + UFW (`DOCKER-USER`)
If you run OpenClaw with Docker on a VPS, remember that published container ports
(`-p HOST:CONTAINER` or Compose `ports:`) are routed through Docker's forwarding
chains, not only host `INPUT` rules.
To keep Docker traffic aligned with your firewall policy, enforce rules in
`DOCKER-USER` (this chain is evaluated before Docker's own accept rules).
On many modern distros, `iptables`/`ip6tables` use the `iptables-nft` frontend
and still apply these rules to the nftables backend.
Minimal allowlist example (IPv4):
```bash
# /etc/ufw/after.rules (append as its own *filter section)
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
-A DOCKER-USER -s 127.0.0.0/8 -j RETURN
-A DOCKER-USER -s 10.0.0.0/8 -j RETURN
-A DOCKER-USER -s 172.16.0.0/12 -j RETURN
-A DOCKER-USER -s 192.168.0.0/16 -j RETURN
-A DOCKER-USER -s 100.64.0.0/10 -j RETURN
-A DOCKER-USER -p tcp --dport 80 -j RETURN
-A DOCKER-USER -p tcp --dport 443 -j RETURN
-A DOCKER-USER -m conntrack --ctstate NEW -j DROP
-A DOCKER-USER -j RETURN
COMMIT
```
IPv6 has separate tables. Add a matching policy in `/etc/ufw/after6.rules` if
Docker IPv6 is enabled.
Avoid hardcoding interface names like `eth0` in docs snippets. Interface names
vary across VPS images (`ens3`, `enp*`, etc.) and mismatches can accidentally
skip your deny rule.
Quick validation after reload:
```bash
ufw reload
iptables -S DOCKER-USER
ip6tables -S DOCKER-USER
nmap -sT -p 1-65535 <public-ip> --open
```
Expected external ports should be only what you intentionally expose (for most
setups: SSH + your reverse proxy ports).
### 0.4.2) mDNS/Bonjour discovery (information disclosure)
### 0.4.1) mDNS/Bonjour discovery (information disclosure)
The Gateway broadcasts its presence via mDNS (`_openclaw-gw._tcp` on port 5353) for local device discovery. In full mode, this includes TXT records that may expose operational details:

View File

@@ -219,7 +219,7 @@ OPENCLAW_LIVE_SETUP_TOKEN=1 OPENCLAW_LIVE_SETUP_TOKEN_PROFILE=anthropic:setup-to
- Defaults:
- Model: `claude-cli/claude-sonnet-4-6`
- Command: `claude`
- Args: `["-p","--output-format","json","--permission-mode","bypassPermissions"]`
- Args: `["-p","--output-format","json","--dangerously-skip-permissions"]`
- Overrides (optional):
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-6"`
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.3-codex"`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -28,9 +28,6 @@ Sandboxing details: [Sandboxing](/gateway/sandboxing)
- Docker Desktop (or Docker Engine) + Docker Compose v2
- At least 2 GB RAM for image build (`pnpm install` may be OOM-killed on 1 GB hosts with exit 137)
- Enough disk for images + logs
- If running on a VPS/public host, review
[Security hardening for network exposure](/gateway/security#04-network-exposure-bind--port--firewall),
especially Docker `DOCKER-USER` firewall policy.
## Containerized Gateway (Docker Compose)

View File

@@ -1,21 +1,28 @@
---
summary: "Perplexity Search API setup for web_search"
summary: "Perplexity Sonar setup for web_search"
read_when:
- You want to use Perplexity Search for web search
- You need PERPLEXITY_API_KEY setup
title: "Perplexity Search"
- You want to use Perplexity Sonar for web search
- You need PERPLEXITY_API_KEY or OpenRouter setup
title: "Perplexity Sonar"
---
# Perplexity Search API
# Perplexity Sonar
OpenClaw uses Perplexity Search API for the `web_search` tool when `provider: "perplexity"` is set.
Perplexity Search returns structured results (title, URL, snippet) for fast research.
OpenClaw can use Perplexity Sonar for the `web_search` tool. You can connect
through Perplexitys direct API or via OpenRouter.
## Getting a Perplexity API key
## API options
1. Create a Perplexity account at <https://www.perplexity.ai/settings/api>
2. Generate an API key in the dashboard
3. Store the key in config (recommended) or set `PERPLEXITY_API_KEY` in the Gateway environment.
### Perplexity (direct)
- Base URL: [https://api.perplexity.ai](https://api.perplexity.ai)
- Environment variable: `PERPLEXITY_API_KEY`
### OpenRouter (alternative)
- Base URL: [https://openrouter.ai/api/v1](https://openrouter.ai/api/v1)
- Environment variable: `OPENROUTER_API_KEY`
- Supports prepaid/crypto credits.
## Config example
@@ -27,6 +34,8 @@ Perplexity Search returns structured results (title, URL, snippet) for fast rese
provider: "perplexity",
perplexity: {
apiKey: "pplx-...",
baseUrl: "https://api.perplexity.ai",
model: "perplexity/sonar-pro",
},
},
},
@@ -44,6 +53,7 @@ Perplexity Search returns structured results (title, URL, snippet) for fast rese
provider: "perplexity",
perplexity: {
apiKey: "pplx-...",
baseUrl: "https://api.perplexity.ai",
},
},
},
@@ -51,83 +61,20 @@ Perplexity Search returns structured results (title, URL, snippet) for fast rese
}
```
## Where to set the key (recommended)
If both `PERPLEXITY_API_KEY` and `OPENROUTER_API_KEY` are set, set
`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`)
to disambiguate.
**Recommended:** run `openclaw configure --section web`. It stores the key in
`~/.openclaw/openclaw.json` under `tools.web.search.perplexity.apiKey`.
If no base URL is set, OpenClaw chooses a default based on the API key source:
**Environment alternative:** set `PERPLEXITY_API_KEY` in the Gateway process
environment. For a gateway install, put it in `~/.openclaw/.env` (or your
service environment). See [Env vars](/help/faq#how-does-openclaw-load-environment-variables).
- `PERPLEXITY_API_KEY` or `pplx-...` → direct Perplexity (`https://api.perplexity.ai`)
- `OPENROUTER_API_KEY` or `sk-or-...` → OpenRouter (`https://openrouter.ai/api/v1`)
- Unknown key formats → OpenRouter (safe fallback)
## Tool parameters
## Models
| Parameter | Description |
| --------------------- | ---------------------------------------------------- |
| `query` | Search query (required) |
| `count` | Number of results to return (1-10, default: 5) |
| `country` | 2-letter ISO country code (e.g., "US", "DE") |
| `language` | ISO 639-1 language code (e.g., "en", "de", "fr") |
| `freshness` | Time filter: `day` (24h), `week`, `month`, or `year` |
| `date_after` | Only results published after this date (YYYY-MM-DD) |
| `date_before` | Only results published before this date (YYYY-MM-DD) |
| `domain_filter` | Domain allowlist/denylist array (max 20) |
| `max_tokens` | Total content budget (default: 25000, max: 1000000) |
| `max_tokens_per_page` | Per-page token limit (default: 2048) |
**Examples:**
```javascript
// Country and language-specific search
await web_search({
query: "renewable energy",
country: "DE",
language: "de",
});
// Recent results (past week)
await web_search({
query: "AI news",
freshness: "week",
});
// Date range search
await web_search({
query: "AI developments",
date_after: "2024-01-01",
date_before: "2024-06-30",
});
// Domain filtering (allowlist)
await web_search({
query: "climate research",
domain_filter: ["nature.com", "science.org", ".edu"],
});
// Domain filtering (denylist - prefix with -)
await web_search({
query: "product reviews",
domain_filter: ["-reddit.com", "-pinterest.com"],
});
// More content extraction
await web_search({
query: "detailed AI research",
max_tokens: 50000,
max_tokens_per_page: 4096,
});
```
### Domain filter rules
- Maximum 20 domains per filter
- Cannot mix allowlist and denylist in the same request
- Use `-` prefix for denylist entries (e.g., `["-reddit.com"]`)
## Notes
- Perplexity Search API returns structured web search results (title, URL, snippet)
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`)
- `perplexity/sonar` — fast Q&A with web search
- `perplexity/sonar-pro` (default) — multi-step reasoning + web search
- `perplexity/sonar-reasoning-pro` — deep research
See [Web tools](/tools/web) for the full web_search configuration.
See [Perplexity Search API docs](https://docs.perplexity.ai/docs/search/quickstart) for more details.

View File

@@ -82,6 +82,12 @@ See [Memory](/concepts/memory).
- **Brave Search API**: `BRAVE_API_KEY` or `tools.web.search.apiKey`
- **Perplexity** (via OpenRouter): `PERPLEXITY_API_KEY` or `OPENROUTER_API_KEY`
**Brave free tier (generous):**
- **2,000 requests/month**
- **1 request/second**
- **Credit card required** for verification (no charge unless you upgrade)
See [Web tools](/tools/web).
### 5) Web fetch tool (Firecrawl)

View File

@@ -1,125 +0,0 @@
---
summary: "Canonical supported vs unsupported SecretRef credential surface"
read_when:
- Verifying SecretRef credential coverage
- Auditing whether a credential is eligible for `secrets configure` or `secrets apply`
- Verifying why a credential is outside the supported surface
title: "SecretRef Credential Surface"
---
# SecretRef credential surface
This page defines the canonical SecretRef credential surface.
Scope intent:
- In scope: strictly user-supplied credentials that OpenClaw does not mint or rotate.
- Out of scope: runtime-minted or rotating credentials, OAuth refresh material, and session-like artifacts.
## Supported credentials
### `openclaw.json` targets (`secrets configure` + `secrets apply` + `secrets audit`)
[//]: # "secretref-supported-list-start"
- `models.providers.*.apiKey`
- `skills.entries.*.apiKey`
- `agents.defaults.memorySearch.remote.apiKey`
- `agents.list[].memorySearch.remote.apiKey`
- `talk.apiKey`
- `talk.providers.*.apiKey`
- `messages.tts.elevenlabs.apiKey`
- `messages.tts.openai.apiKey`
- `tools.web.search.apiKey`
- `tools.web.search.gemini.apiKey`
- `tools.web.search.grok.apiKey`
- `tools.web.search.kimi.apiKey`
- `tools.web.search.perplexity.apiKey`
- `gateway.auth.password`
- `gateway.remote.token`
- `gateway.remote.password`
- `cron.webhookToken`
- `channels.telegram.botToken`
- `channels.telegram.webhookSecret`
- `channels.telegram.accounts.*.botToken`
- `channels.telegram.accounts.*.webhookSecret`
- `channels.slack.botToken`
- `channels.slack.appToken`
- `channels.slack.userToken`
- `channels.slack.signingSecret`
- `channels.slack.accounts.*.botToken`
- `channels.slack.accounts.*.appToken`
- `channels.slack.accounts.*.userToken`
- `channels.slack.accounts.*.signingSecret`
- `channels.discord.token`
- `channels.discord.pluralkit.token`
- `channels.discord.voice.tts.elevenlabs.apiKey`
- `channels.discord.voice.tts.openai.apiKey`
- `channels.discord.accounts.*.token`
- `channels.discord.accounts.*.pluralkit.token`
- `channels.discord.accounts.*.voice.tts.elevenlabs.apiKey`
- `channels.discord.accounts.*.voice.tts.openai.apiKey`
- `channels.irc.password`
- `channels.irc.nickserv.password`
- `channels.irc.accounts.*.password`
- `channels.irc.accounts.*.nickserv.password`
- `channels.bluebubbles.password`
- `channels.bluebubbles.accounts.*.password`
- `channels.feishu.appSecret`
- `channels.feishu.verificationToken`
- `channels.feishu.accounts.*.appSecret`
- `channels.feishu.accounts.*.verificationToken`
- `channels.msteams.appPassword`
- `channels.mattermost.botToken`
- `channels.mattermost.accounts.*.botToken`
- `channels.matrix.password`
- `channels.matrix.accounts.*.password`
- `channels.nextcloud-talk.botSecret`
- `channels.nextcloud-talk.apiPassword`
- `channels.nextcloud-talk.accounts.*.botSecret`
- `channels.nextcloud-talk.accounts.*.apiPassword`
- `channels.zalo.botToken`
- `channels.zalo.webhookSecret`
- `channels.zalo.accounts.*.botToken`
- `channels.zalo.accounts.*.webhookSecret`
- `channels.googlechat.serviceAccount` via sibling `serviceAccountRef` (compatibility exception)
- `channels.googlechat.accounts.*.serviceAccount` via sibling `serviceAccountRef` (compatibility exception)
### `auth-profiles.json` targets (`secrets configure` + `secrets apply` + `secrets audit`)
- `profiles.*.keyRef` (`type: "api_key"`)
- `profiles.*.tokenRef` (`type: "token"`)
[//]: # "secretref-supported-list-end"
Notes:
- Auth-profile plan targets require `agentId`.
- Plan entries target `profiles.*.key` / `profiles.*.token` and write sibling refs (`keyRef` / `tokenRef`).
- Auth-profile refs are included in runtime resolution and audit coverage.
- For web search:
- In explicit provider mode (`tools.web.search.provider` set), only the selected provider key is active.
- In auto mode (`tools.web.search.provider` unset), `tools.web.search.apiKey` and provider-specific keys are active.
## Unsupported credentials
Out-of-scope credentials include:
[//]: # "secretref-unsupported-list-start"
- `gateway.auth.token`
- `commands.ownerDisplaySecret`
- `channels.matrix.accessToken`
- `channels.matrix.accounts.*.accessToken`
- `hooks.token`
- `hooks.gmail.pushToken`
- `hooks.mappings[].sessionKey`
- `auth-profiles.oauth.*`
- `discord.threadBindings.*.webhookToken`
- `whatsapp.creds.json`
[//]: # "secretref-unsupported-list-end"
Rationale:
- These credentials are minted, rotated, session-bearing, or OAuth-durable classes that do not fit read-only external SecretRef resolution.

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