* fix(docker): harden runtime image dependencies
* chore(deps): update container security dependencies
* docs(docker): explain image security contents
* test(browser): align file-chooser and install tests with #114506 contract
* test(browser): restore extension install test isolation
* test(browser): add temporary CI diagnostics for pre-registration refusal
* test(browser): make install fixture interpreter hermetic
The suite passed process.execPath as the native-host interpreter; on
GitHub-hosted runners the hostedtoolcache node binary is group/world-
writable, which installChromeExtensionBootstrap correctly refuses, so
every registration test failed CI-only. The fixture now provides an
owned 0700 interpreter; only the launcher-exec test keeps the real
node it must spawn.
* fix(qa-lab): stop re-polling after a probe consumes the discovery deadline
The Matrix health-probe loop re-entered when the probe timeout fired
marginally before Date.now() crossed the deadline, starting a doomed
extra probe. Flaked on contended CI runners as 'expected 1 fetch, got
2'. A timed-out probe now ends discovery.
* test(ui): poll the callout inset invariant in device-scope E2E
One-shot boundingBox reads raced the nav-collapse transition and
intermittently measured a 20px stale offset on CI.
* fix(ui): make active visibility pill distinct from its hover state
* test(ui): advertise sessions.create in the control-ui mock harness
* ci: refresh pull_request run after zombie-queued Blacksmith jobs
Use the 400, 560, 640, 768, 900, 1100, and 1320px max-width ladder. Round noncanonical thresholds up to the next rung so compact layouts engage before desktop layouts become cramped.
Move the 1180px agent-tool summary breakpoint to 1320px because the persistent desktop rail leaves about a 1020px content column at that viewport; 1100px would coincide with the rail disappearing and the content expanding.
Stylelint now enforces the ladder and the existing compound-query exceptions.
Add semantic, presentation, brand, and shared highlight tokens in base.css.
Remove dead fallbacks and consolidate dark/light syntax colors behind tokens.
Enforce color-no-hex for CSS while exempting token definitions and sprite artwork.
The DOMPurify afterSanitizeAttributes hook added class
chat-link-tail-blur to every rendered chat link whose href merely
contains the substring "tail" (docs.openclaw.ai/tailscale, /details/,
retail, ...). No stylesheet or script anywhere consumes the class: it is
an orphaned producer that survived the Control UI import, polluting
sanitized HTML and the markdown LRU cache, and priming a foot-gun where
any future .chat-link-tail-blur CSS rule would visually blur docs links
in every transcript.
Link presentation classes are owned by markdown-parser.ts
(web-link-classes, hostname-based); delete the substring classifier.
* feat(ui): add mobile desktop document mode
Add a shell-free mobile desktop route that reuses the dock panel controller and lazy noVNC client, with source preselection, touch controls, keyboard input, and retryable inventory failures.
* feat(ios): add desktop viewer entry points
* feat(android): add desktop viewer
* fix(android): keep System Back inside the desktop viewer
The per-session viewer replaces SessionDashboardScreen in place instead of
pushing a shell tab, so System Back fell through to the shell-level handler
and popped the whole Dashboard tab, stranding the operator on Chat. Claim
Back while the viewer is showing.
Also carry over TerminalSettingsScreen's imePadding: the viewer's own touch
toolbar hosts the keyboard affordance, so the soft keyboard would cover it.
Proof (emulator, Medium_Phone_API_36.0, stub control UI on 18789):
pre-fix Back from the viewer lands on Chat; post-fix it returns to Dashboard.
No Robolectric regression test — no existing screen test constructs
MainViewModel, and BackHandler under Robolectric would need new scaffolding
for weaker evidence than the live repro.
* test(ui): stop the pairing views leaking dialogs into the shared document
`ui/vitest.config.ts` runs the unit project with `isolate: false`, so test files
share one jsdom document inside a worker. `view.pairing.test.ts` appends a
container to `document.body` for every case and never tears down, unlike its
sibling `channels-page.test.ts`, so whichever suite the worker scheduled next
inherited a mounted pairing dialog.
That surfaced on this PR's first CI run as ten failures in the untouched
`input-dialog.test.ts`, which found "Approve DM access" where it expected
"Rename session". A rerun went green, so the ordering is scheduler-dependent
rather than deterministic; this removes the contamination source rather than
leaving the next suite to lose the race.
Not a proven fix for that specific run — the leak reproduces only under CI's
file scheduling, and the full suite passes locally either way — but the missing
teardown is a real violation of the shared-environment contract.
* test(ui): stop the background-tasks rail asserting on a ticking clock
The rail e2e captured the main transcript's text before opening a task detail
and required it to be byte-identical afterwards. A running task renders a live
elapsed label, so the assertion failed whenever a second ticked over between
the two reads — twice while landing this PR, both times "12s" against "13s"
with no other difference.
Normalize elapsed labels on both sides instead of weakening the assertion. The
invariant it protects, that opening a detail leaves the main transcript alone,
still holds: a real content change is still caught, and only complete duration
tokens collapse, so diffstat figures like +14/-3 and phrases like "5 messages"
are untouched.
* feat(ui): sync browser viewport to panel
* docs(web): note browser panel follows dock size
* refactor(ui): split browser panel input handling out of the controller
* refactor(ui): keep browser panel input host interface module-local
* fix(ui): fence browser viewport sync on dock close and document change
ClawSweeper review findings: a debounced resize could fire after the dock
closed (silently resizing the agent-controlled browser), and the per-target
resize guard survived tab/document changes, suppressing a needed re-sync when
revisiting a tab an agent had resized. Gate syncViewport on the panel being
open and clear the guard in invalidateViewOperations; both paths now have
regression tests that fail pre-fix.
* fix(ui): restore the composer model picker dialog
Partial revert of #122964: the chat/new-session composer returns to the
rich searchable model picker dialog (provider groups, CLI-agent targets
folded inside). Settings pages keep the shared unified picker.
* test(ui): restore chat-model-picker in the Web Awesome ownership inventory
The restored composer picker legitimately owns its details-menu and ARIA
combobox usage; #122964 had removed it from both expected lists.
* test(ui): stop leaking pairing-prompt modals into document.body
view.pairing.test.ts mounted containers (including an open
openclaw-modal-dialog from the approve-prompt tests) into document.body
without cleanup, poisoning later dialog-owning test files in the same
worker. Surfaced by this branch's changed test scheduling in checks-ui;
input-dialog.test.ts then saw the leaked 'Approve DM access' dialog.
* fix(ui): keep a persisted model override selectable when the catalog drops it
Port #122964's synthesized-override row into the restored composer
picker: a session pinned to a retired/unavailable model gets its row in
the dialog so the selection stays visible and escapable. An entirely
empty catalog keeps its no-models state.
* fix(ui): reconcile #123085 with the restored composer picker
The rebase merged #123085's wa-select label-width mechanics into the
restored dialog picker: its chat/layout.css hunks dropped the old
trigger reservation while its tests asserted the new geometry, and its
new-session e2e block targeted a wa-select that no longer exists there.
Restore the pre-#122964 chat layout CSS and responsive test, and drop
the unified-picker label block from the new-session e2e. #123085's
shared select-picker fix stays for the settings surfaces that use it.
* refactor(plugin-sdk): replace API baselines with diffs
* perf(plugin-sdk): bound API diff resources
* fix(plugin-sdk): isolate API diff dependencies
* fix(release): forward Plugin SDK acknowledgement
* fix(release): enforce SDK acknowledgement on publish
* chore: preserve generated-doc ignore policy
* fix(release): freeze SDK API evidence before publish
* fix(ci): satisfy SDK evidence guards
* fix(release): bind complete SDK evidence
* fix(release): authenticate plugin SDK evidence
* fix(plugin-sdk): abort interrupted API diffs
* test(ui): freeze page clock in background-tasks rail e2e
The rail transcript is compared byte-for-byte across the detail-panel
round-trip while it renders live relative ages; on slow CI runners the
second boundary ticks between the two reads (11s -> 12s) and fails the
equality assertion. Fix the page Date with Playwright setFixedTime while
keeping timers running so the tasks.list polling assertions still hold.
Repro: a 1.5s stall between the reads fails pre-fix with the exact CI
diff and passes post-fix.
* fix(scripts): drop unused export on dependency-evidence CLI main
Knip's workflow scan re-roots script references after an actions/checkout
step that sets path:, so the new trusted-tooling checkout in
openclaw-npm-release.yml stops marking this CLI as a workflow entry and
its exported main() surfaces as an unused export in check-dependencies.
Nothing imports main; the module invokes it through its own entry guard,
so the export keyword was dead surface either way.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): click the desktop to take control
View-only desktop connections now escalate to control by clicking
anywhere on the desktop stage (transparent overlay button, keyboard
accessible, same 'Take control' accessible name). The toolbar button is
removed; the connecting status overlay becomes click-through so control
can still be requested mid-connect.
* fix(ui): use cursor-action token for desktop take-control overlay
* test(ui): prove take-control click above a real noVNC surface
In-page RFB 3.8 fake server (security None) lets the production
DesktopClient drive the real noVNC client; the overlay click must
hit-test above the mounted canvas and reconnect with control.
* fix(ui): narrow fake RFB socket payload to ArrayBuffer-backed bytes
* fix(ui): keep picker labels readable on mobile
Preserve readable unified model and channel option labels while allowing new-session mode controls to wrap at narrow widths.
* fix(ui): propagate shared picker minimum
* feat(protocol): add portal methods and event
Bump the reviewed protocol owner-module count from 55 to 56.
* feat(gateway): add portal service and reverse proxy
* feat(agents): add portal tool
* fix(gateway): refine portal URL and proxy auth
* refactor(gateway): keep portal helper types private
* fix(gateway): declare portal transport service
* test(gateway): satisfy portal proxy lint
* test(gateway): narrow websocket payload types
* refactor(protocol): compact portal schema exports
* fix(gateway): export portal protocol types
* feat(ui): add portals page
* docs(gateway): add portals guide
* fix(gateway): dial portal targets via localhost dual-stack
Vite and other Node >=17 dev servers bind ::1 only for localhost, so a
fixed 127.0.0.1 dial 502s on the default path. Use hostname localhost
with family autoselection and rewrite Host to match.
* fix(gateway): type portal dual-stack connection
* fix: satisfy portal integration gates
* fix(gateway): isolate portal cookie jars per target
Cookies are hostname-scoped, not port-scoped, so the per-port origin
split alone let Gateway plugin-auth cookies reach agent-run targets.
Forward only cookies carrying this portal's own name prefix (stripped),
rewrite target Set-Cookie names to the prefixed form incl. the WS 101
handshake, and drop Domain attributes.
* fix(ui): detect unreachable portals behind proxied gateways
Probe the portal origin from the browser (no-cors, 4s timeout) and show
a recovery notice with the gateway-host URL instead of a dead iframe
when only the gateway port is exposed (Serve/Funnel/reverse proxy).
Docs: cookie isolation + reachability; zh-CN glossary entry.
* test(ui): satisfy portal reachability lint
* test(gateway): provide control UI request hosts
* chore(protocol): regenerate after rebase
* fix(gateway): namespace portal auth cookies by listener
* fix(gateway): scope portal token URLs to write-capable clients
The portal bearer token rides in the summary url/tokenQuery; portal.list
is operator.read and portal.changed fans out to read subscribers, so a
read-only client could harvest an openable URL. Make those fields
optional, redact them from read-scope list responses, and drop them from
every portal.changed broadcast; write/admin clients still receive them
and the UI refetches the list on change.
* docs(web): list the portals route
* fix(gateway): type portal open credentials
* docs(gateway): clarify portals PORT/PUBLIC_URL are agent-set
Opening a portal creates only the proxy listener; the agent sets PORT
and PUBLIC_URL in its own exec command, matching the portal tool
contract. Removes the implication of an automatic env handoff.
* chore(protocol): regenerate portal models
* style(gateway): format portal method-order assertions
Rebase union-merge left the portal.list assertion wrapped; oxfmt fits it
on one line.
* chore(plugin-sdk): refresh API baseline after rebase
* chore(plugin-sdk): refresh API baseline after rebase
* chore(protocol): refresh portal event order after rebase
* chore(plugin-sdk): refresh API baseline after rebase
* fix(gateway): pin portal referrer policy to no-referrer
The portal URL carries its bearer token in the query, and upstream
response headers are copied verbatim, so a target answering with
Referrer-Policy: unsafe-url could leak that URL to every third-party
origin it references. Force no-referrer after the copy and drop any
inbound Referer that still carries the token before forwarding.
* refactor(ui): unify task details in sidebar panel
* docs(ui): describe task detail sidebar
* refactor(ui): collapse onSelectTask into onOpenTaskDetail
Two names for the same open-the-panel action; rows now call the
callback directly.
* fix(ui): clear rail open-task highlight when detail slot closes
openTaskId derived from sidebarContent alone, but region close only
collapses the layout and retains content, leaving a stale
aria-current row; gate the highlight on detail-slot visibility.
* fix(ui): keep requester sessions out of subagent transcripts and let reopen retry failed lookups
A queued subagent has no child session yet; falling back to its
sessionKey rendered the requester's conversation as the task
transcript. And a failed tasks.get permanently suppressed the panel's
render-driven load; selection now clears the recorded error so
reopening retries, matching the old rail behavior.
* fix(ui): unify channel pickers with icons
Preserve channel values and mutation ownership across Automations, pairing, setup, and Workboard while sharing accessible picker rendering and artwork.
* refactor(ui): unify model pickers
Share provider-aware model selection across chat, new sessions, Automations, Agents, model defaults, Session Observer, Talk, and Memory while preserving owner-specific values and locks.
* fix(ui): keep picker change callbacks value-only
Keep the Web Awesome element private to model-picker custom-mode handling while preserving one-argument callbacks for every picker consumer.
* fix(ui): satisfy model picker types
Keep custom sentinel options within the shared model option shape and guard optional Memory placeholder translation keys.
* refactor(ui): remove obsolete model option template import
Drop the Lit import after Agents model options became pure shared-picker data.
* fix(ui): satisfy picker lint gates
Use explicit model option mapping, remove stale E2E locals, and keep the Cron behavior suite within its enforced line budget.
* test(ui): target unified session model picker
* fix(ui): defer GitHub project clone until submit
Keep remote project selection as draft state and materialize it before sessions.create on Start Session.
* fix(ui): consolidate deferred project selection
* test(ui): drop duplicate remote project assertion
* fix(skills): keep ClawHub publisher identity from search through install
ClawHub search returns one entry per publisher, so several results can share a
slug. Every client collapsed the selection to that bare slug before calling
skills.detail and skills.install, and ClawHub answered 409 AMBIGUOUS_SKILL_SLUG
with no in-product way forward.
searchClawHubSkills now records the publisher-qualified reference once, on the
result that carries it, and the Gateway protocol documents it. skills.detail
parses the same reference grammar skills.install already accepted, so review and
install cannot resolve to different publishers. Control UI carries that one
reference through row actions, detail, busy state, and acknowledgement retries,
and shows it so otherwise identical rows are distinguishable.
Fixes#117633
* fix(apps): send the ClawHub publisher reference from native skill browsers
macOS, iOS, and Android read the qualified reference from search results and use
it for skills.detail, install, busy state, installed matching, and list identity,
so two publishers sharing a slug stay distinct instead of collapsing into one
ambiguous request.
* fix(skills): refuse external-source skill detail instead of reading a same-slug skill
ClawHub has no source-qualified read endpoint, so a skills-sh reference parsed
down to its bare slug would have returned a registry skill's card while install
resolved the external artifact. Review and install could name different skills.
skills.detail now fails closed on any reference that carries a source, and the
macOS and AgentPro rows show the publisher reference next to the summary instead
of only when a summary is missing, so same-slug rows stay distinguishable.
* chore(apps): refresh native i18n source baseline for the skill row references
* refactor(skills): drop the unread search-result ownerHandle field
installRef is the one reference clients send back, and no client reads the
publisher handle separately, so the protocol and Control UI carry one field
instead of two.
* fix(skills): name the next step when external skill detail is refused
Clients that gate install behind a successful review would otherwise see only a
refusal, so the error names the direct install path and the CLI equivalent.
* fix(macos): use a doc comment on the ClawHub row subtitle
swift-format's docComments rule requires doc comments on declarations; the
subtitle property carried a regular comment and failed macos-swift.
* fix(skills): carry ClawHub trust state to clients that can install
Forwarding installRef let clients install the exact publisher the operator
picked, including external skills-sh sources. It did not forward the trust
state that says ClawHub never scanned that source, so iOS AgentPro — the one
surface that installs in a single tap with no review step — could install an
unscanned artifact with nothing on screen saying so. The CLI already labels
these (docs/clawhub/cli.md, docs/cli/skills.md); native clients could not,
because trustState was never on the wire.
trustState becomes an optional field on SkillsSearchResultSchema. It is purely
additive: older clients ignore an unknown key and the field is absent for
registry results, so downgraded readers are unaffected and no protocol version
moves.
Every client that renders a search row now shows "Not scanned by ClawHub",
matching the CLI wording exactly: iOS AgentPro in the row above the install
button, macOS and Android beside the review action, and Control UI on the row
that explains why review is refused for these sources.
Covered by a wire assertion that the state reaches clients for an external
source and stays absent for registry rows, plus decode-and-label tests on the
shared Swift kit and the Android parser, and a Control UI render assertion.
* fix(ui): size the ClawHub detail dialog to a refusal message
Refusing detail for an external source made an error-only dialog reachable.
The shared preview panel reserves a tall reader height for skill documents, so
a two-line refusal rendered in a mostly empty dialog and read as broken rather
than deliberate. Found by inspecting the review captures.
* revert(ui,apps): drop the ClawHub trust label layer
Maintainer product decision: skills.sh runs its own scanners, so OpenClaw does
not add a second alert layer in the apps. Removes the label from Control UI,
iOS, macOS and Android, and drops the trustState wire field that nothing would
render. The CLI keeps its existing label; changing that is a separate call.
Publisher identity, the fail-closed detail refusal, and the message-only dialog
are unchanged. Splits the oversized skills view test file to satisfy max-lines
without a suppression.
* test(ui): fix ClawHub skill fixture checks
* chore(plugin-sdk): refresh API baseline
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* feat(ui): open subagent details in chat sidebar
* chore: drop changelog edit (release generation owns it)
* refactor(ui): drop duplicate close in subagent detail panel
The sidebar region header already owns a Close Details control in both
wide and narrow layouts; the panel-local X duplicated it 40px away.
* fix(ui): stop subagent transcript loader when pane presentation retires
Pane retention wipes sidebarContent directly, so the detail slot's
render-time reset can never run again; a pending refresh timer plus
incoming task events kept refetching chat.history for a hidden panel.
* docs(ui): note close-control ownership in subagent detail header
* fix(ui): break transcript renderer import cycle
* fix(ui): use shared action cursor for subagent rows
* fix(ui): keep composer model picker stable while typing
* test(ui): prove typing keeps model picker stable
Co-authored-by: Colin Johnson <colin@solvely.net>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): cascade session stop to subagents
Cascade exact and session-wide aborts through the subagent registry and keep Control UI Stop visible while descendants remain active.
* refactor(gateway): absorb stop cascade orchestration
* chore(gateway): refresh plugin SDK API contract for abort refactor
Regenerates the export-closure contract after the stop-cascade refactor
moved gateway abort orchestration exports, and drops a redundant Boolean()
wrapper flagged by lint. Public SDK surface gate is unchanged.
* chore(gateway): regenerate plugin SDK API baseline
A steer rejected on the run/leaf fence parks as a failed steered queue row.
Retry previously refused whenever the target run was gone, leaving the
user's message permanently stuck with 'that run is no longer active'.
Retry now converts the parked row into a plain queued send when the session
is idle, so the text delivers as a fresh turn; active-run retries keep the
original steer binding, and disconnected retries keep the existing error.
* feat(ui): show environment facts in Where picker
Preserve environment platform, trust, session-host, and capability metadata through new-session discovery while keeping placement availability tied to live executable nodes. Render bounded quiet device/cloud facts and add mocked browser coverage for task #33.
* docs(plan): track picker environment facts
* refactor(ui): split place browser rendering
* test(microsoft-foundry): remove global test bridge
* test(microsoft-foundry): type boundary fixtures
* perf(ui): keep route transition out of startup
* fix(ui): constrain composer capability menus
* fix(ui): cap composer capability menu height
* test(ui): cover long capability menu states
* test(ui): capture capability menu interaction states
* fix(ui): keep dark capability states legible