feat(security): add provider-based external secrets management

This commit is contained in:
joshavant
2026-02-25 17:39:31 -06:00
committed by Peter Steinberger
parent bb60cab76d
commit 4e7a833a24
35 changed files with 1779 additions and 669 deletions

View File

@@ -183,7 +183,7 @@ describe("resolveApiKeyForProfile secret refs", () => {
[profileId]: {
type: "api_key",
provider: "openai",
keyRef: { source: "env", id: "OPENAI_API_KEY" },
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
},
},
@@ -217,7 +217,7 @@ describe("resolveApiKeyForProfile secret refs", () => {
type: "token",
provider: "github-copilot",
token: "",
tokenRef: { source: "env", id: "GITHUB_TOKEN" },
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
},
},
},
@@ -236,4 +236,70 @@ describe("resolveApiKeyForProfile secret refs", () => {
}
}
});
it("resolves inline ${ENV} api_key values", async () => {
const profileId = "openai:inline-env";
const previous = process.env.OPENAI_API_KEY;
process.env.OPENAI_API_KEY = "sk-openai-inline";
try {
const result = await resolveApiKeyForProfile({
cfg: cfgFor(profileId, "openai", "api_key"),
store: {
version: 1,
profiles: {
[profileId]: {
type: "api_key",
provider: "openai",
key: "${OPENAI_API_KEY}",
},
},
},
profileId,
});
expect(result).toEqual({
apiKey: "sk-openai-inline",
provider: "openai",
email: undefined,
});
} finally {
if (previous === undefined) {
delete process.env.OPENAI_API_KEY;
} else {
process.env.OPENAI_API_KEY = previous;
}
}
});
it("resolves inline ${ENV} token values", async () => {
const profileId = "github-copilot:inline-env";
const previous = process.env.GITHUB_TOKEN;
process.env.GITHUB_TOKEN = "gh-inline-token";
try {
const result = await resolveApiKeyForProfile({
cfg: cfgFor(profileId, "github-copilot", "token"),
store: {
version: 1,
profiles: {
[profileId]: {
type: "token",
provider: "github-copilot",
token: "${GITHUB_TOKEN}",
},
},
},
profileId,
});
expect(result).toEqual({
apiKey: "gh-inline-token",
provider: "github-copilot",
email: undefined,
});
} finally {
if (previous === undefined) {
delete process.env.GITHUB_TOKEN;
} else {
process.env.GITHUB_TOKEN = previous;
}
}
});
});

View File

@@ -5,7 +5,7 @@ import {
type OAuthProvider,
} from "@mariozechner/pi-ai";
import { loadConfig, type OpenClawConfig } from "../../config/config.js";
import { isSecretRef } from "../../config/types.secrets.js";
import { coerceSecretRef } 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";
@@ -257,14 +257,34 @@ export async function resolveApiKeyForProfile(
return null;
}
const refResolveCache: SecretRefResolveCache = { fileSecretsPromise: null };
const refResolveCache: SecretRefResolveCache = {};
const configForRefResolution = cfg ?? loadConfig();
const refDefaults = configForRefResolution.secrets?.defaults;
if (cred.type === "api_key") {
let key = cred.key?.trim();
if (!key && isSecretRef(cred.keyRef)) {
if (key) {
const inlineRef = coerceSecretRef(key, refDefaults);
if (inlineRef) {
try {
key = await resolveSecretRefString(inlineRef, {
config: configForRefResolution,
env: process.env,
cache: refResolveCache,
});
} catch (err) {
log.debug("failed to resolve inline auth profile api_key ref", {
profileId,
provider: cred.provider,
error: err instanceof Error ? err.message : String(err),
});
}
}
}
const keyRef = coerceSecretRef(cred.keyRef, refDefaults);
if (!key && keyRef) {
try {
key = await resolveSecretRefString(cred.keyRef, {
key = await resolveSecretRefString(keyRef, {
config: configForRefResolution,
env: process.env,
cache: refResolveCache,
@@ -284,9 +304,28 @@ export async function resolveApiKeyForProfile(
}
if (cred.type === "token") {
let token = cred.token?.trim();
if (!token && isSecretRef(cred.tokenRef)) {
if (token) {
const inlineRef = coerceSecretRef(token, refDefaults);
if (inlineRef) {
try {
token = await resolveSecretRefString(inlineRef, {
config: configForRefResolution,
env: process.env,
cache: refResolveCache,
});
} catch (err) {
log.debug("failed to resolve inline auth profile token ref", {
profileId,
provider: cred.provider,
error: err instanceof Error ? err.message : String(err),
});
}
}
}
const tokenRef = coerceSecretRef(cred.tokenRef, refDefaults);
if (!token && tokenRef) {
try {
token = await resolveSecretRefString(cred.tokenRef, {
token = await resolveSecretRefString(tokenRef, {
config: configForRefResolution,
env: process.env,
cache: refResolveCache,