You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: daemon unlocks passphrase-protected profiles on switch/refresh
A user's diagnostics showed the dashboard "Session active and ready" while the
daemon stayed "anonymous" after switching to a passphrase-protected profile,
and "Refresh state" never recovered. Root cause: the long-lived daemon reads
PERPLEXITY_VAULT_PASSPHRASE from SecretStorage only ONCE at spawn; a profile
switch just touched .reinit, so init() re-ran with the stale/absent passphrase
("Vault locked: no keychain, no env var, no TTY"), and the vault unseal cache
pinned the first profile's material ("wrong passphrase"). The extension could
read the vault (user typed the passphrase) — hence the two badges disagreed.
Fix (authed reinit endpoint + global passphrase, per maintainer decision):
- daemon/server.ts: new loopback-only, bearer-authed POST /daemon/reinit
{passphrase} → sets the daemon's env, __resetKeyCache(), hot-reloads the
client (no restart, no dropped MCP connections). Blocked from tunnel by the
H11 admin allowlist (404) + an in-handler loopback check; audit logs only
method+path, never the body.
- client.ts: reinit({passphrase}) sets env (when supplied) and ALWAYS resets
the vault cache before re-init, so a profile switch isn't blocked by the
previous profile's pinned material. DaemonAuthStatus gains a machine-readable
reason (ok / vault-locked / not-logged-in), surfaced via config.ts
wasLastVaultLocked().
- DashboardProvider.ts + extension.ts: Refresh state, webview profile:switch,
and command-palette switchAccount/addAccount POST the SecretStorage
passphrase to /daemon/reinit (non-spawning — only targets an already-running
daemon; falls back to .reinit on HTTP failure / non-2xx).
- views.tsx: daemon badge is reason-aware (vault-locked vs not-logged-in).
Tests: reinit-http (bearer/passphrase/empty), /daemon/reinit in the tunnel
admin-allowlist suite (tunnel 404 / loopback handler), wasLastVaultLocked
coverage. Adversarial review (4 dims) — all confirmed findings fixed. Full
typecheck + build clean; 1154 unit tests pass. Version 0.8.46 -> 0.8.47.
Deferred: reconnecting loader. Needs live Windows daemon smoke to confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
> Reported via a diagnostics bundle: after switching to a **passphrase-protected** profile, the dashboard showed "Session active and ready" while the daemon showed "Daemon sees anonymous session — use Refresh state to reconnect", and Refresh did not help. Root cause: the long-lived daemon is spawned with `PERPLEXITY_VAULT_PASSPHRASE` read from SecretStorage **once at spawn**; a profile switch only touched `.reinit`, which re-ran `init()` with the *stale/absent* passphrase, so the daemon could not unseal the new profile's vault (`Vault locked: no keychain, no env var, no TTY`). The vault unseal cache also pinned the first profile's material (`Vault decrypt failed: wrong passphrase`). The extension could read the vault (the user had typed the passphrase) — hence the two badges disagreed.
12
+
13
+
### Fixed
14
+
15
+
-**The daemon now re-receives the vault passphrase on profile-switch / "Refresh state"** instead of staying anonymous. New **loopback-only, bearer-authed `POST /daemon/reinit {passphrase}`** ([`daemon/server.ts`](packages/mcp-server/src/daemon/server.ts)) sets `PERPLEXITY_VAULT_PASSPHRASE` in the running daemon, clears the vault unseal cache, and hot-reloads the client — no daemon restart, no dropped MCP connections. The endpoint is blocked from tunnel callers by the H11 admin allowlist (404) plus an in-handler loopback check (defense-in-depth); the audit log records only method+path, never the body.
16
+
-**`client.reinit({ passphrase })`** ([`client.ts`](packages/mcp-server/src/client.ts)) sets the env (when supplied) and **always `vault.__resetKeyCache()`** before re-init, so a profile switch is never blocked by the previous profile's pinned `_unsealMaterialCache`/`_keyCache`. The daemon's own active-profile-switch reinit benefits automatically.
17
+
-**The extension re-supplies the passphrase** ([`DashboardProvider.ts`](packages/extension/src/webview/DashboardProvider.ts)): "Refresh state", webview `profile:switch`, and the command-palette `Perplexity.switchAccount` / `Perplexity.addAccount` now POST the SecretStorage passphrase to `/daemon/reinit` (non-spawning — it only targets an already-running daemon; falls back to touching `.reinit` if the HTTP path is unavailable or returns non-2xx).
18
+
19
+
### Added
20
+
21
+
-**`DaemonAuthStatus.reason`** (`ok` / `vault-locked` / `not-logged-in`) in [shared](packages/shared/src/models.ts), surfaced from `getSavedCookies()` → `wasLastVaultLocked()` → `init()` → `writeDaemonStatus()`. The dashboard's daemon badge is now **reason-aware** ([`views.tsx`](packages/webview/src/views.tsx)) — it distinguishes "daemon can't unlock this profile's vault" from "not logged in" instead of always saying "use Refresh state".
22
+
23
+
### Known follow-up
24
+
25
+
- An animated "reconnecting…" loader while a daemon reinit is in flight is deferred (needs transient webview-store state). One vault passphrase per machine is intentional (global SecretStorage key); per-profile passphrases are out of scope.
26
+
27
+
### Verification
28
+
29
+
- New unit tests: `test/daemon/reinit-http.test.js` (bearer required, passphrase forwarded, empty/missing handled), `/daemon/reinit` added to the tunnel-admin-allowlist suite (tunnel→404, loopback→handler), and `wasLastVaultLocked()` coverage in `test/config-getSavedCookies.test.js`. Adversarial review (4 dimensions) — all confirmed findings fixed (command-palette wiring, `res.ok` fallback, no-spawn). Full typecheck + build clean across all four packages. **Live daemon + passphrase-profile-switch flow on Windows still needs the manual smoke** (the SecretStorage→daemon round-trip can't be exercised without a real daemon).
> Ref [#8](https://github.com/Automations-Project/VSCode-Perplexity-MCP/issues/8). Regression from the 0.8.43 issue-#5 fix (commit `c841124`): Phase 2 (headless search) switched from an *ephemeral* `chromium.launch()` to a long-lived `launchPersistentContext(browser-data)` so it could reuse the on-disk `cf_clearance` written by Phase 1. The side effect — the daemon now holds the `browser-data` Chromium process-singleton lock for its entire lifetime, and Chromium forbids a second `launchPersistentContext` on the same `--user-data-dir`. Three independent callers still targeted that dir: the Doctor probe (a separate process that ran its own `init()` against `browser-data`), the daemon's own Phase 1→Phase 2 transition, and the reinit cycle. On Windows each collision surfaced as `exitCode 21` / "Target page, context or browser has been closed", leaving the daemon stuck in anonymous mode in a self-reinforcing deadlock that only a full restart cleared. The fix makes the daemon the **single owner** of `browser-data` and hardens the lock handling — without reverting the `cf_clearance` reuse.
0 commit comments