From 6a251d8d7460213ecd65934b3aba2424f001065f Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:01:41 -0600 Subject: [PATCH] Auth profiles: resolve keyRef/tokenRef outside gateway --- src/agents/auth-profiles/oauth.test.ts | 69 ++++++++++++++++++++++++++ src/agents/auth-profiles/oauth.ts | 41 +++++++++++++-- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/agents/auth-profiles/oauth.test.ts b/src/agents/auth-profiles/oauth.test.ts index a91d3e4a5b7..d5b229e45dc 100644 --- a/src/agents/auth-profiles/oauth.test.ts +++ b/src/agents/auth-profiles/oauth.test.ts @@ -168,3 +168,72 @@ describe("resolveApiKeyForProfile token expiry handling", () => { }); }); }); + +describe("resolveApiKeyForProfile secret refs", () => { + it("resolves api_key keyRef from env", async () => { + const profileId = "openai:default"; + const previous = process.env.OPENAI_API_KEY; + process.env.OPENAI_API_KEY = "sk-openai-ref"; + try { + const result = await resolveApiKeyForProfile({ + cfg: cfgFor(profileId, "openai", "api_key"), + store: { + version: 1, + profiles: { + [profileId]: { + type: "api_key", + provider: "openai", + keyRef: { source: "env", id: "OPENAI_API_KEY" }, + }, + }, + }, + profileId, + }); + expect(result).toEqual({ + apiKey: "sk-openai-ref", + provider: "openai", + email: undefined, + }); + } finally { + if (previous === undefined) { + delete process.env.OPENAI_API_KEY; + } else { + process.env.OPENAI_API_KEY = previous; + } + } + }); + + it("resolves token tokenRef from env", async () => { + const profileId = "github-copilot:default"; + const previous = process.env.GITHUB_TOKEN; + process.env.GITHUB_TOKEN = "gh-ref-token"; + try { + const result = await resolveApiKeyForProfile({ + cfg: cfgFor(profileId, "github-copilot", "token"), + store: { + version: 1, + profiles: { + [profileId]: { + type: "token", + provider: "github-copilot", + token: "", + tokenRef: { source: "env", id: "GITHUB_TOKEN" }, + }, + }, + }, + profileId, + }); + expect(result).toEqual({ + apiKey: "gh-ref-token", + provider: "github-copilot", + email: undefined, + }); + } finally { + if (previous === undefined) { + delete process.env.GITHUB_TOKEN; + } else { + process.env.GITHUB_TOKEN = previous; + } + } + }); +}); diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index a4f10b6a587..258eb215bc2 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -4,9 +4,11 @@ import { type OAuthCredentials, type OAuthProvider, } from "@mariozechner/pi-ai"; -import type { OpenClawConfig } from "../../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../../config/config.js"; +import { isSecretRef } from "../../config/types.secrets.js"; import { withFileLock } from "../../infra/file-lock.js"; import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js"; +import { resolveSecretRefString, type SecretRefResolveCache } from "../../secrets/resolve.js"; import { refreshChutesTokens } from "../chutes-oauth.js"; import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js"; import { formatAuthDoctorHint } from "./doctor.js"; @@ -255,15 +257,48 @@ export async function resolveApiKeyForProfile( return null; } + const refResolveCache: SecretRefResolveCache = { fileSecretsPromise: null }; + const configForRefResolution = cfg ?? loadConfig(); + if (cred.type === "api_key") { - const key = cred.key?.trim(); + let key = cred.key?.trim(); + if (!key && isSecretRef(cred.keyRef)) { + try { + key = await resolveSecretRefString(cred.keyRef, { + config: configForRefResolution, + env: process.env, + cache: refResolveCache, + }); + } catch (err) { + log.debug("failed to resolve auth profile api_key ref", { + profileId, + provider: cred.provider, + error: err instanceof Error ? err.message : String(err), + }); + } + } if (!key) { return null; } return buildApiKeyProfileResult({ apiKey: key, provider: cred.provider, email: cred.email }); } if (cred.type === "token") { - const token = cred.token?.trim(); + let token = cred.token?.trim(); + if (!token && isSecretRef(cred.tokenRef)) { + try { + token = await resolveSecretRefString(cred.tokenRef, { + config: configForRefResolution, + env: process.env, + cache: refResolveCache, + }); + } catch (err) { + log.debug("failed to resolve auth profile token ref", { + profileId, + provider: cred.provider, + error: err instanceof Error ? err.message : String(err), + }); + } + } if (!token) { return null; }