fix: prevent default profile credential collision (#1628)

* fix: prevent default profile credential collision

* fix: isolate default profile credential fallback
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-01 01:15:45 -04:00
1 parent 20df3445d9
commit 343899f6c1
4 files changed
+124 -17

No files matched your search

+5 -1
View File
@@ -36,6 +36,7 @@ export const DEFAULT_ACCOUNT_CONTEXT_GROUP = 'default';
export const DEFAULT_ACCOUNT_CONTINUITY_MODE: AccountContinuityMode = 'standard';
export const MAX_CONTEXT_GROUP_LENGTH = 64;
export const ACCOUNT_PROFILE_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
const RESERVED_ACCOUNT_PROFILE_NAMES = new Set(['default']);
const CONTEXT_GROUP_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
@@ -57,7 +58,10 @@ export function isValidContextGroupName(value: string): boolean {
* Validate account profile naming constraints.
*/
export function isValidAccountProfileName(value: string): boolean {
return ACCOUNT_PROFILE_NAME_PATTERN.test(value);
return (
ACCOUNT_PROFILE_NAME_PATTERN.test(value) &&
!RESERVED_ACCOUNT_PROFILE_NAMES.has(value.toLowerCase())
);
}
/**
+24 -16
View File
@@ -497,24 +497,31 @@ function serveCached(state: ProviderState): BarSummaryRow | null {
* is absent or unparseable, returns null — the caller emits a parked row.
* Never calls security/Keychain — zero new keychain access from this feature.
*/
function readClaudeCredentialsForProfileFromDisk(profile: string): ClaudeNativeCredentials | null {
// The bare `ccs` default login uses the standard global credential lookup:
// ~/.claude/.credentials.json, falling back to the single global
// "Claude Code-credentials" Keychain item that Claude Code itself maintains.
// This is the ONE pre-existing global read the shipped Bar already performs --
// NOT a per-profile Keychain scan. Isolated `ccs auth` profiles below stay
// file-only and never touch the Keychain.
if (profile === DEFAULT_PROFILE) {
return readClaudeCredentials();
}
function readClaudeCredentialsForProfileFromDisk(
profile: string,
readDefaultCredentials: () => ClaudeNativeCredentials | null = readClaudeCredentials
): ClaudeNativeCredentials | null {
try {
const instanceDir = path.join(getCcsDir(), 'instances', profile);
const credFile = path.join(instanceDir, '.credentials.json');
if (!fs.existsSync(credFile)) return null;
const raw = fs.readFileSync(credFile, 'utf8');
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as ClaudeNativeCredentials;
if (fs.existsSync(credFile)) {
const raw = fs.readFileSync(credFile, 'utf8');
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as ClaudeNativeCredentials;
}
return null;
}
// The bare `ccs` default login uses the standard global credential lookup:
// ~/.claude/.credentials.json, falling back to the single global
// "Claude Code-credentials" Keychain item that Claude Code itself maintains.
// This is the ONE pre-existing global read the shipped Bar already performs --
// NOT a per-profile Keychain scan. Isolated `ccs auth` profiles stay
// file-only and never touch the Keychain; a real instance directory named
// "default" is therefore parked when its file is absent.
if (profile === DEFAULT_PROFILE && !fs.existsSync(instanceDir)) {
return readDefaultCredentials();
}
return null;
} catch {
@@ -829,9 +836,10 @@ async function collectClaudeRowForProfile(
}
// For per-profile reads: use the injected seam (file-only, no keychain).
const readDefaultCredentials = deps.readCredentials ?? readClaudeCredentials;
const readCreds =
deps.readClaudeCredentialsForProfile ??
((p: string) => readClaudeCredentialsForProfileFromDisk(p));
((p: string) => readClaudeCredentialsForProfileFromDisk(p, readDefaultCredentials));
const fetchQuota = deps.fetchClaudeQuota ?? fetchClaudeQuotaWithToken;
const sleep = deps.sleep ?? defaultSleep;
+5
View File
@@ -20,6 +20,11 @@ describe('account context helpers', () => {
expect(isValidAccountProfileName('gemini:default')).toBe(false);
});
it('rejects reserved account profile names', () => {
expect(isValidAccountProfileName('default')).toBe(false);
expect(isValidAccountProfileName('Default')).toBe(false);
});
it('falls back to default shared group for invalid persisted metadata', () => {
const resolved = resolveAccountContextPolicy({
context_mode: 'shared',
@@ -1251,6 +1251,96 @@ describe('multi-profile: Claude file-only reader', () => {
expect(row?.quotaStatus).toBe('unsupported');
expect(row?.is_subscription).toBe(true);
});
it('existing default profile directory without creds does not read global credentials', async () => {
const originalCcsHome = process.env.CCS_HOME;
const tempCcsHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-default-profile-'));
const clock = { now: 1_000_000 };
let defaultReadCount = 0;
let fetchCount = 0;
try {
process.env.CCS_HOME = tempCcsHome;
fs.mkdirSync(path.join(tempCcsHome, '.ccs', 'instances', 'default'), { recursive: true });
const rows = await getNativeAccountRows({
readCredentials: () => {
defaultReadCount += 1;
return { claudeAiOauth: { accessToken: 'global-token', subscriptionType: 'max' } };
},
listClaudeProfiles: () => ['default'],
listCodexProfiles: () => [],
defaultClaudeProfile: () => 'default',
defaultCodexProfile: () => null,
fetchClaudeQuota: async () => {
fetchCount += 1;
return successQuota();
},
getCodexQuota: async () => null,
now: () => clock.now,
sleep: async () => {},
});
const row = rows.find((r) => r.surface === 'ccs' && r.profile === 'default');
expect(defaultReadCount).toBe(0);
expect(fetchCount).toBe(0);
expect(row?.needsReauth).toBe(true);
expect(row?.quotaStatus).toBe('unsupported');
} finally {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
fs.rmSync(tempCcsHome, { recursive: true, force: true });
}
});
it('existing default profile credentials win over global credentials', async () => {
const originalCcsHome = process.env.CCS_HOME;
const tempCcsHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-default-profile-'));
const clock = { now: 1_000_000 };
let fetchedToken: string | null = null;
try {
process.env.CCS_HOME = tempCcsHome;
const defaultInstanceDir = path.join(tempCcsHome, '.ccs', 'instances', 'default');
fs.mkdirSync(defaultInstanceDir, { recursive: true });
fs.writeFileSync(
path.join(defaultInstanceDir, '.credentials.json'),
JSON.stringify({ claudeAiOauth: { accessToken: 'profile-token', subscriptionType: 'max' } })
);
const rows = await getNativeAccountRows({
readCredentials: () => ({
claudeAiOauth: { accessToken: 'global-token', subscriptionType: 'max' },
}),
listClaudeProfiles: () => ['default'],
listCodexProfiles: () => [],
defaultClaudeProfile: () => 'default',
defaultCodexProfile: () => null,
fetchClaudeQuota: async (token) => {
fetchedToken = token;
return successQuota();
},
getCodexQuota: async () => null,
now: () => clock.now,
sleep: async () => {},
});
const row = rows.find((r) => r.surface === 'ccs' && r.profile === 'default');
expect(fetchedToken).toBe('profile-token');
expect(row?.needsReauth).toBe(false);
expect(row?.quotaStatus).toBe('ok');
} finally {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
fs.rmSync(tempCcsHome, { recursive: true, force: true });
}
});
});
describe('multi-profile: per-profile circuit breaker isolation', () => {