* feat(ui): show activity cards for online people
Record live person timing independently of heartbeat freshness and expose keyboard- and touch-accessible cards with visible session links. Preserve continuous online intervals across overlapping tabs without adding persistent activity history.
Closes#130649
* build(protocol): refresh generated presence timing fields
* fix(gateway): scope presence snapshots to read-access operators
Apply one recipient projection to hello, system-presence, and events. Keep person timing and time-zone details behind read access, and filter watched references with the canonical session-list policy without leaking hidden counts. Preserve idle-person metadata for readers and prevent non-reader activity-driven frames.
* fix(plugins): preserve Gateway capability load context
Carry the owning registry's metadata generation, workspace, install records, and built-artifact preference through capability discovery. Preserve standalone source loading and existing speech eligibility while avoiding synchronous source transforms on cold Talk catalog requests.
Fixes#130777
* test: align integrated presence and capability coverage
Keep the recipient matrix in its timing-aware owner, close the suite-owned Gateway for shutdown proof, and spy on the canonical install-record reader. Remove the retired private carrier's assertion allowance.
* fix(gateway): broadcast presence when clients connect
Publish completed connection rows through the canonical scoped broadcaster so established readers see first connections and reconnects without waiting for profile edits or activity. Preserve shared online intervals and reset them only after the final socket closes.
* fix(ci): align presence fixtures and sidebar type ownership
Reuse the canonical sidebar host type to remove the type-only lazy-runtime cycle. Assert omitted scopes through hello auth and denied presence reads, and supply presence version callbacks in the shared WebSocket context fixture. Retain startup admission and drain assertions without changing deadlines.
* fix(ui): retain latest scroll commands until their destination
Do not treat the virtualizer's idle debounce or a decreasing offset as reader takeover. Settle at the actual DOM end within one pixel, retaining the separate eight-pixel UI-follow policy and explicit input cancellation. Cover retargeted idle delivery and near-end precision with the real dependency, synchronize pointer baseline capture, and type the existing composer snapshot as its textarea contract.
Fixes#130892
* fix(android): retain Talk playback ownership through cleanup
Keep local request leases and realtime playback facts in one synchronous state owner. Preserve the underlying Talk status across temporary playback phases, retire stale cleanup, and keep reported failures visible until a new action.
* fix(android): scope Talk playback ownership to speaking state
Keep the canonical local playback lease and realtime speaking fact so retiring playback cannot clear an active replacement. Restore the existing independent status flow and retain all five speaking-state regressions; status and turn correlation remain separate follow-up work.
* 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
* refactor(state): make cron and subagent rows JSON-canonical
* refactor(state): make gateway origin device tokens canonical at v13
The lazy ensure predates the table joining the canonical schema; at the
v13 bump the schema owns creation, so the feature-local DDL, WeakSet
dedupe, and lazy-list entry retire. The legacy-file guard the ensure
carried stays at each call site.
* test: drop obsolete lazy-ensure coverage for origin device tokens
The table is canonical at v13; same-version lazy creation no longer
exists to protect. Origin CRUD, isolation, and rotation coverage remains
in the surviving cases.
* refactor(state): fold installed_plugin_index into config_machine_state
The singleton index row becomes one JSON value under
plugins.installedIndex with its rollback-fencing revision inside the
value; reads, CAS restore, and the lease-held write transactions use
direct Kysely on config_machine_state so the state_leases assertion
stays in-transaction. The v13 migration imports the row and drops the
table; the additive workspace_dir entry folds with it. Doctor guidance,
docker staging, and the e2e probes name the machine-state row.
* refactor(state): merge workspace_attestations into workspace_setup_state
One row per workspace now carries both setup milestones and the
attestation clock: nullable setup columns represent attestation-only
workspaces (replaceWorkspaceAttestation can precede any setup write) and
setupExists derives from a non-null version. The bootstrap-hash FK
repoints to the merged table; migration receipts keep the historical
workspace_attestations discriminator string. The v13 migration grows and
rebuilds the table, merges attestation rows (orphans without a path
alias drop — their hashes re-derive at the next bootstrap attestation),
and the consolidation kind is renamed state-consolidation-v13 to cover
the batch.
* test(state): cover the workspace merge and consolidation fallout
The v12-to-v13 regression seeds merged, attestation-only, and orphan
attestation workspaces; the 13-to-12 downgrade fixture recreates
workspace_attestations and installed_plugin_index from the folded data;
the fold-in migration gates the additive workspace_dir column for
pre-additive rows; the workspace merge now triggers on the setup table's
own shape so stable-era databases without an attestations table still
reshape; the consolidation applied-message covers the batch.
* refactor(state): fold shared auth profile singletons into config_machine_state
The shared-state auth_profile_stores/auth_profile_state rows (fixed key
'shared') become authProfiles.store/authProfiles.state machine-state
values; the agent-DB tables of the same names are untouched. Git-backup
redaction moves from table-drop to the authProfiles. secret prefix with
seeded-secret absence proof; migration receipts keep the historical
table-name discriminators; the shared-auth relocation and receipt
verification project the KV cells back to the receipt-era row shapes so
persisted digests stay byte-compatible. mcp_oauth_stores stays a table —
its multi-key fold is a named follow-up.
* test(state): finish shared-auth fold coverage and annotate boundary casts
Auth seeders and assertions across the e2e/scripts/secrets suites target
the authProfiles machine-state cells; the v12-to-v13 regression proves
payload-byte fidelity, non-shared-row drop, and insert-if-absent
precedence; the downgrade fixture recreates and repopulates both v12
tables. Boundary type assertions in the plugin-index store carry SAFETY
invariants per the ratchet.
* chore: shrink assertion-safety baseline for plugin-index store
* refactor(doctor): delete the dead onboarding-recommendations migration
Its input — the unscoped 'primary' onboarding row — existed only between
9a93a52a8a and 473962b7de, a two-day beta window; no shipped stable
can produce it and the runtime table folded away at v12. The audit
backup list keeps recognizing system-agent.jsonl artifacts because beta
installs that ran that import may still carry its backups.
* docs: sync the 13-to-12 downgrade example with the executable fixture
* style: format the synced downgrade example
* style: drop unused import and duplicate union constituent
* fix(state): keep orphan attestations across the v13 workspace merge
The merged workspace_setup_state required a workspace path, but legacy
orphan hashed-key attestations never recorded one. workspace_path is now
nullable (setup rows still enforce it via CHECK), the v13 migration and
the doctor file import keep orphans with a NULL path that heals on the
next live access, and the 13-to-12 downgrade keeps attestation-owned
hashes. Doctor test seeds move to the folded KV row.
* perf(state): retire unused cron indexes
* fix(state): preserve v13 migration recovery
* fix(state): preserve v12 lazy-table upgrade
* docs(state): document v13 auth relocation
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Share ticketed media URL resolution between iOS and macOS, retaining the connected gateway encoded context path and transport-owned TLS and credential policy. Remove the duplicate native URL and playback resolvers.
Fixes#130746. Related to Android #129957.
Revalidate the speech operation token before delayed cancellation cleanup stops the shared Apple synthesizer. Preserve active cancellation and explicit stop, and retain an opt-in real-audio regression for the replacement race. Fixes#130674.
Put the full-item equality boundary before expensive row preparation, preserving the existing disclosure state and diff semantics. Related to #124759; controlled shared-view updates improve without changing initial diff preparation.
* feat(apps): resolve the per-profile accent live on iOS, macOS, and Android
Named follow-up from #130340: native apps now fetch the caller's own
profile accent (users.prefs.get, strict #rrggbb normalization) and prefer
it over the gateway accent, refetching on users.prefs.changed — the
gateway targets that event at the caller's own profile, so clients need no
identity logic. macOS stores it separately from the seam color so
settings-pane config refreshes cannot clobber it, and fetches bypass
ControlChannel.request to avoid degrading the channel on older gateways.
Profile-less and token connections are unchanged. Removes the ios/android
users.prefs.changed allowlist entries now that handlers exist.
* chore(macos): satisfy swiftformat explicit-self on profileAccentHex
Reuse the existing Peekaboo live permission service so Quick Chat, Settings, screenshot gating, and computer-control diagnostics retain confirmed grants instead of re-reading stale CoreGraphics denials. Passive checks remain consent-gated. Document same-build grant recovery.
* test(macos): add debug menu fixtures and submenu-open capture hooks
* feat(macos): render automation summaries as hosted cards
The Automations/Usage/Devices rows were native items cramming count and
next-run into a fitted title that middle-ellipsized into garbage
('5 …s'). They are now hosted summary cards - title left, compact detail
right, own chevron - which escape the native title budget and pick up
the selection highlight. Automation submenu jobs become hosted rows
(name left, next run right, full name on the accessibility label) that
open the Automations page on click instead of dead disabled text.
* style(macos): swiftformat pass on menu fixture hooks
* test(macos): capture hooks accept CLI arguments
launchctl setenv races open(1), which made screenshot launches flaky;
--debug-open-menu / --debug-menu-fixtures / --debug-probe-rightclick now
work as arguments. The arrow-key submenu navigation never fired inside
the tracking loop and is removed.
* feat(ui): save appearance preferences per user profile
When a Control UI connection is bound to an authenticated user profile
(trusted-proxy, Tailscale, GitHub identity), theme, theme mode, and accent
become per-profile: stored in the existing user_preferences KV store via
users.prefs.set, overriding gateway-wide ui.prefs, with live cross-device
updates over a new additive users.prefs.changed event scoped to the same
merged profile. Restore default deletes only the profile key and falls back
to the gateway-wide value. talk.config projects the caller's profile accent
for native clients. Profile-less connections (token/password/none auth)
keep the existing gateway-config behavior byte-identically.
Release-note context: on multi-person gateways appearance choices are now
personal and follow you across devices; an admin restyling their own UI no
longer reskins the whole team.
* chore(protocol): allowlist users.prefs.changed for mobile clients
iOS and Android resolve the profile accent through talk.config on connect
and config refresh; live per-profile appearance push on natives is a named
follow-up.
* chore(protocol): regenerate Swift/Kotlin models for users.prefs.changed
* fix(ui): honor explicit defaults and identity switches for profile appearance
Addresses the three ClawSweeper review findings:
- resetValue for profile-bound appearance keys is the deletion fallback
(gateway value), so an explicit product-default selection persists as a
profile write instead of being misclassified as a reset.
- An identity switch between two known scopes forces a full appearance
reconcile and clears appearance keys the returning identity never set,
so a shared browser cannot keep the previous profile's look (boot keeps
the last-seen shortcut).
- talk.config joins the profile-dependent dispatch gate so a pending
GitHub identity sync completes before the profile accent is projected;
token clients pass through untouched.
* chore(ui): satisfy assertion-safety ratchet and docs formatting
* refactor(ui): extract server-prefs storage primitives
server-prefs.ts crossed the 700-line cap after the review fixes; the
stateless localStorage persistence primitives move to
server-prefs-storage.ts (no behavior change). Shrink-prunes the
assertion-safety baseline for the moved casts.
* fix(ui): keep imported custom themes out of profile storage
Custom palettes are browser-local by contract, so a profile must never
carry theme=custom to a browser that cannot render it. The wire contract
drops custom from the storable theme values (readers self-heal any stored
value), a profile-bound custom selection stays retained browser-local
(including the offline-queued path), and the exhaustive theme test now
encodes the exception.
* fix(ios): distinguish incomplete gateway setup from credential save failures
Record received and persisted device-auth roles separately, and centralize setup handoff failure handling without changing credential grants or persistence policy. Related: #108888. Thanks @rhubain for reporting the misleading recovery guidance.
* fix(ios): refresh handoff diagnostic localization inventory
* fix(ui): distinguish inherited model defaults from session pins
The Gateway session projection collapsed inherited agent defaults and
persisted conversation pins into the same effective model fields, so the
Control UI picker marked a row Default while its footer simultaneously
called it a session override and offered a meaningless reset.
Project the authoritative persisted provenance instead: the session owner
resolves `modelOverrideSource` (normalizing entries written before source
tracking), `buildGatewaySessionRow` and the session change event carry the
additive `"user" | "auto" | null` protocol field, and the Control UI treats
only `"user"` as a conversation pin.
Closes#120752
* test(ui): scope model provenance to active pane
* fix(ui): derive the model picker's inherited sentinel from recorded provenance
Deriving "inherits the agent default" from the effective model matching the
default meant a session pinned to model X stopped reading as pinned once the
agent default moved to X. The picker then rendered the default row as already
selected, so clicking it matched its own commit value and was swallowed, leaving
the stored pin unreachable from the UI.
Project the session store's existing modelOverrideSource onto the gateway row and
let the UI resolve a closed selection source (inherited/pinned/fallback) from it,
falling back to the old equality guess only for gateways too old to send the
marker. Preserve the null tombstone in reconcile so a cleared pin stays
distinguishable from that silence.
* test(ui): pin the model picker's digit-key ownership
The numbered keycaps and the digit handler are paired by search focus, but the
guard that keeps digits in the search input had no coverage outside the browser
e2e lane. Add a unit regression for both halves and record the contract at the
numbering site, where reviewers keep reading the two gates as unrelated.
* fix(ui): keep session model selection provenance authoritative
Project saved model provenance through Gateway rows and events, retire temporary UI claims at their owner, and use canonical refresh after slash and status model changes. Preserve model intent through rollback and deletion without retaining a public cache setter.
Co-authored-by: Vyctor H. Brzezowski <krzyszchweski@gmail.com>
* refactor(ui): keep recovery with scoped session operations
Preserve recovery notifications, visible errors, and connection checks while keeping the optimistic mutation owner within its module limit.
Co-authored-by: Vyctor H. Brzezowski <krzyszchweski@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(macos): stabilize status menu width across hosted and native rows
* fix(macos): format node worker termination status without optionals
* refactor(macos): give status menu width one owner
fittedTitle's hasImage/hasSubmenu/keyEquivalent parameters never changed
the computation - the budget always included image, submenu, and shortcut
chrome - because AppKit aligns those columns across the whole menu, so a
per-item budget is the wrong model. Collapse to one menu-wide budget and
drop the misleading knobs from all 13 call sites.
* refactor(macos): keep the status menu header quiet when healthy
A working system now says nothing: the header renders only operator-
actionable trouble lines (colored text, no status bubbles), the healthy
state is title + toggle + capability strip, and the churn sources that
made the open menu jump are gone - relative check ages, 'health check
running', and the per-tool activity label (the critter icon already
animates work). Session-row kind icons go monochrome; green conveyed
nothing actionable. Healthy title is 'OpenClaw' (or the primary gateway
name with multiple gateways) since the toggle already communicates
active. DashboardGatewayMenuModel.connectionLabel lost its last caller
and is deleted with its test.
* fix(macos): preserve structured node worker diagnostics in status menu
* feat(macos): highlight hosted menu rows from AppKit's selection signal
AppKit paints no selection behind NSMenuItem.view, so hosted session and
device rows had no hover or keyboard highlight. HostedMenuRowView now
draws the native selection material (rounded, emphasized) and flips the
SwiftUI content through the existing menuItemHighlighted environment,
driven by NSMenuDelegate.menu(_:willHighlight:) so pointer and arrow-key
navigation both light up. Submenus get a shared highlight-only delegate;
the controller's root-menu guards keep open/refresh paths unaffected.
* refactor(macos): drop the manual Open Canvas action
Canvas panels are agent-opened; a manual open action was noise. The
Allow Canvas capability tile stays - that is the permission. Removes the
menu action, the dock-menu item, and the now-orphaned
AppNavigationActions.toggleCanvas.
* fix(macos): clear hosted submenu selection when the submenu closes
AppKit sends no willHighlight(nil) on submenu close, so a hosted device
row selected there reopened still lit. The shared highlight delegate now
resets its menu's hosted rows in menuDidClose; regression covers the
close cycle.
* style(macos): apply swiftformat to highlight delegate
* refactor(state): fold singleton tables into config_machine_state at schema v11
Eight singleton tables (skill_curator_state, update_check_state,
clawhub_promotions_feed_state, model_catalog_remote, voicewake_triggers,
voicewake_routing_config, voicewake_routing_routes,
onboarding_recommendations) were each one logical JSON value behind a
fixed key; their bespoke schemas, lazy ensures, and per-table accessors
collapse onto the shared config_machine_state KV under namespaced keys.
cron_store_epochs retires outright: it was born write-only in #114388
and no reader ever existed in any language. Durable values (update
check state, voicewake triggers and routing, per-workspace onboarding
answers) migrate insert-if-absent during the v10->v11 migration; cache
class contents rebuild on next use. Deferred with named reasons:
exec_approvals_config (macOS direct-SQL contract), installed_plugin_index
(same-tx lease fence), node_host_config and web_push_vapid_keys
(secret-table git-backup redaction).
# Conflicts:
# src/skills/workshop/collection-review-state.ts
# src/skills/workshop/collection-review.gateway-admission.test.ts
* test: register v11 guard carve-outs and suppression pin
The v11 migration module joins the raw-SQLite allowlist (migrations are
the named guardrail exception), the lint-suppression allowlist records
the second type-parameter suppression in config-machine-state, and the
identity module keeps only externally consumed exports.
* test: surface CLI stderr when migration-diagnostic assertion fails
* test: expect migration diagnostics on stderr for models plain commands
The #129037 pending-migration cases asserted that aliases/fallbacks
lists never open the state database, but config-health observation
(observeConfigSnapshot -> readConfigHealthStateFromStore) full-opens it
on any config read whose file exists — reproduced identically on clean
main with a main-built dist. The protected contract is exact stdout;
the diagnostic legitimately lands on stderr for every case.
* test: drop unused defaults import from CLI stdout e2e
* test: split session path derivation out of oversized session-files suite
#130016 pushed session-files.test.ts to 1008 lines, over the 1000-line
lint cap and red for every PR's check-lint. The sessionPathForFile
describe moves to a self-contained sibling following the existing
session-files.*.test.ts split pattern; no assertions change.
* refactor(state): fold four more singleton tables into schema v12
tui_last_sessions (cache-class, regenerates on next session switch),
sidebar_sections (persistent section order, migrated as one JSON array),
node_host_config, and web_push_vapid_keys join the v12 fold-in, taking
the retirement to thirteen tables at the same version. The two secret
singletons were blocked on table-granular git-backup redaction; backups
now exclude config_machine_state rows by secret key prefix (nodeHost.*,
webPush.vapidKeys) with a fail-closed row filter and regression proof,
so STATE_SECRET_TABLE_NAMES sheds both tables. The sidebar fold also
retires its lazy-ensure WeakSet and inline DDL; sidebar edits stay
inside the existing session-group write transaction via direct Kysely.
* fix(node-host): omit absent Cloudflare Access config like the column reader
The KV rewrite returned gateway.cloudflareAccess as an own undefined
property where the retired column reader omitted the key; toStrictEqual
consumers (state-migrations doctor-repair test) caught the shape drift.
Mirror the column reader's conditional spread at both construction
sites.
* fix(backup): disclose redacted machine-state prefixes after restore
The prefix-granular secret redaction recorded omitted key prefixes in
the backup manifest but the restore result exposed only excludedTables,
so a redacted restore looked complete while nodeHost.* and
webPush.vapidKeys configuration were intentionally absent. The restore
result and CLI output now disclose the omitted prefixes (JSON mode
carries them via the result shape), with restore-side regression
coverage.
* fix(tui): compare-and-delete retired session pointers
Doctor cleanup read matching pointer keys then deleted them
unconditionally, so a replacement pointer written between the scan and
the delete was erased. The delete now re-checks the stored value inside
the write transaction and only removes pointers that still name a
retired session; a live replacement survives (regression covered).
Also corrects the stale schema-version line in database-first.md.
Replace the cancellable one-shot startup stream with a replayable result. Join process cleanup and durable SSH receipt retirement before admitting a replacement tunnel, and keep stale callers fenced across actor suspension.
* feat(portals): expose portals to sessions on node-backed cloud workers
Worker-turn sessions placed on cloud workers (bundled Crabbox) can now open
portals. The portal tool joins the closed worker session-tool catalog and
executes on the Gateway, which mints a single-use ticket per proxied
connection; the enrolled node redeems it over a TLS-pinned WebSocket via the
new private worker.portal.stream.v1 command and splices to the worker's
loopback port. The desktop stream broker is generalized with discriminated
desktop/portal tickets, and shared node stream transport is extracted from
the desktop stream command. Portal availability is negotiated via a new
gateway hello capability plus the node's reconnect-scoped
workerHost.portalStream declaration, so old node bundles simply do not
receive the tool. Environment teardown and owner-epoch replacement close a
worker's portals; node disconnects destroy live streams while the listener
keeps serving the waiting page.
Live-proven on a Crabbox AWS worker: agent opened a portal over the worker
protocol, python3 -m http.server on the box served through the Gateway
portal URL with token auth intact.
* fix(portals): break portal proxy/service type cycle; regenerate Swift protocol models
* fix(portals): drop dead PortalTarget re-export; fix test import
* fix(portals): only tear down turn-created portals on post-open authority loss
ClawSweeper finding: a revoked turn's duplicate open could close the live
portal a still-authorized predecessor established, because service.open
returns the existing same-environment portal and the executor's error path
closed whatever id it received. portal.open now reports created-vs-reused;
the executor tears down only portals it created and closes its redundant
carrier handle whenever an existing portal is reused.
* feat(portals): fence portal reuse behind the caller's live-authority assert
Rank-up move from review: portal.open revalidates the caller inside the
serialized operation before a reuse can mutate a live portal's metadata,
mirroring close(assertCurrent). Regression test proves a lapsed caller
cannot rewrite a reused portal's title.
* test(portals): expect assertCurrent in worker portal open call
* refactor(gateway): extract worker request dispatch into its own module
worker-connection.ts crossed the 700-line ratchet after rebasing onto
current main; dispatchWorkerRequest and its rejection helper move to
worker-connection-dispatch.ts unchanged (434 + 329 lines).
* feat(approvals): typed approval scope summaries on channel cards
Approval owners can attach a closed ApprovalScope union (message-send,
payment, external-post) describing an action's blast radius. The gateway
sanitizes it once at the producer boundary, the core view model renders a
Scope metadata row so Slack/Discord/Google Chat cards show it unchanged,
shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and
the durable presentation carries it additively for operator surfaces.
Scope is display-only, never authorization; missing scope keeps today's
cards.
* fix(approvals): emit native ApprovalScope union and clamp recipient previews
Name the three scope variants as registered protocol schemas so the Swift
generator emits the ApprovalScope discriminated union the presentation
structs reference, and commit the regenerated GatewayModels.swift. Clamp
recipient previews to the declared recipientCount at the sanitize boundary
so a count of 1 with 2 previews can no longer render inconsistently.
Addresses both ClawSweeper findings on #130116.
* refactor(approvals): extract text sanitizer to break the exec-approvals import cycle
check:architecture flagged approval-scope joining the exec-approvals SCC
through exec-approval-command-display. Move the self-contained display
sanitizer into a leaf module (exec-approval-text-sanitize) with no
exec-approvals imports and migrate all sanitize importers; command-display
keeps only the payload-typed command/preview resolver.
* chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction
The approval display sanitizers left the publicly reachable SDK graph when
they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338,
callable exports 2582 -> 2578. Shrink-only budget pin.
* feat(secrets): agent-requested credentials the model never sees
The new main-session secrets tool lets the agent request a credential by
name: the human enters the value in a masked question card (Control UI,
/ask/<id> deep link, iOS/macOS/Android), and the gateway diverts the
answer straight into the shared secret store at question.resolve. The
record, broadcast, waitAnswer, tool result, transcript, and model context
only ever carry a synthetic stored marker.
- protocol: additive secretStore binding, secretStoreExisting replacement
metadata, and resolve-time secretStoreAllowedHosts (since 2026.8)
- gateway: store-bound question validation, admin-gated minting (blocks
questions-scope self-answer escalation past secrets.store.set), shared
redaction-first store write service reused by secrets.store.set
- tool: secrets request/list/delete; write-only by design, delete carries
verified agent runtime identity; channel delivery is link-only so chat
text is never captured as a secret
- Control UI: masked composer card with requester identity, store banner,
editable allowed hosts, replacement warning, retry-on-validation-error,
a standalone /ask/<id> page, and a named startup-JS baseline bump
- mobile: SecureField / password transformation for isSecret questions,
no answer echo in terminal summaries; new native string registered in
the locale-refresh inventory (generated artifacts stay workflow-owned)
- regression: claimed harness secret input stays out of session transcripts
Live-proven on an isolated dev gateway: real model turn, masked entry via
Playwright, value present only in secret_store_entries, absent from every
transcript, log, and the DOM.
* chore(protocol): regenerate protocol models and tool display
* fix(cli): read image string options through a typed helper
PR #129463 added four commander option narrowings in image.ts without
SAFETY coverage, leaving the assertion-safety ratchet red (21 > 17) for
every branch on current main. Replace the casts with a typeof-checked
read so the assertions are removed rather than annotated; each value is
still validated by its normalizer. SAFETY comments cannot work in this
file: the ratchet's raw scanner never rescans template tokens, so
comments after the first substitution template are unreadable to it.
* chore(protocol): refresh Swift models against current main
* chore(i18n): re-baseline the native inventory on current main
* docs(secrets): state the default-on tool policy and how to disable it
* fix(secrets): tell the model what the store actually does
The shipped tool description named the three actions and nothing else,
and no parameter carried a description. The model could not tell that
request blocks a human, that reason is shown to that human, what secret
and env select, or - the silent-failure case - that a secret stored with
no allowedHosts can never be substituted, so a successful request could
produce a permanently unusable credential. Move the description to the
presets module beside ask_user and document every parameter.
* refactor(agents): share one blocking-question lifecycle between tools
ask_user and secrets each carried their own registration, wait, and
cancel logic, and they had diverged: ask_user recovers an answer that
lands between its wait timeout and the cancel, while secrets discarded
it and reported no_answer even though the Gateway had already stored the
credential. One shared canceller and answer reader fixes that race for
both, folds the two divergent gateway-call types into one, and drops two
type assertions in favour of the canonical record guard (ask_user's
assertion baseline shrinks 11 -> 8).
Net +49 production lines: the shared module costs more than the
duplication it removes, and buys the correctness fix plus a single owner
for question lifecycle.
* fix(ui): keep the allowed-hosts field readable as an input
Main's composer restructure moved the free-text input styling into the
option-row context, so the store-request hosts field - which sits outside
a row - lost its border and read as static text. It is the one field the
operator is meant to review and edit before releasing a credential, so
give it its own border and focus ring.
* fix(secrets): close two credential-boundary holes in agent requests
Requests are now protected-secret only. list renders env values, so an
agent could request kind=env, watch a human type it into a masked box
under a no-visibility promise, then read it straight back; the tool text
even claimed values are never returned. Environment values stay operator
-set in Settings or the CLI, where they are agent-readable by design.
Store-bound questions are also bound to the run that requested them. The
resolve path authorized only the answering client, so a terminated or
replaced agent run could still have a credential written on its behalf -
the recorded runId was provenance, not closure-bound authority. Minting
now requires a runId and resolution revalidates that exact live run
immediately before the store write, with no await in between, failing
closed as QUESTION_REQUESTER_INACTIVE.
Both reported by ClawSweeper as P1 credential-boundary findings.
* test(macos): add OPENCLAW_DEBUG_OPEN_MENU screenshot hook
* feat(macos): add live execution approval queue
* refactor(macos): replace menu injectors with owned status menu
* fix(macos): keep unconfigured menu header calm
* fix(macos): route status-item right-clicks through a local event monitor
NSControl's send-action mask ignores right mouse buttons, so the previous
sendAction(on: [.rightMouseUp]) wiring never fired and the menu was
unreachable by pointer. A local monitor now owns pointer routing (left =
dashboard, right = menu) — the same mechanism the shipped
StatusItemMouseRouter used — and menuWillOpen gained the re-entrancy
guard the old injector carried, since reconciling tracked rows can
re-enter the callback without a close.
* chore(i18n): refresh native inventory for status menu strings
* chore(macos): remove menu-refactor dead code
Periphery flagged the orphans the status-menu refactor left behind: the
ExecApprovalQuickMode enum and AppState's entire quick-mode read/retry
surface (its only consumer was the deleted menu picker; the Settings
pane owns exec-approval policy UI), SessionMenuLabelView,
TrackingAreaSupport, NodeMenuMultilineView, UpdateStatus.disabled, and
two fixture-only initializers. StatusMenuController.stop() is now wired
into applicationWillTerminate. The menu-highlight environment key moved
from the deleted view file into MenuItemHighlightColors.
* chore(macos): fix status-menu lint style and refresh i18n inventory
* fix(macos): converge approval cards after losing a resolution race
The status-menu queue and the modal prompter intentionally share the
gateway approval event stream: the gateway resolves each approval
exactly once, the resolved broadcast removes the card, and the modal
stays the active presentation owner while the menu is the passive,
ambient one. What was missing: when the menu's resolve loses the race
(modal or another client answered first), the gateway rejection left a
zombie card if the resolved event was dropped. Resolve failures now
re-list from the authoritative queue. Regression test simulates the
race at the socket boundary and fails pre-fix.