Skip to content

feat(plugins): a first install takes effect when the user approves it (L1) - #511

Closed
nt-nerdtechnic wants to merge 6 commits into
mainfrom
feat/live-activation-l1-first-install
Closed

nt-nerdtechnic wants to merge 6 commits into
mainfrom
feat/live-activation-l1-first-install

Conversation

@nt-nerdtechnic

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

Copy link
Copy Markdown
Collaborator

Summary

Live activation L1: approving the install of a plugin that is not installed yet makes it take effect immediately. There is no Restart button to find and no relaunch (ANALYSIS-plugin-live-activation, approved by the user 2026-10-20 23:46).

Stacked on #505 (fix/sideload-relaunch-promote, head ed5808b). L1 reuses the restartStagedCandidate extraction from #505. Once #505 merges, this branch merges main normally (no rebase), and the diff shrinks to the L1 commit c5030ae.

What changed (commit c5030ae)

  • plugins:commitInstall and plugins:commitLocalInstall: after the commit, outside its transaction lock, a first install is promoted through the same restartStagedCandidate cutover Restart runs. A first install here means a candidate with no active package, no activation in progress and nothing loaded under the id. A factory package or an update is left untouched.
  • The cutover is unchanged:
    • trust and digest re-verification at the cutover seam
    • the version-bound grant from this approval
    • the backend activation, handed to the Host's admission gate
  • Nothing runs under the id yet, so nothing is drained. A native backend still asks for its own consent before it starts.
  • The answer is either restartRequired: false, activatedVersion, or, when activation fails, restartRequired: true, activationError. On failure the candidate stays staged with Restart and Discard, and the install flow shows the reason (settings.extensions.activationFailed, en-US/zh-TW/ja-JP).
  • plugins:restart on a plugin that is already active with nothing staged returns success with nothing to do, so a flow that still calls Restart after the commit keeps working.
  • fix(plugins): activate a first install staged without Restart on the next launch #505's launch-time rule shares the new isStagedFirstInstall check.

Disclosure

  • On a first install there is no earlier view, so no transient state can be lost.
  • What changed is shown in place: the installed list shows the active version straight away instead of "Update vX is ready".
  • An activation failure is disclosed with its reason, and the package stays staged.
  • One-click undo of a first install is Remove, as before.
  • The switch record with "退回 vA" belongs to updates and comes in L3.
  • Consent rules do not loosen:
    • nothing is written before approval (the rejected-commit assertions stay as they were)
    • the approval itself writes the grant and activates the package
    • the native backend keeps its own separate consent

Test plan (red → green)

Red against #505's pluginIpc.ts, pnpm vitest run src/main/plugins/pluginIpc.localInstall.test.ts -t "L1" --maxWorkers 1: 2 failed, 1 passed.

  • is active right after the commit, with no Restart and no relaunch
  • stays staged with Restart and Discard, and says why, when the activation fails
  • (guard, already green) leaves an update from a file for Restart

Green: all three pass.

Assertions that encoded the old "commit only stages" contract were updated (10 tests):

  • the grant and installedAt are now written on approval instead of on Restart
  • the commit result shape
  • the TOCTOU test now reads the active copy instead of the staged one

The installLocal helper calls Restart only while something is staged. The relaunch tests from #505 leave a first install staged by failing its activation once ("the App went away").

Local run before the coordinator paused local tests:

  • pnpm vitest run pluginIpc.localInstall pluginIpc pluginIpc.localInstall.review pluginIpc.agentFileInstall pluginIpc.developerMode pluginIpc.testplace pluginIpc.digestBlock navideBrowserRegistryInstall pluginRegistry.e2e pluginAgentAction --maxWorkers 2: 10 files, 331 passed, 12 skipped
  • pnpm typecheck: exit 0
  • After that the host load reached 268, so per the coordinator nothing else ran locally. CI is the check from here.

Review fixes (planb/0228/review-live-activation.md)

  • H2 (CI red, classified, not rerun): pluginRegistry.e2e (3) still asserted the pre-L1 contract. It now checks that the approved commit activates 0.1.0 with its grant, and that a following Restart has nothing to do. Local run with the Registry venv: 13/13 pass. Commit 2b0d415.

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' src/renderer/src/composables/useQuotaFailover.ts | wc -l gives 0.
  • No function deleted: a definition list diff of pluginIpc.ts, preload/index.ts and usePluginInstallFlow.ts shows nothing removed.
  • Signature changes: the commitInstall / commitLocalInstall result type widens from restartRequired: true to restartRequired: boolean plus optional activatedVersion and activationError (preload and env.d.ts). No caller reads restartRequired. plugins:restart gains the already-active, nothing-to-do answer.
  • No limits added.

🤖 Generated with Claude Code

nt-nerdtechnic and others added 3 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>
… (L1)

Live activation L1 (ANALYSIS-plugin-live-activation, approved 2026-10-20
23:46). Approving an install used to only stage the package; it ran after
the user found Restart in Settings, or (since #505) on the next launch.

- plugins:commitInstall and plugins:commitLocalInstall now promote a first
  install (candidate, no active package, nothing loaded under the id) right
  after the commit, outside its transaction, through the same
  restartStagedCandidate cutover Restart runs: trust and digest
  re-verification, the version-bound grant from this approval, the
  activation the Host's backend admission gate uses. Nothing runs under the
  id yet, so nothing is drained. A native backend still asks for its own
  consent before it starts.
- The answer says what happened: restartRequired false with
  activatedVersion, or the candidate stays staged (Restart and Discard
  remain) with activationError, which the install flow shows. An update is
  left for Restart (L3).
- plugins:restart on a plugin that is already active with nothing staged
  answers success with nothing to do, so a flow that still follows the
  commit with Restart keeps working.
- The launch-time rule from #505 shares the same isStagedFirstInstall check.

Consent is unchanged: nothing is written before the approval, and the
approval is what activates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nt-nerdtechnic and others added 3 commits October 21, 2026 00:40
…pproval (L1)

CI on #511 (classified, not rerun): pluginRegistry.e2e (3) still asserted
the pre-L1 contract (commit only stages, the grant waits for Restart). It now
checks that the approved commit activates 0.1.0 with its grant, and that a
Restart that still follows has nothing left to do. Locally the suite was
skipped for lack of the Registry venv; with it, 13/13 pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l1-first-install

# Conflicts:
#	src/main/plugins/pluginIpc.localInstall.test.ts
#	src/main/plugins/pluginIpc.ts
@nt-nerdtechnic
nt-nerdtechnic marked this pull request as ready for review October 20, 2026 22:21
nt-nerdtechnic added a commit that referenced this pull request Oct 20, 2026
… (L1) (#511)

Squash-merged from origin/pr-511.

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 2e22e4a.

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