From 4cfe0077ff0a3079d210338bdd296baaea69d2ff Mon Sep 17 00:00:00 2001 From: RickLin <83101411+ObliviateRickLin@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:20:28 +0800 Subject: [PATCH] fix(models): redact synthetic auth credentials from models status --json (#104734) The provider auth overview passed the caller's synthetic-auth object through to the JSON payload. Status hands it the full runtime shape, which also carries the raw credential, so structural typing let live API keys/tokens ship in a diagnostic surface (#104713). Re-project to the declared value/source fields at the overview boundary. Fixes #104713 --- .../models/list.auth-overview.test.ts | 25 +++++++++++++++++++ src/commands/models/list.auth-overview.ts | 12 ++++++++- src/commands/models/list.status.test.ts | 7 ++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/commands/models/list.auth-overview.test.ts b/src/commands/models/list.auth-overview.test.ts index e5e1ff26c08e..6c602fe7d433 100644 --- a/src/commands/models/list.auth-overview.test.ts +++ b/src/commands/models/list.auth-overview.test.ts @@ -99,6 +99,31 @@ describe("resolveProviderAuthOverview", () => { vi.mocked(resolveEnvApiKey).mockClear(); }); + it("projects synthetic auth to value/source and drops runtime credential fields", () => { + // #104713: status callers pass their richer runtime object (credential, + // mode, expiresAt); the overview must not let those reach JSON output. + const runtimeSyntheticAuth = { + value: "plugin-owned", + source: "xAI plugin config", + credential: "xai-raw-credential-material", + mode: "api-key", + expiresAt: Date.now() + 60_000, + }; + const overview = resolveProviderAuthOverview({ + provider: "xai", + cfg: {}, + store: { version: 1, profiles: {} } as never, + modelsPath: "/tmp/models.json", + syntheticAuth: runtimeSyntheticAuth, + }); + + expect(overview.syntheticAuth).toStrictEqual({ + value: "plugin-owned", + source: "xAI plugin config", + }); + expect(JSON.stringify(overview)).not.toContain("xai-raw-credential-material"); + }); + it("labels token profiles that only have tokenRef", () => { const overview = resolveProviderAuthOverview({ provider: "github-copilot", diff --git a/src/commands/models/list.auth-overview.ts b/src/commands/models/list.auth-overview.ts index 2eeb84d84342..f615b679ad79 100644 --- a/src/commands/models/list.auth-overview.ts +++ b/src/commands/models/list.auth-overview.ts @@ -218,6 +218,16 @@ export function resolveProviderAuthOverview(params: { }, } : {}), - ...(params.syntheticAuth ? { syntheticAuth: params.syntheticAuth } : {}), + // Re-project instead of passing the caller's object through: status callers + // hand richer runtime shapes that also carry the raw synthetic credential, + // and structural typing would let it leak into `--json` output verbatim. + ...(params.syntheticAuth + ? { + syntheticAuth: { + value: params.syntheticAuth.value, + source: params.syntheticAuth.source, + }, + } + : {}), }; } diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index a1a4a9c3322d..4cc62629e86d 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -1221,6 +1221,13 @@ describe("modelsStatusCommand auth overview", () => { value: "plugin-owned", source: "codex-app-server", }); + // #104713: the summary must ship only the projected fields; the runtime + // synthetic-auth object also carries the raw credential and must never + // reach the JSON payload. + expect( + Object.keys(requireRecord(codexProvider.syntheticAuth, "codex synthetic auth")), + ).toStrictEqual(["value", "source"]); + expect(JSON.stringify(payload)).not.toContain("codex-runtime-token"); expectRecordFields(requireRecord(codexProvider.effective, "codex effective auth"), { kind: "synthetic", detail: "codex-app-server",