Skip to content

Commit ad410ef

Browse files
Merge pull request #1007 from corbitsdev/cl-6728-do-not-let-oauth-projections-overwrite-hand-named-provider
Do not let OAuth projections overwrite hand-named provider entries
2 parents d3d8465 + cd829f7 commit ad410ef

2 files changed

Lines changed: 340 additions & 21 deletions

File tree

‎src/config/index.ts‎

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,17 @@ function applyPersistedOAuthDefaults(
135135
// OAuth entries in settings.json carry no credentials; they are only usable
136136
// while a matching auth-store profile exists. Drop orphans in memory so a
137137
// removed profile does not pin resolution to an unauthenticatable provider.
138-
function dropOrphanedOAuthEntries(
138+
export function dropOrphanedOAuthEntries(
139139
settings: Settings | null,
140140
projected: Record<string, ProviderSettings>,
141141
): Settings | null {
142142
if (settings === null) return null;
143143
const providers = Object.fromEntries(
144144
Object.entries(settings.providers).filter(
145-
([name]) =>
145+
([name, provider]) =>
146146
(!isCodexProviderName(name) && !isXaiProviderName(name)) ||
147-
projected[name] !== undefined,
147+
projected[name] !== undefined ||
148+
isHandNamedProviderEntry(provider),
148149
),
149150
);
150151
const { defaultProvider, ...rest } = settings;
@@ -158,6 +159,35 @@ function dropOrphanedOAuthEntries(
158159
};
159160
}
160161

162+
// A codex/<slug> or xai/<slug> settings row carrying its own credential is the
163+
// operator's explicit config, not an OAuth placeholder: OAuth profile
164+
// projections must never overwrite it, orphan-sweep it, or drop it from the
165+
// catalog. Credential-less namespaced rows stay placeholders (CL-6728).
166+
function isHandNamedProviderEntry(
167+
entry: Pick<ProviderSettings, "apiKey" | "keyless"> | undefined,
168+
): boolean {
169+
if (entry === undefined) return false;
170+
if (entry.keyless === true) return true;
171+
return typeof entry.apiKey === "string" && entry.apiKey.length > 0;
172+
}
173+
174+
// Overlay live OAuth profile projections onto settings for runtime provider
175+
// resolution. Exported for tests; loadConfig is the only production caller.
176+
export function overlayOAuthProjections(
177+
settings: Settings | null,
178+
projected: Record<string, ProviderSettings>,
179+
): Settings | null {
180+
if (Object.keys(projected).length === 0) return settings;
181+
const providers = { ...(settings?.providers ?? {}) };
182+
for (const [name, entry] of Object.entries(projected)) {
183+
if (!isHandNamedProviderEntry(providers[name])) providers[name] = entry;
184+
}
185+
return {
186+
...(settings ?? { providers: {} }),
187+
providers,
188+
};
189+
}
190+
161191
function hasExaEntry(servers: MCPServerSettingsEntry[] | undefined): boolean {
162192
return (
163193
servers?.some((server) => server.name === EXA_MCP_SERVER_NAME) === true
@@ -936,16 +966,10 @@ export async function loadConfig(
936966
const liveSettings = useOAuthProfiles
937967
? dropOrphanedOAuthEntries(settings, projectedOAuthProviders)
938968
: settings;
939-
const settingsForResolution: Settings | null =
940-
Object.keys(projectedOAuthProviders).length > 0
941-
? {
942-
...(liveSettings ?? { providers: {} }),
943-
providers: {
944-
...(liveSettings?.providers ?? {}),
945-
...projectedOAuthProviders,
946-
},
947-
}
948-
: liveSettings;
969+
const settingsForResolution: Settings | null = overlayOAuthProjections(
970+
liveSettings,
971+
projectedOAuthProviders,
972+
);
949973

950974
// The per-repo selection file still applies on top of a --config source: that
951975
// file supplies provider definitions, while .corbits/settings.json supplies
@@ -1194,15 +1218,33 @@ export function mergeOAuthCatalog(
11941218
? ["xai"]
11951219
: []),
11961220
]);
1221+
const settingsRows = buildProviderCatalog(settings, resolved);
1222+
// A hand-named codex/<slug> or xai/<slug> API-key row is the operator's
1223+
// explicit config, not an OAuth placeholder: keep it and skip the colliding
1224+
// live profile projection instead of overwriting it (CL-6728). Read the raw
1225+
// settings rows only: buildProviderCatalog synthesizes a [resolved] row when
1226+
// settings is null/empty, and when resolved is itself codex/<slug> that row
1227+
// carries the live apiKey with no profile marker — treating it as hand-named
1228+
// would eject the real marked entry for a stale token snapshot.
1229+
const handNamed = new Set(
1230+
Object.entries(settings?.providers ?? {})
1231+
.filter(
1232+
([name, provider]) =>
1233+
(isCodexProviderName(name) || isXaiProviderName(name)) &&
1234+
isHandNamedProviderEntry(provider),
1235+
)
1236+
.map(([name]) => name),
1237+
);
11971238
return [
1198-
...buildProviderCatalog(settings, resolved).filter(
1239+
...settingsRows.filter(
11991240
(e) =>
1200-
!isCodexProviderName(e.name) &&
1201-
!isXaiProviderName(e.name) &&
1202-
!dropBare.has(e.name),
1241+
handNamed.has(e.name) ||
1242+
(!isCodexProviderName(e.name) &&
1243+
!isXaiProviderName(e.name) &&
1244+
!dropBare.has(e.name)),
12031245
),
1204-
...codexEntries,
1205-
...xaiEntries,
1246+
...codexEntries.filter((e) => !handNamed.has(e.name)),
1247+
...xaiEntries.filter((e) => !handNamed.has(e.name)),
12061248
].map((entry) =>
12071249
isOpenCodeGoProvider(entry)
12081250
? { ...entry, models: [...selectableGoModelIds()] }
@@ -1272,11 +1314,20 @@ export function runtimeSettingsWithCatalog(
12721314
if (settings === undefined) {
12731315
return { providers: fromCatalog };
12741316
}
1317+
// OAuth-marked catalog rows carry live profile tokens; they overlay
1318+
// credential-less placeholders but never a hand-named API-key row (CL-6728).
1319+
const overlaid = Object.fromEntries(
1320+
Object.entries(fromCatalog).filter(
1321+
([name]) =>
1322+
(!isCodexProviderName(name) && !isXaiProviderName(name)) ||
1323+
!isHandNamedProviderEntry(settings.providers[name]),
1324+
),
1325+
);
12751326
return {
12761327
...settings,
12771328
providers: {
12781329
...settings.providers,
1279-
...fromCatalog,
1330+
...overlaid,
12801331
},
12811332
};
12821333
}

0 commit comments

Comments
 (0)