mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(secrets): normalize inline SecretRef token/key to tokenRef/keyRef in runtime snapshot (#31047)
* fix(secrets): normalize inline SecretRef token/key to tokenRef/keyRef in runtime snapshot
When auth-profiles.json uses an inline SecretRef as the token or key
value directly (e.g. `"token": {"source":"file",...}`), the resolved
plaintext was written back to disk on every updateAuthProfileStoreWithLock
call, overwriting the SecretRef.
Root cause: collectTokenProfileAssignment and collectApiKeyProfileAssignment
detected inline SecretRefs but did not promote them to the canonical
tokenRef/keyRef fields. saveAuthProfileStore only strips plaintext when
tokenRef/keyRef is set, so the inline case fell through and persisted
plaintext on every save.
Fix: when an inline SecretRef is detected and no explicit tokenRef/keyRef
exists, promote it to the canonical field and delete the inline form.
saveAuthProfileStore then correctly strips the resolved plaintext on write.
Fixes #29108
* fix test: cast inline SecretRef loadAuthStore mocks to AuthProfileStore
* fix(secrets): fix TypeScript type error in runtime test loadAuthStore lambda
* test(secrets): keep explicit keyRef precedence over inline key ref
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { loadConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
@@ -83,6 +83,102 @@ describe("secrets runtime snapshot", () => {
|
||||
type: "api_key",
|
||||
key: "sk-env-openai",
|
||||
});
|
||||
// After normalization, inline SecretRef string should be promoted to keyRef
|
||||
expect(
|
||||
(snapshot.authStores[0].store.profiles["openai:inline"] as Record<string, unknown>).keyRef,
|
||||
).toEqual({ source: "env", provider: "default", id: "OPENAI_API_KEY" });
|
||||
});
|
||||
|
||||
it("normalizes inline SecretRef object on token to tokenRef", async () => {
|
||||
const config: OpenClawConfig = { models: {}, secrets: {} };
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config,
|
||||
env: { MY_TOKEN: "resolved-token-value" },
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: ((_agentDir?: string) =>
|
||||
({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"custom:inline-token": {
|
||||
type: "token",
|
||||
provider: "custom",
|
||||
token: { source: "env", provider: "default", id: "MY_TOKEN" },
|
||||
},
|
||||
},
|
||||
}) as unknown as AuthProfileStore) as (agentDir?: string) => AuthProfileStore,
|
||||
});
|
||||
|
||||
const profile = snapshot.authStores[0]?.store.profiles["custom:inline-token"] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
// tokenRef should be set from the inline SecretRef
|
||||
expect(profile.tokenRef).toEqual({ source: "env", provider: "default", id: "MY_TOKEN" });
|
||||
// token should be resolved to the actual value after activation
|
||||
activateSecretsRuntimeSnapshot(snapshot);
|
||||
expect(profile.token).toBe("resolved-token-value");
|
||||
});
|
||||
|
||||
it("normalizes inline SecretRef object on key to keyRef", async () => {
|
||||
const config: OpenClawConfig = { models: {}, secrets: {} };
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config,
|
||||
env: { MY_KEY: "resolved-key-value" },
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: ((_agentDir?: string) =>
|
||||
({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"custom:inline-key": {
|
||||
type: "api_key",
|
||||
provider: "custom",
|
||||
key: { source: "env", provider: "default", id: "MY_KEY" },
|
||||
},
|
||||
},
|
||||
}) as unknown as AuthProfileStore) as (agentDir?: string) => AuthProfileStore,
|
||||
});
|
||||
|
||||
const profile = snapshot.authStores[0]?.store.profiles["custom:inline-key"] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
// keyRef should be set from the inline SecretRef
|
||||
expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "MY_KEY" });
|
||||
// key should be resolved to the actual value after activation
|
||||
activateSecretsRuntimeSnapshot(snapshot);
|
||||
expect(profile.key).toBe("resolved-key-value");
|
||||
});
|
||||
|
||||
it("keeps explicit keyRef when inline key SecretRef is also present", async () => {
|
||||
const config: OpenClawConfig = { models: {}, secrets: {} };
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config,
|
||||
env: {
|
||||
PRIMARY_KEY: "primary-key-value",
|
||||
SHADOW_KEY: "shadow-key-value",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () =>
|
||||
({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"custom:explicit-keyref": {
|
||||
type: "api_key",
|
||||
provider: "custom",
|
||||
keyRef: { source: "env", provider: "default", id: "PRIMARY_KEY" },
|
||||
key: { source: "env", provider: "default", id: "SHADOW_KEY" },
|
||||
},
|
||||
},
|
||||
}) as AuthProfileStore,
|
||||
});
|
||||
|
||||
const profile = snapshot.authStores[0]?.store.profiles["custom:explicit-keyref"] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "PRIMARY_KEY" });
|
||||
activateSecretsRuntimeSnapshot(snapshot);
|
||||
expect(profile.key).toBe("primary-key-value");
|
||||
});
|
||||
|
||||
it("resolves file refs via configured file provider", async () => {
|
||||
|
||||
@@ -241,6 +241,10 @@ function collectApiKeyProfileAssignment(params: {
|
||||
if (!resolvedKeyRef) {
|
||||
return;
|
||||
}
|
||||
if (inlineKeyRef && !keyRef) {
|
||||
params.profile.keyRef = inlineKeyRef;
|
||||
delete (params.profile as unknown as Record<string, unknown>).key;
|
||||
}
|
||||
if (keyRef && isNonEmptyString(params.profile.key)) {
|
||||
params.context.warnings.push({
|
||||
code: "SECRETS_REF_OVERRIDES_PLAINTEXT",
|
||||
@@ -271,6 +275,10 @@ function collectTokenProfileAssignment(params: {
|
||||
if (!resolvedTokenRef) {
|
||||
return;
|
||||
}
|
||||
if (inlineTokenRef && !tokenRef) {
|
||||
params.profile.tokenRef = inlineTokenRef;
|
||||
delete (params.profile as unknown as Record<string, unknown>).token;
|
||||
}
|
||||
if (tokenRef && isNonEmptyString(params.profile.token)) {
|
||||
params.context.warnings.push({
|
||||
code: "SECRETS_REF_OVERRIDES_PLAINTEXT",
|
||||
|
||||
Reference in New Issue
Block a user