fix(secrets): preserve auth profile key refs during provider scrub (#77489)

* fix(secrets): preserve auth profile key refs during provider scrub

* Add changelog for secrets apply fix

* Seed auth profile ref for scrub regression

* fix(secrets): guard auth profile ref scrub

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Brandon
2026-05-04 20:50:39 -04:00
committed by GitHub
parent b378a91257
commit e2e0908055
3 changed files with 65 additions and 4 deletions

View File

@@ -110,7 +110,8 @@ async function seedDefaultApplyFixture(fixture: ApplyFixture): Promise<void> {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-openai-plaintext", // pragma: allowlist secret
key: "sk-ope...text", // pragma: allowlist secret
keyRef: OPENAI_API_KEY_ENV_REF,
},
},
});
@@ -290,7 +291,11 @@ describe("secrets apply", () => {
profiles: { "openai:default": { key?: string; keyRef?: unknown } };
};
expect(nextAuthStore.profiles["openai:default"].key).toBeUndefined();
expect(nextAuthStore.profiles["openai:default"].keyRef).toBeUndefined();
expect(nextAuthStore.profiles["openai:default"].keyRef).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
});
const nextAuthJson = JSON.parse(await fs.readFile(fixture.authJsonPath, "utf8")) as Record<
string,
@@ -303,6 +308,58 @@ describe("secrets apply", () => {
expect(nextEnv).toContain("UNRELATED=value");
});
it("preserves auth-profile tokenRef during provider scrub", async () => {
await writeJsonFile(fixture.authStorePath, {
version: 1,
profiles: {
"openai:bot": {
type: "token",
provider: "openai",
token: "sk-token-plaintext", // pragma: allowlist secret
tokenRef: OPENAI_API_KEY_ENV_REF,
},
},
});
const plan = createPlan({
targets: [createOpenAiProviderTarget()],
options: createOneWayScrubOptions(),
});
await runSecretsApply({ plan, env: fixture.env, write: true });
const nextAuthStore = JSON.parse(await fs.readFile(fixture.authStorePath, "utf8")) as {
profiles: { "openai:bot": { token?: string; tokenRef?: unknown } };
};
expect(nextAuthStore.profiles["openai:bot"].token).toBeUndefined();
expect(nextAuthStore.profiles["openai:bot"].tokenRef).toEqual(OPENAI_API_KEY_ENV_REF);
});
it("scrubs malformed auth-profile ref residue during provider scrub", async () => {
await writeJsonFile(fixture.authStorePath, {
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-openai-plaintext", // pragma: allowlist secret
keyRef: "secretref-managed", // pragma: allowlist secret
},
},
});
const plan = createPlan({
targets: [createOpenAiProviderTarget()],
options: createOneWayScrubOptions(),
});
await runSecretsApply({ plan, env: fixture.env, write: true });
const nextAuthStore = JSON.parse(await fs.readFile(fixture.authStorePath, "utf8")) as {
profiles: { "openai:default": { key?: string; keyRef?: unknown } };
};
expect(nextAuthStore.profiles["openai:default"].key).toBeUndefined();
expect(nextAuthStore.profiles["openai:default"].keyRef).toBeUndefined();
});
it("skips exec SecretRef checks during dry-run unless explicitly allowed", async () => {
if (process.platform === "win32") {
return;

View File

@@ -15,7 +15,7 @@ import {
type OpenClawConfig,
} from "../config/config.js";
import type { ConfigWriteOptions } from "../config/io.js";
import type { SecretProviderConfig } from "../config/types.secrets.js";
import { coerceSecretRef, type SecretProviderConfig } from "../config/types.secrets.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { resolveConfigDir, resolveUserPath } from "../utils.js";
import { iterateAuthProfileCredentials } from "./auth-profiles-scan.js";
@@ -411,7 +411,10 @@ function scrubAuthStoresForProviderTargets(params: {
delete profile.profile[profile.valueField];
mutated = true;
}
if (profile.refField in profile.profile) {
if (
profile.refField in profile.profile &&
coerceSecretRef(profile.refValue, params.nextConfig.secrets?.defaults) === null
) {
delete profile.profile[profile.refField];
mutated = true;
}