diff --git a/CHANGELOG.md b/CHANGELOG.md index da538633aa1..18d51b7fb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai - Twilio voice-call: report malformed successful API JSON responses with provider-owned errors instead of leaking raw parser failures. - Voice-call provider APIs: report malformed successful guarded JSON responses with provider-prefixed errors instead of leaking raw parser failures. - Realtime transcription: report malformed provider websocket JSON frames with owned parser errors instead of leaking raw `SyntaxError` objects. +- Microsoft Foundry: report malformed Azure CLI token JSON with owned auth errors instead of leaking raw parser failures. - Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom. - Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd. - CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf. diff --git a/extensions/microsoft-foundry/cli.ts b/extensions/microsoft-foundry/cli.ts index 7c0b52e11e8..517813280dd 100644 --- a/extensions/microsoft-foundry/cli.ts +++ b/extensions/microsoft-foundry/cli.ts @@ -85,7 +85,7 @@ export function isAzCliInstalled(): boolean { export function getLoggedInAccount(): AzAccount | null { try { - return JSON.parse(execAz(["account", "show", "--output", "json"])) as AzAccount; + return parseAzJson(execAz(["account", "show", "--output", "json"]), "account") as AzAccount; } catch { return null; } @@ -93,8 +93,9 @@ export function getLoggedInAccount(): AzAccount | null { export function listSubscriptions(): AzAccount[] { try { - const subs = JSON.parse( + const subs = parseAzJson( execAz(["account", "list", "--output", "json", "--all"]), + "subscriptions", ) as AzAccount[]; return subs.filter((sub) => sub.state === "Enabled"); } catch { @@ -102,6 +103,14 @@ export function listSubscriptions(): AzAccount[] { } } +function parseAzJson(raw: string, label: string): unknown { + try { + return JSON.parse(raw) as unknown; + } catch { + throw new Error(`Azure CLI returned malformed ${label} JSON.`); + } +} + type AccessTokenParams = { subscriptionId?: string; tenantId?: string; @@ -125,13 +134,16 @@ function buildAccessTokenArgs(params?: AccessTokenParams): string[] { } export function getAccessTokenResult(params?: AccessTokenParams): AzAccessToken { - return JSON.parse(execAz(buildAccessTokenArgs(params))) as AzAccessToken; + return parseAzJson(execAz(buildAccessTokenArgs(params)), "access token") as AzAccessToken; } export async function getAccessTokenResultAsync( params?: AccessTokenParams, ): Promise { - return JSON.parse(await execAzAsync(buildAccessTokenArgs(params))) as AzAccessToken; + return parseAzJson( + await execAzAsync(buildAccessTokenArgs(params)), + "access token", + ) as AzAccessToken; } export async function azLoginDeviceCode(): Promise { diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index 5f40df1fb6a..d7e632e4d15 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -235,6 +235,19 @@ function mockAzureCliToken(params: { accessToken: string; expiresInMs: number; d ); } +function mockAzureCliTokenRaw(stdout: string) { + execFileMock.mockImplementationOnce( + ( + _file: unknown, + _args: unknown, + _options: unknown, + callback: (error: Error | null, stdout: string, stderr: string) => void, + ) => { + callback(null, stdout, ""); + }, + ); +} + function mockAzureCliLoginFailure(delayMs?: number) { execFileMock.mockImplementationOnce( ( @@ -306,6 +319,14 @@ describe("microsoft-foundry plugin", () => { expect(config.auth?.order?.["microsoft-foundry"]).toEqual(["microsoft-foundry:default"]); }); + it("reports malformed Azure CLI token JSON with an owned error", async () => { + mockAzureCliTokenRaw("{not json"); + + await expect(getAccessTokenResultAsync()).rejects.toThrow( + "Azure CLI returned malformed access token JSON.", + ); + }); + it("fails clearly when the selected Azure subscription is not in the enabled list", async () => { const provider = registerProvider(); execFileSyncMock.mockImplementation((_file: string, args: string[]) => {