diff --git a/extensions/speech-core/src/tts.test.ts b/extensions/speech-core/src/tts.test.ts index 90d19a4e252..9755f554775 100644 --- a/extensions/speech-core/src/tts.test.ts +++ b/extensions/speech-core/src/tts.test.ts @@ -506,6 +506,84 @@ describe("speech-core per-agent TTS config", () => { }); }); + it("composes per-agent TTS overrides with active persona bindings", async () => { + const cfg = { + messages: { + tts: { + enabled: true, + provider: "mock", + providers: { + mock: { + model: "base-model", + voice: "base-voice", + }, + }, + persona: "alfred", + personas: { + alfred: { + provider: "mock", + providers: { + mock: { + voice: "alfred-voice", + }, + }, + }, + jarvis: { + provider: "mock", + providers: { + mock: { + style: "jarvis-style", + }, + }, + }, + }, + }, + }, + agents: { + list: [ + { + id: "reader", + tts: { + persona: "jarvis", + providers: { + mock: { + voice: "agent-voice", + }, + }, + }, + }, + ], + }, + } satisfies OpenClawConfig; + + let mediaDir: string | undefined; + try { + const result = await maybeApplyTtsToPayload({ + payload: { text: "This agent reply should use the composed persona config." }, + cfg, + channel: "slack", + kind: "final", + agentId: "reader", + }); + + expect(synthesizeMock).toHaveBeenCalledWith( + expect.objectContaining({ + providerConfig: expect.objectContaining({ + model: "base-model", + voice: "agent-voice", + style: "jarvis-style", + }), + }), + ); + expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/); + mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined; + } finally { + if (mediaDir) { + rmSync(mediaDir, { recursive: true, force: true }); + } + } + }); + it("ignores prototype-pollution keys in agent TTS overrides", () => { const cfg = { messages: { diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 5ae5837b27f..870637b1562 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -6549,6 +6549,181 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { type: "string", minLength: 1, }, + persona: { + type: "string", + }, + personas: { + type: "object", + propertyNames: { + type: "string", + }, + additionalProperties: { + type: "object", + properties: { + label: { + type: "string", + }, + description: { + type: "string", + }, + provider: { + type: "string", + minLength: 1, + }, + fallbackPolicy: { + anyOf: [ + { + type: "string", + const: "preserve-persona", + }, + { + type: "string", + const: "provider-defaults", + }, + { + type: "string", + const: "fail", + }, + ], + }, + prompt: { + type: "object", + properties: { + profile: { + type: "string", + }, + scene: { + type: "string", + }, + sampleContext: { + type: "string", + }, + style: { + type: "string", + }, + accent: { + type: "string", + }, + pacing: { + type: "string", + }, + constraints: { + type: "array", + items: { + type: "string", + }, + }, + }, + additionalProperties: false, + }, + providers: { + type: "object", + propertyNames: { + type: "string", + }, + additionalProperties: { + type: "object", + properties: { + apiKey: { + anyOf: [ + { + type: "string", + }, + { + oneOf: [ + { + type: "object", + properties: { + source: { + type: "string", + const: "env", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + pattern: "^[A-Z][A-Z0-9_]{0,127}$", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "file", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + { + type: "object", + properties: { + source: { + type: "string", + const: "exec", + }, + provider: { + type: "string", + pattern: "^[a-z][a-z0-9_-]{0,63}$", + }, + id: { + type: "string", + }, + }, + required: ["source", "provider", "id"], + additionalProperties: false, + }, + ], + }, + ], + }, + }, + additionalProperties: { + anyOf: [ + { + type: "string", + }, + { + type: "number", + }, + { + type: "boolean", + }, + { + type: "null", + }, + { + type: "array", + items: {}, + }, + { + type: "object", + propertyNames: { + type: "string", + }, + additionalProperties: {}, + }, + ], + }, + }, + }, + }, + additionalProperties: false, + }, + }, summaryModel: { type: "string", }, @@ -27972,6 +28147,10 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { sensitive: true, tags: ["security", "auth"], }, + "agents.list[].tts.personas.*.providers.*.apiKey": { + sensitive: true, + tags: ["security", "auth", "media"], + }, "agents.list[].tts.providers.*.apiKey": { sensitive: true, tags: ["security", "auth", "media"],