Skip to content

Commit 8e232b3

Browse files
committed
Recover non-catalog OAuth models and aliased home settings
1 parent 51dc51a commit 8e232b3

4 files changed

Lines changed: 74 additions & 3 deletions

File tree

‎src/config/settings.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,16 @@ export function resolveLocalSettingsPath(cwd: string, globalPath: string): strin
419419
return physicalPathIdentity(localPath) === physicalPathIdentity(globalPath) ? null : localPath;
420420
}
421421

422+
// True when `settingsPath` is a distinct settings file from the default home
423+
// path. Symlink and lexical aliases of the default path are not overrides —
424+
// treating them as such would suppress OAuth profile projection after setup.
425+
export function isProgrammaticSettingsOverride(
426+
settingsPath: string,
427+
defaultGlobalPath: string = globalSettingsPath(),
428+
): boolean {
429+
return physicalPathIdentity(settingsPath) !== physicalPathIdentity(defaultGlobalPath);
430+
}
431+
422432
function isENOENT(err: unknown): boolean {
423433
return (
424434
typeof err === "object" &&
@@ -758,7 +768,9 @@ function recoverClobberedOAuthSelection(
758768
projected: Record<string, ProviderSettings> | undefined,
759769
): Settings | undefined {
760770
const provider = projected?.[selection.provider];
761-
if (provider === undefined || !provider.models.includes(selection.model)) return undefined;
771+
// Auth-profile presence is enough: the selected model may be outside the
772+
// projected fallback catalog (CODEX_DEFAULT_MODELS / xAI equivalents).
773+
if (provider === undefined) return undefined;
762774
return {
763775
defaultProvider: selection.provider,
764776
providers: {

‎src/settings.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,41 @@ describe("loaders", () => {
632632
}
633633
});
634634

635+
test("loadSettings recovers a non-catalog OAuth model when the auth profile exists", async () => {
636+
const dir = await mkdtemp(join(tmpdir(), "ic-settings-"));
637+
try {
638+
const path = join(dir, "settings.json");
639+
await writeFile(
640+
path,
641+
JSON.stringify({ provider: "codex/work", model: "gpt-special-custom" }),
642+
);
643+
const recovered = await loadSettings(path, {
644+
recoverableOAuthProviders: {
645+
"codex/work": {
646+
baseURL: "https://chatgpt.com/backend-api",
647+
apiKey: "oauth-token",
648+
models: ["gpt-5.2-codex", "gpt-5.1-codex"],
649+
defaultModel: "gpt-5.2-codex",
650+
},
651+
},
652+
});
653+
expect(recovered).toEqual({
654+
defaultProvider: "codex/work",
655+
providers: {
656+
"codex/work": {
657+
baseURL: "https://chatgpt.com/backend-api",
658+
models: ["gpt-special-custom"],
659+
defaultModel: "gpt-special-custom",
660+
},
661+
},
662+
});
663+
expect(JSON.stringify(recovered)).not.toContain("oauth-token");
664+
expect(await loadSettings(path)).toEqual(recovered);
665+
} finally {
666+
await rm(dir, { recursive: true, force: true });
667+
}
668+
});
669+
635670
test("loadSettings keeps malformed clobber documents strict", async () => {
636671
const dir = await mkdtemp(join(tmpdir(), "ic-settings-"));
637672
try {

‎src/tui/onboarding.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { runTUI } from "./runner.js";
22
import { buildProviderSubmitHandler } from "./provider-setup-submit.js";
33
import { loadConfig, type UnconfiguredConfig } from "../config/index.js";
4-
import { globalSettingsPath, loadSettings, resolveLocalSettingsPath } from "../config/settings.js";
4+
import {
5+
globalSettingsPath,
6+
isProgrammaticSettingsOverride,
7+
loadSettings,
8+
resolveLocalSettingsPath,
9+
} from "../config/settings.js";
510
import { activateHeldTelemetry, telemetryFirstRunPending } from "../telemetry/first-run.js";
611
import { runProviderSetup } from "./provider-setup.js";
712

@@ -49,7 +54,7 @@ export async function runOnboarding(config: UnconfiguredConfig): Promise<number>
4954
// settings source and suppresses the home-level OAuth profile projection.
5055
// Passing the default path would therefore hide a provider the operator just
5156
// signed into, so it is only forwarded when it really is an override.
52-
const overridesSettingsPath = settingsPath !== globalSettingsPath();
57+
const overridesSettingsPath = isProgrammaticSettingsOverride(settingsPath);
5358
const newConfig = await loadConfig(
5459
argv,
5560
overridesSettingsPath ? { globalSettingsPath: settingsPath } : {},

‎tests/unit/config.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ test("local settings target detects a symlink alias before the settings file exi
119119
}
120120
});
121121

122+
test("symlink alias of the default global settings path is not a programmatic override", async () => {
123+
const { globalSettingsPath, isProgrammaticSettingsOverride } =
124+
await import("../../src/config/settings.js");
125+
const root = await mkdtemp(join(tmpdir(), "ic-unit-config-global-symlink-"));
126+
const home = join(root, "home");
127+
try {
128+
await mkdir(join(home, ".corbits"), { recursive: true });
129+
const realPath = globalSettingsPath(home);
130+
await writeFile(realPath, JSON.stringify({ providers: {} }));
131+
const aliasPath = join(root, "alias-settings.json");
132+
await symlink(realPath, aliasPath);
133+
134+
expect(isProgrammaticSettingsOverride(aliasPath, realPath)).toBe(false);
135+
expect(isProgrammaticSettingsOverride(join(root, "other.json"), realPath)).toBe(true);
136+
} finally {
137+
await rm(root, { recursive: true, force: true });
138+
}
139+
});
140+
122141
test("loadSettings recovers only an exact clobbered local selection", async () => {
123142
const { loadSettings } = await import("../../src/config/settings.js");
124143
const cwd = await mkdtemp(join(tmpdir(), "ic-unit-config-recovery-"));

0 commit comments

Comments
 (0)