diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 7eab3b30c..3da36ee85 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -172,6 +172,7 @@ - Fixed OAuth `launchUrl` advertisement for flows whose redirect never returns to the local callback server: custom-scheme redirects (e.g. GitLab Duo's `vscode://` URI, which `new URL` parses without complaint) and fixed non-loopback hosts no longer receive a `http://localhost:/launch` copy target that misrepresents the callback endpoint and resolves nowhere for remote users. - Codex load balancing: clear stale persisted and in-memory usage-limit blocks for an `openai-codex` account when a fresh live usage report shows it is allowed and below all limits, including broker-backed gateway snapshots, so traffic returns to recovered accounts instead of funneling to one sibling. - Fixed Cursor `max_mode` requests to send discovered max-mode metadata on both model payload fields. ([#4797](https://github.com/can1357/oh-my-pi/issues/4797)) +- Fixed `auth-broker` config discovery ignoring nested `auth.broker.url` / `auth.broker.token` YAML keys. `readConfigYaml` only read the literal flat dotted key, so standard nested YAML was silently dropped; it now resolves both nested and flat forms (nested wins). ([#4734](https://github.com/can1357/oh-my-pi/issues/4734)) ## [16.3.11] - 2026-07-06 diff --git a/packages/ai/src/auth-broker/discover.ts b/packages/ai/src/auth-broker/discover.ts index 71ef2c5fe..7cb08cb26 100644 --- a/packages/ai/src/auth-broker/discover.ts +++ b/packages/ai/src/auth-broker/discover.ts @@ -72,6 +72,26 @@ interface ConfigSnapshot { token?: string; } +/** + * Resolve a dotted config key (e.g. `auth.broker.url`) against a parsed YAML + * record, accepting both nested form (`auth: { broker: { url } }`) and the + * legacy flat literal-dot key (`"auth.broker.url": ...`). Nested wins when both + * are present. Returns the value only when it is a string. + */ +function readDottedString(record: Record, dottedKey: string): string | undefined { + let current: unknown = record; + for (const segment of dottedKey.split(".")) { + if (current === null || typeof current !== "object" || Array.isArray(current)) { + current = undefined; + break; + } + current = (current as Record)[segment]; + } + if (typeof current === "string") return current; + const flat = record[dottedKey]; + return typeof flat === "string" ? flat : undefined; +} + async function readConfigYaml(agentDir: string): Promise { for (const filename of MAIN_CONFIG_FILENAMES) { const configPath = path.join(agentDir, filename); @@ -80,9 +100,8 @@ async function readConfigYaml(agentDir: string): Promise { const parsed = YAML.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; const record = parsed as Record; - const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined; - const token = - typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined; + const url = readDottedString(record, "auth.broker.url"); + const token = readDottedString(record, "auth.broker.token"); return { url, token }; } catch (err) { if (isEnoent(err)) continue; diff --git a/packages/ai/test/auth-broker-nested-config.test.ts b/packages/ai/test/auth-broker-nested-config.test.ts new file mode 100644 index 000000000..b2f69e1fa --- /dev/null +++ b/packages/ai/test/auth-broker-nested-config.test.ts @@ -0,0 +1,65 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { resolveAuthBrokerConfig } from "@oh-my-pi/pi-ai/auth-broker"; +import { removeWithRetries } from "../../utils/src/temp"; +import { withEnv } from "./helpers"; + +const CLEAR_BROKER_ENV = { + OMP_AUTH_BROKER_URL: undefined, + OMP_AUTH_BROKER_TOKEN: undefined, +} as const; + +describe("auth-broker config.yml key resolution", () => { + let agentDir = ""; + + beforeEach(async () => { + agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-ai-auth-broker-config-")); + }); + + afterEach(async () => { + if (agentDir) await removeWithRetries(agentDir); + agentDir = ""; + }); + + async function writeConfig(yaml: string): Promise { + await Bun.write(path.join(agentDir, "config.yml"), yaml); + } + + test("nested YAML keys resolve broker url and token", async () => { + await writeConfig( + ["auth:", " broker:", " url: https://broker.example", " token: nested-token", ""].join("\n"), + ); + await withEnv(CLEAR_BROKER_ENV, async () => { + const config = await resolveAuthBrokerConfig({ agentDir }); + expect(config).toEqual({ url: "https://broker.example", token: "nested-token" }); + }); + }); + + test("legacy flat dotted keys still resolve", async () => { + await writeConfig(['"auth.broker.url": https://flat.example', '"auth.broker.token": flat-token', ""].join("\n")); + await withEnv(CLEAR_BROKER_ENV, async () => { + const config = await resolveAuthBrokerConfig({ agentDir }); + expect(config).toEqual({ url: "https://flat.example", token: "flat-token" }); + }); + }); + + test("nested value wins over the flat dotted key", async () => { + await writeConfig( + [ + '"auth.broker.url": https://flat.example', + '"auth.broker.token": flat-token', + "auth:", + " broker:", + " url: https://nested.example", + " token: nested-token", + "", + ].join("\n"), + ); + await withEnv(CLEAR_BROKER_ENV, async () => { + const config = await resolveAuthBrokerConfig({ agentDir }); + expect(config).toEqual({ url: "https://nested.example", token: "nested-token" }); + }); + }); +});