Skip to content

Commit ebaecbb

Browse files
committed
fix talk secretrefs in config payload
1 parent 66b91d7 commit ebaecbb

3 files changed

Lines changed: 121 additions & 20 deletions

File tree

apps/ios/Tests/TalkModeConfigParsingTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,45 @@ import Testing
252252
#expect(parsed.rawConfigApiKey == "__OPENCLAW_REDACTED__")
253253
}
254254

255+
@Test func parsesResolvedSecretRefApiKeyForNativeTalk() {
256+
let config: [String: Any] = [
257+
"talk": [
258+
"provider": "elevenlabs",
259+
"providers": [
260+
"elevenlabs": [
261+
"apiKey": [
262+
"source": "env",
263+
"provider": "default",
264+
"id": "ELEVENLABS_API_KEY",
265+
],
266+
"voiceId": "voice-from-source",
267+
],
268+
],
269+
"resolved": [
270+
"provider": "elevenlabs",
271+
"config": [
272+
"apiKey": "resolved-test-key", // pragma: allowlist secret
273+
"modelId": "eleven_v3",
274+
"voiceId": "voice-from-source",
275+
],
276+
],
277+
],
278+
]
279+
280+
let parsed = TalkModeGatewayConfigParser.parse(
281+
config: config,
282+
defaultProvider: "elevenlabs",
283+
defaultModelIdFallback: "eleven_v3",
284+
defaultRealtimeModelIdFallback: "gpt-realtime-2",
285+
defaultSilenceTimeoutMs: 900)
286+
287+
#expect(parsed.activeProvider == "elevenlabs")
288+
#expect(parsed.executionMode == .native)
289+
#expect(parsed.defaultModelId == "eleven_v3")
290+
#expect(parsed.defaultVoiceId == "voice-from-source")
291+
#expect(parsed.rawConfigApiKey == "resolved-test-key")
292+
}
293+
255294
@Test func leavesNativeModeForManagedRoomRealtimeTransport() {
256295
let config: [String: Any] = [
257296
"talk": [

src/gateway/server-methods/talk.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,6 @@ function resolveTalkResponseFromConfig(params: {
411411
return undefined;
412412
}
413413

414-
if (params.includeSecrets) {
415-
return payload;
416-
}
417-
418414
const sourceResolved = resolveActiveTalkProviderConfig(normalizedTalk);
419415
const runtimeResolved = resolveActiveTalkProviderConfig(params.runtimeConfig.talk);
420416
const activeProviderId = sourceResolved?.provider ?? runtimeResolved?.provider;
@@ -453,7 +449,7 @@ function resolveTalkResponseFromConfig(params: {
453449
timeoutMs: typeof selectedBaseTts.timeoutMs === "number" ? selectedBaseTts.timeoutMs : 30_000,
454450
}) ?? providerInputConfig;
455451
const responseConfig =
456-
sourceProviderConfig.apiKey === undefined
452+
params.includeSecrets || sourceProviderConfig.apiKey === undefined
457453
? resolvedConfig
458454
: { ...resolvedConfig, apiKey: sourceProviderConfig.apiKey };
459455

src/gateway/server.talk-config.test.ts

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Talk config tests cover speech-provider config resolution, secret redaction,
22
// device-authenticated access, and protocol payload validation.
3+
import fs from "node:fs";
34
import os from "node:os";
45
import path from "node:path";
56
import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -17,6 +18,7 @@ import {
1718
connectOk,
1819
createGatewaySuiteHarness,
1920
installGatewayTestHooks,
21+
onceMessage,
2022
readConnectChallengeNonce,
2123
rpcReq,
2224
} from "./test-helpers.js";
@@ -62,8 +64,9 @@ afterAll(async () => {
6264
});
6365

6466
async function createFreshOperatorDevice(scopes: string[], nonce: string) {
67+
const identityRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-talk-config-device-"));
6568
const identity = loadOrCreateDeviceIdentity(
66-
path.join(os.tmpdir(), `openclaw-talk-config-device-${process.pid}-${talkConfigDeviceSeq++}`),
69+
path.join(identityRoot, `${process.pid}-${talkConfigDeviceSeq++}`),
6770
);
6871
const signedAtMs = Date.now();
6972
const payload = buildDeviceAuthPayload({
@@ -129,6 +132,32 @@ async function fetchTalkConfig(
129132
return rpcReq<TalkConfigPayload>(ws, "talk.config", params ?? {}, 60_000);
130133
}
131134

135+
async function fetchTalkConfigWithRuntimeSnapshot(
136+
ws: GatewaySocket,
137+
params?: { includeSecrets?: boolean } | Record<string, unknown>,
138+
) {
139+
const { randomUUID } = await import("node:crypto");
140+
const id = randomUUID();
141+
const responsePromise = onceMessage<{
142+
type: "res";
143+
id: string;
144+
ok: boolean;
145+
payload?: TalkConfigPayload | null | undefined;
146+
error?: { message?: string; code?: string };
147+
}>(
148+
ws,
149+
(obj) => {
150+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
151+
return false;
152+
}
153+
return obj.type === "res" && obj.id === id;
154+
},
155+
60_000,
156+
);
157+
ws.send(JSON.stringify({ type: "req", id, method: "talk.config", params: params ?? {} }));
158+
return await responsePromise;
159+
}
160+
132161
async function fetchOkTalkConfig(
133162
ws: GatewaySocket,
134163
params?: { includeSecrets?: boolean } | Record<string, unknown>,
@@ -138,6 +167,15 @@ async function fetchOkTalkConfig(
138167
return res;
139168
}
140169

170+
async function fetchOkTalkConfigWithRuntimeSnapshot(
171+
ws: GatewaySocket,
172+
params?: { includeSecrets?: boolean } | Record<string, unknown>,
173+
) {
174+
const res = await fetchTalkConfigWithRuntimeSnapshot(ws, params);
175+
expect(res.ok, JSON.stringify(res.error)).toBe(true);
176+
return res;
177+
}
178+
141179
async function withTalkConfigConnection<T>(
142180
scopes: string[],
143181
run: (ws: GatewaySocket) => Promise<T>,
@@ -159,6 +197,25 @@ function talkApiSecretRef() {
159197
} satisfies SecretRef;
160198
}
161199

200+
async function activateCurrentTalkSecretsRuntimeSnapshot() {
201+
const [
202+
{ readConfigFileSnapshot },
203+
{ activateSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot },
204+
] = await Promise.all([import("../config/config.js"), import("../secrets/runtime.js")]);
205+
const snapshot = await readConfigFileSnapshot();
206+
const prepared = await prepareSecretsRuntimeSnapshot({
207+
config: snapshot.sourceConfig,
208+
includeAuthStoreRefs: false,
209+
loadablePluginOrigins: new Map(),
210+
});
211+
activateSecretsRuntimeSnapshot(prepared);
212+
}
213+
214+
async function clearTalkSecretsRuntimeSnapshot() {
215+
const { clearSecretsRuntimeSnapshot } = await import("../secrets/runtime.js");
216+
clearSecretsRuntimeSnapshot();
217+
}
218+
162219
function speechProviderFixture(params: {
163220
pluginId: string;
164221
label: string;
@@ -186,17 +243,22 @@ function speechProviderFixture(params: {
186243
async function expectTalkSecretsConfig(
187244
expected: Omit<Parameters<typeof expectTalkConfig>[1], "provider">,
188245
) {
189-
await withTalkConfigConnection(
190-
["operator.read", "operator.write", "operator.talk.secrets"],
191-
async (ws) => {
192-
const res = await fetchOkTalkConfig(ws, { includeSecrets: true });
193-
expect(validateTalkConfigResult(res.payload)).toBe(true);
194-
expectTalkConfig(res.payload?.config?.talk, {
195-
provider: GENERIC_TALK_PROVIDER_ID,
196-
...expected,
197-
});
198-
},
199-
);
246+
await activateCurrentTalkSecretsRuntimeSnapshot();
247+
try {
248+
await withTalkConfigConnection(
249+
["operator.read", "operator.write", "operator.talk.secrets"],
250+
async (ws) => {
251+
const res = await fetchOkTalkConfigWithRuntimeSnapshot(ws, { includeSecrets: true });
252+
expect(validateTalkConfigResult(res.payload)).toBe(true);
253+
expectTalkConfig(res.payload?.config?.talk, {
254+
provider: GENERIC_TALK_PROVIDER_ID,
255+
...expected,
256+
});
257+
},
258+
);
259+
} finally {
260+
await clearTalkSecretsRuntimeSnapshot();
261+
}
200262
}
201263

202264
function expectTalkConfig(
@@ -307,13 +369,16 @@ describe("gateway talk.config", () => {
307369
});
308370
});
309371

310-
it("returns Talk SecretRef payloads that satisfy the protocol schema", async () => {
372+
it("returns source SecretRef and resolved Talk secret payloads that satisfy the protocol schema", async () => {
311373
await writeTalkConfig({
312374
apiKey: talkApiSecretRef(),
313375
});
314376

315377
await withEnvAsync({ [GENERIC_TALK_API_ENV]: "env-acme-key" }, async () => {
316-
await expectTalkSecretsConfig({ apiKey: talkApiSecretRef() });
378+
await expectTalkSecretsConfig({
379+
providerApiKey: talkApiSecretRef(),
380+
resolvedApiKey: "env-acme-key",
381+
});
317382
});
318383
});
319384

@@ -402,7 +467,8 @@ describe("gateway talk.config", () => {
402467

403468
await expectTalkSecretsConfig({
404469
voiceId: "voice-secretref",
405-
apiKey: talkApiSecretRef(),
470+
providerApiKey: talkApiSecretRef(),
471+
resolvedApiKey: "env-acme-key",
406472
});
407473
},
408474
);

0 commit comments

Comments
 (0)