* fix(plugins): roll back cleared runtime registrations when an activating reload aborts
loadOpenClawPlugins clears the process-global agent-harness, plugin-command,
compaction, detached-task, interactive-handler, embedding, memory-embedding,
and memory stores up front on an activating load and only re-registers them at
the end via activatePluginRegistry. The body has a finally with no catch, so a
throw during discovery or manifest load (a corrupted or half-written plugin
manifest mid-upgrade) leaves the previously active registry marked active while
its runtime stores are gone process-wide; the gateway reload driver only logs
and swallows the error.
Snapshot the process-global stores before the clear with the existing
snapshotPluginProcessGlobalState helper and restore them on any throw before
activation. clearActivatedPluginRuntimeState moves next to the snapshot and
restore helpers it pairs with in plugin-registration-transaction.ts, which
already owns atomic plugin registration state and imports from every module the
clear touches; loader.ts re-exports it so existing callers are unchanged.
* fix(plugins): make reload rollback activation-safe
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Bound bundle command file reads with size cap
* fix: use buffer.toString for readRegularFileSync result
* fix: log oversized bundle command file diagnostic instead of silent skip
The catch block now captures the error and emits a console.warn with the file path and error detail, so upgrades do not silently remove oversized installed commands.
* test: verify oversized bundle command file is skipped and siblings continue
PR #110594: Add focused regression coverage:
- Test: an oversized bundle command markdown file (>1 MB) is skipped via catch + continue
- Test: normal sibling command files still load correctly
- Verifies console.warn diagnostic is emitted for the oversized file
* refactor(plugins): log rejected bundle commands
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
* style(plugins): format bundle warning
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): custom session icons with agent-drawable SVG and nav-parity pinned rows
* wip: slice-2 interleaved sidebar zone (pre-review)
* fix(ui): trim icon exports, bypass protocol barrel in startup bundle, regen Swift protocol client
* fix(ui): preserve unknown-agent zone entries on writes, prune entries on any unpin
* fix(ui): reset keeps unloaded session slots; enforce SVG byte cap on canonical form
* fix(ui): archiving a pinned session retires its sidebar zone slot
* fix(ui): persist drag-pinned zone slot only after the pin patch lands
* fix(ui): recompute drop insertion against freshest zone order on pin ack
* fix(ui): consume self-drop events before the zone bailout
* docs(ui): note the non-sidebar unpin pruning contract on pruneSidebarSessionEntry
* refactor(ui): extract pure session-tree projection; drop unused zone type export
* feat(chat): rewind and fork a session from a message bubble
* docs(web): document chat rewind and fork bubble actions
* fix(chat): lint cleanups, protocol regen, and test splits for rewind/fork CI
* fix(ui): fit chat rewind disabled styles inside the startup CSS budget
* fix(plugins): bound plugin manifest metadata file reads
* fix(plugins): use correct Error type check in oversized manifest catch
readRegularFileSync throws a plain Error (not RangeError) when the file
exceeds maxBytes. Change the catch-block type check from RangeError to
Error to properly detect oversized plugin manifests and emit the
subsystem warning.
* test(plugins): exercise valid oversized manifests
Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): skip runtime plugin reload when a registry is already active
ensureRuntimePluginsLoaded rebuilt and activated a replacement plugin
registry on every call site invocation (agent turns, cron isolated-agent
runs, the gateway prewarm sidecar). Activating a replacement retires the
active registry and runs its plugin host cleanup, which cron.remove()s
persistent plugin-scheduled session turns before the replacement's async
re-registration catches up — silently deleting the cron row with only a
low-level 'cron service unavailable' WARN. Plugins using
api.session.workflow.scheduleSessionTurn for recurring jobs lose their
schedule on the next agent turn after boot.
Guard with getActiveRuntimePluginRegistry(): when a registry is already
active every caller's request is satisfied, so return before building a
replacement.
* fix(agents): make active-registry reuse compatibility-aware, not presence-only
The original guard (1de432a9) returned early whenever ANY registry was
active, without checking whether it was actually compatible with this
call's workspace, plugin scope, config, or runtime mode. A later call
requesting a different workspace could silently keep an unrelated
workspace's registry active instead of loading its own — a correctness
and workspace-isolation issue (caught in upstream review, PR #107752).
Fix: compute workspaceDir/startupPluginIds/loadOptions first, then check
getLoadedRuntimePluginRegistry({loadOptions, workspaceDir, requiredPluginIds})
before deciding to skip — reusing the existing loader cache-key
compatibility rules (workspace, config, runtime subagent mode, required
plugin IDs) that already govern normal cache-hit reuse elsewhere. Only
skips the reload when an active registry is genuinely compatible; still
loads a fresh one otherwise.
Tests: replaced the presence-only regression test with three cases —
compatible-registry reuse (skip), incompatible/different-workspace (still
loads), and startup-scoped plugin IDs passed through to the compatibility
check. All 5 acceptance criteria from the review verified locally:
runtime-plugins.test.ts, active-runtime-registry.test.ts,
standalone-runtime-registry-loader.test.ts, and the full cron lane
(33 files / 460 tests, including run.runtime-plugins.test.ts) all green;
pnpm build completes clean (5m14s, no errors).
* fix(plugins): preserve live scheduled turns
Scope dynamic scheduler cleanup to the retiring registry owner and keep runtime workflow APIs callable while their registry remains pinned and live.\n\nCo-authored-by: Dan Scarafoni <dan@poweredbyzon.com>
* test(plugins): prove recurring cron ownership
* chore(plugin-sdk): refresh api baseline
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugins): handle NaN from Date.parse in catalog rollback detection
isHostedCatalogSignedFeedRollback compared generatedAt timestamps using
Date.parse without checking for NaN. An invalid date string in the
candidate feed would produce NaN < validNumber === false, silently
accepting the candidate instead of treating it as a potential rollback.
Now, if the candidate's generatedAt is invalid, we conservatively treat
it as a rollback. If the current feed's generatedAt is invalid, we
treat the candidate as newer (not a rollback).
* fix(plugins): handle NaN from Date.parse in snapshot store monotonic rollback
isMonotonicRollback in official-external-plugin-catalog-snapshot-store.ts
had the same NaN comparison bug as isHostedCatalogSignedFeedRollback:
Date.parse(candidate.generatedAt) < Date.parse(current.generatedAt)
returns false when either timestamp is invalid, silently accepting a
candidate that should be rejected as a rollback.
Mirror the catalog.ts fix: if the candidate's generatedAt is invalid,
conservatively treat it as a rollback; if the current snapshot's
generatedAt is invalid, treat the candidate as newer (not a rollback).
Export __testing for isMonotonicRollback and add five regression tests
covering newer/older sequence, equal-sequence timestamp comparison, and
both invalid-timestamp branches.
* fix(plugins): export rollback guards directly instead of __testing wrapper
The knip production deadcode scan flags __testing as an unused export
because test files are not entry points in that mode. Export
isHostedCatalogSignedFeedRollback and isMonotonicRollback as named
exports instead of wrapping them in __testing objects. Both are already
called from production code, so knip will see them as used.
* fix(plugins): keep rollback guards as internal functions, remove unit tests
The knip production deadcode scan flags any export that is only
referenced by test files as unused, even if the function is called
within the same source file. Both isHostedCatalogSignedFeedRollback
and isMonotonicRollback are internal helpers called from production
code in the same module, so exporting them solely for test access
triggers the check-dependencies CI gate.
Revert both functions to private (non-exported) and remove their
direct unit tests. The NaN-handling fix is still validated by the
existing catalog snapshot integration tests that exercise rollback
detection through the public API.
* fix(plugins): remove stray blank line before max-lines disable comment
* fix(plugins): validate catalog feed timestamps
* fix(plugins): validate recovered catalog sequence
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* fix(plugins): canonicalize manifest plugin ids to lowercase
Plugin config policy lists (plugins.deny, plugins.allow, plugins.entries)
are lowercase-normalized through normalizePluginId, but a plugin's
self-declared manifest id was only trimmed, never lowercased. A plugin
publishing "id": "Malicious-Scraper" therefore never matched an operator
denylist entry of "malicious-scraper" and fell through to default
activation, loading its hooks, channels, secret integrations, and tools.
The same gap let a mixed-case spelling evade the core reserved-id check.
Canonicalize the id at the manifest parse boundary where
PluginManifestRecord.id is minted, so every downstream policy consumer
compares against the same canonical form.
* test(plugins): prove mixed-case deny at gateway startup
* test(plugins): assert denied gateway record is absent
* test(plugins): materialize allowed secret fixture
* fix(plugins): compare a derived policy key instead of rewriting manifest identity
Manifest ids stay exactly as declared. Deny, allow, and per-entry checks now
compare a lowercase policy key derived at each enforcement boundary, matching
the lowercase-normalized config lists.
The previous approach lowercased PluginManifestRecord.id at the parse boundary.
That id is also matched against the plugin runtime export id, and a mismatch is
a hard load failure, so an existing plugin declaring the same mixed-case id in
both its manifest and its runtime export would stop loading after upgrade.
The Gateway fixture now declares a mixed-case manifest id with a matching
mixed-case runtime export, covering that upgrade case, and drops the allowlist
that previously scoped discovery by a lowercase id and masked the denylist
behavior under test.
* fix(plugins): close mixed-case policy gaps
* test(plugins): use tracked policy fixture temp dir
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Replace the bare onTurnAdopted callback and queuedFollowupLifecycle with one
turnAdoptionLifecycle surface (onSettled guaranteed via finally; adoption-loss
aborts queued steering turns without transcript replay), threaded through the
turn kernel and agent runner, exposed to plugins via
runtime.state.openChannelIngressDrain and the channel-outbound SDK barrel,
with surface budgets re-measured against the narrowed baselines (#108656).
* feat: consume live featured catalog state
* fix(plugins): honor hosted curation for published bundled plugins
* fix(plugins): accept package-less trusted ClawHub provenance