mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(gateway): iOS Talk treats SecretRef-backed API keys as missing (#98210)
* fix(gateway): resolve Talk SecretRefs for scoped native clients * fix(gateway): constrain Talk secret materialization * fix(gateway): redact Talk source provider secrets * fix(gateway): satisfy Talk config lint * docs(gateway): clarify Talk secret config payload --------- Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
@@ -243,6 +243,9 @@ Common scopes:
|
||||
|
||||
`talk.config` with `includeSecrets: true` requires `operator.talk.secrets`
|
||||
(or `operator.admin`).
|
||||
When secrets are included, clients should read the active Talk provider
|
||||
credential from `talk.resolved.config.apiKey`; `talk.providers.<id>.apiKey`
|
||||
stays source-shaped and may be a SecretRef object or a redacted string.
|
||||
|
||||
Plugin-registered gateway RPC methods may request their own operator scope, but
|
||||
reserved core admin prefixes (`config.*`, `exec.approvals.*`, `wizard.*`,
|
||||
|
||||
@@ -516,6 +516,326 @@ describe("talk.config handler", () => {
|
||||
expectRecordFields(resolved, { provider: "acme" });
|
||||
expectRecordFields(resolved?.config, { apiKey: "__OPENCLAW_REDACTED__" });
|
||||
});
|
||||
|
||||
it("returns runtime-resolved Talk provider SecretRefs to authorized clients", async () => {
|
||||
const sourceConfig = createTalkConfig({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ACME_SPEECH_API_KEY",
|
||||
});
|
||||
const runtimeConfig = createTalkConfig("runtime-resolved-talk-key");
|
||||
|
||||
mocks.getSpeechProvider.mockReturnValue(undefined);
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/openclaw.json",
|
||||
hash: "test-hash",
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await talkHandlers["talk.config"]({
|
||||
req: { type: "req", id: "1", method: "talk.config" },
|
||||
params: { includeSecrets: true },
|
||||
client: { connect: { scopes: ["operator.talk.secrets"] } } as never,
|
||||
isWebchatConnect: () => false,
|
||||
respond: respond as never,
|
||||
context: { getRuntimeConfig: () => runtimeConfig } as never,
|
||||
});
|
||||
|
||||
const response = expectRespondOk(respond) as { config?: { talk?: Record<string, unknown> } };
|
||||
const talkConfig = response.config?.talk;
|
||||
expectRecordFields(talkConfig, { provider: "acme" });
|
||||
const providers = talkConfig?.providers as Record<string, unknown> | undefined;
|
||||
const providerConfig = expectRecordFields(providers?.acme, { voiceId: "stub-default-voice" });
|
||||
expectRecordFields(providerConfig.apiKey, {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ACME_SPEECH_API_KEY",
|
||||
});
|
||||
const resolved = talkConfig?.resolved as Record<string, unknown> | undefined;
|
||||
expectRecordFields(resolved, { provider: "acme" });
|
||||
expectRecordFields(resolved?.config, { apiKey: "runtime-resolved-talk-key" });
|
||||
});
|
||||
|
||||
it("materializes only the active Talk provider apiKey for authorized clients", async () => {
|
||||
const sourceConfig = {
|
||||
talk: {
|
||||
provider: "acme",
|
||||
providers: {
|
||||
acme: {
|
||||
apiKey: { source: "env", provider: "default", id: "ACME_SPEECH_API_KEY" },
|
||||
voiceId: "active-voice",
|
||||
},
|
||||
other: {
|
||||
apiKey: { source: "env", provider: "default", id: "OTHER_SPEECH_API_KEY" },
|
||||
voiceId: "inactive-voice",
|
||||
},
|
||||
},
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_REALTIME_API_KEY" },
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const runtimeConfig = {
|
||||
talk: {
|
||||
provider: "acme",
|
||||
providers: {
|
||||
acme: {
|
||||
apiKey: "runtime-active-talk-key",
|
||||
voiceId: "active-voice",
|
||||
},
|
||||
other: {
|
||||
apiKey: "runtime-inactive-talk-key",
|
||||
voiceId: "inactive-voice",
|
||||
},
|
||||
},
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "runtime-realtime-key",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
mocks.getSpeechProvider.mockReturnValue(undefined);
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/openclaw.json",
|
||||
hash: "test-hash",
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await talkHandlers["talk.config"]({
|
||||
req: { type: "req", id: "1", method: "talk.config" },
|
||||
params: { includeSecrets: true },
|
||||
client: { connect: { scopes: ["operator.talk.secrets"] } } as never,
|
||||
isWebchatConnect: () => false,
|
||||
respond: respond as never,
|
||||
context: { getRuntimeConfig: () => runtimeConfig } as never,
|
||||
});
|
||||
|
||||
const response = expectRespondOk(respond) as { config?: { talk?: Record<string, unknown> } };
|
||||
const talkConfig = response.config?.talk;
|
||||
const providers = talkConfig?.providers as Record<string, unknown> | undefined;
|
||||
expectRecordFields((providers?.acme as Record<string, unknown> | undefined)?.apiKey, {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ACME_SPEECH_API_KEY",
|
||||
});
|
||||
expectRecordFields((providers?.other as Record<string, unknown> | undefined)?.apiKey, {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OTHER_SPEECH_API_KEY",
|
||||
});
|
||||
const realtime = talkConfig?.realtime as Record<string, unknown> | undefined;
|
||||
const realtimeProviders = realtime?.providers as Record<string, unknown> | undefined;
|
||||
expectRecordFields((realtimeProviders?.openai as Record<string, unknown> | undefined)?.apiKey, {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_REALTIME_API_KEY",
|
||||
});
|
||||
const resolved = talkConfig?.resolved as Record<string, unknown> | undefined;
|
||||
expectRecordFields(resolved, { provider: "acme" });
|
||||
expectRecordFields(resolved?.config, { apiKey: "runtime-active-talk-key" });
|
||||
|
||||
const serialized = JSON.stringify(response);
|
||||
expect(serialized).toContain("runtime-active-talk-key");
|
||||
expect(serialized).not.toContain("runtime-inactive-talk-key");
|
||||
expect(serialized).not.toContain("runtime-realtime-key");
|
||||
});
|
||||
|
||||
it("does not expose resolver-returned secret-like fields beyond apiKey", async () => {
|
||||
const sourceConfig = createTalkConfig({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ACME_SPEECH_API_KEY",
|
||||
});
|
||||
const runtimeConfig = createTalkConfig("runtime-resolved-talk-key");
|
||||
|
||||
mocks.getSpeechProvider.mockReturnValue({
|
||||
id: "acme",
|
||||
label: "Acme Speech",
|
||||
resolveTalkConfig: ({
|
||||
talkProviderConfig,
|
||||
}: {
|
||||
talkProviderConfig: Record<string, unknown>;
|
||||
}) => ({
|
||||
...talkProviderConfig,
|
||||
voiceId: "resolver-voice",
|
||||
clientSecret: "resolver-client-secret",
|
||||
authToken: "resolver-auth-token",
|
||||
}),
|
||||
});
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/openclaw.json",
|
||||
hash: "test-hash",
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await talkHandlers["talk.config"]({
|
||||
req: { type: "req", id: "1", method: "talk.config" },
|
||||
params: { includeSecrets: true },
|
||||
client: { connect: { scopes: ["operator.talk.secrets"] } } as never,
|
||||
isWebchatConnect: () => false,
|
||||
respond: respond as never,
|
||||
context: { getRuntimeConfig: () => runtimeConfig } as never,
|
||||
});
|
||||
|
||||
const response = expectRespondOk(respond) as { config?: { talk?: Record<string, unknown> } };
|
||||
const resolved = response.config?.talk?.resolved as Record<string, unknown> | undefined;
|
||||
expectRecordFields(resolved?.config, {
|
||||
apiKey: "runtime-resolved-talk-key",
|
||||
voiceId: "resolver-voice",
|
||||
clientSecret: "__OPENCLAW_REDACTED__",
|
||||
authToken: "__OPENCLAW_REDACTED__",
|
||||
});
|
||||
const serialized = JSON.stringify(response);
|
||||
expect(serialized).not.toContain("resolver-client-secret");
|
||||
expect(serialized).not.toContain("resolver-auth-token");
|
||||
});
|
||||
|
||||
it("does not expose source provider raw keys or secret-like sibling fields", async () => {
|
||||
const sourceConfig = {
|
||||
talk: {
|
||||
provider: "acme",
|
||||
providers: {
|
||||
acme: {
|
||||
apiKey: "source-active-talk-key",
|
||||
voiceId: "active-voice",
|
||||
clientSecret: "source-client-secret",
|
||||
},
|
||||
other: {
|
||||
apiKey: "source-inactive-talk-key",
|
||||
voiceId: "inactive-voice",
|
||||
},
|
||||
},
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "source-realtime-key",
|
||||
authToken: "source-realtime-auth-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const runtimeConfig = {
|
||||
talk: {
|
||||
provider: "acme",
|
||||
providers: {
|
||||
acme: {
|
||||
apiKey: "runtime-active-talk-key",
|
||||
voiceId: "active-voice",
|
||||
clientSecret: "runtime-client-secret",
|
||||
},
|
||||
other: {
|
||||
apiKey: "runtime-inactive-talk-key",
|
||||
voiceId: "inactive-voice",
|
||||
},
|
||||
},
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "runtime-realtime-key",
|
||||
authToken: "runtime-realtime-auth-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
mocks.getSpeechProvider.mockReturnValue(undefined);
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/openclaw.json",
|
||||
hash: "test-hash",
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await talkHandlers["talk.config"]({
|
||||
req: { type: "req", id: "1", method: "talk.config" },
|
||||
params: { includeSecrets: true },
|
||||
client: { connect: { scopes: ["operator.talk.secrets"] } } as never,
|
||||
isWebchatConnect: () => false,
|
||||
respond: respond as never,
|
||||
context: { getRuntimeConfig: () => runtimeConfig } as never,
|
||||
});
|
||||
|
||||
const response = expectRespondOk(respond) as { config?: { talk?: Record<string, unknown> } };
|
||||
const resolved = response.config?.talk?.resolved as Record<string, unknown> | undefined;
|
||||
expectRecordFields(resolved?.config, {
|
||||
apiKey: "runtime-active-talk-key",
|
||||
clientSecret: "__OPENCLAW_REDACTED__",
|
||||
});
|
||||
const serialized = JSON.stringify(response);
|
||||
expect(serialized).toContain("runtime-active-talk-key");
|
||||
expect(serialized).not.toContain("source-active-talk-key");
|
||||
expect(serialized).not.toContain("source-inactive-talk-key");
|
||||
expect(serialized).not.toContain("source-realtime-key");
|
||||
expect(serialized).not.toContain("source-client-secret");
|
||||
expect(serialized).not.toContain("source-realtime-auth-token");
|
||||
expect(serialized).not.toContain("runtime-inactive-talk-key");
|
||||
expect(serialized).not.toContain("runtime-realtime-key");
|
||||
expect(serialized).not.toContain("runtime-client-secret");
|
||||
expect(serialized).not.toContain("runtime-realtime-auth-token");
|
||||
});
|
||||
|
||||
it("redacts runtime-resolved Talk provider SecretRefs without Talk secret scope", async () => {
|
||||
const sourceConfig = createTalkConfig({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ACME_SPEECH_API_KEY",
|
||||
});
|
||||
const runtimeConfig = createTalkConfig("runtime-resolved-talk-key");
|
||||
|
||||
mocks.getSpeechProvider.mockReturnValue(undefined);
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/openclaw.json",
|
||||
hash: "test-hash",
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await talkHandlers["talk.config"]({
|
||||
req: { type: "req", id: "1", method: "talk.config" },
|
||||
params: {},
|
||||
client: { connect: { scopes: ["operator.read"] } } as never,
|
||||
isWebchatConnect: () => false,
|
||||
respond: respond as never,
|
||||
context: { getRuntimeConfig: () => runtimeConfig } as never,
|
||||
});
|
||||
|
||||
const response = expectRespondOk(respond) as { config?: { talk?: Record<string, unknown> } };
|
||||
const resolved = response.config?.talk?.resolved as Record<string, unknown> | undefined;
|
||||
expectRecordFields(resolved, { provider: "acme" });
|
||||
const resolvedConfig = expectRecordFields(resolved?.config, {});
|
||||
expectRecordFields(resolvedConfig.apiKey, {
|
||||
source: "__OPENCLAW_REDACTED__",
|
||||
provider: "__OPENCLAW_REDACTED__",
|
||||
id: "__OPENCLAW_REDACTED__",
|
||||
});
|
||||
const serialized = JSON.stringify(response);
|
||||
expect(serialized).not.toContain("runtime-resolved-talk-key");
|
||||
expect(serialized).not.toContain("ACME_SPEECH_API_KEY");
|
||||
});
|
||||
});
|
||||
|
||||
describe("talk.session unified handlers", () => {
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
normalizeTalkSection,
|
||||
resolveActiveTalkProviderConfig,
|
||||
} from "../../config/talk.js";
|
||||
import type { TalkConfigResponse, TalkProviderConfig } from "../../config/types.gateway.js";
|
||||
import type {
|
||||
TalkConfigResponse,
|
||||
TalkProviderConfig,
|
||||
TalkRealtimeConfig,
|
||||
} from "../../config/types.gateway.js";
|
||||
import type { OpenClawConfig, TtsConfig, TtsProviderConfigMap } from "../../config/types.js";
|
||||
import { listRealtimeTranscriptionProviders } from "../../realtime-transcription/provider-registry.js";
|
||||
import {
|
||||
@@ -45,6 +49,7 @@ import {
|
||||
type TtsDirectiveOverrides,
|
||||
} from "../../tts/tts.js";
|
||||
import { ADMIN_SCOPE, TALK_SECRETS_SCOPE } from "../operator-scopes.js";
|
||||
import { resolveConfiguredSecretInputString } from "../resolve-configured-secret-input-string.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import { talkClientHandlers } from "./talk-client.js";
|
||||
import { talkSessionHandlers } from "./talk-session.js";
|
||||
@@ -396,24 +401,23 @@ function inferMimeType(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveTalkResponseFromConfig(params: {
|
||||
async function resolveTalkResponseFromConfig(params: {
|
||||
includeSecrets: boolean;
|
||||
sourceConfig: OpenClawConfig;
|
||||
runtimeConfig: OpenClawConfig;
|
||||
}): TalkConfigResponse | undefined {
|
||||
}): Promise<TalkConfigResponse | undefined> {
|
||||
const normalizedTalk = normalizeTalkSection(params.sourceConfig.talk);
|
||||
if (!normalizedTalk) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payload = buildTalkConfigResponse(normalizedTalk);
|
||||
if (!payload) {
|
||||
const sourcePayload = buildTalkConfigResponse(normalizedTalk);
|
||||
if (!sourcePayload) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (params.includeSecrets) {
|
||||
return payload;
|
||||
}
|
||||
const payload = params.includeSecrets
|
||||
? projectTalkSourcePayloadForSecrets(sourcePayload)
|
||||
: sourcePayload;
|
||||
|
||||
const sourceResolved = resolveActiveTalkProviderConfig(normalizedTalk);
|
||||
const runtimeResolved = resolveActiveTalkProviderConfig(params.runtimeConfig.talk);
|
||||
@@ -436,15 +440,18 @@ function resolveTalkResponseFromConfig(params: {
|
||||
Object.keys(runtimeBaseTts).length > 0
|
||||
? runtimeBaseTts
|
||||
: stripUnresolvedSecretApiKeysFromBaseTtsProviders(sourceBaseTts);
|
||||
// Prefer runtime-resolved provider config (already-substituted secrets) and
|
||||
// fall back to source. Strip any apiKey that is still a SecretRef wrapper —
|
||||
// provider plugins (ElevenLabs/OpenAI) call strict secret helpers that throw
|
||||
// on unresolved wrappers, and the discovery path doesn't need the resolved
|
||||
// value: the response's apiKey is restored from source so the UI keeps the
|
||||
// SecretRef shape, and redaction strips the value when includeSecrets=false.
|
||||
const providerInputConfig = stripUnresolvedSecretApiKey(
|
||||
Object.keys(runtimeProviderConfig).length > 0 ? runtimeProviderConfig : sourceProviderConfig,
|
||||
);
|
||||
// Prefer runtime-resolved provider config and fall back to source. Provider
|
||||
// plugins (ElevenLabs/OpenAI) call strict secret helpers that throw on
|
||||
// unresolved wrappers, so only the already-authorized includeSecrets path may
|
||||
// materialize SecretRef apiKey values before provider resolution. Read-scope
|
||||
// calls keep the old strip/redact behavior.
|
||||
const providerInputConfig = await resolveTalkProviderInputConfig({
|
||||
includeSecrets: params.includeSecrets,
|
||||
config: params.runtimeConfig,
|
||||
providerConfig:
|
||||
Object.keys(runtimeProviderConfig).length > 0 ? runtimeProviderConfig : sourceProviderConfig,
|
||||
provider,
|
||||
});
|
||||
const resolvedConfig =
|
||||
speechProvider?.resolveTalkConfig?.({
|
||||
cfg: params.runtimeConfig,
|
||||
@@ -452,10 +459,11 @@ function resolveTalkResponseFromConfig(params: {
|
||||
talkProviderConfig: providerInputConfig,
|
||||
timeoutMs: typeof selectedBaseTts.timeoutMs === "number" ? selectedBaseTts.timeoutMs : 30_000,
|
||||
}) ?? providerInputConfig;
|
||||
const responseConfig =
|
||||
sourceProviderConfig.apiKey === undefined
|
||||
? resolvedConfig
|
||||
: { ...resolvedConfig, apiKey: sourceProviderConfig.apiKey };
|
||||
const responseConfig = projectTalkResolvedProviderConfig({
|
||||
includeSecrets: params.includeSecrets,
|
||||
sourceProviderConfig,
|
||||
resolvedConfig,
|
||||
});
|
||||
|
||||
return {
|
||||
...payload,
|
||||
@@ -467,6 +475,86 @@ function resolveTalkResponseFromConfig(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function projectTalkResolvedProviderConfig(params: {
|
||||
includeSecrets: boolean;
|
||||
sourceProviderConfig: TalkProviderConfig;
|
||||
resolvedConfig: TalkProviderConfig;
|
||||
}): TalkProviderConfig {
|
||||
if (!params.includeSecrets) {
|
||||
return params.sourceProviderConfig.apiKey === undefined
|
||||
? params.resolvedConfig
|
||||
: { ...params.resolvedConfig, apiKey: params.sourceProviderConfig.apiKey };
|
||||
}
|
||||
|
||||
// includeSecrets authorizes the active Talk provider key only. Keep resolver
|
||||
// defaults in the resolved payload, but do not turn arbitrary provider-owned
|
||||
// secret-like fields into a new native-client credential surface.
|
||||
const projected = redactConfigObject(params.resolvedConfig);
|
||||
const apiKey = normalizeOptionalString(params.resolvedConfig.apiKey);
|
||||
return apiKey === undefined ? projected : { ...projected, apiKey };
|
||||
}
|
||||
|
||||
function projectTalkSourceProviderConfigForSecrets(config: TalkProviderConfig): TalkProviderConfig {
|
||||
const projected = redactConfigObject(config);
|
||||
if (config.apiKey === undefined || typeof config.apiKey === "string") {
|
||||
return projected;
|
||||
}
|
||||
return { ...projected, apiKey: config.apiKey };
|
||||
}
|
||||
|
||||
function projectTalkSourceProviderMapForSecrets(
|
||||
providers: Record<string, TalkProviderConfig> | undefined,
|
||||
): Record<string, TalkProviderConfig> | undefined {
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(providers).map(([providerId, providerConfig]) => [
|
||||
providerId,
|
||||
projectTalkSourceProviderConfigForSecrets(providerConfig),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function projectTalkRealtimeForSecrets(realtime: TalkRealtimeConfig): TalkRealtimeConfig {
|
||||
const projected = redactConfigObject(realtime);
|
||||
const providers = projectTalkSourceProviderMapForSecrets(realtime.providers);
|
||||
return providers ? { ...projected, providers } : projected;
|
||||
}
|
||||
|
||||
function projectTalkSourcePayloadForSecrets(payload: TalkConfigResponse): TalkConfigResponse {
|
||||
const projected = redactConfigObject(payload);
|
||||
const providers = projectTalkSourceProviderMapForSecrets(payload.providers);
|
||||
if (providers) {
|
||||
projected.providers = providers;
|
||||
}
|
||||
if (payload.realtime) {
|
||||
projected.realtime = projectTalkRealtimeForSecrets(payload.realtime);
|
||||
}
|
||||
return projected;
|
||||
}
|
||||
|
||||
async function resolveTalkProviderInputConfig(params: {
|
||||
includeSecrets: boolean;
|
||||
config: OpenClawConfig;
|
||||
providerConfig: TalkProviderConfig;
|
||||
provider: string;
|
||||
}): Promise<TalkProviderConfig> {
|
||||
const strippedConfig = stripUnresolvedSecretApiKey(params.providerConfig);
|
||||
if (!params.includeSecrets || params.providerConfig.apiKey === undefined) {
|
||||
return strippedConfig;
|
||||
}
|
||||
const resolved = await resolveConfiguredSecretInputString({
|
||||
config: params.config,
|
||||
env: process.env,
|
||||
value: params.providerConfig.apiKey,
|
||||
path: `talk.providers.${params.provider}.apiKey`,
|
||||
});
|
||||
return resolved.value === undefined
|
||||
? strippedConfig
|
||||
: { ...params.providerConfig, apiKey: resolved.value };
|
||||
}
|
||||
|
||||
function stripUnresolvedSecretApiKey(config: TalkProviderConfig): TalkProviderConfig {
|
||||
return stripUnresolvedSecretApiKeyFromRecord(config) as TalkProviderConfig;
|
||||
}
|
||||
@@ -564,7 +652,7 @@ export const talkHandlers: GatewayRequestHandlers = {
|
||||
const runtimeConfig = context.getRuntimeConfig();
|
||||
const configPayload: Record<string, unknown> = {};
|
||||
|
||||
const talk = resolveTalkResponseFromConfig({
|
||||
const talk = await resolveTalkResponseFromConfig({
|
||||
includeSecrets,
|
||||
sourceConfig: snapshot.config,
|
||||
runtimeConfig,
|
||||
|
||||
@@ -302,7 +302,8 @@ describe("gateway talk.config", () => {
|
||||
expect(res.ok).toBe(true);
|
||||
expectTalkConfig(res.payload?.config?.talk, {
|
||||
provider: GENERIC_TALK_PROVIDER_ID,
|
||||
apiKey: "secret-key-abc",
|
||||
providerApiKey: "__OPENCLAW_REDACTED__",
|
||||
resolvedApiKey: "secret-key-abc",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -313,7 +314,10 @@ describe("gateway talk.config", () => {
|
||||
});
|
||||
|
||||
await withEnvAsync({ [GENERIC_TALK_API_ENV]: "env-acme-key" }, async () => {
|
||||
await expectTalkSecretsConfig({ apiKey: talkApiSecretRef() });
|
||||
await expectTalkSecretsConfig({
|
||||
providerApiKey: talkApiSecretRef(),
|
||||
resolvedApiKey: "env-acme-key",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -402,7 +406,8 @@ describe("gateway talk.config", () => {
|
||||
|
||||
await expectTalkSecretsConfig({
|
||||
voiceId: "voice-secretref",
|
||||
apiKey: talkApiSecretRef(),
|
||||
providerApiKey: talkApiSecretRef(),
|
||||
resolvedApiKey: "env-acme-key",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user