* 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.
* fix(nodes): approve silent initial node surfaces and name the pending approval
Silent same-host device pairing carries local-grade auth — the operator
themselves — but the inline initial-surface approval covered only
ssh-verified and bootstrap routes, so headless silent connects (which have
no approval UI to consume the silent hint) stranded the node: surface
pending forever, inventory publication retrying every 5s. The gate now uses
the existing deviceApprovedNonInteractively predicate. trusted-cidr stays on
the manual prompt (network origin must not approve a command surface), and
its dead end is repaired at the message: the inventory rejection names the
pending request and the exact openclaw nodes approve command. Docs state
what autoApproveCidrs does and does not approve.
Fixes#128446
* fix(nodes): keep silent surfaces on the manual prompt; the message is the repair
Reverts the silent-route inline surface approval: gateway suites
(roles-allowlist-update, node-pairing-rate-limit) protect the deliberate
contract that a silently paired local device exposes no declared commands
until an operator approves its surface — auto-approval would hand system.run
to any local process that pairs. The #128446 dead end is repaired for every
route by the actionable inventory rejection naming the pending request and
approve command; docs state the surface prompt applies to silent and
trusted-cidr device approvals alike.
* feat(nodes): opt-in container isolation for node-hosted worker sessions
nodeHost.workerRuns.isolation=container runs each worker session inside a
Docker-compatible container (docker/OrbStack/podman): stdio launch transport,
exactly two bind mounts (bundle read-only, workspace read-write), allowlisted
env, and the container itself as the durable launch identity — created stopped
and journaled before the descriptor is delivered, killed/removed on cancel,
fence, shutdown, and recovery, with an owned-orphan sweep at startup. Missing
or changed engines disable hosting with an explicit diagnostic instead of
falling back to bare processes. Additive nullable worker_container_json column
on node_worker_launches (schema version unchanged).
* fix(test): base-shape fixture skips additive columns of stripped tables
node_worker_launches is excluded from claw-scoped state schemas, so the
read-only compat fixture cannot drop its additive column there; drop only
columns whose owning table exists.
* fix(test): guard split destructuring in base-shape fixture
* test(state): record worker_container_json in the canonical additive-column list
* fix(nodes): harden container isolation per review findings
- Revalidate the engine daemon target immediately before container creation;
a replaced daemon receives zero create/start requests (regression proves it).
- A pending launch keeps its worker slot until the journaled container is
killed/removed; cancellation cannot free capacity over a live container.
- Windows node hosts fail closed at startup for isolation=container with an
actionable diagnostic (native paths cannot be container mount targets).
- Container identity moves from an additive column to the same-version
companion table node_worker_launch_containers (bare STRICT, lazily ensured
on first container write, pruned with the launch journal): the launch table
is contract-optional and shipped readers reject additive columns there. An
exact v9 predecessor now provably opens and uses the journal after the
candidate populated container rows.
* 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.
The exec docs claimed a per-call `host=node` request is always allowed
from `tools.exec.host=auto`, gating only `host=gateway` on whether a
sandbox runtime is active. The runtime treats both identically:
`isRequestedExecTargetAllowed` rejects `node` and `gateway` alike from
`auto` while a sandbox is available, and allows both when it is not.
Align the three pages that carried the asymmetric claim with the
test-locked runtime contract, and point readers at the explicit
`tools.exec.host=node` path the rejection error already recommends.
Refs #61009
Node session hosting was capped at a hardcoded 2 concurrent worker sessions.
The default is now one worker slot per available CPU core (clamped to 1..1024),
and nodeHost.workerRuns.capacity overrides it explicitly. The capacity option
was already plumbed through the supervisor; this wires it from config and
deletes the hardcoded constant. Docs and config baselines regenerated.
* fix(node-host): recover MCP catalogs and sessions
Share placement-neutral MCP lifecycle and result projection while keeping Gateway session ownership and node process ownership separate. Refresh node catalogs live, recover closed or expired transports without replay, and preserve MCP application errors across node.invoke.\n\nFixes #125044
* test: register node MCP CI inventory
* fix(node-host): stop MCP recovery after abort
* feat(workers): run device sessions from Gateway bundles
Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice.
* docs(runners): record Gateway bundle cutover
* test(ci): repair runner validation fixtures
# Conflicts:
# src/scripts/test-projects.test.ts
* fix(workers): surface outdated node recovery
Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces.
* fix(workers): reject legacy inventory with recovery
* fix(workers): bundle worker deploy closure
* test(workers): close bundle cutover gates
* fix(workers): compose browser runtime at build
* fix(workers): satisfy bundle cutover gates
* fix(workers): route temp runtime through infra
* docs(workers): align bundle host guidance
* fix(ui): fence outdated session destinations
* feat(gateway): provision paired node worker installs
* docs: record node local-install PR chain
* chore(protocol): regenerate node worker manifest models
* fix(gateway): resolve node worker build through the device runtime
Repairs a rebase artifact: startup referenced a stale deviceNodeRegistry
binding; the device runtime now owns the connected-node lookup via
resolveWorkerBuild.
* feat(macos): add native camera PTZ controls
Add physical UVC pan, tilt, and zoom through the signed Mac app, with camera.ptz.control kept behind dangerous-command approval. Verified against real Insta360 Link 2 Pro hardware.
* refactor(agents): split message tool display config
* fix(mac): harden camera PTZ contracts
* fix(browser): resolve upload paths on the owning browser node, not the Gateway
When a browser session is proxied to a remote node, the upload action
previously ran resolveExistingUploadPaths on the Gateway, pinning paths
to a filesystem the node cannot see and rejecting node-local files. The
node-side /hooks/file-chooser route already re-resolves paths against
its own filesystem, so skip Gateway-local resolution whenever the
request is proxied and forward the requested paths as-is.
Fixesopenclaw/openclaw#115251
* fix(browser): transfer uploads to remote browser nodes
* fix(browser): normalize upload abort errors
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* refactor(infra): move exec approvals into the shared SQLite state DB
Delete the file-runtime exec-approvals store (exec-approvals.json + .lock
sidecar machinery) on both runtimes and make the reserved
exec_approvals_config singleton row canonical. Doctor owns the one-time
import with claim/verify/receipt discipline; runtime fails closed with a
doctor instruction while un-migrated legacy state exists. The wire CAS
contract, socket semantics, and gateway auth-token derivations are
unchanged. Kills the #113929 lock-contention bug class structurally and
nets around -2.9k lines.
* fix(infra): green CI gates and retire file-era exec approvals tests
Break the migration-type import cycle with a leaf contract, regenerate the
plugin-SDK API and native i18n baselines for the intentional surface change,
drop unused exports, and replace the macOS file-era approvals test suite with
SQLite-backed behavior coverage per the obsolete-internals test policy.
* chore: green max-lines ratchet, native i18n baseline, and unused-export scan
* fix(anthropic): accept "cli" entrypoint in Claude session catalog discovery
Claude Code v2.x writes entrypoint: "cli" in its JSONL session files.
Previously only "sdk-cli" was recognized, causing all v2.x sessions to
be silently skipped.
Extract CLI_ENTRYPOINTS set and isCliEntrypoint() helper, then apply
the same fix to both TypeScript catalog and macOS paired-node native
catalog. Add matching regression coverage on both platforms.
Closes#105164
* fix(anthropic): refresh Claude CLI session discovery
Apply the reviewed cli/sdk-cli allowlist, mirrored negative coverage, and fallback-scope documentation to current main.
Co-authored-by: 黄攀0668000858 <huang.pan@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(onboarding): recommend plugins and skills from installed apps
Scan installed macOS apps during classic onboarding (TCC-free), gather
candidates from official catalogs + ClawHub search, let the configured
model pick genuine matches, and offer an opt-in multiselect install step.
Adds a device.apps node-host command (default-off sharing, Android-parity
envelope) so remote gateways can request a paired Mac's inventory, and a
wizard.appRecommendations kill switch. Custom setup-inference completions
no longer inherit the 32-token verification-probe output cap.
* feat(onboarding): recommend apps in guided flow
* fix(onboarding): harden app recommendations against ClawHub self-promotion
Third-party ClawHub skills are never pre-selected regardless of model tier
(publisher-controlled listing text reaches the matcher prompt and could
promote itself); their labels now say they install third-party code.
Installed-app scans follow symlinked .app bundles. Matcher output stays
bounded by the resolved model's own maxTokens budget (documented invariant).
* fix(onboarding): key official catalog candidates by resolved plugin id
Real catalog entries are package manifests without a top-level id; keying the
candidate map and channel/provider classification by entry.id collapsed the
whole official catalog into one undefined-keyed entry, so no official plugin
or channel was ever recommended. Regression test runs against the bundled
catalogs.
* fix(onboarding): satisfy lint, types, deadcode, and migration gates
Split the guided-onboarding test into a self-contained custodian suite to stay
under max-lines. Narrow app-recommendation exports (drop dead node-payload
normalizer, unexport internal types/helpers, route candidate tests through the
public API), replace map-spread with a helper, unexport device.apps result
types, add installedAppsSharing to node-host migration expectations, cast the
wizard multiselect mock, and regenerate the docs map.
* test(onboarding): register new live test in the shard classifier
Project group heads in the native session catalogs (Codex + Claude) are now
collapse toggles: chevron + aria-expanded, rows hidden while the head and
count stay visible. State persists per catalog+host+project through the
existing collapsed-sections localStorage set. Follow-up to #109575.
* feat(ui): group native session catalog rows by project in the sidebar
Codex and Claude session catalogs in the Control UI sidebar now group each
host's sessions by project folder, mirroring Claude Code desktop: any cwd at
or under <repo>/.claude/worktrees/<name> folds into the origin repo. A folder
toggle icon in the catalog header flattens the list; grouping defaults to on
and persists via localStorage (openclaw:sidebar:sessions:catalog-grouping).
* chore(ui): drop generated i18n fallback metadata from source PR
Post-rebase main forbids catalog fallback metadata in source PRs; the
post-merge locale-refresh workflow reconciles foreign catalog parity.
* chore(ui): keep CatalogProjectGroup type module-local
knip dead-export gate: no external importers; callers rely on the inferred
return shape of groupCatalogSessionsByProject.
* test(scripts): drop retired workspace-state.json expectation
Opportunistic CI fix: b6535fb8de5/905395a3ff5 retired workspace-state
seeding from scripts/e2e/parallels/agent-workspace.ts but the smoke-model
test still asserted the removed string, breaking checks-node-compact-small-2
on every branch.
* test(scripts): restore mainline retired-state regression guard
Rebase conflict resolution wrongly kept this branch's older removal; main's
2256387885 comment plus not.toContain assertion is the canonical fix.
* feat(linux): canvas via CLI-node + Tauri app IPC bridge
* refactor: extract gateway helper modules
* build(linux-canvas): register plugin package in lockfile
* fix(linux-canvas): move canvas advertise test out of core, regen docs/protocol/deadcode
* fix(gateway): break node-catalog/registry import cycle via leaf normalize module; add canvas glossary term
* style: oxfmt invoke.ts and runtime.ts after buildNodeEventParams extraction
* fix(linux): load Canvas WebView via dedicated data_directory context
Wry's Linux/WebKitGTK incognito mode discards Tauri's registered
WebContext (wry webkitgtk/mod.rs), so the Canvas window got a fresh
ephemeral context without the openclaw-canvas:// scheme handler — the
bundled A2UI page never committed (stayed about:blank) and every A2UI
command timed out. Use an isolated cache-backed data_directory instead,
which keeps the protocol handler while still isolating Canvas storage
from the dashboard window.
* fix(linux): keep Canvas WebView ephemeral via incognito + data_directory
Autoreview flagged that a dedicated data_directory alone persists Canvas
browser state (cookies, localStorage, IndexedDB, service workers) across
restarts, so an agent that navigates Canvas to a site could leak an
authenticated session into a later session. iOS uses a non-persistent
store; Linux should match.
Add .incognito(true) alongside .data_directory(): the distinct directory
gives Tauri a fresh WebContext key so it still attaches the
openclaw-canvas:// protocol closure, and incognito makes Wry swap in a
fresh *ephemeral* context carrying those protocols. Live-verified on a
Wayland/WebKitGTK box: the bundled page still loads
(location.href=openclaw-canvas://localhost/index.html, openclawA2UI
present, A2UI renders) and the canvas-webview dir holds no persistent
cookie/storage files.