Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7e29035
chore(repo): pre-push hook refuses to publish private planning artifacts
May 9, 2026
65bd1aa
chore(ci): bump CI matrix and engines from Node 20 to Node 22/24
May 9, 2026
2d1233a
feat(ext): build-daemon-env helper reads SecretStorage passphrase
May 10, 2026
4dbe3fc
feat(ext): daemon RuntimeConfig accepts buildDaemonEnv provider
May 10, 2026
4580862
feat(ext): spawnBundledDaemon awaits buildDaemonEnv and merges result
May 10, 2026
c636e9c
wire(ext): pass buildDaemonEnv into configureDaemonRuntime
May 10, 2026
3a81e5d
feat(mcp): DaemonAttachError typed error with remediation array
May 10, 2026
cc6cbd3
feat(mcp): attachToDaemon throws DaemonAttachError when fallbackStdio…
May 10, 2026
0f9f4c0
breaking(ext): launcher catches DaemonAttachError, stderr+exit2
May 10, 2026
0e334ad
feat(cli): perplexity-user-mcp catches DaemonAttachError on attach paths
May 10, 2026
712f976
docs(vault): create vault-unseal.md (was 404 from vault.js error)
May 10, 2026
9c4b23a
docs(codex): soften Windows-keychain "just works" claim
May 10, 2026
2df8b57
docs(troubleshooting): external MCP clients page
May 10, 2026
5a99a66
docs(readme): cross-link external-mcp-clients troubleshooting
May 10, 2026
7ba92fc
release(0.8.41): vault unseal hardening for external MCP clients
May 10, 2026
7a0570e
test(mcp): align legacy attach.test.js with Task 2.2 contract
May 10, 2026
f8df26a
test(ext): daemon-runtime-spawn polls fast — kills worker-pool flake
May 10, 2026
c1a9af3
docs(smoke): record 0.8.41 vault-unseal-hardening PASS rows (issue #3)
May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [20, 22]
node-version: [22, 24]
defaults:
run:
shell: bash
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
# New files are HIDDEN unless explicitly whitelisted.
# Only production-relevant files are public.
# ═══════════════════════════════════════════════════════
#
# DO NOT `git add -f` files under docs/superpowers/ unless you have a clear,
# considered reason to publish them. Planning artifacts under that path are
# private AI-development content. The pre-push hook in
# scripts/git-hooks/pre-push will refuse pushes that contain them — set
# PERP_ALLOW_PRIVATE_PLAN_PUSH=1 to override for a single push if you really
# mean it. Activate the hook once per clone with:
# git config core.hooksPath scripts/git-hooks
# (or run `npm install` — postinstall does it for you).

# 1. Ignore everything
*
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,47 @@ All notable changes to this project are documented here. Format follows

## [Unreleased]

## [0.8.41] — 2026-05-10 — Vault unseal hardening for external MCP clients

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

### Fixed

- **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.
- **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).

### Added

