Skip to content

feat(plugins): disclose Developer Mode settings changed outside Navide and ask again (A2-5) - #537

Open
nt-nerdtechnic wants to merge 6 commits into
mainfrom
feat/a2-5-devmode-external-change
Open

nt-nerdtechnic wants to merge 6 commits into
mainfrom
feat/a2-5-devmode-external-change

Conversation

@nt-nerdtechnic

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

Copy link
Copy Markdown
Collaborator

Summary

DESIGN-A2 slice A2-5 (gap G5) — security-related, needs its own independent review.

The Developer Mode settings file (.navide-plugin-developer-mode.json) is unsigned: any unsandboxed process of the user, an agent CLI included, could turn the switch on and add a "consented" developer folder, which the next start loaded without the consent dialog and without an activity log entry.

  • Navide now keeps an HMAC-SHA256 of each entry it writes itself (the switch when on, each folder consent) in its own file .navide-plugin-developer-mode.sig.json. The key is 32 random bytes wrapped by Electron safeStorage. The store file keeps its 0.2.27 shape (downgrade-safe; 0.2.27 ignores the new file).
  • An entry Navide did not write is not applied (disclose, don't restrict — nothing is blocked, the write is just not taken as the user's consent):
    • the switch reads as off until the user turns it on in Settings → Navide Plugins (with the existing confirmation dialog);
    • a folder entry is listed as "Added or edited outside Navide; consent again", is never loaded, and Reload opens the normal consent dialog for the folder the entry names; consenting or removing resolves it;
    • its id is still held (assertNoDeveloperFolder / assertFolderIdFree use anyFolder), and turning Developer Mode off unloads it too if it was loaded in this session.
  • Disclosure: launch reminder (DeveloperModeReminder) and a role="alert" banner on the Navide Plugins page with the detection time; one Host activity event developerMode.externalChange the first time it is seen (backend _HOST_EVENT_ADDRESSES updated).
  • Writes are crash-safe: the signature first carries old+new MACs, then the store is written, then the signature is narrowed, so a stop in between never reads as an outside change.
  • RULE ZERO: no crypto is touched while Developer Mode was never used (no keychain/keyring prompt for anyone else); without an integrity provider (tests, older call sites) the store behaves exactly as on main.

