Skip to content

fix(plugins): activate a first install staged without Restart on the next launch - #505

Closed
nt-nerdtechnic wants to merge 3 commits into
mainfrom
fix/sideload-relaunch-promote
Closed

nt-nerdtechnic wants to merge 3 commits into
mainfrom
fix/sideload-relaunch-promote

Conversation

@nt-nerdtechnic

@nt-nerdtechnic nt-nerdtechnic commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A plugin installed (from a file or the Marketplace) is staged as a candidate and only plugins:restart promoted it. If the user reopened Navide without pressing Restart, the plugin never loaded, the log said installed plugin quarantined: … manifest.json is not valid UTF-8: ENOENT, and nothing appeared on screen (packaged checklist 0.2.27, finding 2). This PR activates such a first install at launch exactly as Restart would, makes the log accurate, and shows a main-window notice for any candidate that still needs the user.

Why

Developer experience: the sideload → edit → reinstall loop must work without a hidden extra step. A relaunch has nothing running, so promoting the staged first install is the same cutover Restart performs, with the same checks (trust re-verification, digest identity, version-bound grant from install-time consent).

What changed

  • pluginIpc.ts: the plugins:restart body is now restartStagedCandidate(id) (header and footer only; the body is unchanged). The handler keeps assertAuthorized + assertPluginRemovalTarget and calls it. New controller method activateStagedFirstInstalls() promotes each candidate with no active package, no activation in progress and nothing loaded under its id, and returns those it could not promote (they stay staged).
  • Not auto-promoted on purpose: an update next to an active package, and a package a rollback displaced (activatePrevious / completeRollback / demoteActiveToCandidate keep it as the candidate). The selector does not tell these apart, so the user still decides with Restart, as on main.
  • installedPlugins.ts scanInstalledPlugins: a staged first install (candidate, no active, no manifest.json at the root) is skipped instead of being reported as a broken package. A legacy package at the root still loads as before.
  • index.ts: runs activateStagedFirstInstalls() at startup, next to loadDeveloperFolders(). Each failure is logged with its real reason and sent through the new pluginStagedCandidateRelay (same hold-until-loaded pattern as pluginAgentActionRelay).
  • Preload onPluginStagedCandidateNotActivated, App.vue error toast, and the label.plugin-staged-not-activated string in en-US, zh-TW and ja-JP. The toast names the plugin, the version, the reason, and Settings → Navide Plugins, where Restart and Discard are.

Second commit: disclose a staged package beside the active one (coordinator request)

  • On purpose, a package staged next to an active one is still not applied automatically: an update installed without Restart, and the package a rollback displaced, look the same in the selector. Now it is disclosed instead.
  • pluginIpc.ts stagedUpdates() lists {pluginId, activeVersion, stagedVersion} for every record that has an active package, a candidate and no activation in progress. index.ts sends the list to the main window after the first-install activation, through the same relay (plugins:stagedUpdates).
  • The main window adds one row per package to the announcements centre: "The Navide plugin X has version Y waiting to be applied; version Z is running." It has an Apply now button and also shows a toast.
  • The row stays until the package is applied. Marking it read does not dismiss it.
  • Apply now runs plugins:restart for that package, only while the same version is still staged next to the same active one (pluginStagedApplyApplies). Otherwise the row is dropped with a "changed since" notice.
  • Strings are in en-US, zh-TW and ja-JP.
  • Three relaunch cases are tested:
    • first install: promoted
    • update from a file: stays staged and is listed
    • Registry rollback: the rollback stands, the selector is unchanged, and the package is listed

Regression check vs 0.2.26

