* 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
Embedded runs targeting a CLI runtime provider fall through to the openclaw
harness and call the provider API directly with the runtime's credentials
(cli_runtime_passthrough_openclaw). Anthropic routes direct anthropic-messages
calls on subscription OAuth tokens to metered extra-usage billing — this is
long-standing behavior, not a recent change. Without extra-usage balance,
every such run (e.g. active-memory recall) fails with a billing error; with
extra-usage enabled, the run silently draws paid metered usage instead of the
plan limits the CLI runtime was configured for. Only CLI-backed execution
runs on plan limits for those credentials.
Add an opt-in RunEmbeddedAgentParams.cliBackendDispatch: "subscription-auth"
that dispatches the run through runCliAgent as a one-shot turn when the
provider is claude-cli, a CLI backend is registered, and the ordered auth
profile selection for the passthrough resolves to a subscription (oauth/token)
credential or nothing rather than an API key; resolution stays on stored
credential metadata, with no credential materialization or refresh on the
per-turn path. The dispatch translates toolsAllow into the selectable-backend
surface (native: [], allowlisted loopback MCP tools; wildcard allowlists stay
MCP-only), runs with a fresh CLI process (no live-session reuse; session-
scoped bundle-MCP retirement on run end rather than the process-wide loopback
close), bridges CLI tool result events to onAgentToolResult with native-path
semantics (normalizeToolName + isToolResultError), and drops CLI session
bindings from the result.
The selectable-backend MCP list now also bounds the loopback MCP grant
server-side: the grant carries a per-run gateway tool allowlist enforced in
scoped tool resolution, so tools outside the run's allowlist can be neither
listed nor called even under CLI bypass permission modes where
--allowedTools is advisory.
active-memory recall opts in so recall works on claude-cli subscription-only
instances and stops drawing metered extra usage where it previously could.
Scoped to claude-cli; other CLI runtimes keep the passthrough until their
direct-API contract is verified.
The dispatch also mirrors the run into the run's session transcript through the session
accessor (user turn, tool call/result records as they stream, final assistant
snapshot at run end) so transcript consumers keep parity with embedded runs:
active-memory's persistTranscripts, timeout partial-text salvage, and the
live terminal-search watcher that polls the session file mid-run.
Post-review hardening: canonical anthropic/<model> refs whose configured
agentRuntime is claude-cli resolve through the runtime policy before the
dispatch gate (they previously stayed on the failing passthrough); restricted
dispatches serve an exclusive loopback-only MCP bundle so user/plugin MCP
servers stay outside the run's tool universe; and the transcript recorder
flushes the latest assistant snapshot the moment the run aborts, so timeout
salvage sees partial text even while the killed CLI child is still settling.
Recalls routed to the claude-cli runtime default to a 45s budget (measured
CLI-dispatched runs take 14-20s, over the plain 15s default); explicit
timeoutMs config always wins.
Transcript mirror keeps bare-array tool_result content (claude stream-json
echoes MCP results without a {content} wrapper); dropping it classified every
successful recall as no_relevant_memory.
CLI dispatch resolves inside session/global lane admission so dispatched
runs obey the same lifecycle, placement, and concurrency gates as native
embedded runs.
LOC-ratchet offsets move the loopback grant-context builders to
cli-runner/mcp-grant-context.ts and dedupe the run/prep stage-summary
emitters into attempt-stage-timing.ts; unused type exports dropped and the
now-used stream-message baseline entry removed.
The recall timeout default now consumes the runner's own dispatch
eligibility through a new plugin-runtime seam
(agent.resolveCliBackendDispatchEligibility): API-key and missing-backend
routes keep the passthrough and its plain 15s default.
Eligibility honors an explicitly pinned authProfileId (the credential the
run executes on) before ordered profile selection, in both directions.
Transcript mirror composes buildAssistantMessage + buildUsageWithNoCost
directly; main trimmed the zero-usage wrapper export (ab0ccc244b) before
this branch's usage landed.
Dispatch eligibility is provider-owned: the anthropic plugin's claude-cli
backend declares CliBackendPlugin.subscriptionAuthDispatch and core reads
the registered descriptor instead of a core provider allowlist.
Dispatch fails closed on tool policy (only non-empty named allowlists are
expressible on the CLI surface; deny-all, wildcards, absent allowlists, and
disableTools/modelRun keep the passthrough), threads the pinned
authProfileId into CLI runtime resolution, and emits onExecutionStarted at
the admitted dispatch boundary.
* fix(workboard): record resolved runtime metadata instead of hardcoded codex engine
Workboard executions labeled every dispatched run engine=codex, model=default,
and id suffix :codex even for Claude/other harness agents. The gateway agent
admission phase now returns the resolved {harness, provider, model} for plugin
subagent runs; the dispatcher records it verbatim and omits engine/model when
unresolved. Engine becomes an open runtime identifier in the workboard
contract (built-in launch choices stay a closed list), store/UI normalizers
preserve historical labels as written instead of inventing codex, and new
execution ids use an :agent-session suffix. Fixes#108362
* fix(workboard): accept undefined engine in ui engineModel helper
* refactor(channels): generalize channel retry runner and retry-after parsing; drop dead configured-state probes
* fix(channels): align retry and metadata contracts
* fix(plugins): handle ENOTDIR gracefully in npm-project-roots
The listManagedPluginNpmProjectRootsSync and listManagedPluginNpmProjectRoots
functions only caught ENOENT errors when reading the projects directory,
but an ENOTDIR error (when a file path is mistakenly used as a directory)
would propagate unhandled and crash the CLI.
Replace the local isMissing helper with the shared isNotFoundPathError from
infra/path-guards.ts, which already covers both ENOENT and ENOTDIR.
Fixes#107467
* test(plugins): use managed temp directory fixture
* chore: keep release notes out of contributor PR
* fix(plugins): restore not-found path guard export
* fix(plugins): normalize Windows managed install paths
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>