* fix(gateway): stop fabricating scope-upgrade requests on trusted-proxy reconnects
Same-key trusted-proxy reconnects whose auto-approvable scopes are already
covered by the paired grant now narrow the session in place instead of
running the pairing request/approve machinery: no pending-store writes and
no false 'device access upgrade requested' / 'auto-approved' security-audit
warns on every Control UI page load. The upgrade audit warn moved into
requirePairing so it fires only when a pairing request is materialized.
Durable widening (broadened deviceAutoApprove.scopes) and key-mismatch
repairs keep the existing lanes.
* fix(gateway): revalidate the live paired grant in the reconnect fast path
Address ClawSweeper P1: the narrowing decision now rereads the
authoritative paired row as the last await before returning, so a
concurrent revoke, key replacement, or grant reduction fails closed into
the pairing lane instead of riding the pre-plan snapshot. Extract the
pairing approval-plan resolver into connect-pairing-approval-plan.ts to
keep the authorization owner under the 700-line lint boundary.
* fix(agents): enforce session permissions for shared terminal input
* chore: keep terminal release notes in pull request
* fix(agents): fence full terminal input to active run authority
* fix(gateway): refresh provider usage asynchronously
* fix(ui): report a stalled provider-usage refresh on Model Providers
The page observed the incomplete-usage marker but discarded the exhausted
outcome, so once the retry budget was spent it rendered ordinary provider
cards with no usage and no explanation — indistinguishable from providers
that report no usage at all. Keep the outcome and render the warning the
Usage page already owns, reusing usage.providerUsage.stalled rather than
minting a Model Providers key so no locale baseline churns.
A user-initiated refresh now restarts the retry budget. The notice tells the
operator to refresh, so the button has to hand back attempts to spend; only
the forced path resets it, or the budget could never exhaust.
Also fixes tsgo:core:test on the current head: createStore's inferred literal
had no usageStats, so the run-bookkeeping case could not stamp it, and
view.test.ts needed the new prop.
Closes the ClawSweeper P2 at model-providers-page.ts:169-175.
* fix(ui): keep the stalled usage notice when usage.status starts rejecting
loadModelProvidersData turned a rejected usage.status into providerUsage:
null, which the page read as a completed load. observe(false) then reset the
retry budget and cleared the stalled callout, so a permanently broken usage
endpoint rendered as ordinary cards with no usage and no explanation — the
same silent failure the callout was added to prevent. The reset also fired
mid-cycle: one incomplete response followed by one rejection restarted the
budget, so the notice could be deferred indefinitely.
Record the failure at its producer instead of inferring it downstream. A null
providerUsage also means "not loaded yet", and no caller can tell the two
apart, so load.ts now reports providerUsageFailed explicitly and the page
treats a failed read as unresolved rather than resolved-empty.
Found by a Codex review of 417d43b65d.
* revert(gateway): drop the opportunistic model-catalog fast path
It broke two chat.history tests on main — both assert the cold catalog loader
runs exactly once, and reading the prepared snapshot first means it never does.
checks-node-compact-small-10 was red for that reason.
The change was a separate-surface latency fix that this PR picked up in passing,
and the body already offered to split it. Dropping it is the honest resolution:
rewriting main's assertions to accommodate a drive-by optimization would trade
one concern's proof for another's convenience. optional-model-catalog.ts,
server-model-catalog-auth.ts and their test return to the merge-base.
This PR is now only the usage.status non-blocking contract and its clients.
* fix(usage): preserve incomplete retry state
* perf(ui): keep usage capability startup-neutral
* fix(ui): restore provider usage retry convergence
* fix(usage): restore retry and cache invariants
* fix(usage): stabilize provider convergence
* test(ui): exercise provider recovery path
* test(ui): remove stale usage route fixture field
* fix(macos): show provider usage errors
* fix(macos): bound usage retries per menu open
* fix(macos): end usage retries on menu close
---------
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
Stop OpenClaw from copying or refreshing Claude CLI OAuth tokens.
Claude CLI now owns native login and refresh state; Doctor removes retired copies while preserving CLI routing.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(systemd): honor effective drop-in configuration
Read one coherent manager-loaded service snapshot while retaining the managed base definition for repairs. Track drop-in ownership, surface pending daemon reloads, and keep update and doctor rewrites from adopting operator-owned values.
Closes#128929
* fix(systemd): expose reload status in service snapshot
* test(systemd): cover Linux doctor ownership
CI jobs are not installs; unchecked they outnumber operators by orders of
magnitude and make version and platform counts meaningless. A configured
telemetry endpoint still reports, so update-path E2E lanes keep working.
* fix(gateway): bound live health aggregation
Keep verbose health within a Gateway-owned deadline, preserve healthy sibling results when channel probes stall, and document that the CLI timeout controls the Gateway connection.
Fixes#128890
* fix(gateway): retain health probe permits
Keep process-wide health capacity occupied until timed-out plugin work actually settles, and prove repeated refreshes cannot replace five stalled probes.
* fix(macos): honor user accent precedence in config snapshot and live-update chat window
The Control UI user accent (ui.prefs.accent) landed in #128432/#128577 with
precedence user accent -> operator ui.seamColor -> theme default, and the
gateway's talk.config payload already applies it. The macOS app had two gaps:
- ChannelsStore.applyUIConfig read raw ui.seamColor from the config.get
snapshot and clobbered the user accent set from talk.config depending on
arrival order. It now resolves ui.prefs.accent ?? ui.seamColor via a
testable helper mirroring the gateway precedence.
- The native chat window read AppStateStore.seamColorHex once at window
construction, so accent changes never live-updated. MacChatSurface now
reads the @Observable store in body, deleting the one-shot userAccent
plumbing.
Docs: configuration-reference.md documents the precedence for native-app
chrome. Regression test fails pre-fix (snapshot returned the operator seam
color instead of the user accent).
* fix(macos): refresh config from gateway config.changed events
Addresses the review finding that no macOS consumer turned the gateway's
hash-only config.changed broadcast into refreshed shared state, so a
Control UI accent change never reached an open native chat window while
the app ran. ChannelsStore now subscribes to gateway pushes and re-fetches
config.get on config.changed, reconnect snapshots, and sequence gaps.
The refresh applies non-force so an in-progress local settings draft wins
(the gateway rejects stale-hash writes anyway). The in-flight reload queue
gains a closed pending level (none/refresh/force) so a refresh arriving
during a load is coalesced instead of dropped, and a requeued refresh
cannot clobber a dirty draft the way the old boolean force-pending did.
* refactor(anthropic): explore official Claude Agent SDK runtime
* refactor(anthropic): replace handwritten Claude sessions with SDK
* refactor(anthropic): collapse SDK live-session ownership
* refactor(anthropic): simplify SDK ownership and preserve live skills
* fix(anthropic): fence cancelled SDK runs before process startup
* fix(anthropic): harden SDK approvals, lifecycle, and packaging
* refactor(anthropic): own SDK process trees and streamline runtime
* fix(anthropic): repair rebased packaging and legacy test fixtures
* feat(gateway): generic operator roles for non-maintainer access
Adds gateway.roles: named role bundles over a closed capability set —
sessions.others (none/view/suggest/write), an agents allowlist, and an
operator-scope ceiling. Roles are person-level (additive user_profiles.role
column, SQLite stays at v9); users.setRole (admin-only) assigns them. With
no gateway.roles config, behavior is unchanged for solo deployments.
Enforcement is deny-by-default from a host-minted actor identity
(system vs operator+profileId on server-only client.internal, never
accepted from the wire) and covers every entry point: WS RPCs, OpenAI-compat
and Responses HTTP, tools invocation, cron, questions, usage, task
suggestions, session catalog/sharing/reads. The agents allowlist gates both
session creation and run-start on existing sessions. Subagent completion
announce and descendant wake mint explicit system authority so role
boundaries never silently drop parent notifications.
The enforcement surface is expressed through a narrow policy vocabulary
(operatorSessionCap, hasOperatorBoundary, authorizeSessionSharing) rather
than per-handler policy internals.
* fix(gateway): heal PR CI after rebase onto main
- Break import cycles: extract GatewayOperatorRoleActor leaf contract; merge
session-group-mutation-targets into session-sharing-target-input.
- Split sessions-suggestions.test.ts (max-lines) into a visibility suite.
- Add users.setRole to the 2026.8 train registry test and regenerate the
Kotlin protocol client.
- Startup UNAVAILABLE gating now precedes session authorization: session
stores are not loaded during startup, so authorization reads would deny
with a misleading non-retryable error.
- sessions.assignOwner keeps its documented visibility-authorized contract
when no operator role caps the caller; view/suggest-capped roles still
cannot reassign foreign session ownership.
- Test stubs updated for main's socket readyState guard (#128144) and the
system-authority arg on channel-native resets.
* test(gateway): chat.send pending-profile dispatch carries its required session target
chat.send requires a non-empty sessionKey at the protocol level; the mutation
pipeline now rejects targetless frames before profile-dependent dispatch, so
the pending-profile test must send a realistic frame.
* docs(channels): scope implicitMentions overrides to the channels that read them
Only Mattermost, Slack, and Tlon call resolveChannelImplicitMentions, and they
are the only channels whose schemas accept the key. The other documented
producers pass no policy, so allowedImplicitMentionKinds stays undefined and
every produced fact counts as a mention.
* docs(channels): correct implicitMentions override wording
QQBot is a listed reply-to-bot producer with a passthrough schema, so the key
is accepted and ignored there rather than rejected. Also drops 'yet', which
implied a planned rollout while #80234 is still an open product decision.
* docs: limit implicit mention claims to bundled channels
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* docs(channels): record the WhatsApp ack-reaction exceptions
The shared reference promises channel and account ackReaction overrides plus an
identity fallback for every channel, but only Discord, Matrix, Slack, and
Telegram accept those keys, and WhatsApp sends no acknowledgment at all when
messages.ackReaction is unset. Also notes that group activation always bypasses
the group-mentions mention check.
* docs(whatsapp): qualify the ack-reaction exception
The eligibility path reads the account-aware channels.whatsapp.reactionLevel
before the message settings, so "only" applies to the emoji and scope rather
than to the whole decision.
* docs(whatsapp): describe supported ack reaction overrides precisely
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(telemetry): add opt-in anonymous usage reporting
* docs(telemetry): document anonymous update and privacy controls
* feat(telemetry): report show state as JSON
Give the reporting command a machine-readable form and classify the
group and mutations against the CLI JSON-output policy.
* feat(nodes): automatic device placement for sessions.dispatch
sessions.dispatch gains autoDevice: true — the gateway selects the eligible
session-host node with the most available worker slots (deterministic
tie-break), retries up to three candidates when a node churns at the
pre-provisioning eligibility fence, and reports the chosen device in the
placement runner projection. Control UI offers Any available node with
actionable disabled reasons. No-eligible-host failures state why.
* fix(ui): break draft-place-state/draft-session-placement import cycle
resolveDraftSessionPlacement only needs four scalar fields; a structural
param type replaces the Pick<DraftPlaceState,...> import that created the
madge cycle.
* fix(ui): keep the devices section hidden when no devices are paired
The Any available node row lives inside the Your devices section; rendering
it with zero paired devices resurrected the section on gateway-only setups.
Gate it on device presence — Connect a machine remains the discoverability
path — and cover both the empty and non-hostable cases.
* fix(gateway): project dispatch runner state through the canonical reader
The dispatch reply no longer synthesizes an available device runner; it uses
the fenced workerPlacementRunnerAvailabilityReader (and disk-space reader)
exactly like session reads, so a node lost after durable provisioning
projects offline consistently. Documents placement.runner.deviceId in the
protocol reference.
* feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram
Make custom emojis discoverable by the agent. The message tool's emoji
param now documents custom-emoji syntax per channel (gate-aware, only
naming emoji-list when the action is actually advertised). Discord
emoji-list defaults guildId from the current conversation and returns
reaction-ready { name, identifier, animated? } entries; Slack returns
normalized shortcodes with aliasOf. Telegram gains emoji-list backed by
one canonical allowed-reactions owner (getChat available_reactions,
custom_emoji entries preserved), numeric custom-emoji reactions, and
replaces the dead 'reaction disallow list' error advice with a bounded
sample of the chat's allowed reactions.
* test(channels): expect telegram emoji-list provider-owned read gate in plugin shape contract
* test(telegram): prove emoji-list authority chain via mock-gateway e2e
Ephemeral gateway + mock Bot API + mock OpenAI provider: current-chat
emoji-list returns normalized standard and custom_emoji identifiers with
exactly one getChat call; a delegated cross-chat request is rejected with
the conversation-binding error and zero Bot API requests reference the
foreign chat.
* docs: fix five broken cross-page anchor links
Five internal doc links pointed at anchors that do not exist on the target
page, so readers following them landed at the top of the page instead of the
referenced section.
- /gateway/cloud-workers#desktop -> #desktop-interactive
(heading is "## Desktop (interactive)"), from experimental-features and
the gateway protocol reference.
- /web/urls#special-documents-and-startup-modes ->
#other-special-documents-and-startup-modes
(heading is "## Other special documents and startup modes"), twice from
the Control UI page.
- /plugins/sdk-runtime#api-runtime-agent -> #runtime-namespaces
"api.runtime.agent" is an <Accordion> title, not a heading, so it emits no
anchor; the enclosing "## Runtime namespaces" heading is the real target.
Sibling links on all three target pages already use heading-derived anchors
(#coordinator-backed-crabbox, #route-table, #reusable-runtime-utilities),
and this was the only link in docs/** aiming at an Accordion title out of
1342 such titles.
Docs-only change; no runtime or config surface is touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: restore the sdk-entrypoints deep link
Running the real Mintlify anchor audit (pnpm docs:check-links:anchors) showed
that /plugins/sdk-runtime#api-runtime-agent already resolves: Mintlify does
generate anchors from <Accordion title="..."> elements, so the original link
was correct.
The audit flags fragments it cannot resolve -- it reported both
cloud-workers#desktop and urls#special-documents-and-startup-modes on main --
and it never reported this one. Retargeting it to #runtime-namespaces would
have replaced a precise deep link with a coarser section link, so this restores
the original.
The remaining four link instances in this PR are confirmed broken by the same
audit: main reports 313 broken links across 45 files, this branch reports 309
across 42, with no newly broken links.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
With agents.defaults.heartbeat.every set to "0m", a background exec completion queued its system event and requested a targeted exec-event wake, but the shared unscheduled-wake policy only admitted immediate wakes, so the follow-up agent turn silently never ran. Admit the exact exec-event/event producer shape (session or agent target required) in one per-source predicate, dispatch wake timers outside the requesting attempt's transcript-writer context, and document that "0m" disables recurring cadence only.
Closes#62505
Co-authored-by: Jason O'Neal <jason.allen.oneal@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Adds a generic plugin-declared selectable-context-window surface mirroring thinkingLevels: ModelCatalogEntry.contextWindows + contextWindowDefault through catalog normalization and the gateway protocol, session validation on sessions.create/patch, and a 200K/1M switch inside the Control UI model picker for Claude CLI 5-series models. The Anthropic plugin owns the option mapping: explicit 1m → `[1m]` argv suffix, 200k → bare id + CLAUDE_CODE_DISABLE_1M_CONTEXT=1, omitted → bare id (shipped default argv). Run budgets follow the selection on both CLI and native paths, so a 200K session gets a matching auto-compact window instead of a silent 1M budget.
Review fixes landed in this PR: run-owner prepared-fact plumbing so ordinary replies honor the selection; atomic catalog overlay merge and normalization for the options/default tuple; one-owner tuple reads in the picker; sessions.create key-presence patch semantics; native-run budget capping.
Feature direction and in-picker switch shape by @obviyus (maintainer review).
Preserve opaque source strings across tool-result replay while retaining masking for direct .env reads, registered or recognizable secrets, structured sensitive fields, and payment data.
Prevent ClawHub skill updates from silently replacing locally modified files. Verify recorded install digests at the rollback boundary, restore on mismatch, and require explicit --force for unverifiable legacy installs.
Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(onboard): honor secret-input-mode ref for the generated gateway token
`openclaw onboard --secret-input-mode ref` was silently ignored for
`gateway.auth.token`: onboarding generated the token and wrote it into
`openclaw.json` as a plaintext string, so `openclaw doctor` warned about
`gateway.auth.token` on the install it had just created. The flag was
honored for provider credentials, so an operator who explicitly opted into
references still ended up with a plaintext secret and a remediation
(`openclaw secrets configure`) that cannot migrate a self-generated value,
because it validates a ref by resolving one that already exists.
Setup mints this token itself, so reference mode now provisions it:
- an ambient OPENCLAW_GATEWAY_TOKEN keeps an `env` ref to that variable, so a
later rotation stays authoritative instead of being pinned by a stale copy
- anything else (freshly generated, or an existing plaintext token being
migrated) goes into the shared SQLite secret store as a write-only `secret`
entry, with config holding only `{source:"store",...}`
An existing store entry wins over a freshly generated one, so reruns never
rotate a token already paired with clients. The store write precedes the
config write: a ref persisted without its value would leave the gateway
unauthenticatable, while an orphaned entry is reused by the next run.
The interactive wizard had the same dead end and is fixed the same way.
Default (plaintext) onboarding is unchanged.
User impact: `--secret-input-mode ref` now keeps the gateway token out of
openclaw.json, and a fresh install no longer self-reports a plaintext-secret
warning.
* test(onboard): split gateway onboarding suite under the max-lines gate
The added gateway auth-token tests pushed
onboard-non-interactive.gateway.test.ts to 1014 lines, over the max-lines
limit (check-lint-core-3). Repo policy is to split, never suppress.
Extract the shared vi.mock/harness preamble into
onboard-non-interactive.gateway.test-mocks.ts, following the existing
agent-command.test-mocks.ts pattern, and move the four gateway auth-token
storage tests into their own suite. The reachability mock becomes a holder
object so both suites can swap it across the module boundary, and hoisted
mocks are re-exported in a separate export clause because Vitest rejects
exporting a vi.hoisted binding at its declaration.
Test set is unchanged: the it-declaration multiset matches the pre-split
file exactly, with no duplication across the two suites.
* test(onboard): give the shared gateway onboarding mocks unique export names
check-export-name-collisions flagged `runtime` and `readConfigFileSnapshotMock`
as colliding with program.test-mocks.ts and plugins-cli-test-helpers.ts once the
gateway onboarding preamble became a shared module. Rename the exports to
gatewayOnboardRuntime / gatewayOnboardConfigSnapshotMock per the repo's
unique-export-name rule; suites alias them locally so the assertions read the
same as before.
* test(tooling): route the new gateway auth-token suite from its test helper
test-projects asserts which suites a change to
onboard-non-interactive.test-helpers.ts should run. The new
onboard-non-interactive.gateway-auth-token.test.ts imports that helper, so it
belongs in the expected routing plan.