Known limits (documented in plugin-development-v2.md)

  • Checked only after app is ready and while secure storage is available. The startup load of developer folders therefore moved into app.whenReady(); the pre-ready self-hosted Registry override read (index.ts top level) is not checked (it also needs env vars in Navide's own environment). Linux without a keyring: no check.
  • A pre-0.2.28 file is signed once as it is → HIGH-1 (a), user decision 01:44: a missing signature file with a non-empty settings file is fail-closed: the switch reads as off and every folder asks again once (notice reason signature-missing). 0.2.27 Developer Mode users re-consent each local folder once after upgrading (CHANGELOG, user guide en-US/zh-TW/ja-JP).
  • MED-1 (disclosed): the MACs cover entries, not their age, and the key does not rotate, so restoring an older copy of both files together (taken while the switch was on, or before a folder was removed) makes that state trusted again without a notice.
  • MED-2 (fixed): an encrypt/decrypt or signature-file failure never throws out of isEnabled()/folders(); the user's write is still written, unsigned, reads as "ask again" and is disclosed (reason signing-unavailable).
  • On Windows/Linux the safeStorage key is readable by any process of the same user, so a determined program can forge an entry there; on macOS the keychain item is per-app.
  • A signature file that cannot be read or decrypted (another machine, new keychain) asks about every entry again.

Review round 1 fixes (7b976fa, 6128e04, d47a324, 143d053)

  • MED-2 above; LOW-2: the user's "off" writes over an outside "on" and clears its notice; LOW-3: a test that the two stores the app keeps report an outside change once; LOW-1 / MED-1 documented; HIGH-1 (a).
  • Local: store integrity tests 13/13 green (new MED-2, LOW-2 and both HIGH-1 tests red on the previous store); pluginIpc.developerMode.test.ts + store 42/42; pluginIpc.developerMode + DeveloperMode.test.ts 33/33; pnpm typecheck exit 0 (on 7b976fa's code).

Why

Approved DESIGN-A2 §2.5, Approval (b) "G5 要做".

Test plan (red → green)

  • src/main/plugins/pluginDeveloperModeStore.integrity.test.ts (new, 9 tests): 9/9 red before the store change, 9/9 green after.
  • src/main/plugins/pluginIpc.developerMode.test.ts new describe (3 tests): with main's pluginIpc.ts swapped in, 2 red / 1 green (the third is a regression guard that also passes on main); with the branch, green.
  • pnpm vitest run src/main/plugins/pluginIpc.developerMode.test.ts src/main/plugins/pluginDeveloperModeStore.integrity.test.ts --maxWorkers 1 → 38 passed (38) (all 26 existing developer-mode tests unchanged and green). Run before merging origin/main (fix(plugins): activate a first install staged without Restart on the next launch #505).
  • Not run locally (5-min load 72–87 > 60, handed to CI): DeveloperMode.test.ts, ExtensionsPane.test.ts (new tests added), backend/tests/test_plugin_activity_host_event.py (new case), typecheck, and the full suites. Will run them locally once load < 60 and report any difference from CI.

Regression: existing plugins

Pending local run (load); CI runs the full frontend and backend suites. Behaviour with Developer Mode never used: no new file read beyond the store, no crypto, identical state shape (externalChange / externallyChanged are only added when present, so toEqual({ enabled: false, folders: [] }) still holds).

Protection check

  • git diff $(git merge-base HEAD origin/main) HEAD -- backend/agent_team_backend/credential_*.py quota_*.py usage_*.py → empty. No account-switching code touched. The HMAC key is a new file under the plugins root, not a CLI credential.
  • No function deleted. Signature changes: PluginDeveloperModeStore constructor gains optional integrity; private write gains optional evaluated/decision; registerPluginIpc options gain optional developerModeIntegrity; HostEventAddress gains 'developerMode.externalChange'; new FrontendPluginManager.recordDeveloperModeExternalChange. New store methods: untrustedFolders, anyFolder, externalChange.
  • No new count/size/concurrency limit.
  • Merge with origin/main (fix(plugins): activate a first install staged without Restart on the next launch #505): conflict in src/main/index.ts resolved keeping both — developer folders load at app.whenReady(), fix(plugins): activate a first install staged without Restart on the next launch #505's staged first-install activation unchanged.

Overlaps with A2-1/A2-2/A2-4 in pluginIpc.ts (developerModeState, setDeveloperMode(false), reloadDeveloperPlugin); whichever lands second merges main normally.

🤖 Generated with Claude Code

nt-nerdtechnic and others added 6 commits October 21, 2026 01:37
…e and ask again

DESIGN-A2 slice A2-5 (gap G5). The Developer Mode settings file is unsigned,
so any unsandboxed process of the user (an agent CLI included) could turn the
switch on and add a "consented" folder that the next start loaded without the
consent dialog and without an activity log entry.

- Navide keeps an HMAC-SHA256 of each entry it writes itself (the switch when
  on, each folder consent) in its own file,
  .navide-plugin-developer-mode.sig.json; the key is random and wrapped by
  Electron safeStorage. The store file keeps its 0.2.27 shape (downgrade-safe).
- An entry Navide did not write is not applied: the switch reads as off until
  the user turns it on in Settings (with the usual confirmation), and the
  folder is listed as "Added or edited outside Navide; consent again" and
  loads only after the user consents again (Reload opens the consent dialog).
  Nothing is blocked; the other program's write is just not the user's consent.
- Disclosed in the launch reminder and on the Navide Plugins page, and as one
  Host activity event developerMode.externalChange (backend allowlist updated).
- No crypto is touched while Developer Mode was never used; nothing is checked
  without secure storage or before the app is ready. The startup load of
  developer folders now waits for app ready so it is checked. A file from
  before 0.2.28 is signed once as it is. Limits are documented.

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

# Conflicts:
#	src/main/index.ts
…s an outside on

Review of #537:
- MED-2: an encrypt/decrypt or signature-file failure never leaves isEnabled()
  or folders(). Navide's own write is still written, unsigned, so it reads as
  "ask again" with its own notice (reason signing-unavailable, shown in the
  launch reminder and on the Navide Plugins page).
- LOW-2: turning Developer Mode off writes over a switch another program
  turned on, so that entry and its notice go.
- LOW-3: covered by a test that the two stores the app keeps report an outside
  change once.
- MED-1 / LOW-1: replay of an older copy of both files, the pre-ready window
  and Linux basic_text are disclosed in plugin-development-v2.md.

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

Review HIGH-1 on #537, option (a): with no signature file, every entry of a
non-empty settings file was signed as Navide's own, so deleting
.navide-plugin-developer-mode.sig.json made any outside edit trusted. Now a
missing signature means every entry is asked about again once (switch reads
as off, each folder needs consent) with its own notice (reason
signature-missing). Cost: a 0.2.27 Developer Mode user consents once per
folder after upgrading.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…disclosed and asked again

en-US, zh-TW, ja-JP: replace "unsigned file, changes are not recorded" with
the behaviour after #537 and HIGH-1 (a), including the one-time re-consent
after upgrading or when the signature file is missing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ce after upgrading

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 22:52

This branch has not been deployed

No deployments
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