* fix(slack): use truncateUtf16Safe for message body preview truncation
One .slice(0, 160) truncation site in the Slack message handler may
cut UTF-16 surrogate pairs in half when the message body preview
contains multi-byte characters such as emoji. Replace it with
truncateUtf16Safe to keep the truncated output valid Unicode.
* test(slack): cover UTF-16 preview boundary
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(browser): remove hardcoded profile="user" suggestion from browser tool description
The browser tool description hardcoded a suggestion to use profile="user"
for logged-in sessions, even when the operator has configured a different
default profile (e.g. browser.defaultProfile: "openclaw").
Replace with profile-agnostic guidance that references the configured
browser.defaultProfile setting instead of prescribing a specific value.
Fixes#102566
* test: update browser tool description assertion for profile-agnostic wording
* refactor(browser): unify tool profile guidance
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Adds client-capability-gated tool availability: gateway clients declare
capabilities at connect (new inline-widgets cap), chat.send stamps them into
the run context, and every tool assembly path (embedded runner, queued
followups, Codex app-server harness, plugin-only construction plans) drops
tools whose requiredClientCaps the originating client did not declare. The
Canvas plugin ships the first such tool, show_widget: agents pass SVG or an
HTML fragment plus a title; the plugin hosts it as a bounded, retention-scoped
Canvas document and returns the existing canvas preview handle, which web chat
renders as a sandboxed iframe fitted to the widget's reported content height.
Widget frames never get allow-same-origin (per-preview sandbox ceiling,
including the sidebar path) and the Canvas host serves widget documents with a
CSP sandbox header so direct navigation runs in an opaque origin. Verified
live end-to-end on a Testbox with gpt-5.5 (screenshots on the PR). CLI-backed
model backends do not carry client caps yet and stay fail-closed (#102577).
Closes#101790
* fix(openai): use truncateUtf16Safe for image gen log value truncation
One .slice(0, N) truncation site in the OpenAI image generation
provider may cut UTF-16 surrogate pairs in half when the log value
contains multi-byte characters such as emoji. Replace it with
truncateUtf16Safe to keep the truncated output valid Unicode.
* fix(openai): keep image auth logs UTF-16 safe
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(msteams): surface quoted message body in Teams quote replies
Teams sends the quoted text of a 1:1 DM quote-reply in <p itemprop="preview">
(a truncated snippet), not <p itemprop="copy"> which the parser matched. The
match failed, so quoteInfo was undefined and nothing was surfaced to the agent.
Fix 1 (primary): extractMSTeamsQuoteInfo now accepts copy OR preview (prefers
copy, falls back to preview) and captures the blockquote itemid as the quoted
message id.
Fix 2 (enhancement): when the quoted message id is known, fetch the full text
via the app-only Graph endpoint GET /chats/{chatId}/messages/{id} (permitted
with Chat.Read.All, unlike the delegated /me/chats listing) and use it as the
quote body. Restricted to chats (DM + group) whose Graph chat id is a 19: id;
any failure degrades to the truncated preview so message handling never breaks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(msteams): address review — DM-only full-text fetch, drop unsupported $select, fix mocks
Addresses ClawSweeper review on the quote-reply PR:
- Security (P1): restrict the app-only Graph full-text quote fetch to 1:1 DMs.
Previously it ran for any non-channel chat, so in a group an allowlisted
sender could quote a non-allowlisted member and the fetched full body would
bypass the supplemental-quote visibility allowlist. Group/channel quotes keep
the (now-surfaced) truncated preview from fix 1.
- Graph contract (P2): the get-chatMessage endpoint does not support OData
query params; drop the ?$select=id,body that tenants enforcing the contract
would reject (which would silently fall back to the preview).
- Tests (P1): add fetchChatMessageText to the two vi.mock(../graph-thread.js)
factories prod now touches, and add a group-chat regression test proving the
Graph full-text fetch does not fire for group quotes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(msteams): add redacted real Teams quote-reply proof screenshots
Real-behavior evidence for the quote-reply fix (personal names + avatars
redacted): a 1:1 DM where the bot now reads back the full quoted message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(msteams): remove committed proof screenshots from branch
Proof media should live as external PR artifacts, not in the product tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(msteams): bound and prove quote enrichment
* test(msteams): use valid open DM fixture
---------
Co-authored-by: Yash Inani <yashinani@Yashs-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugin-sdk): update ssrf-runtime import path in cdp-proxy-bypass.ts
The import path `openclaw/plugin-sdk/ssrf-runtime-internal` was renamed
to `openclaw/plugin-sdk/ssrf-runtime` in a previous version, but
cdp-proxy-bypass.ts still referenced the old path. The neighbouring
cdp.helpers.ts already uses the correct path. This caused the browser
plugin to fail to load with "Package subpath is not defined by exports".
Fixes#96639
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(plugin-sdk): update ssrf-runtime import path in cdp-proxy-bypass
The import path `openclaw/plugin-sdk/ssrf-runtime-internal` was renamed
to `openclaw/plugin-sdk/ssrf-runtime` in a previous version, but
cdp-proxy-bypass.ts still referenced the old path. The neighbouring
cdp.helpers.ts already uses the correct path. This caused the browser
plugin to fail to load with "Package subpath is not defined by exports".
Update the corresponding vi.mock path in the test file to match.
Fixes#96639
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(plugin-sdk): promote managed-proxy bypass exports to public ssrf-runtime
Export registerManagedProxyBrowserCdpBypass and fetchConfiguredLocalOriginWithSsrFGuard
from the public ssrf-runtime path so that the browser plugin and ollama embedding
provider can lazy-load without hitting the private ssrf-runtime-internal subpath
that is excluded from package.json exports.
Fixes#96639
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(build): bundle private SSRF runtime for packaged plugins
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Feedback pass on the sessions redesign: status pills become a plain colored
dot + label (green pulse for live, green dot with neutral label for done,
muted idle, red failed) in both the roster and the drawer hero, and the
Runtime column moves out of the table into the row drawer as Runtime (agent
runtime) plus Run duration. Runtime stays searchable. Responsive breakpoints
reflow for the 7-column roster and the actions column shrinks to the chevron
and row menu that landed in #102564.
Closes#102654
* fix(inworld): use truncateUtf16Safe for error body and parse error truncation
Two .slice(0, N) truncation sites in the Inworld TTS extension may
cut UTF-16 surrogate pairs in half when the error body or parse error
message contains multi-byte characters such as emoji. Replace both
with truncateUtf16Safe to keep the truncated output valid Unicode.
* test(inworld): cover UTF-16-safe TTS errors
Co-authored-by: lizeyu-xydt <li.zeyu@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Reset reconnect attempts only after a provider-ready connection stays stable, and cover both flap exhaustion and stable recovery.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugins): terminate git clone args so git: specs cannot inject options
A git: install spec whose base ends in .git or is scp-form was passed to
git clone as a bare positional. A spec beginning with a dash, such as
git:--upload-pack=/path/pwn.git, was parsed by git as an option rather than
a repository, reaching a command-execution primitive. The sibling
git switch --detach -- <ref> in the same files was already hardened; the
clone sites were missed.
Add a -- option terminator before the URL at both clone call sites
(installPluginFromGitSpec and installSkillFromSource) and reject a
leading-dash URL in isGitUrl as defense in depth.
* test(plugins): prove git option rejection boundary
Co-authored-by: yetval <yetvald@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(backup): write backup archive with owner-only 0o600 permissions
The published openclaw backup .tar.gz was created via createWriteStream
with no mode, so it landed at 0644 and world-readable. The archive
bundles auth.json OAuth access and refresh tokens, openclaw.json provider
API keys, and the state database, exposing all of them to any local user.
The intermediate SQLite snapshot is already chmod 0o600, but that
mitigation never reached the final artifact that actually matters. Create
the archive stream with mode 0o600 so the published backup is owner-only.
publishTempArchive preserves the source mode: fs.link shares the inode and
fs.copyFile copies the source mode, so the single write-site fix reaches
both publish paths.
* fix(backup): create archives owner-only
Co-authored-by: yetval <yetvald@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(browser): preserve strict CDP discovery policy on target-list lookup
findPageByTargetIdViaTargetList fetched the CDP /json/list endpoint without
passing an ssrfPolicy argument, so the caller's discovery policy was dropped at
the fetch layer while the sibling tryTerminateExecutionViaCdp scoped it. Build
the scoped control policy with scopeCdpPolicyToConfiguredEndpoint and pass it to
fetchJson, matching the sibling lookup path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(browser): tighten target-list CDP policy test comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(browser): consolidate target-list SSRF coverage
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugins): fall back to os.homedir() when HOME/OPENCLAW_HOME is empty
manifest-metadata-scan resolved the home directory with
`env.OPENCLAW_HOME ?? env.HOME ?? env.USERPROFILE ?? os.homedir()`. The nullish
coalescing operator only catches null/undefined, so an empty or whitespace home
variable (e.g. HOME="" in a stripped-down container/sandbox env) was kept as ""
and path.join("", ".openclaw") resolved to a RELATIVE ".openclaw" under the cwd.
Plugin discovery then read global extensions from the wrong directory.
Normalize each candidate with the already-imported normalizeOptionalString (which
returns undefined for empty/whitespace) so an empty value falls through to
os.homedir(), matching the canonical home resolver in src/infra/home-dir.ts.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(plugins): reuse canonical manifest paths
Co-authored-by: liyuanbin <li.yuanbin1@xydigit.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* [AI] fix(agents): emit model.failover diagnostic on normal model fallback transitions
Restructure observeFailedCandidate to emit the existing model.failover
diagnostic event for candidate run failures when an actual next fallback
candidate exists.
Previously emitFailoverEvent was only called in the cooldown lane-suspension
branch. Normal fallback transitions only reached the trajectory recorder
(onFallbackStep) and were invisible in OTel traces.
Key design decisions:
- Emit inside observeFailedCandidate so both LiveSessionModelSwitchError
and generic error paths are covered automatically
- Guard emission on nextCandidate to avoid false telemetry for single-model
runs or terminal candidate exhaustion (the cooldown suspension branch
already handles that case with suspended=true)
- Extract error reason via existing describeFailoverError helper
Related to #102015
Co-Authored-By: claude-4.6 <noreply@anthropic.com>
* [AI] test(agents): add regression tests for model.failover diagnostic emission
Covers three scenarios:
- Primary fails, fallback succeeds: verifies model.failover event payload
(from/to provider/model, reason, cascadeDepth, suspended)
- Single-model run: verifies no false event emission
- Exhausted chain: verifies only real transition emits, terminal does not
Related to #102015
Co-Authored-By: claude-4.6 <noreply@anthropic.com>
* test(agents): consolidate failover diagnostics coverage
* test(agents): satisfy fallback diagnostic gates
---------
Co-authored-by: claude-4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(ui): align split-view toolbar segments with the pane edges they label
* fix(ui): track pane gutter and horizontal overflow in the split toolbar
* fix(ui): reset split toolbar scroll offset on mode flips and reserve docked terminal space
* fix(ui): scroll clipped split panes into view when their toolbar segment gains focus
* fix(ollama): use truncateUtf16Safe for malformed NDJSON log warning
The parseNdjsonStream function uses naive .slice(0, 120) on malformed
NDJSON data in log warnings which can split surrogate pairs. Replace
with truncateUtf16Safe().
* test(ollama): cover UTF-16-safe NDJSON warnings
* test(ollama): satisfy lint in NDJSON coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(parallel): use truncateUtf16Safe for MCP error JSON truncation
The extractMcpToolPayload function and error messages use naive
.slice(0, 500) on JSON-serialized errors and results which can split
surrogate pairs. Replace with truncateUtf16Safe().
* test(parallel): cover UTF-16-safe MCP errors
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>