* fix(agents): keep merged delivery routes account-bound
mergeDeliveryContext gated route-field crossing on channel only, so a
completion origin that knew its account but not a concrete target
inherited a different account's to/threadId on the same channel. A
subagent, cron, or media completion for bot-a could be addressed to
bot-b's chat but sent through bot-a (cross-account misroute) or dropped.
This restores the account-bound guard added in 1ed8592467 and removed as
collateral by 025db6cf9e (PR #89949); same-account and missing-account
merges still backfill so the media route-pin path is preserved. Restores
the deleted regression test.
* fix(agents): centralize account-bound completion routes
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* 'main' of https://github.com/openclaw/openclaw: (29 commits)
refactor(gateway): trim attach grant implementation
feat(gateway): scoped attach grants for external MCP loopback clients
fix(gateway): iOS Talk treats SecretRef-backed API keys as missing (#98210)
test(infra): add unit tests for SQLite number normalization (#98009)
test(config): add unit tests for resolveExecCommandHighlighting (#98087)
test(utils): add unit tests for chunkItems (#98219)
fix(core): propagate caller env PATHEXT through isExecutableFile on Windows (#98093)
fix(matrix): guard JSON.parse against malformed homeserver response bodies (#97973)
fix(sms): guard Twilio JSON.parse against malformed API response bodies (#97999)
Add Swedish mobile app localization (#98043)
fix(anthropic): surface Discord pre-tool commentary
fix(tui): correct disconnect copy for device scope upgrades (#98144)
chore(ui): refresh fa control ui locale
chore(ui): refresh nl control ui locale
chore(ui): refresh vi control ui locale
chore(ui): refresh th control ui locale
chore(ui): refresh pl control ui locale
chore(ui): refresh uk control ui locale
chore(ui): refresh id control ui locale
chore(ui): refresh tr control ui locale
...
Per-session, TTL-bounded, revocable bearer grants (mcp-grant-store) let an external/interactive
harness reach the gateway's scoped MCP loopback tools without the cli-backend's process-global
token. A grant is a lower-trust boundary: it binds the session server-side AND fail-closes on every
caller-supplied context header (x-session-key plus message-channel/account/current-channel/thread/
source-reply/event-kind), so a grant holder can neither scope-shop the session nor spoof
delivery/action context into scoped tools or the message tool. New attach.grant/attach.revoke
operator methods mint/revoke grants and return the loopback MCP config. Owner/non-owner cli-backend
path unchanged.
* fix(core): propagate caller env PATHEXT through isExecutableFile on Windows
isExecutableFile hardcoded undefined when calling resolveWindowsExecutableExtSet,
ignoring any caller-provided custom env.PATHEXT. This meant resolveExecutablePath
and resolveExecutableFromPathEnv would fall back to process.env.PATHEXT even when
the caller supplied a different env with an extended PATHEXT (e.g. .PS1).
- Add optional options.env parameter to isExecutableFile
- resolveWindowsExecutableExtSet now reads from options?.env
- All 3 callers pass their available env through
Affects Windows deployments using sandbox/container environments where
PATHEXT differs from process.env (Docker Windows containers, CI runners, tests).
Fully backward compatible: undefined env falls back to process.env.PATHEXT.
* fix(core): also propagate caller env PATHEXT in node-host invoke resolver
- Fix sibling system.which resolver in src/node-host/invoke.ts:378
to use caller env PATHEXT instead of process.env only
- Add comprehensive Windows-mocked tests for caller env PATHEXT
propagation through isExecutableFile, resolveExecutableFromPathEnv,
and resolveExecutablePath
- Tests cover: custom env accepted, fallback to process.env,
path-separator and PATH-based resolution paths
* fix(core): also propagate caller env PATHEXT through node-host invoke resolver
- Add env?.PathExt and process.env.PathExt casing to both
resolveWindowsExecutableExtSet and resolveWindowsExecutableExtensions
for compatibility with callers using PascalCase env keys
- Isolate positive PATHEXT tests from process.env.PATHEXT by
explicitly setting it to .TXT before each test, ensuring they
prove caller env propagation rather than host env leak
* fix(core): add env?.PathExt casing to node-host system.which resolver
---------
Co-authored-by: wendy-chsy <wan.wenyan@xydigit.com>
Wrap JSON.parse(text) in MatrixAuthedHttpClient.requestJson with try/catch to prevent a malformed Matrix homeserver response from throwing an unhandled SyntaxError.
On parse failure, throw an Error with statusCode attached (matching the buildHttpError convention) so callers can handle it like any other Matrix API error.
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-authored-by: Claude <noreply@anthropic.com>
Wrap JSON.parse in parseTwilioListPayload and retrieveTwilioMessagingService with try/catch to prevent a malformed Twilio API response from throwing an unhandled SyntaxError.
- parseTwilioListPayload: return [] on parse failure (fail-safe for phone number listing)
- retrieveTwilioMessagingService: throw descriptive Error on parse failure
Note: parseTwilioApiError and parseTwilioSuccessPayload already had try/catch guards (lines 85-97, 104-121).
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(tui): correct disconnect copy for device scope upgrades
On disconnect, the TUI told users "Pairing required. Run `openclaw devices
list`, approve your request ID, then reconnect." This is misleading: the
gateway is asking for a device *scope upgrade* (the device is already
paired), and "pairing" points users at `openclaw pairing`, which only
handles chat DM pairing — a different subsystem.
- Reword the hint to name the scope upgrade and the actual recovery command
(`openclaw devices approve --latest`), including the `--token`/`--password`
escape hatch for when the device can't approve its own upgrade.
- Also match the gateway's "scope upgrade" disconnect reason, not just
"pairing required".
AI-assisted (Claude Code).
* fix(tui): clarify device approval preview hint
Keyed preamble commentary was appended after the whole tool loop, so it relied solely on the final visible-time sort for placement and lost the insertion-order tiebreaker against tool cards. Splice each keyed commentary segment into the items list before the first item with a strictly-later timestamp, so a preamble that arrived before a later tool renders above that tool while the run is live (not only after final materialization). Tools sharing the commentary timestamp that are already visible stay above it. Adds a buildChatItems regression covering a keyed preamble between two tools.
Add a per-viewer 'Keep commentary' toggle (UiSettings.chatPersistCommentary,
default true) that controls whether keyed Codex preamble/commentary blocks
stay after the final answer or clear with it.
- Persist (default): keyed commentary materializes as durable blocks, current
behavior, existing proof unchanged.
- Transient (toggle off): commentary stays live during streaming but is never
materialized, so it disappears as the final message arrives. This is the
transient-only behavior from #92236, now user-selectable instead of a
maintainer-level either/or policy choice.
Single gating point in materializeVisibleStreamState (skip itemId-keyed parts
when persistCommentary is false); threaded from settings through the chat
event handler. Adds desktop + mobile header toggles and an en.ts label
(locale bundles regenerated via ui:i18n:sync, English fallback).
Tests: reconciliation persist/transient coverage, final-event handler honors
the setting, settings round-trip + header button assertions updated.
* fix(system-prompt): move exec-approval + Authorized Senders below cache boundary
buildExecApprovalPromptGuidance (channel-varying: CLI /approve vs native
approval UI) and buildUserIdentitySection / "## Authorized Senders"
(owner/identity-varying, dropped when minimal) were emitted into the static,
cacheable prefix *above* SYSTEM_PROMPT_CACHE_BOUNDARY. They fork the cacheable
prefix at ~token 1,460, invalidating client-side prefix caching for the rest of
the ~17.8K-token system prompt — causing minutes-long cold prefills on local
models (llama.cpp / MLX / Ollama) after any channel-varying cron/heartbeat turn.
Follow-on to #40256, which moved Messaging/Voice/Reactions below the boundary
but missed these two. Relocates both into the existing below-boundary
channel-guidance block. Pure cache-stability change with no behavioural change
(the guidance is position-independent for correctness). Extends the boundary
test to assert both sections sit below SYSTEM_PROMPT_CACHE_BOUNDARY.
Measured on a local deployment: shared cross-channel prefix grew ~1,460 ->
~15,486 tokens; post-cron interactive turns went from minutes to ~10s.
Fixes#98261
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6Hz9UEpxQ4W3d8XecvupH
* fix(system-prompt): suppress relocated exec-approval line under tool_call_style override
Addresses review (clawsweeper): the exec-approval guidance lived inside the
`tool_call_style` fallback, so a provider override of that section previously
replaced it. Relocating it below the boundary emitted it unconditionally, which
changed behaviour for providers/plugins that override `tool_call_style`. Gate the
relocated line on the absence of a `tool_call_style` override, restoring the
original "override replaces the whole section" contract. Extends the
provider-override test to assert the default approval line is suppressed when
the section is overridden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6Hz9UEpxQ4W3d8XecvupH
* fix(system-prompt): tighten cache boundary proof
* fix(system-prompt): tighten cache boundary proof
* fix(system-prompt): tighten cache boundary proof
---------
Co-authored-by: headbouyJB <23249268+headbouyJB@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(device-pairing): don't churn requestId on subset re-requests
A reconnect that re-requested a subset of an already-pending device
pairing request still superseded it with a fresh requestId. This is the
root cause of the "unknown requestId" failures during device approval:
1. A TUI connect files a broad scope-upgrade request; the owner copies its
id from `openclaw devices list`.
2. `openclaw devices approve <id>` reconnects as a CLI probe that only needs
`operator.pairing` — a subset of the pending scopes.
3. That subset re-request superseded the pending request with a new id, so
the originally-listed id no longer existed and approve failed.
Refresh the existing request in place when the incoming request only asks
for roles/scopes a single pending request (same device key + role) already
covers. Escalations that request *more* than the pending request still
supersede with a fresh id, so the requestId stays bound to at least the
scope snapshot the owner saw (the existing security-motivated behavior and
its test are preserved).
AI-assisted (Claude Code).
* fix(device-pairing): align subset pairing with scope coverage
* test(telegram): add regression test for forum topic message_thread_id with streamed reasoning
After thorough code tracing of all delivery paths (draft stream, durable,
non-durable, preview hooks), the current main branch already correctly
passes message_thread_id through all paths for the streaming reasoning +
final answer scenario in forum topics. The bug reported in #89352 may have
been resolved by earlier merged changes (media-path preservation,
draft/progress streaming, etc.).
This commit adds a focused regression test covering the combined
scenario to prevent future regressions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(telegram): assert forum thread at draft-stream boundary for both lanes
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>