Skip to content

Commit ce94c60

Browse files
authored
Merge pull request #4 from Automations-Project/fix/vault-unseal-hardening
fix(vault): unseal hardening for external MCP clients (issue #3)
2 parents 503e7f4 + c1a9af3 commit ce94c60

29 files changed

Lines changed: 1105 additions & 40 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
fail-fast: false
2020
matrix:
2121
os: [ubuntu-latest, windows-latest]
22-
node-version: [20, 22]
22+
node-version: [22, 24]
2323
defaults:
2424
run:
2525
shell: bash

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
# New files are HIDDEN unless explicitly whitelisted.
44
# Only production-relevant files are public.
55
# ═══════════════════════════════════════════════════════
6+
#
7+
# DO NOT `git add -f` files under docs/superpowers/ unless you have a clear,
8+
# considered reason to publish them. Planning artifacts under that path are
9+
# private AI-development content. The pre-push hook in
10+
# scripts/git-hooks/pre-push will refuse pushes that contain them — set
11+
# PERP_ALLOW_PRIVATE_PLAN_PUSH=1 to override for a single push if you really
12+
# mean it. Activate the hook once per clone with:
13+
# git config core.hooksPath scripts/git-hooks
14+
# (or run `npm install` — postinstall does it for you).
615

716
# 1. Ignore everything
817
*

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,47 @@ All notable changes to this project are documented here. Format follows
66

77
## [Unreleased]
88

9+
## [0.8.41] — 2026-05-10 — Vault unseal hardening for external MCP clients
10+
11+
> Refs [#3](https://github.com/Automations-Project/VSCode-Perplexity-MCP/issues/3). Driver: an external user (Claude Code on Win11) hit "Vault locked" because the extension-managed daemon never received the SecretStorage passphrase, AND the launcher silently fell back to direct vault access in the client's runtime.
12+
13+
### Fixed
14+
15+
- **Daemon spawn now receives the SecretStorage passphrase via a narrowly-scoped env builder.** The `configureDaemonRuntime` config gained an optional `buildDaemonEnv` async provider; the extension wires `() => buildDaemonEnv(context)` which calls `peekStoredVaultPassphrase`. Provider env is merged AFTER `process.env` and BEFORE the hard-coded `ELECTRON_RUN_AS_NODE` / `PERPLEXITY_CONFIG_DIR` / `PERPLEXITY_OAUTH_CONSENT_TTL_HOURS` overrides — the provider cannot clobber critical spawn env. Passphrase status is logged as `set` / `unset` only; the value never appears in logs and the extension host's ambient `process.env` is never mutated.
16+
- **Generated `stdio-daemon-proxy` launcher refuses silent fallback to in-process stdio.** Pre-0.8.41, when daemon attach failed, the launcher would spawn a fresh in-process MCP server in the client's Node runtime — which on Claude Code (Node 24+), Antigravity, or any non-Electron runtime would then try to read `vault.enc` with no SecretStorage access and no keytar that loads. Now the launcher catches a typed `DaemonAttachError`, writes a structured remediation to **stderr only** (stdout is the JSON-RPC framing channel), and exits 2 (operator-actionable misconfiguration).
17+
18+
### Added
19+
20+
- **`DaemonAttachError`** in `packages/mcp-server/src/daemon/attach.ts` with `code: "DAEMON_UNREACHABLE"`, `remediation: readonly string[]`, optional `cause`. Used by the launcher and by `cli.js`'s `daemon:attach` subcommand. `attach.ts` is forbidden from calling `process.exit` — the entrypoint layer (launcher, CLI) owns process-termination semantics.
21+
- **Reserved exit code `2`** for "operator-actionable misconfiguration" (distinct from `1` = generic crash). Documented in launcher comments.
22+
- **`docs/vault-unseal.md`** — was referenced from the "Vault locked" error message since v0.4.x but never existed. Now documents the keychain → env var → TTY unseal chain, standalone vs. extension-managed paths, per-platform notes, and recovery flow.
23+
- **`docs/troubleshooting/external-mcp-clients.md`** — single canonical page for users hitting "Vault locked" or "DAEMON_UNREACHABLE" from external IDEs (Claude Code, Antigravity, Codex CLI, Cursor). Linked from both READMEs.
24+
- Softened the Windows-keychain "just works" claim in `docs/codex-cli-setup.md` with a "what if it fails" paragraph pointing at `setup-vault` and the new recovery doc.
25+
- **Repo tooling:** pre-push hook (`scripts/git-hooks/pre-push`) refuses to publish `docs/superpowers/` paths. Auto-installed via `npm install` postinstall.
26+
27+
### Changed
28+
29+
- **CI matrix:** Node 20 → Node 22 + Node 24. Node 20 reached End-of-Life on 2026-04-30. Resolved two pre-existing Node-20-specific failures (Linux tsup DTS worker OOM + Windows leaked FSWatcher in `launcher.test.js`).
30+
- **`engines.node`:** `>=20``^22.0.0 || ^24.0.0` in both `packages/extension/package.json` and `packages/mcp-server/package.json`. Matches what we test; pattern lifted from Vite/Vitest's engines style.
31+
32+
### Migration notes
33+
34+
- **No breakage** for users on Win11/macOS with working keychain. Daemon runs; attach succeeds; business as usual.
35+
- **Behavior change** for users currently relying on the silent in-process fallback: they now see an actionable stderr remediation instead of "anonymous mode" silently. This is the intended outcome — issue #3 reporters are exactly this cohort.
36+
- The 0.8.40 launcher on disk gets rewritten by `ensureLauncher`'s byte-comparison logic on next extension activation. No manual user action needed.
37+
38+
### Verification
39+
40+
- Phase 0 keytar probe passed on Win11 + VS Code Code.exe (Electron 39.6.0, Node 22.22.0 internally) — keytar loads reliably under the daemon's spawn runtime.
41+
- All 4 CI matrix entries green: ubuntu-latest × {22, 24}, windows-latest × {22, 24}.
42+
- Manual smoke (Win11 + Claude Code Node 24+ → `perplexity_reason` returns Pro reply) gates issue #3 closure; recorded in `docs/smoke-tests.md` post-release.
43+
44+
### Out of scope (deferred)
45+
46+
- **Envelope v4 vault format** (multi-source unseal envelopes) — Phase 0 verification passed on the daemon's actual spawn runtime, so v4 is no longer load-bearing for closing #3. Tracked as future hardening.
47+
- **HTTP loopback port-drift UX** — scheduled for 0.8.43.
48+
- **`keytar → @napi-rs/keyring`** swap — 0.9.x hardening track.
49+
950
## [0.8.40] — 2026-05-04 — IDE-expansion + auth/profile/vault self-healing
1051

1152
> **Versioning note:** 0.8.29 through 0.8.39 were local pre-release iterations and never tagged. The cumulative work below — IDE expansion, login deadlock fixes, profile-switch propagation, vault key-rotation tolerance, CLI vault setup wizard — is rolled into this release. Diagnostics from a real user session (`perplexity-mcp-diagnostics-2026-05-04T*`) drove the auth + vault fixes.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ For deeper internals, see:
444444

445445
---
446446

447+
## Troubleshooting
448+
449+
- **External MCP clients (Claude Code, Antigravity, Codex CLI, Cursor) hitting "Vault locked":** [docs/troubleshooting/external-mcp-clients.md](docs/troubleshooting/external-mcp-clients.md)
450+
451+
---
452+
447453
## Find Us
448454

449455
<div align="center">

docs/codex-cli-setup.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ If you have the extension installed, prefer section 1 — the daemon owns the va
9191

9292
- Credential Manager is always available. Same as macOS: section 2a works after a one-time `perplexity_login`.
9393

94+
**Troubleshooting "Vault locked" on Windows.** The bundled keytar talks to the OS Credential Vault and works for most installs under VS Code's Electron. If your launcher runs under a different Node ABI (Claude Code on Node 24+, sandboxed runtimes, or fresh installs of `perplexity-user-mcp` on a host where `keytar` can't load) and reports "Vault locked", run:
95+
96+
```bash
97+
npx perplexity-user-mcp setup-vault
98+
```
99+
100+
This generates a strong passphrase, prints a `setx`/PowerShell snippet to persist it as a User-scope env var, and (when the issue is the launcher's keytar load) restores access immediately. See [vault-unseal.md](./vault-unseal.md#recovery) for the full unseal model and recovery flow.
101+
94102
---
95103

96104
## 4. Verifying the setup

docs/smoke-tests.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,31 @@ The three evidence skeletons for this consolidated smoke live at:
521521
- [docs/smoke-evidence/2026-04-XX-v0.8.6-ubuntu22.md](smoke-evidence/)
522522

523523
Each skeleton carries the full checklist from Phase 8.5 through Phase 8.8 (this document's last four sections), ready for the tester to tick off. Per the release process in [docs/release-process.md](release-process.md): one fully-green platform + two waived platforms is acceptable if the waived platforms document a distinct reason (hardware unavailability, not "no time").
524+
525+
---
526+
527+
## 0.8.41 — Vault unseal hardening for external MCP clients (issue #3)
528+
529+
**Release gate for closure of [#3](https://github.com/Automations-Project/VSCode-Perplexity-MCP/issues/3):** at least one Win11 + external-IDE row PASS recorded below before the PR is marked ready-for-review and the issue is closed.
530+
531+
### Smoke matrix
532+
533+
| Date | Platform | IDE / external client | Daemon spawn telemetry | `perplexity_reason` result | Status |
534+
|---|---|---|---|---|---|
535+
| 2026-05-10 | Windows 11 Pro 26200, VS Code 1.119.0 (Node 22.22.1 internal) | VS Code dashboard (extension host) | `[daemon] PERPLEXITY_VAULT_PASSPHRASE: unset` (keytar happy-path) | Doctor: `vault: pass` (`unseal-path: OS keychain holds master key`, `unseal-verify: vault.enc decrypts cleanly`); models refresh succeeded `accountTier=Enterprise` | **PASS** |
536+
| 2026-05-10 | Windows 11 Pro 26200, Windsurf (VS Code 1.110.1-next, Node 22.22.0) running Claude Code as MCP host | Claude Code (this maintainer's session) routed through the bundled `stdio-daemon-proxy` | `[daemon] PERPLEXITY_VAULT_PASSPHRASE: set` (SecretStorage-passphrase fallback path — Windsurf's runtime triggered the env-var route) | `perplexity_reason "...current open question in cosmology..."` returned a substantive Pro reply with 15 citations. Daemon `pid=28768 port=10368 version=0.8.41`; live MCP roundtrip confirmed. | **PASS** |
537+
538+
### What both rows together prove
539+
540+
The 0.8.41 fix ships and works on **both unseal paths** in production:
541+
542+
- **Keychain path (`unset`):** keytar in the daemon's runtime loaded successfully; the daemon read the `vault-master-key` directly from Windows Credential Manager. No env-var injection needed. (Common case for Win11 + native VS Code.)
543+
- **Passphrase path (`set`):** keytar in this runtime did not fully work; SecretStorage had a passphrase from a prior login; `buildDaemonEnv(context)` injected it into the daemon's spawn env; the daemon decrypted via the passphrase-derived key. (This was the path that was previously broken — the daemon never received the passphrase before 0.8.41.)
544+
545+
The Windsurf row is the **definitive evidence for closing issue #3** because Windsurf is exactly the class of "external-IDE-with-Node-runtime-that-may-mismatch-keytar-ABI" that the issue reporter hit. The MCP client (Claude Code) sitting inside Windsurf successfully invoked an authenticated Pro tool end-to-end.
546+
547+
### Out of scope from this smoke
548+
549+
- Linux + headless-no-libsecret (Codex CLI path) — same code path covered by the `set` row above; deferred until a clean Linux box is available.
550+
- macOS — covered by the existing 0.8.x release-gate matrix; no behavior change in 0.8.41 specific to macOS.
551+
- Cross-IDE soak — Antigravity, Cursor outside VS Code, Codex CLI — pending; will be folded into the 0.8.42 / 0.8.43 smoke checklists as those releases ship.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Troubleshooting: External MCP Clients
2+
3+
> "External" = anything outside VS Code itself: Claude Code, Antigravity, Codex CLI, Cursor (when run as its own app), JetBrains MCP integrations, etc.
4+
5+
## TL;DR
6+
7+
Use the **default `stdio-daemon-proxy` transport** generated by the VS Code Perplexity dashboard. The launcher script (`~/.perplexity-mcp/start.mjs`) attaches to the long-running daemon spawned by the VS Code extension; the daemon owns vault credentials, you don't have to.
8+
9+
If your client says "Vault locked" or you see "DAEMON_UNREACHABLE" in stderr, see the matching section below.
10+
11+
## Symptom: "Vault locked: no keychain, no env var, no TTY"
12+
13+
This means your client's launcher process tried to read the vault directly — which only happens when daemon attach failed AND your launcher silently fell back to in-process stdio. **Fix in 0.8.41 and later:** the default launcher no longer falls back silently; you'll see "DAEMON_UNREACHABLE" instead. If you're seeing this on 0.8.40 or earlier, upgrade.
14+
15+
## Symptom: "Perplexity MCP: cannot reach the extension-managed daemon" (DAEMON_UNREACHABLE)
16+
17+
The daemon is not running or not reachable. Three remediations, in order of likelihood:
18+
19+
1. **Reload the VS Code window.** The extension respawns a healthy daemon on activation. After the dashboard shows "Daemon: running", retry from your external client.
20+
2. **Switch this client's transport to `http-loopback`** in the VS Code dashboard's MCP Config Management. This sidesteps the launcher entirely. Note: port-drift across daemon restarts is a known limitation tracked for 0.8.43; if it bites you, switch back to `stdio-daemon-proxy` after a daemon restart.
21+
3. **Advanced: opt into in-process stdio.** Set `PERPLEXITY_NO_DAEMON=1` in this client's MCP env block. Then run:
22+
```bash
23+
npx perplexity-user-mcp setup-vault
24+
```
25+
This generates a passphrase + persistence snippet so the in-process server can unseal the vault in your client's runtime.
26+
27+
**Sync-folder warning:** if the persistence snippet writes the env var into a synced shell rc file (Dropbox, OneDrive, iCloud Drive), the passphrase syncs too. Use a User-scope env var (Windows `setx`, macOS plist, Linux `~/.profile`) for client-only env-block persistence.
28+
29+
## Per-IDE support matrix
30+
31+
| IDE | Transport | Status | Smoke evidence |
32+
|---|---|---|---|
33+
| Claude Code | stdio-daemon-proxy | Supported (0.8.41) | Pending — see [smoke-tests.md](../smoke-tests.md) |
34+
| Antigravity | stdio-daemon-proxy | Supported (0.8.41) | Pending |
35+
| Codex CLI | stdio-daemon-proxy | Supported (0.8.41) | Pending |
36+
| Cursor (outside VS Code) | stdio-daemon-proxy | Supported (0.8.41) | Pending |
37+
| Claude Desktop | stdio-daemon-proxy / http-tunnel | Supported | See main README |
38+
| LM Studio | UI-only | Manual config | See main README |
39+
40+
The "Pending" rows fill in as 0.8.41 smoke runs land in `docs/smoke-tests.md`.
41+
42+
## Why the daemon owns the credentials
43+
44+
Putting the vault passphrase into every IDE's MCP config would mean (a) plaintext secrets in JSON files that often live in synced folders, (b) divergent setups across IDEs, (c) credential rotation requires editing N configs. By concentrating unsealing in the extension-managed daemon, the configs stay credential-free; rotation is one dashboard click.
45+
46+
## Related
47+
48+
- [vault-unseal.md](../vault-unseal.md) — full unseal model and recovery flow
49+
- [codex-cli-setup.md](../codex-cli-setup.md) — Codex CLI walkthrough with Windows troubleshooting
50+
- [smoke-tests.md](../smoke-tests.md) — per-platform smoke evidence

docs/vault-unseal.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Vault Unsealing
2+
3+
> If you got here from a "Vault locked" error, jump to [Recovery](#recovery).
4+
5+
## Overview
6+
7+
The Perplexity MCP server stores authentication cookies encrypted at rest in `~/.perplexity-mcp/profiles/<name>/vault.enc`. To use the file, the server must unlock ("unseal") the encryption key. There are three unseal paths, tried in order:
8+
9+
1. **OS keychain** (preferred) — Windows Credential Manager, macOS Keychain, Linux libsecret/gnome-keyring. The server stores a 32-byte random key under service `perplexity-user-mcp`, account `vault-master-key`.
10+
2. **Env var**`PERPLEXITY_VAULT_PASSPHRASE` (fallback for headless Linux, sandboxed runtimes, or when the keychain is unavailable).
11+
3. **TTY prompt** — interactive only (CLI use). Skipped when running as an stdio MCP server (no TTY).
12+
13+
The vault file is encrypted with AES-256-GCM. The KDF for passphrase-derived keys is scrypt (logN=17, r=8, p=1). Format details live in inline comments at the top of [`packages/mcp-server/src/vault.js`](../packages/mcp-server/src/vault.js).
14+
15+
## Standalone CLI vs. VS Code extension
16+
17+
- **Standalone `perplexity-user-mcp`** (npm package) uses the chain above directly. If you need to set a passphrase, run `npx perplexity-user-mcp setup-vault` — it generates a strong 256-bit base64url passphrase and prints per-platform persistence snippets.
18+
- **VS Code extension** uses the same chain in its login runner, but ALSO stores a SecretStorage-backed passphrase if the keychain probe fails. Starting with **0.8.41**, the extension passes that passphrase to the long-running daemon at spawn time via a narrowly-scoped env builder, so external IDE clients (Claude Code, Antigravity, Codex CLI, Cursor) routed through the daemon don't need their own vault credentials.
19+
20+
## Per-platform notes
21+
22+
### Windows
23+
24+
Windows Credential Manager works out of the box for the extension's bundled `keytar` under VS Code's Electron runtime. If you see "Vault locked" in an external client's launcher (Claude Code on Node 24+, Antigravity, sandboxed Codex CLI), the issue is almost certainly that the launcher's runtime can't load `keytar` — but the **extension-managed daemon** still owns the credentials. Fix: ensure your extension is **0.8.41 or later**, then reload VS Code.
25+
26+
### macOS
27+
28+
Same as Windows — macOS Keychain works under the bundled keytar.
29+
30+
### Linux
31+
32+
Headless Linux has no libsecret by default. Two options:
33+
1. Install libsecret + gnome-keyring (or kwallet) so keytar succeeds.
34+
2. Set `PERPLEXITY_VAULT_PASSPHRASE` in your IDE's MCP env block. Run `npx perplexity-user-mcp setup-vault` for a strong generated passphrase + persistence snippet.
35+
36+
## Recovery
37+
38+
If you see one of these errors:
39+
40+
- `Vault decrypt failed: wrong passphrase or corrupted ciphertext`
41+
- `Vault locked: no keychain, no env var, no TTY`
42+
43+
The vault was written under unseal material that is no longer available (rotated keychain key, changed `PERPLEXITY_VAULT_PASSPHRASE`, lost SecretStorage entry). There is **no recovery without the original material** — AES-256-GCM is authenticated and refuses to decrypt under the wrong key, by design.
44+
45+
Recovery flow:
46+
47+
1. Quarantine and discard the unreadable vault:
48+
```bash
49+
npx perplexity-user-mcp logout --purge --profile <name>
50+
```
51+
(replace `<name>` with your profile name; default is `default`)
52+
2. Log in again from the VS Code dashboard, or:
53+
```bash
54+
npx perplexity-user-mcp login --profile <name>
55+
```
56+
3. The new vault is written under whatever unseal material is currently available.
57+
58+
## Vault format versions
59+
60+
| Version | Status | KDF | Notes |
61+
|---|---|---|---|
62+
| v1 | legacy, decrypt-only | HKDF-SHA256 (static salt) | 0.6.x and earlier |
63+
| v2 | legacy, decrypt-only | HKDF-SHA256 (per-blob salt) | 0.7.x |
64+
| v3 | current | scrypt logN=17 | 0.8.x; per-blob salt + KDF params |
65+
66+
Reads never mutate the file. Writes always emit the latest supported version.
67+
68+
## Related
69+
70+
- [Troubleshooting external MCP clients](troubleshooting/external-mcp-clients.md) — Claude Code, Antigravity, Codex CLI specifics
71+
- [Codex CLI setup](codex-cli-setup.md) — Codex CLI configuration walkthrough

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"package:vsix": "npm run bump && npm run package:vsix -w perplexity-vscode",
2020
"test:coverage": "npm run build -w @perplexity-user-mcp/shared && vitest run --coverage",
2121
"bump": "node scripts/bump-version.mjs",
22-
"bump:dry": "node scripts/bump-version.mjs --dry-run"
22+
"bump:dry": "node scripts/bump-version.mjs --dry-run",
23+
"postinstall": "node scripts/install-git-hooks.mjs"
2324
},
2425
"dependencies": {
2526
"@modelcontextprotocol/sdk": "^1.29.0",

packages/extension/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "perplexity-vscode",
33
"displayName": "Perplexity MCP",
4-
"version": "0.8.39",
4+
"version": "0.8.41",
55
"publisher": "Nskha",
66
"private": true,
77
"description": "Perplexity AI search, reasoning, research, and compute — MCP server, dashboard, and multi-IDE auto-config for VS Code.",
@@ -35,7 +35,7 @@
3535
],
3636
"engines": {
3737
"vscode": "^1.100.0",
38-
"node": ">=20"
38+
"node": "^22.0.0 || ^24.0.0"
3939
},
4040
"categories": [
4141
"AI",

0 commit comments

Comments
 (0)