feat(profiles): add profile wizard, migration, and activation flow#448
feat(profiles): add profile wizard, migration, and activation flow#4480r0b0r011 wants to merge 5 commits into
Conversation
Store profile-scoped secrets in an encrypted SQLite vault with IPC handlers, preload namespace, and a Vault screen for credential CRUD. Co-authored-by: Cursor <cursoragent@cursor.com>
Multi-step profile creation with templates, vault-backed secret migration, gateway activation with rollback, and UI overlays for wizard/migration/switcher. Co-authored-by: Cursor <cursoragent@cursor.com>
Ensure profile switcher, chat-with, and delete flows wait for handleSelectProfile to finish gateway activation before continuing. Co-authored-by: Cursor <cursoragent@cursor.com>
pmos69
left a comment
There was a problem hiding this comment.
Thanks for splitting this out and for making the dependency on #446 explicit. I did a static review only because this PR wires credential storage, plaintext env migration, profile activation, and gateway restarts into the app. I do not think this is safe to merge yet.
This PR is stacked on #446, so the vault concerns from that PR still apply here. More importantly, #448 turns several previously dormant issues into live profile-switching behavior.
Blocking findings
1. Migration leaves plaintext secrets behind indefinitely
profile-migrate-secrets copies every migrated .env to ${envFile}.backup, imports the values, then replaces .env with comments that explicitly point to the backup. That means after the user chooses the advertised “migrate plaintext secrets into the encrypted vault” path, the same secrets still remain in plaintext at a predictable path.
Relevant code:
src/main/ipc/profile-handlers.ts:126-130src/main/ipc/profile-handlers.ts:146-152
This defeats the main purpose of the vault migration. At minimum, migration needs a verified import/readback path and then removal of plaintext copies, or it should avoid creating a plaintext backup in the first place. If a recoverable backup is required, it needs an explicit user-facing export/save flow, not an automatic forever file beside the original .env.
2. Env key migration is lossy and can break gateway auth
The migration stores only a derived provider id, then activation reconstructs an env var from that provider. These transforms are not inverses.
Example: API_SERVER_KEY
parseEnvFile()acceptsAPI_SERVER_KEY.envKeyToProvider("API_SERVER_KEY")stores providerapi-server.resolveEnvKey("api-server")later reconstructsAPI_SERVER_API_KEY.- The gateway still expects
API_SERVER_KEY, so the migrated/activated profile can lose gateway auth.
Relevant code:
src/main/vault/service.ts:87-98src/main/vault/service.ts:329-371
This also affects custom provider env vars and other non-standard keys. The vault row needs to preserve the original env var name, or migration must otherwise be round-trip-lossless.
3. Profile activation now makes the inherited .env clobber live
activateProfileWithRollback() calls vaultActivate(profile), and vaultActivate() rewrites .env with only the decrypted vault entries. Any non-vault .env keys disappear: gateway keys, HERMES_INFERENCE_PROVIDER, custom provider settings, platform-specific variables, etc.
Relevant code:
src/main/profiles/wizard.ts:214src/main/vault/service.ts:237-255
In #446 this was concerning but mostly dormant. In this PR it runs during real profile switching, so it becomes a data-loss / configuration-corruption path. Activation should merge managed secret keys into the existing .env, preserving unrelated keys, and it should properly quote/escape values.
4. Activation backups create more plaintext secret sprawl
Every activation copies .env and auth.json into a timestamped desktop/activation-backup/... directory and never removes that backup on success.
Relevant code:
src/main/profiles/wizard.ts:202-212
This means regular profile switching can accumulate plaintext API keys and OAuth tokens indefinitely. If rollback backups are needed, they should be scoped to the transaction, cleaned up on success, and avoid copying unmanaged token files unless strictly necessary.
5. Failed activation can leave the active profile/gateway state inconsistent
The renderer first calls setActiveProfile(name), then calls profileWizard.activate(name). The activation path also calls setActiveProfile(profile) before waiting for gateway readiness. If gateway startup fails, the catch block restores some files but does not restore the previous active profile, remove newly-created files, or restart the gateway back onto the restored configuration.
Relevant code:
src/renderer/src/screens/Layout/Layout.tsx:236-250src/main/profiles/wizard.ts:214-235
Rollback needs to cover the full transaction: files, active profile state, and gateway process state.
6. New profile/vault IPC is not enforced in main-process remote mode
The UI hides some local-only surfaces in remote mode, but these handlers are still registered and callable from renderer code. The new profile activation/migration paths can write local profile files, mutate the local vault, and start/restart the local gateway even when the app is connected to SSH/remote mode.
Relevant code:
src/main/ipc/profile-handlers.ts:25-143src/main/ipc/vault-handlers.ts:15-66src/main/index.ts:1550-1551
This needs the same main-process enforcement pattern used by other local-only features. Renderer gating is not a security or correctness boundary.
7. Migration detection/import is not actually one-time per profile
profile-detect-migration returns [] as soon as the vault has any secrets, regardless of which profile they belong to. So migrating profile A can prevent profile B from ever being offered migration. Conversely, migratePlaintextEnv() always inserts fresh rows and has no dedupe guard, so direct/repeated calls can duplicate imported secrets.
Relevant code:
src/main/ipc/profile-handlers.ts:87-111src/main/vault/service.ts:359-371
The migration state needs to be per-profile/per-env-key, and imports should be idempotent.
8. Archive-on-delete can preserve plaintext secrets
profile-delete with archive copies the whole profile home before removing vault secrets. If the profile contains .env, .env.backup, or other plaintext files, those are retained under archived-profiles.
Relevant code:
src/main/ipc/profile-handlers.ts:63-79
Given this PR introduces migration backups and activation backups, archive behavior needs to be reviewed as part of the same secret-retention model.
Inherited #446 blockers still apply here
Because this branch includes the vault commit from #446, the #446 issues are also present here: vault IPC remote-mode bypass, overly broad renderer authority, raw vault export, locked/password fallback UI path, vault DB permissions, key format ambiguity between safeStorage/password modes, non-atomic key rotation, and insufficient tests around security boundaries.
Minimum bar before this can be reconsidered
- Land/fix #446 first, then rebase this PR.
- Store original env var names or otherwise make migration/activation round-trip-lossless.
- Preserve unrelated
.envkeys during activation; do not overwrite the whole file with only vault entries. - Do not leave plaintext
.env.backup, activation backups, or archived plaintext secrets behind silently. - Make migration detection/import per-profile and idempotent.
- Add main-process remote-mode guards for all new vault/profile IPC handlers.
- Roll back active profile and gateway process state on activation failure.
- Add tests for lossy env-key round trips (
API_SERVER_KEYspecifically),.envpreservation, plaintext-backup cleanup, per-profile migration, duplicate migration, failed activation rollback, and remote-mode IPC rejection.
The wizard structure and the activation rollback shape are good directions, but the current integration can both leak plaintext secrets and corrupt a working Hermes configuration. Requesting changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Adds guided profile creation and migration on top of the encrypted vault (#446):
.envfiles and offers one-time import into vault.env, starts/restarts gateway with rollbackCtrl/Cmd+Pprofile switcher, gateway status in sidebartests/wizard-profile-write.test.tscovers filesystem write safetyDepends on #446 (vault). This branch is stacked on
split/vault-credentials. Merge #446 first, then rebase this PR ontomainbefore final merge.Part of the split from #438.
Security notes for reviewers
.env— review rollback on failurewaitForGatewayReadypolls API health after restart during activationTest plan
npm run typechecknpm test -- tests/wizard-profile-write.test.ts tests/vault.test.ts tests/ipc-handlers.test.ts tests/preload-api-surface.test.tsnpm run build.envCtrl/Cmd+Pprofile switcherMade with Cursor