Not a regression. git diff v0.2.26..origin/main touches nothing on the scanner or promote path (installedPlugins.ts unchanged; the selector only gained web in grant validation). The root fallback in scanInstalledPlugins dates from 088b4ca (#133, 2026-09-26). No App was launched.

Test plan (red → green)

Red on origin/main, 4/4 (pnpm vitest run src/main/plugins/pluginIpc.localInstall.test.ts -t "on the next launch" --maxWorkers 1):

  • is not reported as a broken package by the startup scan: the scan returned the ENOENT error entry
  • is activated exactly as Restart would have done it: no such method (active, grant, local-unsigned source and the next launch's scan are all checked)
  • leaves an update staged next to the active package for the user to restart
  • reports a candidate that can no longer be activated and keeps it staged: a tampered candidate is reported as quarantined and stays staged

Green: all 4.

Also: src/main/pluginStagedCandidateRelay.test.ts (2) and src/renderer/src/composables/__tests__/pluginStagedCandidateNotice.test.ts (2: text in all three locales, and wiring main → relay → preload → App.vue).

Regression: existing plugins

  • pnpm vitest run pluginIpc.localInstall pluginIpc.localInstall.review pluginIpc.agentFileInstall installedPlugins pluginStagedCandidateRelay pluginStagedCandidateNotice plansBackendStoppedNotice pluginAgentActionRelay --maxWorkers 2: 8 files, 249 passed
  • pnpm vitest run pluginIpc pluginIpc.developerMode pluginIpc.testplace pluginIpc.digestBlock navideBrowserRegistryInstall --maxWorkers 2 (every suite that drives plugins:restart): 5 files, 230 passed
  • pnpm typecheck: exit 0 (node, integration, public packages, built-in plugins, web, public), on both commits
  • Second commit: pnpm vitest run pluginIpc.localInstall pluginIpc pluginStagedCandidateRelay pluginStagedCandidateNotice useAnnouncements AnnouncementsPanel --maxWorkers 2 gives 6 files, 243 passed
  • No local full suites (0.2.28 dispatch rule); CI runs the full suites.

Protection check

  • Credential / account switching / quota paths: git diff origin/main -- 'backend/agent_team_backend/credential_*.py' 'backend/agent_team_backend/quota_*.py' 'backend/agent_team_backend/usage_*.py' | wc -l gives 0. No backend file is touched.
  • No function deleted: a definition list diff (function|const|let names, origin/main vs branch) of pluginIpc.ts, index.ts, installedPlugins.ts, preload/index.ts and App.vue shows nothing removed. App.vue's NUL byte is preserved.
  • Signature changes: none to existing exports. Added: PluginTrustRefreshController.activateStagedFirstInstalls() and stagedUpdates(), the preload onPluginStagedCandidateNotActivated and onPluginStagedUpdates, the announcement kind plugin-staged and the action plugin-staged-apply.
  • Consent and security: nothing gets looser. Promotion runs the same Restart code: trust re-verification, artifact digest identity, and the grant bound to the candidate at install time. A candidate that fails a check stays staged and is disclosed.
  • No limits added or removed.

🤖 Generated with Claude Code

nt-nerdtechnic and others added 2 commits October 20, 2026 23:15
…next launch

Installing a plugin (from a file or the Marketplace) stages it as a
candidate; only plugins:restart promoted it. Reopening the App without
pressing Restart left the lifecycle selector with a candidate and no active
package, so the plugin never loaded, the startup scan fell back to
<root>/<id>/manifest.json and logged a misleading "installed plugin
quarantined: ... manifest.json is not valid UTF-8: ENOENT", and nothing was
shown on screen.

- plugins:restart's body becomes restartStagedCandidate(id); the handler
  keeps its authorization and target checks and calls it.
- New controller method activateStagedFirstInstalls(), run at startup:
  promotes each candidate that has no active package, no activation in
  progress and nothing loaded under its id (a factory rollback also leaves
  only a candidate), exactly as Restart would. Updates and packages a
  rollback displaced stay staged: the selector does not tell them apart.
- The startup scan skips a staged first install instead of reporting it as
  a broken package (a legacy package at the root still loads as before).
- A candidate that cannot be promoted stays staged, is logged with the real
  reason, and the main window shows a toast naming the plugin, the reason
  and Settings -> Navide Plugins (Restart / Discard). en-US, zh-TW, ja-JP.

Not a 0.2.27 regression: the scan fallback dates from 088b4ca (#133) and
the promote path is unchanged since v0.2.26.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Apply now"

A package staged beside the active one (an update installed without Restart,
or the package a rollback displaced) is not applied for the user at launch:
the selector does not tell the two apart. Reopening Navide used to run the
old version with nothing on screen.

- pluginIpc: stagedUpdates() lists every record with an active package, a
  candidate and no activation in progress.
- index.ts: after the first-install activation, the list goes to the main
  window through pluginStagedCandidateRelay (now carrying both notices,
  held until a main window has loaded).
- Renderer: an announcements-centre row per package, "The Navide plugin X
  has version Y waiting to be applied; version Z is running", with "Apply
  now" (plugins:restart for that package, only while the same version is
  still staged beside the same active one), plus a toast. en-US, zh-TW,
  ja-JP.
- Tests for the three relaunch cases: first install (promoted), update
  (stays staged, disclosed) and rollback (the rollback stands, disclosed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nt-nerdtechnic added a commit that referenced this pull request Oct 20, 2026
…decision A)

User decision A (2026-10-21 00:13): an update an agent applied without
asking (O1: it discloses exactly what the running version does) also takes
effect immediately.

- commitUpdate stages the update as before, then runs the same
  restartStagedCandidate switch with the requesting agent as the trigger.
- The switch record carries the agent: the announcements row and toast say
  "An automatic update started by <agent> switched X from vA to vB"
  (en-US, zh-TW, ja-JP), with the same "Back to vA".
- A failed switch restores the old version (the restart's own rollback),
  keeps the update staged, and the main window keeps a "switch failed" row
  with the reason and Apply now (the #505 staged-package row, which now
  merges a newer report of a plugin instead of replacing the whole list).
- The O1 rules themselves are unchanged: anything that discloses differently
  still goes back to the user's approval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nt-nerdtechnic
nt-nerdtechnic marked this pull request as ready for review October 20, 2026 17:22
nt-nerdtechnic added a commit that referenced this pull request Oct 20, 2026
…next launch (#505)

Squash-merged from origin/pr-505.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nt-nerdtechnic

Copy link
Copy Markdown
Collaborator Author

Squash-merged into main as 8f51ac0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant