* feat(fleet): per-cell disk limits, egress policy, backup/restore, logs, and doctor
- fleet create --disk <size> caps the container writable layer via --storage-opt;
unsupported storage backends fail create with an actionable support-matrix
error, and the limit replays across upgrade/restore through a fleet-owned
container label because Podman inspect has no HostConfig.StorageOpt.
- fleet create --network bridge|internal adds an opt-in no-egress mode on
Podman (published loopback port keeps working, verified live); Docker
internal cells are rejected fail-closed because Docker does not publish
loopback ports on internal networks.
- fleet backup/restore: per-tenant 0600 tar archives with manifest tenant
binding, symlink/hardlink rejection, byte and path-segment budgets,
root-bounded extraction, atomic no-overwrite publish, lease fencing, and
Gateway token rotation on restore; failures preserve displaced data and
never leave a force-stopped or half-started cell serving silently.
- fleet logs: ownership-asserted, bounded, token-redacted on both streams,
with a generation re-check so a concurrent restore cannot leak a rotated
token.
- fleet doctor: read-only per-cell audit of ownership labels, health,
hardening drift, loopback port binding, token presence, network egress
mode, and 0700 state-dir permissions; any failed finding exits nonzero.
Cross-runtime checks are grounded in live-verified Docker 28/Podman 4.9
inspect shapes (CapDrop vs EffectiveCaps, missing StorageOpt, missing
network containers map).
Related: #104436 (v1 shipped in #104527)
* fix(fleet): harden streamed log redaction and root restore ownership
- redacting stream writer honors target backpressure, retains secret-prefix
overlap across forced long-line flushes, and never splits a token between
emitted chunks
- root-invoked restores repair ownership for explicit non-root user mappings
instead of leaving root-owned 0700 state trees
- fleet logs merges the shipped --follow streaming surface (#104669) with the
v1.1 token-redaction contract
* fix(fleet): stream partial log lines live and report phase-accurate restore recovery paths
- the redacting log writer emits safe text on every chunk (retaining only a
possible token prefix) so unterminated progress output streams immediately
- restore failure notes distinguish pre-swap, displaced, and swapped states so
operators recover the correct tree, and an unavailable runtime inspection is
reported as an unverified replacement instead of silence
* fix(fleet): reject backslash archive paths, guard stderr pipes, and validate disk-limit labels in doctor
- restore/backup path rules reject literal backslashes so a tampered entry
cannot validate as one path and extract as another on POSIX
- fleet logs handles broken pipes on stderr as well as stdout
- fleet doctor fails malformed disk-limit labels that would break
upgrade/restore replay instead of reporting them as passing
* refactor(sessions): migrate runtime storage to sqlite
* test(sessions): fix sqlite CI regressions
* test(sessions): align remaining sqlite fixtures
* fix(codex): require sqlite trajectory recorder
* test(sessions): align orphan recovery sqlite fixture
* test(sessions): align sqlite rebase fixtures
* fix(sessions): finish current-main integration of the sqlite flip
Resolve the whole-store SDK removal across its owner boundary: drop the
loadSessionStore re-export and the registry whole-store wrappers, wire
hasTrackedActiveSessionRun into gateway chat, complete the
preserveLockedHarnessIds cleanup contract, flip the codex thread-history
import to storePath targets, and port remaining main-side tests from
file-store helpers to session accessor reads.
* chore: drop committed pebbles log, revert plugin-inspector bump, refresh generated docs
Remove the 1.8k-line .pebbles/events.jsonl work log from the branch, restore
the plugin-inspector advisory lane to main's pinned 0.3.10 so the supply-chain
bump gets its own review, and regenerate docs_map, the plugin SDK API baseline,
and the export-surface ratchet for the merged tree.
* feat(sessions): keep archived transcripts by default with zstd cold storage
Codex-style retention: deleting or resetting a session archives its
transcript as a zstd-compressed JSONL artifact (plain when the runtime
lacks node:zlib zstd) and keeps it until the disk budget evicts oldest
first. resetArchiveRetention now governs both deleted and reset archives
and defaults to keep; maxDiskBytes defaults to 2gb so retention stays
bounded, with archives evicted before live sessions. The cron reaper
follows the same knob instead of deleting archives on its own timer.
* fix(state): converge agent DB migration lineages and bound database growth
Merge coherence: run both structure-gated legacy memory-schema repairs
(flip-lineage drop, main-lineage identity rebuild) before the flip
migration so pre-flip v1/v2 and pre-merge flip v1/v4 databases all
converge, and hoist foreign_keys=OFF outside the schema transaction
where the pragma was silently ignored and the v1 sessions rebuild
cascade-deleted session_entries.
Growth guards: fresh agent DBs enable auto_vacuum=INCREMENTAL, WAL
maintenance releases freed pages in bounded passes (never a blocking
full VACUUM), and doctor reports state/agent DB bloat from freelist
stats.
* fix(codex): resolve the store path for thread-history import via the SDK
The supervision catalog passed the legacy sessionFile locator to the
storePath-targeted transcript mirror; resolve the agent store path with
the session-store SDK helper instead of a runtime-object seam so test
fakes and headless callers need no extra surface. Drop the obsolete
missing-session-id preprocessing case: sessions rows are NOT NULL on
session_id and upsert repairs id-less patches at write time.
* fix(sessions): fail safe on malformed disk-budget config and doctor stat errors
A malformed explicit maxDiskBytes disables the budget instead of
falling back to the destructive 2gb default the user never chose, and
the doctor bloat check skips databases whose paths stat-fail instead of
aborting doctor.
* fix(sessions): complete sqlite conflict translations
* test(sqlite): align hardening checks with maintenance
* test(sessions): inspect compressed transcript archives
* fix(tests): await session seeds and drop unused helpers flagged by CI lint
The five unawaited writeSessionStoreSeed calls raced their SQLite seeds
against the assertions, failing compact shards; the bloat probe drops a
useless initializer and the merged tests drop now-unused helpers.
* test(sessions): type legacy proof events directly
* test(sessions): align hardening contracts
* perf(sessions): read usage transcript sizes from SQL aggregates
Usage/cost scans walked every session and materialized every transcript
event just to re-stringify it for a byte estimate — the #86718 stall
class reborn on the DB. readTranscriptStatsSync sums stored JSON bytes
in SQLite without loading a single row.
* fix(sessions): re-root foreign-root transcript paths onto the current sessions dir
Restored backups, moved OPENCLAW_STATE_DIR, and rehearsal copies carry
absolute sessionFile paths from the old root; the containment fallback
kept those foreign paths, so migration read (and would archive) files in
the original root and reported local copies missing. Re-root the
canonical agents/<id>/sessions suffix onto the current dir when the file
exists there; genuine cross-root layouts still fall through unchanged.
* test(agents): seed harness admission through sqlite
* fix(sqlite): close agent db on pragma setup failure
* fix(doctor): compact and retrofit incremental auto-vacuum after session import
The migration is the sanctioned offline window: post-import compact
reclaims import churn and applies auto_vacuum=INCREMENTAL to databases
created before the fresh-DB pragma existed, so runtime maintenance can
release pages in bounded passes on every install.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat: add metadata-only message audit events
* chore(protocol): restore generated swift models and config baseline after rebase
* fix(state): gate agent-db ownership check before schema version
* fix(cli): sync audit command description into the root-help catalog
* fix(audit): require destination proof before classifying outbound messages as direct
* refactor(audit): drop dead migration guard and add contract comments
* docs(gateway): add dedicated audit history page and cross-links
* test(e2e): enable direct-mode message audit in the telegram proof SUT config
* test(channels): expect declared conversationKind in durable delivery session context
* fix(audit): record the routing channel id for inbound rows from plugin channels
* fix(audit): match channel-prefixed delivery targets in the destination route gate
* fix(audit): validate explicit target kind against the destination route kind
* fix(audit): use the canonical target-prefix grammar in the destination route gate and fail closed on foreign migration tables
* fix(audit): normalize nested provider and kind target prefixes in the destination gate
* fix(audit): strip registered provider aliases and the direct kind prefix in the destination gate
* chore(docs): refresh config baseline after rebase
* feat(fleet): add openclaw fleet cell supervisor for multi-tenant hosting
* test(fleet): cover cell profile, registry, containers, and service flows
* docs(fleet): document multi-tenant hosting and the fleet CLI
* fix(fleet): verify upgrade replacements and release foreign-collision reservations
* fix(fleet): gate upgrade commit on replacement health
* docs(fleet): mark fleet experimental and pin its single-host scope
* test(fleet): make incomplete-profile casts explicit for test-type lane
* fix(state): regenerate kysely schema artifacts after rebase conflict
* fix(logs): clean up gateway and channel startup/shutdown log output
Scope Discord slash-command deploy REST diagnostics to command routes so
concurrent startup traffic (voice-state probes, channel lookups) keeps its
owner's error handling; make per-request deploy error lines verbose-only and
drop JSON bodies that only repeat message+code. Log allowlist summaries one
line per call so unresolved lines keep their timestamp/subsystem prefix, and
skip identity lookups that resolved to themselves. Remove embedded subsystem
prefixes, demote routine signal/shutdown/force/postbuild/diag chatter to
debug or verbose, merge the duplicate Control UI build notices, drop doctor's
duplicate backup line, and name the config surface or platform limit in the
transcripts autoStart and command-limit warnings.
Fixes#104163
* fix(slack): keep bare-name allowlist resolutions in startup log summary
Only omit identity lookups where the input already is the resolved id;
name-based lookups that translated to an id stay logged even when the
display name matches the input.
* fix(telegram): keep isolated-ingress readiness marker on the runtime log
test/e2e/qa-lab telegram-bot-token-runtime waits for this line via the
injected RuntimeEnv.log; verbose-only logging would deterministically time
out that live proof. Comment the contract at the call site.
* feat(ui): add plugin catalog management
* feat(gateway): add plugins.uninstall and richer plugin catalog metadata
Adds a plugins.uninstall gateway method (operator.admin, control-plane write)
backed by a lock-guarded uninstallManagedPlugin that mirrors the CLI flow:
config cleanup, install-record removal, managed file deletion, and registry
refresh. Bundled plugins stay disable-only. Catalog entries now carry a
manifest-derived category and a removable flag; ClawHub search results expose
download counts and verification tiers.
* feat(ui): redesign plugins page with inventory, store shelves, and cover art
Rebuilds /settings/plugins around three tabs: Installed (category-grouped
inventory with overview stats, state filters, uninstall for external plugins,
and inline MCP server management through the shared config seam), Discover
(featured/official shelves plus one-click MCP connectors and curated ClawHub
searches), and ClawHub (search with download counts and verification badges).
Every catalog entry renders bundled cover art or a deterministic gradient
monogram tile - no more empty boxes. Artwork generated with Codex CLI, shipped
as 512px WebP under ui/public/plugin-art.
* chore(ui): regenerate locale bundles for plugins manager strings
* docs: describe plugins manager tabs, uninstall, and MCP connectors
* fix(plugins): human catalog labels and un-pinned hosted fallback ids
listManagedPlugins now prefers manifest names over registry package-name
backfill, falls back to channel catalog labels and blurbs, and stops pinning
expectedPluginId when a hosted feed entry only exposes its package name
(which rejected every legitimate install of that package). Found via live
gateway testing against ClawHub.
* fix(ui): send minimal RFC 7396 merge patches for MCP server edits
config.patch merges rather than replaces, so key removal needs an explicit
null; sending the full config back made MCP server removal a no-op. Found
via live gateway testing.
* fix(ui): write explicit MCP transports for URL servers
The MCP runtime defaults URL-only servers to SSE, so streamable HTTP
endpoints saved by the add form or connector templates would fail at
connect time. Connector templates now declare their transport and the
add form infers streamable-http unless the URL follows the /sse
convention. Flagged by autoreview against the transport resolver.
* test(ui): wait for deferred plugin requests before resolving in e2e
* feat(ui): plugins detail view, action menus, and unified ClawHub search
Reworks the plugins page from PR #103176 feedback: merges the ClawHub tab into
Discover (typing searches ClawHub inline and appends a quiet From ClawHub
section, with Browse ClawHub demoted to a header text link), makes every row and
store card open a plugin detail overlay (hero art, primary enable/install
action, metadata table), and replaces enable/disable switches with a state chip
plus an overflow menu (Enable/Disable, Remove for external plugins, View
details) matching the ChatGPT-store install+menu pattern. MCP rows use the same
menu; refresh is now icon-only.
* chore(ui): regenerate locale bundles for plugins UI iteration
* feat(ui): vetted, grouped connector catalog for the plugins store
Expands Connect your world to 28 connectors organized into use-case shelves
(Work & productivity, Coding & infrastructure, Home & media, Everyday life).
Every entry passed a three-stage subagent review: official-docs verification
plus live endpoint probes for MCP servers, ClawHub result-quality and
malware/typosquat screening for curated searches, and an adversarial pass
that dynamically registered OAuth clients to prove one-click viability.
That review removed Figma (registration allowlisted, 403) and Atlassian
(OAuth issuer-mismatch bug upstream), downgraded GitHub to PAT-based setup
(no dynamic client registration upstream), fixed Linear (/sse retired) and
Home Assistant (/api/mcp, streamable HTTP) endpoints, retargeted poisoned or
dead searches (youtube, finance, hue dropped; calendar -> google calendar;
stocks replaces finance), and added Todoist, Airtable, Canva, Stripe,
Context7, DeepWiki, Hugging Face one-click MCP servers plus Jira, PDF,
transcription, Kubernetes, Reddit, maps, translation, and notes searches.
Keyless servers get a ready-to-use success message; new cover art included.
* chore(ui): regenerate locale bundles for connector groups
* fix(plugins): suppress hosted catalog rows once their package is installed
Hosted feed entries without a declared runtime id fall back to their package
name as catalog id, which never matches the installed runtime id, so the
Discover shelf kept offering an already-installed package. Installed package
names now also suppress official rows. Flagged by autoreview.
* fix(plugins): pin declared runtime ids and surface connector errors in place
The runtime-id pin now keys off explicitly declared catalog ids (plugin,
channel, or provider) instead of string-comparing against the package name,
so declared ids that equal their package name stay enforced while entry-id
fallbacks stay unpinned. Connector add failures on Discover now render on the
triggering card instead of the Installed tab's MCP section. Both flagged by
autoreview; regression tests included.
* feat(ui): full inventory artwork, pulse header, and two-column plugin list
Every bundled plugin now ships distinctive cover art (113 new Codex CLI
illustrations; 172 total, ~2.1MB WebP), so inventory rows and detail views
never fall back to monogram tiles. The four stat cards give way to a compact
inventory pulse: a segmented enabled/disabled/issues meter whose legend and
counts live inside the filter chips. Inventory, MCP, and search rows flow
into two columns when the panel is wide enough.
* chore(ui): regenerate locale bundles for pulse header
* fix(ui): omit stdio args from the MCP server row target
Stdio MCP args routinely carry tokens, and the inventory is visible to
read-only operators; mirror the config page and show only the command.
Flagged by autoreview; regression test included.
* fix(merge): point crestodian setup at relocated plugin commit/refresh modules
* fix(merge): add bootstrapToken to plugins page test gateway harness
* fix(plugins): name catalog install-action branches so Swift emits the union
* fix(ui): satisfy strict lint on plugins page form parsing and mocks
* chore(build): regen docs map, raise plugin-sdk declaration budget for new protocol surface
* fix(ui): type the plugins page patch mock with its real call signature
Paired devices can now carry a durable operator-assigned label: device.pair.rename { deviceId, label } (schema-bounded, admin/ownership-gated) stores an operatorLabel that persists in the shared SQLite state DB, survives device repair and re-approval, and takes display precedence over the client-reported name in CLI devices list and the Control UI inventory (operatorLabel, then displayName, then clientId, then deviceId). The label was previously dropped on write because the pairing store had no column for it. CLI: openclaw devices rename --device <id> --name <label>. Docs cover the command and precedence.
Fixes#13870
Thanks to @bladin for the contribution.
Co-authored-by: heichl_xydigit <1740879+bladin@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
The resident pauses its fidgeting and turns to watch passers cross the
ledge. After a droop it tidies up with a little broom. On lobster days
(shared day hash with the CLI banner cousin, now in src/shared) it wears
a sailor cap unless the seed already rolled headwear. The Dreams sleeper
half-opens one eye when pressed.
* feat(cli,webchat): lobster wild cards - a CLI cousin, moving day, and opt-in sounds
The openclaw banner gains a rare day-seeded ASCII lobster (rich TTYs,
random tagline mode, never CI). The web pet notices gateway upgrades and
arrives carrying a bindle for one load. A new default-off Lobster sounds
quick setting adds tiny WebAudio chirps on pokes and pets. The lobster
docs page learns about petting, sounds, and this batch's field notes.
* chore(webchat): regenerate keyless raw-copy i18n baseline after rebase
* fix(cli): bound node camera URL downloads
* refactor(cli): enforce camera download policy at guard
* refactor(cli): enforce camera download policy at guard
* refactor(cli): enforce camera download policy at guard
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* fix(doctor): gate cross-state-dir legacy imports behind explicit doctor opt-in
A gateway or CLI command started with OPENCLAW_STATE_DIR pointing at a
non-default directory used to import legacy files FROM the default
~/.openclaw state dir (exec-approvals.json, plugin-binding-approvals.json)
and archive the originals with a .migrated suffix. Any isolated, test, or
staging run on a host with production state silently captured and archived
the production files.
Cross-state-dir imports now run only with the new crossStateDirImports
opt-in: openclaw doctor --fix (or an interactive doctor confirm) performs
the import; the implicit CLI/gateway preflight leaves the default dir
untouched and emits a notice pointing at doctor instead.
* test(doctor): include notices in legacy-state detector mocks