#1972

fix(desktop): stop two accounts sharing one credential file

Opencarlosjarenomwants to merge intofrom

What Problem This Solves

getEnvPath() and getEmailFolderPath() derived the account directory from email.split('@')[0], discarding the domain. Three different accounts collapsed onto the same directory:

alice@personal.com  ->  alice
alice@work.com      ->  alice
alice@corp.internal ->  alice

Those accounts shared one .env file holding all three sets of API keys, and one MCP config directory. A malformed address such as @nodomain.com produced an empty name, which made the account directory ~/.eigent itself.

Reading the code also turned up two things the issue did not mention:

  • .replace('.', '_') has no g flag, so it replaced only the first dot. The issue assumed all dots were replaced.
  • The same expression was duplicated verbatim in subscriptionAuth/credentialStore, with a comment noting it had to be kept in sync by hand. test/mocks/electronMocks.ts carried a third copy.

Solution Details

The directory name is now <readable>~<hash>:

  • readable — the sanitized address, so a path can still be traced back to the account. This addresses the "impossible to trace" part of the issue.
  • hash — a truncated SHA-256 of the exact original address.

The digest is what makes the mapping injective. Sanitization alone is lossy: a b@x.com and a_b@x.com both reduce to a_b@x.com. Injectivity is the property that matters here, because these directories hold credentials, so where readability and distinctness conflict, distinctness wins and the readable part is kept for debugging.

Also:

  • Spaces are sanitized. The old expression handled them; my first attempt did not, and a name containing spaces needs quoting in every shell and is rejected at the tail on Windows. A test caught this.
  • Traversal and empty names are impossible by construction, with assertUsableAccountDirName as a backstop.
  • The derivation is now in one module — electron/main/utils/accountPath.ts, side-effect free so credentialStore can import it without pulling in envUtil's filesystem side effects. The three copies, including the test mocks, now use it.

Why old directories are not migrated

A directory created under the old scheme may contain configuration from several accounts merged together. It cannot be attributed to any one of them — that ambiguity is the bug — so this leaves it in place rather than handing it to whichever account signs in next. Deleting it would throw away MCP configuration that may still be needed. Happy to migrate explicitly if you would rather have it, but it would need a decision about how to split or discard merged contents.

Evidence

13 tests in test/unit/electron/main/accountPath.test.ts, including:

TestPins
keeps accounts apart when only the domain differsthe reported defect
does not reproduce the legacy collisionsasserts the old scheme collapsed to 1 of 3
keeps apart addresses that differ only by an unsafe characterdigest is what prevents the collision
never produces an empty namethe @nodomain.com case
cannot escape its parent directorytraversal
stays within the filesystem component limit255-byte component limit
replaces characters that are unsafe on Windowsreserved characters
  • vitest run test/unit/electron/main/accountPath.test.ts — 13 passed
  • vitest run accountPath + index + port — 112 passed
  • npm run type-check — clean
  • eslint on the touched files — clean

Note: test/unit/electron/main/envUtil.test.ts has 14 failures on main before this change, unrelated to it. They fail with util.isString is not a function — the jsdom test environment resolves path to a browser polyfill whose util.isString the shimmed util does not provide. The same environment issue is why the new tests target the pure module rather than envUtil. Worth a separate fix; I have not touched it here to keep the diff focused.

User Impact

Accounts on a shared machine stop sharing credentials, and a path in the UI can be traced back to the account it belongs to. Existing .env.<name> files stay where they are under their old name, so an account may need to re-enter an API key once; the new file is created on next write and the old one is left untouched rather than deleted.

Closes #1669

Activity

2
carlosjarenom1mo ago

Rebased onto current main and re-verified while it waited for review:

  • npm run type-check and eslint are clean.
  • Full backend suite: 3696 passed, 12 skipped — identical to the main baseline.
  • Conflicts with main resolved by hand; the fix is now scoped to just the files it needs.

The linked issue is still open, and this is ready for a look whenever you have time.