Skip to content

Commit 7ad873e

Browse files
committed
refactor(config): drop the OAuth projection wrapper and flag narration
Inline applyOAuthProviderProjections back into loadConfig and remove comments that only restate alias spellings. Flag parse results are unchanged.
1 parent 8da7c96 commit 7ad873e

9 files changed

Lines changed: 62 additions & 166 deletions

File tree

‎docs/IMPLEMENTATION.md‎

Lines changed: 21 additions & 23 deletions
Large diffs are not rendered by default.

‎docs/PERFTRACE.md‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,8 @@ Precedence:
6666
`resourceAttributes["service.name"]` > `corbits-code`. After merge,
6767
`resourceAttributes["service.name"]` is always set to the resolved name so
6868
the two never diverge.
69-
- **resourceAttributes:** env list replaces settings wholesale when
70-
`OTEL_RESOURCE_ATTRIBUTES` is set (no per-key merge); otherwise settings
71-
apply alone. Set-but-empty counts as set: `OTEL_RESOURCE_ATTRIBUTES=""`
72-
replaces the settings attributes with nothing (and does not re-apply the
73-
settings map) — the same set-but-empty rule as `OTEL_EXPORTER_OTLP_HEADERS`.
69+
- **resourceAttributes:** when `OTEL_RESOURCE_ATTRIBUTES` is set, it fully
70+
replaces settings attributes (including set-but-empty)
7471
- **enabling export:** setting an endpoint (settings or env) enables export;
7572
`otel.enabled` need not be set. Service name or resource attributes alone do
7673
not enable export.