- **`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.
- **Reserved exit code `2`** for "operator-actionable misconfiguration" (distinct from `1` = generic crash). Documented in launcher comments.
- **`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.
- **`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.
- 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.
- **Repo tooling:** pre-push hook (`scripts/git-hooks/pre-push`) refuses to publish `docs/superpowers/` paths. Auto-installed via `npm install` postinstall.

### Changed

- **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`).
- **`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.

### Migration notes

- **No breakage** for users on Win11/macOS with working keychain. Daemon runs; attach succeeds; business as usual.
- **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.
- The 0.8.40 launcher on disk gets rewritten by `ensureLauncher`'s byte-comparison logic on next extension activation. No manual user action needed.

### Verification

- 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.
- All 4 CI matrix entries green: ubuntu-latest × {22, 24}, windows-latest × {22, 24}.
- Manual smoke (Win11 + Claude Code Node 24+ → `perplexity_reason` returns Pro reply) gates issue #3 closure; recorded in `docs/smoke-tests.md` post-release.

### Out of scope (deferred)

- **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.
- **HTTP loopback port-drift UX** — scheduled for 0.8.43.
- **`keytar → @napi-rs/keyring`** swap — 0.9.x hardening track.

## [0.8.40] — 2026-05-04 — IDE-expansion + auth/profile/vault self-healing

> **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.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ For deeper internals, see:

---

## Troubleshooting

- **External MCP clients (Claude Code, Antigravity, Codex CLI, Cursor) hitting "Vault locked":** [docs/troubleshooting/external-mcp-clients.md](docs/troubleshooting/external-mcp-clients.md)

---

## Find Us

<div align="center">
Expand Down
8 changes: 8 additions & 0 deletions docs/codex-cli-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ If you have the extension installed, prefer section 1 — the daemon owns the va

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

**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:

```bash
npx perplexity-user-mcp setup-vault
```

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.

---

## 4. Verifying the setup
Expand Down
28 changes: 28 additions & 0 deletions docs/smoke-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,31 @@ The three evidence skeletons for this consolidated smoke live at:
- [docs/smoke-evidence/2026-04-XX-v0.8.6-ubuntu22.md](smoke-evidence/)

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").

---

## 0.8.41 — Vault unseal hardening for external MCP clients (issue #3)

**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.

### Smoke matrix

| Date | Platform | IDE / external client | Daemon spawn telemetry | `perplexity_reason` result | Status |
|---|---|---|---|---|---|
| 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** |
| 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** |

### What both rows together prove

The 0.8.41 fix ships and works on **both unseal paths** in production:

- **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.)
- **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.)

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.

### Out of scope from this smoke

- Linux + headless-no-libsecret (Codex CLI path) — same code path covered by the `set` row above; deferred until a clean Linux box is available.
- macOS — covered by the existing 0.8.x release-gate matrix; no behavior change in 0.8.41 specific to macOS.
- 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.
50 changes: 50 additions & 0 deletions docs/troubleshooting/external-mcp-clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Troubleshooting: External MCP Clients

> "External" = anything outside VS Code itself: Claude Code, Antigravity, Codex CLI, Cursor (when run as its own app), JetBrains MCP integrations, etc.

## TL;DR

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.

If your client says "Vault locked" or you see "DAEMON_UNREACHABLE" in stderr, see the matching section below.

## Symptom: "Vault locked: no keychain, no env var, no TTY"

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.

## Symptom: "Perplexity MCP: cannot reach the extension-managed daemon" (DAEMON_UNREACHABLE)

The daemon is not running or not reachable. Three remediations, in order of likelihood:

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.
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.
3. **Advanced: opt into in-process stdio.** Set `PERPLEXITY_NO_DAEMON=1` in this client's MCP env block. Then run:
```bash
npx perplexity-user-mcp setup-vault
```
This generates a passphrase + persistence snippet so the in-process server can unseal the vault in your client's runtime.

**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.

## Per-IDE support matrix

| IDE | Transport | Status | Smoke evidence |
|---|---|---|---|
| Claude Code | stdio-daemon-proxy | Supported (0.8.41) | Pending — see [smoke-tests.md](../smoke-tests.md) |
| Antigravity | stdio-daemon-proxy | Supported (0.8.41) | Pending |
| Codex CLI | stdio-daemon-proxy | Supported (0.8.41) | Pending |
| Cursor (outside VS Code) | stdio-daemon-proxy | Supported (0.8.41) | Pending |
| Claude Desktop | stdio-daemon-proxy / http-tunnel | Supported | See main README |
| LM Studio | UI-only | Manual config | See main README |

The "Pending" rows fill in as 0.8.41 smoke runs land in `docs/smoke-tests.md`.

## Why the daemon owns the credentials

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.

## Related

- [vault-unseal.md](../vault-unseal.md) — full unseal model and recovery flow
- [codex-cli-setup.md](../codex-cli-setup.md) — Codex CLI walkthrough with Windows troubleshooting
- [smoke-tests.md](../smoke-tests.md) — per-platform smoke evidence
71 changes: 71 additions & 0 deletions docs/vault-unseal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Vault Unsealing

> If you got here from a "Vault locked" error, jump to [Recovery](#recovery).

## Overview

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:

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`.
2. **Env var** — `PERPLEXITY_VAULT_PASSPHRASE` (fallback for headless Linux, sandboxed runtimes, or when the keychain is unavailable).
3. **TTY prompt** — interactive only (CLI use). Skipped when running as an stdio MCP server (no TTY).

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).

## Standalone CLI vs. VS Code extension

- **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.
- **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.

## Per-platform notes

### Windows

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.

### macOS

Same as Windows — macOS Keychain works under the bundled keytar.

### Linux

Headless Linux has no libsecret by default. Two options:
1. Install libsecret + gnome-keyring (or kwallet) so keytar succeeds.
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.

## Recovery

If you see one of these errors:

- `Vault decrypt failed: wrong passphrase or corrupted ciphertext`
- `Vault locked: no keychain, no env var, no TTY`

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.

Recovery flow:

1. Quarantine and discard the unreadable vault:
```bash
npx perplexity-user-mcp logout --purge --profile <name>
```
(replace `<name>` with your profile name; default is `default`)
2. Log in again from the VS Code dashboard, or:
```bash
npx perplexity-user-mcp login --profile <name>
```
3. The new vault is written under whatever unseal material is currently available.

## Vault format versions

| Version | Status | KDF | Notes |
|---|---|---|---|
| v1 | legacy, decrypt-only | HKDF-SHA256 (static salt) | 0.6.x and earlier |
| v2 | legacy, decrypt-only | HKDF-SHA256 (per-blob salt) | 0.7.x |
| v3 | current | scrypt logN=17 | 0.8.x; per-blob salt + KDF params |

Reads never mutate the file. Writes always emit the latest supported version.

## Related

- [Troubleshooting external MCP clients](troubleshooting/external-mcp-clients.md) — Claude Code, Antigravity, Codex CLI specifics
- [Codex CLI setup](codex-cli-setup.md) — Codex CLI configuration walkthrough
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"package:vsix": "npm run bump && npm run package:vsix -w perplexity-vscode",
"test:coverage": "npm run build -w @perplexity-user-mcp/shared && vitest run --coverage",
"bump": "node scripts/bump-version.mjs",
"bump:dry": "node scripts/bump-version.mjs --dry-run"
"bump:dry": "node scripts/bump-version.mjs --dry-run",
"postinstall": "node scripts/install-git-hooks.mjs"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "perplexity-vscode",
"displayName": "Perplexity MCP",
"version": "0.8.39",
"version": "0.8.41",
"publisher": "Nskha",
"private": true,
"description": "Perplexity AI search, reasoning, research, and compute — MCP server, dashboard, and multi-IDE auto-config for VS Code.",
Expand Down Expand Up @@ -35,7 +35,7 @@
],
"engines": {
"vscode": "^1.100.0",
"node": ">=20"
"node": "^22.0.0 || ^24.0.0"
},
"categories": [
"AI",
Expand Down
Loading
Loading