* feat(plugins): surface plugin capability consent in Control UI and CLI
Adds plugins.inspect (declared manifest surface, operator grants, install
provenance/integrity, ClawHub trust), a Control UI consent dialog on install
and external-plugin enable, a server-side acceptance gate persisted on the
install record, artifact-anchored widen diffing, and --accept-capabilities
for non-interactive CLI use.
NOT READY TO LAND: autoreview found critical gaps (see PR notes) — the
declared surface omits 20 of 21 contract families, native plugins always
report zero hooks, several install/enable paths bypass the gate, and the
acknowledgment is not bound to the reviewed surface.
* refactor(plugins): bind capability consent to the reviewed surface
Collapses the consent error payload to the fields the client cannot fetch
(reviewToken, widened, acceptedAt) and pulls identity/declared/grants/source/
trust from plugins.inspect, shrinking the registry-free protocol reader from
395 to 91 lines and removing its divergence from the closed schema.
Acknowledgment now carries the SHA-256 reviewToken of the surface the operator
saw; the server recomputes the final staged artifact's surface and rejects any
mismatch before persisting acceptance. That closes review-then-swap, laundering
of forged acceptance through an unchanged update, and cross-artifact replay.
All 22 manifest contract families are now declared, hashed and diffed, so a
privileged family such as gatewayMethodDispatch can no longer be added without
re-consent. Consent reads the manifest runtime discovery will execute, ambiguous
install ownership fails closed, integrity resolution has one owner and no longer
labels npm SHA-1 shasums as SHA-256, and code plugins disclose that hooks
register at runtime instead of rendering an empty "no hooks" row.
* fix(gateway): register plugins.inspect in method inventories and regenerate protocol
Adds plugins.inspect to the advertised-method inventories (widening the
fixed-size slice windows so older indices stay stable), regenerates the Kotlin
protocol bindings, drops an unused exported type, and replaces two nested
conditional spreads with a plain conditional.
* refactor(plugins): split oversized consent modules and clear lint findings
Extracts the MCP controller out of the plugins page, unchanged-install
reconciliation out of update-installed, and the install lifecycle suite out of
the management-service tests, bringing all three back under the max-lines limit
without suppressions. Also renames a shadowed binding, drops an unnecessary
generic, removes a spread-to-modify in a map, and types catch callbacks as
unknown.
* chore(protocol): regenerate Kotlin bindings after rebase
* feat(plugins): let chat /plugins install review and accept capabilities
The consent gate applies to chat installs too, but the command had no way to
give consent, so external installs dead-ended on a CLI-only flag. Chat now
replies with the plugin's declared capability surface and the exact command to
rerun, and accepts a trailing --accept-capabilities mirroring the existing
--force acknowledgement. ClawHub trust acknowledgement stays CLI-only.
Staged-artifact verification is unchanged: the reviewToken is still checked
against the final artifact before acceptance is recorded.
* refactor(plugins): single-source the declared-surface groups and manifest precedence
The ordered capability group list was defined independently in the consent
engine, the protocol error reader, the CLI formatter and the Control UI, so a
new contract family had to be added in four places with nothing enforcing it.
All four now derive from one canonical list in the protocol schema with a
compile-time exhaustiveness guard.
Native-versus-bundle manifest precedence is centralized in one helper that both
discovery and staged consent call, so the two cannot drift again — that
divergence was a real bug where consent read one manifest and the runtime
executed another.
Also documents that carrying acceptance forward requires pinned artifact
integrity, so integrity-less sources such as local paths ask on every install.
* fix(plugins): enforce reviewed consent across activation flows
Route setup, repair, linked installs, updates, and chat activation through artifact-bound capability consent. Reuse canonical package discovery and recheck staged activation before config publication. Invalidate stale Control UI review requests on reconnect.
Verified focused owner and sibling tests, runtime rebuild, and real isolated CLI/Gateway install, inspect, enable, widening, and stale-token rejection flows.
* test(plugins): cover beta installs through capability consent
* test(plugins): align consent fixtures with staged artifacts
* fix(ui): review staged plugin capabilities once
* test(ui): inline the remaining plugin consent confirmation
* test(plugins): verify consent with deferred install transactions
* refactor(setup): share inference execution plan construction
* test(ui): settle applied config before deferring refresh
* fix(plugins): protect consent provenance and reuse acceptance
* fix(i18n): keep surrogate pairs intact when bounding process output tail
appendBoundedProcessOutput used nextText.slice(-maxChars) to keep the
newest maxChars of captured process output. When the boundary landed
inside a UTF-16 surrogate pair (e.g. emoji in stderr), the retained
tail began with a dangling low surrogate, corrupting downstream JSON
serialization and fatal TextDecoder paths.
Switch to sliceUtf16Safe(nextText, -maxChars) from normalization-core,
which adjusts the boundary off the surrogate pair. The helper was
already imported elsewhere in the dependency graph.
* fix(i18n): count actually-dropped units when bounding surrogate-safe tail
Address ClawSweeper P2 finding on PR #120114: sliceUtf16Safe may
return fewer than maxChars code units when it advances past a low
surrogate at the boundary, so truncatedChars must derive from the
actual retained tail length rather than maxChars. For the emoji
case ("ab😀cdef", maxChars=5) the safe slice retains "cdef" (4
units), so 4 units are dropped, not 3.
Compute truncatedChars from nextText.length - text.length and
update the regression test expectation to 4.
* feat(tooling): enforce noUncheckedIndexedAccess in the scripts lane
Burns down all 153 scripts-lane errors (bench aggregation, release
checks, i18n inventories, argv indexing) and flips the flag in
tsconfig.scripts.json. Direct-Node-executed release harness scripts use
local narrowing instead of workspace imports, which do not resolve
under plain node execution. Benchmark measured loops untouched.
* fix(scripts): import expect helpers via relative package sources
tsconfig path aliases resolve from cwd under tsx, so release wrapper
scripts running against old release target cwds could not resolve
@openclaw/normalization-core (not a linked root dependency). Relative
package-source imports match the established pattern on the adjacent
lines and are cwd-independent; old-target planning verified directly.
* feat(ui): cron automation-ideas gallery and Create & run now in quick-create
The cron page gains a curated automation-ideas gallery (six starter
automations that pre-fill the quick-create wizard with editable prompt,
name, schedule, and delivery presets); it stays expanded until the first
job exists, then collapses to a slim band. The quick-create wizard's
final step adds Create & run now, which chains cron.run (force) on the
job id returned by cron.add so a fresh automation shows output
immediately instead of waiting for its first scheduled tick.
Also hardens scripts/control-ui-i18n.ts against models wrapping JSON
replies in Markdown code fences.
Closes#104746
* chore(ui): regenerate locale bundles for cron suggestions strings
Generated via authenticated ui:i18n:sync (claude-sonnet-4-6) for the new
cron.suggestions/quickCreate keys; keeps the locale structural-alignment
gate green.
* fix(ui): deliver watchdog suggestion results via notify preset
Codex review: the night-watch idea promised alerts but pre-selected the
isolated preset, which maps to deliveryMode none, so results were never
delivered. Use notify and reword the prompt to an explicit one-line
verdict (renamed key so the locale pipeline retranslates).
* chore(ui): regenerate locale bundles for watchdog suggestion
* chore(ui): refresh raw-copy baseline after rebase
* refactor(ui): move dreams page modules to agents/memory (verbatim)
* refactor(ui): sidebar IA cleanup: Activity to Settings, Dreams into Agents Memory tab, single MCP mutation owner, Usage icon
Folds the per-agent Dreams page into the Agents page as a Memory panel and
deletes the dreams route; moves the session-scoped Activity page out of the
sidebar into Settings > System next to Logs; removes the MCP enable/disable
toggle from Settings > MCP so the Plugins page is the single mutation surface
for config.mcp.servers; gives Usage a coins icon so it no longer collides
with Overview.
Closes#104590
* docs: update Control UI docs for Activity/Memory/MCP sidebar changes
* fix(scripts): fail control-ui i18n sync closed on new untranslated keys without provider auth
An unauthenticated ui:i18n:sync silently recorded English fallbacks, which the
shipped-fallback CI gate then rejects; post-merge translation is owned by the
control-ui-locale-refresh workflow. Sync now errors on new untranslated keys
without a provider (OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL=1 opts back in),
and the ui AGENTS guide documents the commit-en.ts-only contributor flow.
* chore(ui): translate new sidebar keys, refresh i18n baseline, localize Memory tab expectation
Refactor the Control UI around route-owned page lifecycle and state while preserving existing behavior and design.
Prepared head SHA: bd51b6fa76
Co-authored-by: Shakker <165377636+shakkernerd@users.noreply.github.com>
Reviewed-by: @shakkernerd
* feat(gateway): add device.pair.setupCode RPC for connect QR
The OpenClaw mobile/companion app scans a pairing setup code to connect to the gateway, but that code + QR could only be produced by the openclaw qr CLI (ASCII to stdout). Non-terminal clients driving onboarding had no way to display the connect QR.
Add a device.pair.setupCode gateway method that reuses resolvePairingSetupFromConfig + encodePairingSetupCode + renderQrPngDataUrl to return { setupCode, qrDataUrl?, gatewayUrl, auth, urlSource }. The embedded setup code mints a short-lived bootstrap token that hands off broad operator scopes (read/write/approvals/talk.secrets), so the method requires operator.admin (matching the wizard methods a companion already uses) and is not advertised. auth is a label only; the gateway credential is never returned, and an oversized QR is omitted so the response always satisfies the result schema.
Refs #94661.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(ui): pair mobile devices from Control UI
* docs: refresh generated docs map
* docs: clarify mobile QR auto-connect
* docs: clarify mobile QR auto-connect
* docs: clarify mobile QR auto-connect
---------
Co-authored-by: Barbara Kudiess <76582160+bkudiess@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>