* 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>
* 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
* 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
* fix(utils): keep reply directive ids unicode-safe
* test(utils): catch trailing lone surrogate in hasUnpairedSurrogate helper
Per PR #96938 review: the test helper missed a high surrogate at end of
string because charCodeAt(out-of-bounds) returns NaN, and NaN comparisons
are always false. Guard bounds explicitly so the truncation test actually
proves what it claims, plus two cases pinning helper behavior.
* chore(utils): rerun QA smoke to confirm memory-index timeout flake on #96938
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Gap finding: tempdir-8h9i0j1k, tempdir-9i0j1k2l
Replace bare fs.mkdtemp/mkdtempSync calls in 4 test files with the
shared createSuiteTempRootTracker() helper, adding proper cleanup
where none existed.
Background: the project has standardized on shared temp dir helpers
since 88b87d893d (withTempDir) -> 13df67ebc8 (createSuiteTempRootTracker)
-> 06431fd99b / #87298 (CI guard + docs guidance). This PR continues
that migration for the highest-risk files.
Files changed:
- src/skills/lifecycle/install-fallback.test.ts: variable overwrite risk
- src/auto-reply/reply/session-hooks-context.test.ts: helper leaks temp dirs
- src/auto-reply/reply/abort.test.ts: createAbortConfig leaks root dir
- src/auto-reply/inbound.test.ts: test cases without cleanup
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cron): clear agentTurn thinking override when patched with null
Cron agentTurn patches could clear model/fallbacks/toolsAllow overrides by
sending an explicit null, but thinking had no clear path: the patch schema and
normalizer dropped thinking:null before it reached the merge logic, and the
payload merge only handled string values. Blanking the Thinking/Effort field in
the Cron Control UI therefore silently preserved the old value.
Add thinking:null support across the patch schema, exported type, normalizer,
and payload merge (mirroring model). The Control UI now sends an explicit clear
for model/thinking when an edited job blanks a previously stored override, and
the CLI gains --clear-thinking for parity with --clear-model.
* docs(cron): document --clear-thinking beside sibling clear flags
postJson reads the Anthropic OAuth token endpoint response body with
an unbounded await response.text(). A compromised or hijacked OAuth
endpoint can stream an arbitrarily large body and force the runtime
to buffer the entire payload before parsing — an OOM/DoS vector.
Replace with readResponseWithLimit at 16 MiB cap + TextDecoder decode
to match the sibling bounded-read pattern (provider-http-errors.ts:308).
Co-authored-by: Claude <noreply@anthropic.com>
* fix(doctor): merge colliding model-ref map keys instead of dropping
When two retired model-ref keys in a models map upgrade to the same
current ref, rewriteModelRefMapKeys dropped the second entry silently
and still logged a success line, so doctor --fix lost one entry's
params/streaming/alias/agentRuntime tuning while falsely reporting it
as upgraded.
Mirror the sibling provider-models-array merge path (#90047): deep-merge
the colliding entries preserving disjoint fields, keep the existing
value deterministically on a true field conflict, and push a merge or
collision change-log line instead of dropping.
* fix(doctor): block prototype keys when merging colliding model-ref map entries
mergeModelRefMapEntries copied arbitrary incoming config fields into a
plain object, so a colliding model entry carrying a __proto__ field could
mutate the merged entry prototype during legacy config migration. Skip
isBlockedObjectKey fields before assigning or recursing, matching the
shared config merge guard. Disjoint-field merging is unchanged.
* fix(doctor): preserve canonical model migration values
* fix(doctor): filter blocked keys on both merge sides and report retired source key
* fix(doctor): preserve colliding retired model settings
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
* fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events
toolcall_delta and toolcall_end events bypassed the output replacement
pipeline, leaking masked tokens (e.g. PII placeholders) into tool call
arguments and external systems.
Closes#97761
* fix(plugins): transform nested tool call arguments in output replacements
Add recursive transformToolCallArgumentText to handle strings, arrays,
and nested objects in tool call arguments, not just the name field.
* fix(plugins): keep tool names unchanged and cover result path
- Remove toolCall.name transformation to prevent breaking tool routing
- Add arguments transform to stream.result()/done.message via
transformContentText for tool-call content blocks
* fix(plugins): narrow argument transform to toolCall content blocks only
Only transform arguments on content blocks with type=toolCall to avoid
touching non-tool-call blocks that happen to have an arguments field.
* test(plugins): assert stream.result() tool-call content block is transformed
* test(plugins): fix result-path fixture to actually contain toolCall block
The previous fixture used makeAssistantMessage('final') which produces
a plain text content block, not a toolCall block. The stream.result()
assertion was vacuously passing.
* style(plugins): fix type cast in result-path test assertion
* fix(agents): preserve tool argument transforms after repair
* refactor(agents): keep tool transform boundary narrow
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
When no chat DM pairing channels are configured, `openclaw pairing list`
(no channel argument) threw `Channel required ... (expected one of: )`
with an empty enum that reads like a bug. Users who hit this are usually
trying to approve a TUI/device or scope-upgrade request, which lives under
`openclaw devices`, not `openclaw pairing` (which only handles chat DM
pairing).
- Guard the channel hint in help text and errors so an empty channel list
no longer renders a bare `()` / `(expected one of: )`.
- When no pairing channels exist, redirect to `openclaw devices list` /
`openclaw devices approve` instead of failing opaquely.
AI-assisted (Claude Code).
Expose `core/doctor/auth-profiles` as a default-disabled structured Doctor lint check.
This maps the existing auth-profile health diagnostics into structured findings while preserving legacy Doctor repair behavior. The check remains opt-in through `--only core/doctor/auth-profiles` or `--all`; default `doctor --lint` behavior is unchanged.
Validation:
- focused auth-profile/hints/contribution Vitest passed
- changed-file oxfmt and oxlint passed
- core tsgo passed
- git diff --check passed
- exact-head hosted CI/Testbox gates passed
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
* fix(usage): reject inverted startDate-endDate range
* chore: retrigger CI for real behavior proof check
* refactor(usage): resolve validated date ranges once
---------
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
The 64 KiB inter-event SSE sanitize buffer added in #96989 rejects a single
legitimate event larger than 64 KiB — e.g. a large gpt-5.5 reasoning summary on
the openai-chatgpt-responses API — throwing "SSE response exceeded max buffer
size (65536 bytes) without event boundary" and failing the whole request. The
default ChatGPT-subscription gpt-5.5 path is unusable (present in v2026.6.11-beta.1).
Decouple the two bounds: keep the non-OK error-body cap tight at 64 KiB
(SSE_NONOK_BODY_MAX_BYTES), and raise the inter-event sanitize buffer to the same
16 MiB ceiling as the JSON-synthesis path. The guard still trips on a genuinely
boundary-less (hostile/broken) stream, just not on a real large event.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A present-but-unreadable openclaw.json (for example EACCES after a sudo
command leaves it root-owned) returns an empty best-effort fallback
snapshot. A later full-file write (openclaw doctor, including the update
doctor pass) then serialized a skeletal config over the still-rich file,
dropping gateway.mode and bricking gateway startup.
The fallback base has no raw bytes and an empty resolved config, so the
existing size-drop and gateway-mode-removed guards never fired, and the
update path passes allowConfigSizeDrop=true.
Record the read failure on the snapshot (readError) and treat an
unreadable base as an always-blocking write reason
(unreadable-config-before-write) that allowConfigSizeDrop does not bypass.
The allowDestructiveWrite escape hatch and the rejected-artifact path are
preserved, so explicit recovery still works and the blocked payload is
saved to openclaw.json.rejected.<timestamp>.
Refs #78493.
When a proxied Anthropic-Messages endpoint degrades a native tool_use block
into plain text, the assistant emits antml:namespaced XML tags
(<antml:invoke>, <antml:parameter>). These were not recognized by the
visible-text sanitizer, leaking tool-call XML into user-visible replies.
Added antml: (and mm:) namespace handling to:
- TOOL_CALL_QUICK_RE regex for fast pre-check
- TOOL_CALL_TAG_NAMES set for tag name recognition
- Tag name scanner to accept ':' in tag names
- TOOL_CALL_XML_PAYLOAD_START_RE for XML payload detection
- shouldDetectXmlPayload to include antml:invoke tags
Fixes#97750.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>