* fix: normalizeMediaSource should handle uppercase FILE:// URI schemes
URI schemes are case-insensitive per RFC 3986. normalizeMediaSource
only matched lowercase 'file://', so MEDIA:FILE:///tmp/generated.png
was left in assistant text instead of being extracted as a local
media attachment.
Replaced startsWith + replace with a case-insensitive regex to handle
FILE://, file://, and any mixed-case variant.
Fixes#103473
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(media): handle spaced uppercase file URLs
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(gateway): session worktree targeting, branch listing, and group catalog
- sessions.create accepts worktreeBaseRef/worktreeName (write scope) and
execNode (admin); worktree binding persists on the session entry as
worktree { id, branch, repoRoot } and projects onto session rows with
execNode so UIs can show checkout/branch/node state
- new worktrees.branches RPC lists local/remote branches (no fetch) for
base-ref pickers; worktrees.remove now reports snapshotError
- sessions.delete reports preserved dirty checkouts as worktreePreserved
- gateway-owned session group catalog in the shared state DB with
sessions.groups.list/put/rename/delete; rename/delete update member
categories server-side without bumping recency; sessions.patch absorbs
ad-hoc categories into the catalog
- group session display names prefer the human chat title (subject or
space #channel) over stored compact tokens
Part of #103431
* fix(gateway): route group category updates through the session accessor and regen protocol models
- bulk member-category rename/clear uses applySessionEntryReplacements
instead of legacy updateSessionStore call sites (session accessor
boundary guard)
- regenerate Swift GatewayModels for the worktrees.branches schemas,
snapshotError, and new sessions.create params
Part of #103431
* fix(gateway): resolvable remote branch refs and workspace-scoped branch listing
- worktrees.branches returns remote-only branches remote-qualified
(origin/feature-a) so every advertised name works as a worktree base ref
- write-scoped worktrees.branches callers are limited to configured agent
workspaces; other host paths require operator.admin, matching the
sessions.create cwd bar
Part of #103431
* fix(gateway): guard worktree name reuse by owner and env-scope group transactions
- managedWorktrees.create rejects a caller-chosen name whose live or
restorable record belongs to a different owner, so write-scoped
sessions.create cannot bind a session into another session's or a
manual checkout
- session group catalog writes run their SQLite transaction on the same
env-scoped handle as their statements, keeping OPENCLAW_STATE_DIR
overrides atomic and away from the default state DB
Part of #103431
* fix(gateway): use truncateUtf16Safe for voice-wake trigger truncation
Replace naive .slice(0, 64) with truncateUtf16Safe() in
normalizeVoiceWakeTriggers to prevent surrogate pair splitting
in user-configured voice wake trigger phrases.
Voice wake triggers are user-configurable text strings that may
contain emoji or non-BMP characters. A naive .slice(0, 64) at
a surrogate pair boundary produces a lone surrogate, which
corrupts the trigger text.
Co-Authored-By: Claude <noreply@anthropic.com>
* test: add proof scripts for C1 sanitize and voice-wake UTF-16
* chore: remove unrelated proof script from voice-wake PR
ClawSweeper review: the console-sanitization proof script belongs
to #103226, not this gateway voice-wake fix. Remove it to keep the
branch clean.
Ref: #103210 review
* fix(gateway): harden voice wake unicode boundary
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix: 429 errors without rate-limit wording skip the same-model retry
Aggregated providers such as OpenRouter provider pools surface transient
upstream throttles as a bare "429 <generic text>" with no RPM/TPM wording
and no Retry-After header. resolveShortWindowRateLimitRetry required one
of those hints, so these errors bypassed the same-model retry ladder and
failed the turn on the first attempt.
Treat a status-prefixed 429 as short-window once the long-window guard
(quota, usage, billing wording) has filtered its phrasings, and let the
existing backoff ladder pace the retry.
* test(agents): streamline bare 429 coverage
---------
Co-authored-by: Altay <altay@hey.com>
* feat(ui): add plugin catalog management
* feat(gateway): add plugins.uninstall and richer plugin catalog metadata
Adds a plugins.uninstall gateway method (operator.admin, control-plane write)
backed by a lock-guarded uninstallManagedPlugin that mirrors the CLI flow:
config cleanup, install-record removal, managed file deletion, and registry
refresh. Bundled plugins stay disable-only. Catalog entries now carry a
manifest-derived category and a removable flag; ClawHub search results expose
download counts and verification tiers.
* feat(ui): redesign plugins page with inventory, store shelves, and cover art
Rebuilds /settings/plugins around three tabs: Installed (category-grouped
inventory with overview stats, state filters, uninstall for external plugins,
and inline MCP server management through the shared config seam), Discover
(featured/official shelves plus one-click MCP connectors and curated ClawHub
searches), and ClawHub (search with download counts and verification badges).
Every catalog entry renders bundled cover art or a deterministic gradient
monogram tile - no more empty boxes. Artwork generated with Codex CLI, shipped
as 512px WebP under ui/public/plugin-art.
* chore(ui): regenerate locale bundles for plugins manager strings
* docs: describe plugins manager tabs, uninstall, and MCP connectors
* fix(plugins): human catalog labels and un-pinned hosted fallback ids
listManagedPlugins now prefers manifest names over registry package-name
backfill, falls back to channel catalog labels and blurbs, and stops pinning
expectedPluginId when a hosted feed entry only exposes its package name
(which rejected every legitimate install of that package). Found via live
gateway testing against ClawHub.
* fix(ui): send minimal RFC 7396 merge patches for MCP server edits
config.patch merges rather than replaces, so key removal needs an explicit
null; sending the full config back made MCP server removal a no-op. Found
via live gateway testing.
* fix(ui): write explicit MCP transports for URL servers
The MCP runtime defaults URL-only servers to SSE, so streamable HTTP
endpoints saved by the add form or connector templates would fail at
connect time. Connector templates now declare their transport and the
add form infers streamable-http unless the URL follows the /sse
convention. Flagged by autoreview against the transport resolver.
* test(ui): wait for deferred plugin requests before resolving in e2e
* feat(ui): plugins detail view, action menus, and unified ClawHub search
Reworks the plugins page from PR #103176 feedback: merges the ClawHub tab into
Discover (typing searches ClawHub inline and appends a quiet From ClawHub
section, with Browse ClawHub demoted to a header text link), makes every row and
store card open a plugin detail overlay (hero art, primary enable/install
action, metadata table), and replaces enable/disable switches with a state chip
plus an overflow menu (Enable/Disable, Remove for external plugins, View
details) matching the ChatGPT-store install+menu pattern. MCP rows use the same
menu; refresh is now icon-only.
* chore(ui): regenerate locale bundles for plugins UI iteration
* feat(ui): vetted, grouped connector catalog for the plugins store
Expands Connect your world to 28 connectors organized into use-case shelves
(Work & productivity, Coding & infrastructure, Home & media, Everyday life).
Every entry passed a three-stage subagent review: official-docs verification
plus live endpoint probes for MCP servers, ClawHub result-quality and
malware/typosquat screening for curated searches, and an adversarial pass
that dynamically registered OAuth clients to prove one-click viability.
That review removed Figma (registration allowlisted, 403) and Atlassian
(OAuth issuer-mismatch bug upstream), downgraded GitHub to PAT-based setup
(no dynamic client registration upstream), fixed Linear (/sse retired) and
Home Assistant (/api/mcp, streamable HTTP) endpoints, retargeted poisoned or
dead searches (youtube, finance, hue dropped; calendar -> google calendar;
stocks replaces finance), and added Todoist, Airtable, Canva, Stripe,
Context7, DeepWiki, Hugging Face one-click MCP servers plus Jira, PDF,
transcription, Kubernetes, Reddit, maps, translation, and notes searches.
Keyless servers get a ready-to-use success message; new cover art included.
* chore(ui): regenerate locale bundles for connector groups
* fix(plugins): suppress hosted catalog rows once their package is installed
Hosted feed entries without a declared runtime id fall back to their package
name as catalog id, which never matches the installed runtime id, so the
Discover shelf kept offering an already-installed package. Installed package
names now also suppress official rows. Flagged by autoreview.
* fix(plugins): pin declared runtime ids and surface connector errors in place
The runtime-id pin now keys off explicitly declared catalog ids (plugin,
channel, or provider) instead of string-comparing against the package name,
so declared ids that equal their package name stay enforced while entry-id
fallbacks stay unpinned. Connector add failures on Discover now render on the
triggering card instead of the Installed tab's MCP section. Both flagged by
autoreview; regression tests included.
* feat(ui): full inventory artwork, pulse header, and two-column plugin list
Every bundled plugin now ships distinctive cover art (113 new Codex CLI
illustrations; 172 total, ~2.1MB WebP), so inventory rows and detail views
never fall back to monogram tiles. The four stat cards give way to a compact
inventory pulse: a segmented enabled/disabled/issues meter whose legend and
counts live inside the filter chips. Inventory, MCP, and search rows flow
into two columns when the panel is wide enough.
* chore(ui): regenerate locale bundles for pulse header
* fix(ui): omit stdio args from the MCP server row target
Stdio MCP args routinely carry tokens, and the inventory is visible to
read-only operators; mirror the config page and show only the command.
Flagged by autoreview; regression test included.
* fix(merge): point crestodian setup at relocated plugin commit/refresh modules
* fix(merge): add bootstrapToken to plugins page test gateway harness
* fix(plugins): name catalog install-action branches so Swift emits the union
* fix(ui): satisfy strict lint on plugins page form parsing and mocks
* chore(build): regen docs map, raise plugin-sdk declaration budget for new protocol surface
* fix(ui): type the plugins page patch mock with its real call signature
* fix(agent): return data URIs for workspace-relative identity card avatars (#97602)
agent.identity.get resolved workspace-relative agent avatars via
resolveAssistantAvatarUrl to /avatar/<agentId> route URLs. <img> tags
in the Control UI Personal card cannot load these URLs because they
lack Bearer auth headers, showing broken images (401).
Read the validated local avatar file into a data URI inline before
constructing the identity response. The file-reading helper lives in
agent.ts as a private (non-exported) function so it does not leak onto
the public Plugin SDK surface. The workspace root is canonicalised with
realpathSync to match the already-resolved filePath from
resolveAgentAvatar, fixing containment checks for symlinked workspaces.
Fixes#97602
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(gateway): inline workspace avatars in bootstrap config Personal card (#97602)
agent.identity.get already returns inline data URIs for workspace-local
avatars, but the Control UI Personal card is initialized from the bootstrap
config (control-ui-config.json), which still mapped workspace avatars to the
auth-gated /avatar/<agentId> route. An <img> cannot carry Bearer auth, so the
linked Personal card 401/broken-image symptom persisted despite the RPC fix.
Consolidate the workspace-safe avatar reader into a single gateway-internal
helper (readLocalAvatarDataUrl in session-utils) and use it in both
agent.identity.get and the bootstrap config handler, so the identity and
bootstrap projections produce identical data URIs. Not re-exported on the
Plugin SDK surface. Adds a bootstrap-config regression test.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(control-ui): inline workspace assistant avatars
Project workspace-local assistant avatars into browser-safe data URLs across bootstrap and agent identity RPCs while preserving same-origin avatar routes and shared size limits.
Co-authored-by: LZY3538 <LZY3538@users.noreply.github.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: LZY3538 <LZY3538@users.noreply.github.com>
* fix(exec): auto-approve recognized read-only boolean flags on default safe bins
Default safe bins (cut, head, tail, tr, uniq, wc) auto-approve stdin-only
text-filter invocations, but the short-option validator only had an accept
path for value-consuming flags: a cluster of pure boolean short flags fell
through to a terminal reject, so common read-only forms like 'wc -l',
'tr -d', 'uniq -c' and 'sort -n' were force-routed to manual approval even
though the bins are stdin-only and the dangerous flags are already denied.
Add an allowedBooleanFlags allowlist to the safe-bin profile model and
populate it for the default bins with their read-only boolean flags. The
short-cluster validator now accepts recognized boolean flags and the long
validator reuses its existing boolean-flag accept branch (previously
unreachable). Unrecognized short flags (e.g. 'tr -S') stay fail-closed, and
denied flags are still rejected first.
* fix(exec): keep safe-bin allowedBooleanFlags off the config-facing fixture type
The boolean-flag allowlist for default safe bins leaked onto SafeBinProfileFixture, the type used for tools.exec.safeBinProfiles, while the strict zod schema and the config normalizer never accepted or preserved the key. Move allowedBooleanFlags to an internal BuiltinSafeBinProfileFixture used only for the curated built-in profiles; custom config profiles keep the allowedValueFlags/deniedFlags model.
Adds tests proving built-in profiles still honor the boolean allowlist and that a custom profile cannot widen it.
* fix(exec): keep tail follow approval-gated
* test(exec): stabilize safe-bin trust fixtures
* test(exec): isolate safe-bin argv fixtures
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Paired devices can now carry a durable operator-assigned label: device.pair.rename { deviceId, label } (schema-bounded, admin/ownership-gated) stores an operatorLabel that persists in the shared SQLite state DB, survives device repair and re-approval, and takes display precedence over the client-reported name in CLI devices list and the Control UI inventory (operatorLabel, then displayName, then clientId, then deviceId). The label was previously dropped on write because the pairing store had no column for it. CLI: openclaw devices rename --device <id> --name <label>. Docs cover the command and precedence.
Fixes#13870
Thanks to @bladin for the contribution.
Co-authored-by: heichl_xydigit <1740879+bladin@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
The resident pauses its fidgeting and turns to watch passers cross the
ledge. After a droop it tidies up with a little broom. On lobster days
(shared day hash with the CLI banner cousin, now in src/shared) it wears
a sailor cap unless the seed already rolled headwear. The Dreams sleeper
half-opens one eye when pressed.
Keep byte-over-cap visible suffixes at their streamed content indexes when terminal message snapshots are normalized.
Co-authored-by: ZOOWH <ZOOWH@users.noreply.github.com>
In messages.visibleReplies "message_tool" sessions, a successful agent turn that produced a substantive private final without calling message(action=send) previously left the user with silence and only an operator log. The gateway now enqueues one protected front-of-queue retry prompting the model to deliver the reply, and falls back to a sanitized visible diagnostic when the retry cannot be enqueued or also strands. Queue overflow protection is unified with the in-flight-aware drop policy (skip in-flight or protected items, reject when nothing is droppable), rejected overflow no longer refreshes the drain debounce, heartbeat turns are excluded from recovery, and recovery retries no longer share the client turn's queued-turn lifecycle.
Fixes#85714
Thanks to Eva (@100yenadmin) for the contribution.
* feat(channels): narrated progress drafts + activity receipt on the final answer
Progress mode replaces raw tool lines with short utility-model narration of
what the agent is doing (streaming.progress.narration, default on, requires
an explicit utilityModel). On Discord the final answer now carries the
-# activity receipt and the working draft is deleted once the answer lands,
so busy channels keep no orphaned tool log above the reply.
Formatting verified remotely via Testbox oxfmt --check (hook bypassed:
no node_modules in this worktree).
* fix(channels): keep narration toggle independent of channel-default stream mode
Discord resolves its own progress default, so the resolver must not re-derive
mode with the generic partial fallback (narration was off for unset config).
* fix(auto-reply): honor status-only command text in narration model input
streaming.progress.commandText: "status" hides raw exec/bash text from the
channel draft; narration input now mirrors that policy so the utility model
never receives more command detail than the draft shows (Codex review P2).
Formatting verified remotely via Testbox oxfmt --check.
* fix(auto-reply): share the draft's command-tool set for narration and regen channel metadata
Reuse isCommandToolName (exec|shell|bash) so narration's commandText policy
matches the draft formatter exactly, and regenerate bundled channel config
metadata for the new streaming.progress.narration key (Codex review round 2).
Gates verified on Testbox: config:channels:check, oxfmt --check, 4 test shards.
* fix(channels): clear stale narration when the narrator stops mid-turn
An empty narration update now falls the draft back to raw tool lines, and the
narrator emits that clear when it disables after consecutive failures or the
per-turn cap, so drafts never pin stale status text (Codex review round 3).
Gates verified on Testbox: oxfmt --check + 4 test shards.
* chore(config): regen bundled channel metadata after rebase onto main
* chore: CI fixups — lint nits, test harness types, docs map, SDK surface budget
Two deliberate public SDK additions (resolveChannelStreamingProgressNarration,
isCommandToolName via the streaming wildcard re-export) bump the pinned
public-surface budgets to current counts (exports 10488, callable 5235).
Gates verified on Testbox: oxfmt, targeted oxlint, docs:map:check,
config:channels:check, check:test-types, 5 test shards.
* feat(cli,webchat): lobster wild cards - a CLI cousin, moving day, and opt-in sounds
The openclaw banner gains a rare day-seeded ASCII lobster (rich TTYs,
random tagline mode, never CI). The web pet notices gateway upgrades and
arrives carrying a bindle for one load. A new default-off Lobster sounds
quick setting adds tiny WebAudio chirps on pokes and pets. The lobster
docs page learns about petting, sounds, and this batch's field notes.
* chore(webchat): regenerate keyless raw-copy i18n baseline after rebase
* fix(agents): keep model fallback turn-local instead of persisting over user pins
* fix(telegram): use live config snapshots per operation
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* test(telegram): fix config snapshot type coverage
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(agents): recover xAI/Grok "could not decrypt encrypted_content" 400 instead of tripping the circuit breaker
openclaw already strips a stale reasoning replay and retries the Responses call, but the
recovery is gated on isInvalidEncryptedContentError(), which only recognizes the
`invalid_encrypted_content` / `thinking_signature_invalid` codes/messages. xAI/Grok returns
a prose 400 with no error code — "Could not decrypt the provided encrypted_content. Ensure
the value is the unmodified encrypted_content from a previous response." — so the matcher
returns false, the call fails, and the per-model circuit breaker trips, blocking ALL
grok-4.3 traffic through the gateway until manual intervention.
Match that message (contains `encrypted_content` and a decrypt-failure phrase) so the
existing strip-and-retry path handles it too. Narrow enough to avoid unrelated
"could not decrypt" messages (e.g. the OAuth sidecar warning), which do not mention
`encrypted_content`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agents): narrow xAI decrypt retry detection
Co-authored-by: rvdlaar <rvdlaar@gmail.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): re-check session runtime model against current agent defaults after hot-reload
Agent model hot-reload silently did not take effect because session entries
cached modelProvider/model from agent defaults during reset, and the resolver
returned these cached values before checking current config.
Fix (three-pronged):
1. Reset-side: only cache modelProvider/model when the resolved model came
from a user override — default-derived values are no longer persisted.
2. Resolver-side: when runtime metadata exists without overrides and an
agentId is available, the values are default-derived and may be stale —
skip them in the persisted-model fallback so current config defaults win.
3. Inheritance: only inherit runtime model metadata from parent when it
carries explicit user overrides (align with reset-side contract).
Reset response includes resolvedModel so API and TUI consumers always get
the effective model identity.
Fixes#102269
* fix(gateway): restore truncateUtf16Safe and emoji-boundary title test per ClawSweeper review
* fix(gateway): only skip stale session runtime model metadata when it actually differs from current defaults
The previous change unconditionally skipped cached modelProvider/model when no
user overrides were present and an agentId was available, assuming it was always
stale. This broke sessions that legitimately had non-default models set through
normal session creation (e.g. custom vision models).
Now the resolver resolves the current agent default first and compares: if the
cached runtime metadata matches the current default it is returned directly
(not stale); only when it differs is it treated as stale and re-resolved.
Also updates tests that set modelProvider/model without overrides to configure
their agent defaults so the expected model matches the resolution result.
* fix(session-model-ref): add stale-metadata detection for config hot-reload
* fix(test): remove strict timeoutMs assertion in provider catalog live-runtime test
The remainingTimeoutMs calculation can be off by 1ms depending on timing
(Date.now() - startedAt = 1ms on fast CI runners), causing a flaky failure.
This assertion is not the test's focus — dedicated timeout behavior is already
covered by 'uses one timeout budget across paginated live catalog discovery'.
* fix: restore AVATAR_MAX_BYTES to 2MB (revert accidental merge contamination)
* fix(gateway): resolve session models from current config
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>