mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:48:28 +00:00
feat(secrets): finalize external secrets runtime and migration hardening
This commit is contained in:
committed by
Peter Steinberger
parent
c5b89fbaea
commit
0e69660c41
@@ -150,6 +150,61 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses explicit inline env ref when secret-input-mode=ref selects existing env key", async () => {
|
||||
process.env.MINIMAX_API_KEY = "env-key";
|
||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||
|
||||
const { confirm, text } = createPromptSpies({
|
||||
confirmResult: true,
|
||||
textResult: "prompt-key",
|
||||
});
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await ensureApiKeyFromEnvOrPrompt({
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: createPrompter({ confirm, text }),
|
||||
secretInputMode: "ref",
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBe("env-key");
|
||||
expect(setCredential).toHaveBeenCalledWith("${MINIMAX_API_KEY}", "ref");
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a ref-mode note when plaintext input is provided in ref mode", async () => {
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||
|
||||
const { confirm, note, text } = createPromptSpies({
|
||||
confirmResult: false,
|
||||
textResult: " prompted-key ",
|
||||
});
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await ensureApiKeyFromEnvOrPrompt({
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: createPrompter({ confirm, note, text }),
|
||||
secretInputMode: "ref",
|
||||
setCredential,
|
||||
});
|
||||
|
||||
expect(result).toBe("prompted-key");
|
||||
expect(setCredential).toHaveBeenCalledWith("prompted-key", "ref");
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
expect.stringContaining("secret-input-mode=ref stores an env reference"),
|
||||
"Ref mode note",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureApiKeyFromOptionEnvOrPrompt", () => {
|
||||
|
||||
@@ -5,6 +5,14 @@ import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import type { SecretInputMode } from "./onboard-types.js";
|
||||
|
||||
const INLINE_ENV_REF_RE = /^\$\{([A-Z][A-Z0-9_]*)\}$/;
|
||||
const ENV_SOURCE_LABEL_RE = /(?:^|:\s)([A-Z][A-Z0-9_]*)$/;
|
||||
|
||||
function extractEnvVarFromSourceLabel(source: string): string | undefined {
|
||||
const match = ENV_SOURCE_LABEL_RE.exec(source.trim());
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
export function createAuthChoiceAgentModelNoter(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): (model: string) => Promise<void> {
|
||||
@@ -205,7 +213,14 @@ export async function ensureApiKeyFromEnvOrPrompt(params: {
|
||||
prompter: params.prompter,
|
||||
explicitMode: params.secretInputMode,
|
||||
});
|
||||
await params.setCredential(envKey.apiKey, mode);
|
||||
const explicitEnvRef =
|
||||
mode === "ref"
|
||||
? (() => {
|
||||
const envVar = extractEnvVarFromSourceLabel(envKey.source);
|
||||
return envVar ? `\${${envVar}}` : envKey.apiKey;
|
||||
})()
|
||||
: envKey.apiKey;
|
||||
await params.setCredential(explicitEnvRef, mode);
|
||||
return envKey.apiKey;
|
||||
}
|
||||
}
|
||||
@@ -215,6 +230,12 @@ export async function ensureApiKeyFromEnvOrPrompt(params: {
|
||||
validate: params.validate,
|
||||
});
|
||||
const apiKey = params.normalize(String(key ?? ""));
|
||||
if (params.secretInputMode === "ref" && !INLINE_ENV_REF_RE.test(apiKey)) {
|
||||
await params.prompter.note(
|
||||
"secret-input-mode=ref stores an env reference, not plaintext key input. Enter ${ENV_VAR} to target a specific variable, or keep current input to use the provider default env var.",
|
||||
"Ref mode note",
|
||||
);
|
||||
}
|
||||
await params.setCredential(apiKey, params.secretInputMode);
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
@@ -229,6 +229,35 @@ describe("modelsStatusCommand auth overview", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not emit raw short api-key values in JSON labels", async () => {
|
||||
const localRuntime = createRuntime();
|
||||
const shortSecret = "abc123";
|
||||
const originalProfiles = { ...mocks.store.profiles };
|
||||
mocks.store.profiles = {
|
||||
...mocks.store.profiles,
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: shortSecret,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await modelsStatusCommand({ json: true }, localRuntime as never);
|
||||
const payload = JSON.parse(String((localRuntime.log as Mock).mock.calls[0]?.[0]));
|
||||
const providers = payload.auth.providers as Array<{
|
||||
provider: string;
|
||||
profiles: { labels: string[] };
|
||||
}>;
|
||||
const openai = providers.find((p) => p.provider === "openai");
|
||||
const labels = openai?.profiles.labels ?? [];
|
||||
expect(labels.join(" ")).toContain("...");
|
||||
expect(labels.join(" ")).not.toContain(shortSecret);
|
||||
} finally {
|
||||
mocks.store.profiles = originalProfiles;
|
||||
}
|
||||
});
|
||||
|
||||
it("uses agent overrides and reports sources", async () => {
|
||||
const localRuntime = createRuntime();
|
||||
await withAgentScopeOverrides(
|
||||
|
||||
Reference in New Issue
Block a user