‎src/config.test.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,6 @@ describe("loadConfig", () => {
572572
{ argv: ["resume", "--pick"] },
573573
{ argv: ["resume"] },
574574
{ argv: ["continue", "--list"] },
575-
// CL-10672: --pick/--list fold into the single --resume picker verb; both
576-
// aliases remain accepted in any resume-flow position.
577575
{ argv: ["resume", "--list"] },
578576
{ argv: ["resume", "--pick", "--list"] },
579577
] as const)("$argv opens the session picker", async ({ argv }) => {
@@ -586,8 +584,6 @@ describe("loadConfig", () => {
586584
});
587585

588586
test("CL-10672 keeps --pick/--list out of flow as unrecognized flags", async () => {
589-
// Without the resume verb these spellings were never recognized; the
590-
// alias fold preserves that fallthrough instead of quietly opening a picker.
591587
await expect(
592588
loadConfig(["--pick"], { globalSettingsPath: NO_SETTINGS }),
593589
).rejects.toThrow("unrecognized flag: --pick");

‎src/config/index.ts‎

Lines changed: 17 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ import { resolveProfile } from "./profiles.js";
109109

110110
// Per-call token ceiling for inference sources, shared by agent creation
111111
// (runner.ts) and live provider switching (the /agent modal) so a live switch
112-
// cannot silently revert it. CL-10672 review: kept as the shared fallback —
113-
// it is NOT an always-overridden default; ProviderSettings.maxTokens is the
114-
// optional override and an unset value resolves to this ceiling.
112+
// cannot silently revert it.
115113
export const SOURCE_MAX_TOKENS = 16384;
116114

117115
// Credential-cell placeholder for keyless local providers (e.g. Ollama). The
@@ -162,9 +160,8 @@ function applyPersistedOAuthDefaults(
162160

163161
// OAuth entries in settings.json carry no credentials; they work only while a
164162
// matching auth-store profile exists. Drop orphans in memory so a removed
165-
// profile does not pin resolution to an unauthenticatable provider. Called
166-
// only from applyOAuthProviderProjections (plus its tests).
167-
function dropOrphanedOAuthEntries(
163+
// profile does not pin resolution to an unauthenticatable provider.
164+
export function dropOrphanedOAuthEntries(
168165
settings: Settings | null,
169166
projected: Record<string, ProviderSettings>,
170167
): Settings | null {
@@ -200,8 +197,8 @@ function isHandNamedProviderEntry(
200197
}
201198

202199
// Overlay live OAuth profile projections onto settings for runtime provider
203-
// resolution. Called only from applyOAuthProviderProjections (plus its tests).
204-
function overlayOAuthProjections(
200+
// resolution. Exported for tests; loadConfig is the only production caller.
201+
export function overlayOAuthProjections(
205202
settings: Settings | null,
206203
projected: Record<string, ProviderSettings>,
207204
): Settings | null {
@@ -216,28 +213,6 @@ function overlayOAuthProjections(
216213
};
217214
}
218215

219-
// The one ordered OAuth projection chain for runtime provider resolution,
220-
// folding the three dedicated helpers at the layer that owns them
221-
// (loadConfig): (1) persisted defaultModel defaults merge into the live
222-
// projections, (2) credential-less orphan entries are dropped from settings
223-
// (only when the projections came from the home auth stores), (3) the
224-
// projections overlay onto the surviving settings without overwriting
225-
// hand-named API-key rows. Exposed for tests.
226-
export function applyOAuthProviderProjections(
227-
settings: Settings | null,
228-
projectedProviderSettings: Record<string, ProviderSettings>,
229-
options: { dropOrphans: boolean },
230-
): Settings | null {
231-
const projected = applyPersistedOAuthDefaults(
232-
settings,
233-
projectedProviderSettings,
234-
);
235-
const liveSettings = options.dropOrphans
236-
? dropOrphanedOAuthEntries(settings, projected)
237-
: settings;
238-
return overlayOAuthProjections(liveSettings, projected);
239-
}
240-
241216
function hasExaEntry(servers: MCPServerSettingsEntry[] | undefined): boolean {
242217
return (
243218
servers?.some((server) => server.name === EXA_MCP_SERVER_NAME) === true
@@ -730,15 +705,6 @@ export interface UnconfiguredConfig {
730705
settingsDiagnostics?: SettingsLoadDiagnostic[];
731706
}
732707

733-
// Skip-permissions spellings. `--dangerously-skip-permissions` is the
734-
// canonical process-only flag; `--yolo` is a kept alias (`/yolo` is the
735-
// persistent settings writer — a separate surface). One flag, one setting
736-
// (`dangerouslySkipPermissions`); the parse consults one alias set.
737-
const SKIP_PERMISSIONS_FLAGS: ReadonlySet<string> = new Set([
738-
"--dangerously-skip-permissions",
739-
"--yolo",
740-
]);
741-
742708
/** Printed for `corbits --help` / `-h`. Keep in sync with docs/IMPLEMENTATION.md. */
743709
export const CLI_HELP_TEXT = `corbits — coding agent CLI
744710
@@ -867,9 +833,6 @@ export async function loadConfig(
867833
let resumeMode: "id" | "pick" | undefined;
868834
let resumeSessionId: string | undefined;
869835
const leading = args[0];
870-
// One exec verb: `exec` is canonical; `run` is a kept alias (help names
871-
// both). One resume verb: `resume` is canonical; `continue` is a kept
872-
// alias of the same flow.
873836
if (leading === "exec" || leading === "run") {
874837
command = "exec";
875838
args.shift();
@@ -878,8 +841,6 @@ export async function loadConfig(
878841
// Bare resume opens the list; a session id is the only direct-resume path.
879842
// Invalid non-flag positionals error instead of falling through (a
880843
// free-form token would otherwise become task while skipInitialTask is set).
881-
// A following --pick/--list falls through to the loop's single resume
882-
// branch, where it is a kept alias of the bare --resume picker flow.
883844
const next = args.slice()[0];
884845
if (next !== undefined && !isFlagToken(next)) {
885846
if (!isSessionId(next)) {
@@ -895,10 +856,9 @@ export async function loadConfig(
895856
}
896857
}
897858

898-
// `-p` is the exec one-shot alias in any position (`-p --provider …`,
859+
// `-p` is the exec one-shot path in any position (`-p --provider …`,
899860
// `--model … -p …`). Detect it before value flags so exec-only options
900-
// that appear before `-p` still see exec mode; the loop keeps the token
901-
// out of the task positionals.
861+
// that appear before `-p` still see exec mode.
902862
if (args.includes("-p")) {
903863
if (leading === "resume" || leading === "continue") {
904864
throw new Error(
@@ -970,13 +930,11 @@ export async function loadConfig(
970930
continue;
971931
}
972932

973-
if (SKIP_PERMISSIONS_FLAGS.has(arg)) {
933+
if (arg === "--dangerously-skip-permissions" || arg === "--yolo") {
974934
dangerouslySkipPermissions = true;
975935
continue;
976936
}
977937
if (arg === "--auto") {
978-
// Explicit re-assertion of the on-by-default auto mode. Kept because
979-
// sequence order still matters (`--no-auto --auto` re-enables).
980938
auto = true;
981939
continue;
982940
}
@@ -985,15 +943,9 @@ export async function loadConfig(
985943
continue;
986944
}
987945
if (arg === "-p") {
988-
// `-p` alias of exec: the pre-scan already fixed command="exec"; the
989-
// token must stay out of the task positionals.
990946
continue;
991947
}
992948
if (arg === "--resume" || arg === "--pick" || arg === "--list") {
993-
// One resume verb: `--resume` carries the optional session id (exec
994-
// requires it; interactive bare `--resume` opens the picker, the alias
995-
// for `resume <session-id>`). `--pick`/`--list` are kept aliases of that
996-
// bare picker flow and may not follow a session id.
997949
if (arg === "--resume") {
998950
if (resumeMode === "id") {
999951
throw new Error("cannot combine a session id with --resume");
@@ -1020,8 +972,6 @@ export async function loadConfig(
1020972
} else if (resumeMode !== undefined) {
1021973
resumeMode = "pick";
1022974
} else {
1023-
// Without the resume verb these spellings are not in flow; preserve
1024-
// the previous fallthrough as an unrecognized flag.
1025975
throw new Error(`unrecognized flag: ${arg}`);
1026976
}
1027977
continue;
@@ -1059,7 +1009,7 @@ export async function loadConfig(
10591009
useOAuthProfiles
10601010
? await Promise.all([listCodexProfiles(), listXaiProfiles()])
10611011
: [[], []];
1062-
const projectedOAuthProviders = {
1012+
let projectedOAuthProviders = {
10631013
...codexProvidersAsSettings(codexProfiles),
10641014
...xaiProvidersAsSettings(xaiProfiles),
10651015
};
@@ -1089,11 +1039,16 @@ export async function loadConfig(
10891039
settings?.dangerouslySkipPermissions === true;
10901040
dangerouslySkipPermissions =
10911041
dangerouslySkipPermissions || settings?.dangerouslySkipPermissions === true;
1092-
// Single ordered projection chain (see applyOAuthProviderProjections).
1093-
const settingsForResolution: Settings | null = applyOAuthProviderProjections(
1042+
projectedOAuthProviders = applyPersistedOAuthDefaults(
10941043
settings,
10951044
projectedOAuthProviders,
1096-
{ dropOrphans: useOAuthProfiles },
1045+
);
1046+
const liveSettings = useOAuthProfiles
1047+
? dropOrphanedOAuthEntries(settings, projectedOAuthProviders)
1048+
: settings;
1049+
const settingsForResolution: Settings | null = overlayOAuthProjections(
1050+
liveSettings,
1051+
projectedOAuthProviders,
10971052
);
10981053

10991054
// The per-repo selection file still applies on top of --config: that file

‎src/config/oauth-catalog.test.ts‎

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import {
88
codexProvidersAsSettings,
99
} from "./codex-providers.js";
1010
import {
11-
applyOAuthProviderProjections,
11+
dropOrphanedOAuthEntries,
1212
mergeOAuthCatalog,
13+
overlayOAuthProjections,
1314
providerCatalogToSettings,
1415
runtimeSettingsWithCatalog,
1516
} from "./index.js";
@@ -140,38 +141,34 @@ describe("CL-6728: OAuth projections do not overwrite hand-named provider entrie
140141
const liveProjected = () => codexProvidersAsSettings([liveMine]);
141142
const liveCatalog = () => codexProfilesToCatalogEntries([liveMine]);
142143

143-
test("the projection chain keeps a hand-named codex/<slug> API-key entry", () => {
144-
const overlaid = applyOAuthProviderProjections(
144+
test("overlayOAuthProjections keeps a hand-named codex/<slug> API-key entry", () => {
145+
const overlaid = overlayOAuthProjections(
145146
settingsWith({ "codex/mine": handNamed() }),
146147
liveProjected(),
147-
{ dropOrphans: true },
148148
);
149149
expect(overlaid?.providers["codex/mine"]?.apiKey).toBe("hand-named-key");
150150
});
151151

152-
test("the projection chain still applies the live token over a credential-less OAuth placeholder", () => {
153-
const overlaid = applyOAuthProviderProjections(
152+
test("overlayOAuthProjections still applies the live token over a credential-less OAuth placeholder", () => {
153+
const overlaid = overlayOAuthProjections(
154154
settingsWith({ "codex/mine": codexEntry() }),
155155
liveProjected(),
156-
{ dropOrphans: true },
157156
);
158157
expect(overlaid?.providers["codex/mine"]?.apiKey).toBe("live-token");
159158
});
160159

161-
test("the chain never drops a hand-named API-key entry (orphan sweep)", () => {
162-
const kept = applyOAuthProviderProjections(
160+
test("dropOrphanedOAuthEntries never drops a hand-named API-key entry", () => {
161+
const kept = dropOrphanedOAuthEntries(
163162
settingsWith({ "codex/mine": handNamed() }),
164163
{},
165-
{ dropOrphans: true },
166164
);
167165
expect(kept?.providers["codex/mine"]?.apiKey).toBe("hand-named-key");
168166
});
169167

170-
test("the chain still drops a credential-less orphan", () => {
171-
const dropped = applyOAuthProviderProjections(
168+
test("dropOrphanedOAuthEntries still drops a credential-less orphan", () => {
169+
const dropped = dropOrphanedOAuthEntries(
172170
settingsWith({ "codex/mine": codexEntry() }),
173171
{},
174-
{ dropOrphans: true },
175172
);
176173
expect(dropped?.providers["codex/mine"]).toBeUndefined();
177174
});
@@ -277,8 +274,8 @@ describe("CL-6728: OAuth projections do not overwrite hand-named provider entrie
277274
expect(runtime.providers["codex/mine"]?.apiKey).toBeUndefined();
278275
});
279276

280-
test("the chain treats a whitespace apiKey as hand-named", () => {
281-
const overlaid = applyOAuthProviderProjections(
277+
test("overlayOAuthProjections treats a whitespace apiKey as hand-named", () => {
278+
const overlaid = overlayOAuthProjections(
282279
settingsWith({
283280
"codex/mine": {
284281
baseURL: "https://hand-named.example.com/v1",
@@ -287,13 +284,12 @@ describe("CL-6728: OAuth projections do not overwrite hand-named provider entrie
287284
},
288285
}),
289286
liveProjected(),
290-
{ dropOrphans: true },
291287
);
292288
expect(overlaid?.providers["codex/mine"]?.apiKey).toBe(" ");
293289
});
294290

295-
test("the chain treats an empty-string apiKey as a placeholder", () => {
296-
const overlaid = applyOAuthProviderProjections(
291+
test("overlayOAuthProjections treats an empty-string apiKey as a placeholder", () => {
292+
const overlaid = overlayOAuthProjections(
297293
settingsWith({
298294
"codex/mine": {
299295
baseURL: "https://hand-named.example.com/v1",
@@ -302,44 +298,7 @@ describe("CL-6728: OAuth projections do not overwrite hand-named provider entrie
302298
},
303299
}),
304300
liveProjected(),
305-
{ dropOrphans: true },
306301
);
307302
expect(overlaid?.providers["codex/mine"]?.apiKey).toBe("live-token");
308303
});
309304
});
310-
311-
describe("applyOAuthProviderProjections: one ordered chain (CL-10672)", () => {
312-
const liveMine: CodexProfile = {
313-
name: "mine",
314-
tokens: { access: "live-token", refresh: "r", expiresAt: 1 },
315-
createdAt: 0,
316-
};
317-
test("persisted defaults feed the projections before the orphan sweep and overlay", () => {
318-
// settings carry a persisted defaultModel on the credential-less
319-
// placeholder; the projection must inherit it (defaults step) while the
320-
// orphan sweep honors the live profile presence (drop guard) and the
321-
// overlay delivers the live token.
322-
const settings = settingsWith({
323-
"codex/mine": { ...codexEntry(), defaultModel: "chosen-model" },
324-
});
325-
const resolvedChain = applyOAuthProviderProjections(
326-
settings,
327-
codexProvidersAsSettings([liveMine]),
328-
{ dropOrphans: true },
329-
);
330-
const row = resolvedChain?.providers["codex/mine"];
331-
expect(row?.apiKey).toBe("live-token");
332-
expect(row?.models).toContain("chosen-model");
333-
expect(row?.defaultModel).toBe("chosen-model");
334-
});
335-
336-
test("dropOrphans: false skips the orphan sweep verbatim (programmatic settings load)", () => {
337-
const placeholder = codexEntry();
338-
const kept = applyOAuthProviderProjections(
339-
settingsWith({ "codex/mine": placeholder }),
340-
{},
341-
{ dropOrphans: false },
342-
);
343-
expect(kept?.providers["codex/mine"]).toBe(placeholder);
344-
});
345-
});

‎src/config/settings.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,11 @@ export interface ProviderSettings {
4242
contextWindow?: number;
4343
// Per-call output cap, overriding SOURCE_MAX_TOKENS when set.
4444
maxTokens?: number;
45-
// Sampling temperature (0..2, OpenAI-compatible); the canonical sampling
46-
// knob. Mutually exclusive with topP — never send both on the wire.
45+
// Sampling temperature (0..2, OpenAI-compatible); mutually exclusive with
46+
// topP — never send both on the wire.
4747
temperature?: number;
48-
// Deprecated non-canonical sampling spelling; honored as-is (loads, flows
49-
// to top_p) so existing files keep working. Mutually exclusive with
50-
// temperature; new config should use temperature.
48+
// Sampling top-p (0..1, OpenAI-compatible); mutually exclusive with
49+
// temperature.
5150
topP?: number;
5251
// Bifrost virtual key (sk-bf-...): routes through the Bifrost adapter
5352
// (injects the x-bf-vk header) and enables /v1/models auto-discovery.

‎src/perf/otel-config.test.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ describe("resolveOtelExportConfig", () => {
213213
expect(result.config.resourceAttributes["deployment.environment"]).toBe(
214214
"prod",
215215
);
216-
// Env wins wholesale: settings-only keys do not survive the merge.
217216
expect(result.config.resourceAttributes.team).toBeUndefined();
218217
expect(result.config.resourceAttributes["service.name"]).toBe(
219218
DEFAULT_OTEL_SERVICE_NAME,
@@ -240,9 +239,6 @@ describe("resolveOtelExportConfig", () => {
240239
});
241240

242241
test("set-but-empty env resource attributes replace with nothing (regression: no settings fallback)", () => {
243-
// OTEL_RESOURCE_ATTRIBUTES="" counts as set for the wholesale rule, matching
244-
// the same edge on OTEL_EXPORTER_OTLP_HEADERS: the settings map is dropped,
245-
// leaving only the service.name sync.
246242
const result = resolveOtelExportConfig(
247243
baseSettings({
248244
endpoint: "https://c.example",

‎src/perf/otel-config.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function validateStringMap(
209209
}
210210

211211
/**
212-
* Precedence (env wins wholesale per field):
212+
* Precedence (env over settings):
213213
* - endpoint: env > settings
214214
* - headers: env list fully replaces settings when set; else settings
215215
* - serviceName: env > settings.serviceName > attrs["service.name"] > default
@@ -297,9 +297,6 @@ export function resolveOtelExportConfig(
297297

298298
let resourceAttributes: Record<string, string> = {};
299299
if (envResourceAttrsRaw !== undefined) {
300-
// Env wins wholesale: a set OTEL_RESOURCE_ATTRIBUTES replaces settings
301-
// attrs entirely, matching the headers rule. An invalid settings map
302-
// never blocks a fully-specified env list.
303300
const parsed = parseOtelKeyValueList(
304301
envResourceAttrsRaw,
305302
OTEL_ENV.resourceAttributes,

0 commit comments

Comments
 (0)