From b190a3c15c1e1f4a00c841be2c69426017dbff8c Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 14 Jul 2026 15:52:21 +0000 Subject: [PATCH] fix(auth-broker): resolve nested auth.broker.url/token yaml keys readConfigYaml only read the literal flat dotted key from config.yml, so standard nested YAML (auth: broker: url:) was silently ignored while the rest of the CLI normalizes nested keys. Resolve the dotted key through nested segments, keeping the legacy flat literal-dot form working (nested wins). Fixes #4734 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/src/auth-broker/discover.ts | 25 ++++++- .../ai/test/auth-broker-nested-config.test.ts | 65 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 packages/ai/test/auth-broker-nested-config.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 9c99508fd..203f46a2e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- 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 ### Fixed diff --git a/packages/ai/src/auth-broker/discover.ts b/packages/ai/src/auth-broker/discover.ts index c66ccdd20..4cee518d6 100644 --- a/packages/ai/src/auth-broker/discover.ts +++ b/packages/ai/src/auth-broker/discover.ts @@ -71,6 +71,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 { const configPath = path.join(agentDir, "config.yml"); try { @@ -78,9 +98,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)) return {}; 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" }); + }); + }); +});