fix(update): mandatory post-core plugin convergence before gateway restart#79143
Conversation
|
Codex review: needs changes before merge. Summary Reproducibility: yes. Current-main source shows the invalid-config path returns Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Land the convergence design after the smoke check shares the existing package-entry boundary and runtime-resolution contract, then refresh targeted tests and proof if the observable update output changes. Do we have a high-confidence way to reproduce the issue? Yes. Current-main source shows the invalid-config path returns Is this the best way to solve the issue? Not yet. The post-core convergence direction is the right narrow fix, but the static payload smoke check should reuse or mirror Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 00d0d4bd1916. |
|
Addressed independent code review (Codex). New commit #1 (blocker) — convergence-then-commit stale record overwrite. #2 (should-fix) — smoke check too strict for OpenClaw extensions. #3 (should-fix) — guidance not surfaced in non-JSON output. Both the per-warning loop and the new invalid-config error path now log each #4 (should-fix) — invalid config silently bypassed convergence. #5 (nit) — phase-helper comment overstates safety. Expanded the
Re-review progress:
|
|
Round 2 of Codex review addressed in commit New blocker from R1 (sync/npm in-memory mutations clobbered by convergence reseed). Fixed by plumbing the baseline through:
#4 incomplete (no test for invalid-config error). Extracted
|
|
Round 3 of Codex review addressed in commit #4 mock omission. Added #2 invalid-config end-to-end test. Exported
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06a490c8e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Codex / clawsweeper review addressed in commit [P2] Scope convergence smoke check to active records. Added [P3] Update docs/cli/update.md for the new failure mode. Added a paragraph to the [P3] CHANGELOG entry. Added under
Real-behavior-proof gate is still green from the prior captured-output update; the active-record filter doesn't change the captured before/after terminal output for the in-scope failure shapes, but I'm happy to recapture if the maintainers want additional coverage of a stale-disabled-record-no-longer-blocking-update scenario. Re-review progress:
|
a8cff07 to
0ef654b
Compare
|
@clawsweeper, @openclaw-barnacle now mergeable |
0ef654b to
7f4e1b2
Compare
7f4e1b2 to
c1ad01f
Compare
|
Rebased onto current upstream/main ( |
|
@clawsweeper proof should be sufficient again. Just needed to rebase |
|
6db984a to
23493da
Compare
|
@clawsweeper for your review |
…s + docs/changelog
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87e899c19f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const extensionPath = path.join(installPath, extensionRel); | ||
| const extensionStat = await safeStat(extensionPath); | ||
| if (!extensionStat?.isFile()) { |
There was a problem hiding this comment.
Validate extension paths stay inside install directory
The smoke check currently builds each extension candidate with path.join(installPath, extensionRel) and only verifies file existence, so a manifest entry like ../dist/index.js can pass if that sibling file exists even though plugin runtime resolution rejects entries that escape the package root (see resolveSafePackageEntry in src/plugins/package-entry-resolution.ts). In that case post-core convergence reports success, the gateway restarts, and the plugin still fails to load—the exact failure mode this convergence gate is meant to prevent.
Useful? React with 👍 / 👎.
87e899c to
2afa84d
Compare
|
Landed via squash merge: 109493b Thanks @BKF-Gitty. Verification before merge:
|
Summary
OPENCLAW_UPDATE_IN_PROGRESS=1) from "post-core convergence" (OPENCLAW_UPDATE_POST_CORE_CONVERGENCE=1); plugin repair no longer defers in the latter.postUpdate.plugins.statusto"error", which the existing pre-restart gate maps toexit 1with precise repair guidance.status: "error"(was"skipped") so the gateway is not restarted with an unverifiable plugin set.Motivation
A live OpenClaw instance had Brave/Discord plugins configured with install records present in the persisted index but their payload directories /
package.jsonmissing on disk.openclaw updateran the in-update doctor pass withOPENCLAW_UPDATE_IN_PROGRESS=1, which deferred configured plugin repair, and then restarted the gateway with broken plugins. This PR makes that scenario an explicit pre-restart failure with actionable guidance instead of a silently broken gateway.What changed
src/commands/doctor/shared/update-phase.ts— new module exportingisUpdatePackageSwapInProgress/isPostCoreConvergencePass(post-core wins).src/commands/doctor/shared/missing-configured-plugin-install.ts— switched to phase helpers; deferral path no longer fires when post-core convergence is set.repairMissingConfiguredPluginInstallsnow accepts an optionalbaselineRecords(so callers can pass in-memory state from earlier post-core steps) and returns the post-mutationrecordsmap.src/commands/doctor/repair-sequencing.ts,src/commands/doctor/shared/release-configured-plugin-installs.ts— switched to the same phase helpers.src/cli/update-cli/plugin-payload-validation.ts— new static payload check (validates package dir, parsespackage.json, only fails onmissing-main-entrywhenmainis explicitly declared and absent — OpenClaw plugins withexports/openclaw.extensionsand nomainare accepted).src/cli/update-cli/post-core-plugin-convergence.ts— orchestrator that sets the env signal, callsrepairMissingConfiguredPluginInstallswith the in-memory baseline, runs the payload check on the post-repair record map, and folds warnings into outcomes.src/cli/update-cli/update-command.ts— wired the convergence call intoupdatePluginsAfterCoreUpdateafter the existing npm-update pass; passes the in-memorypluginConfig.plugins.installsas the baseline and reseedspluginConfigfromconvergence.installRecordsunconditionally before the commit block (so sync/npm in-memory mutations and convergence repairs are both preserved). Convergence failures and invalid configs flipstatusto"error". Per-warningguidance[]lines are surfaced under the warning message in non-JSON mode.__testingexport frommissing-configured-plugin-install.ts(zero consumers).Real behavior proof
Behavior or issue addressed: An OpenClaw instance had configured plugins (Brave, Discord) whose persisted install records pointed at payload directories that no longer existed on disk. Running
openclaw updatedeferred configured plugin repair whileOPENCLAW_UPDATE_IN_PROGRESS=1was set, and then restarted the gateway with broken plugins instead of failing the update with actionable guidance. The fix introduces a mandatory post-core convergence pass that repairs / validates configured plugin payloads after the package swap and before the gateway restart, escalating any unrepairable failure tostatus: "error"so the existing pre-restart gate exits the update with non-zero status and clear repair instructions.Real environment tested: macOS Darwin arm64, Node 22.21.1, branch
fix/post-core-plugin-convergenceat commit77e70802d0, baseline compared againstupstream/main@97d2d40fb7(fix: allow safe exec secret passEnv inheritance). The same synthetic driver script (a small TypeScript program that constructs anOpenClawConfigmirroring the production failure shape and invokesupdatePluginsAfterCoreUpdatedirectly) was executed against both code states. Only the function-under-test code differs between the two runs.Exact steps or command run after this patch:
HOMEandOPENCLAW_STATE_DIRso the persisted installed-plugin index lives in an isolated sandbox.OpenClawConfigwith three failure shapes mirroring the live incident: (a) configured plugin entry + persisted install record + missing payload directory; (b) configured plugin entry + persisted install record + payload directory present but with an unparseablepackage.json; (c)configSnapshot.valid = falseto exercise the invalid-config gate.updatePluginsAfterCoreUpdatefromsrc/cli/update-cli/update-command.tsand invoked it withopts: { json: false }so the human-readable terminal output is captured.tee, plus the returned{ status, reason, warnings }object.upstream/main@97d2d40fb7(after temporarily exporting the otherwise-private function) to capture the before-fix terminal output for comparison.Driver invocations (recorded with
teeso the captured output below is verbatim stdout):Evidence after fix:
Live terminal output captured from the after-fix run on branch HEAD
77e70802d0against the invalid-config shape:Live terminal output captured from the after-fix run on branch HEAD
77e70802d0against the corrupt-payload shape (configuredmade-up-pluginwhose payload directory exists but containspackage.json="this-is-not-json", and whose plugin id does not exist on npm so the existing repair path cannot fix it):For comparison, the same invalid-config invocation against
upstream/main@97d2d40fb7(before fix) — stdout captured verbatim — returned the silently-passing path:And the corrupt-payload invocation against
upstream/main@97d2d40fb7(before fix) silently auto-disabled the plugin without any payload validation:Observed result after fix:
updatePluginsAfterCoreUpdatenow returnsstatus: "error"and a structuredwarnings[].guidancepayload for both the invalid-config shape and the unrepairable-payload shape. The exact same situations onupstream/main@97d2d40fb7returnedstatus: "skipped"(invalid config) or silently auto-disabled the plugin (corrupt payload) and would have allowed the gateway to restart in a broken state. The new payload smoke check surfaces the actual JSON-parse error in the corrupt-payload case (Unexpected token 'h', "this-is-not-json" is not valid JSON) and the human-readable guidance lines (Run \openclaw doctor --fix`,Run `openclaw plugins inspect made-up-plugin --runtime --json`) are emitted to stderr under each warning. The pre-restart gate inupdateCommand(if (postCorePluginUpdate?.status === "error") { exit(1) }`) maps this to a non-zero exit, blocking the gateway restart that previously masked the broken state.What was not tested: An end-to-end run on the original affected production instance after deploying this branch is not yet captured — that needs to land before the PR moves out of draft, and will be appended to this section as additional terminal-capture evidence (
openclaw update,openclaw gateway status --deep, and thegateway/restart.logexcerpt). The synthetic driver also stops short of the post-restart gateway health probe because the harness fakes the config-file hash; the convergence behavior asserted above all happens before that point and is independent of the post-convergence commit step.