ElevenLabs: secure private agents (signed URL) + encrypt key at rest
Builds on the client-provided-key SaveGame work to actually secure usage: - Private agents: connect via a short-lived Signed URL fetched with the stored key (GET /v1/convai/conversation/get-signed-url), instead of a direct agent_id+header connection which ElevenLabs rejects for auth-enabled agents. Falls back to a direct connection only when no key is set (public agents). Enforces 'valid key required': no key -> no signed URL -> no conversation. - Force agent creation/update to private (platform_settings.auth.enable_auth=true) so a public, key-less agent can't be created by mistake. - Encrypt the API key at rest in the SaveGame (AES-256); plaintext kept only in the in-memory cache. - Redact signed-URL query (auth token) from logs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,17 +5,81 @@
|
|||||||
#include "PS_AI_ConvAgent_SaveGame_ElevenLabs.h"
|
#include "PS_AI_ConvAgent_SaveGame_ElevenLabs.h"
|
||||||
#include "Components/SkeletalMeshComponent.h"
|
#include "Components/SkeletalMeshComponent.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "Misc/AES.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// In-memory cache of the client API key. This BlueprintLibrary is the single
|
// In-memory cache of the client API key (PLAINTEXT). This BlueprintLibrary is
|
||||||
// owner of the key — every consumer (WebSocket auth, region probe, editor sync
|
// the single owner of the key — every consumer (WebSocket auth, region probe,
|
||||||
// tools) reads it via GetElevenLabsAPIKey(). The SaveGame on disk is the
|
// editor sync tools) reads it via GetElevenLabsAPIKey(). The SaveGame on disk
|
||||||
// persistent backing store; this cache avoids a disk read on every access.
|
// is the persistent backing store (AES-encrypted); this cache avoids a disk
|
||||||
// All access is on the game thread, so no synchronization is needed.
|
// read + decrypt on every access. All access is on the game thread, so no
|
||||||
|
// synchronization is needed.
|
||||||
bool GElevenLabsKeyLoaded = false;
|
bool GElevenLabsKeyLoaded = false;
|
||||||
FString GElevenLabsKeyCache;
|
FString GElevenLabsKeyCache;
|
||||||
|
|
||||||
|
// 32-byte AES key used to obfuscate the API key at rest in the .sav file.
|
||||||
|
// IMPORTANT: this is embedded in the binary, so it is NOT real security — a
|
||||||
|
// determined attacker can extract it. It only raises the bar against casual
|
||||||
|
// inspection of the save file (agreed "trusted distribution" threat model).
|
||||||
|
void GetObfuscationAESKey(FAES::FAESKey& OutKey)
|
||||||
|
{
|
||||||
|
static const uint8 KeyBytes[FAES::FAESKey::KeySize] = {
|
||||||
|
0x9A, 0x3C, 0x71, 0x05, 0xE2, 0x4F, 0xB8, 0x16,
|
||||||
|
0x2D, 0xC4, 0x88, 0x5B, 0x37, 0xF1, 0x60, 0xAA,
|
||||||
|
0x14, 0x7E, 0xD9, 0x42, 0x6B, 0x90, 0x0C, 0xF5,
|
||||||
|
0x53, 0x21, 0xBE, 0x8D, 0x47, 0xA0, 0x39, 0xCF
|
||||||
|
};
|
||||||
|
FMemory::Memcpy(OutKey.Key, KeyBytes, FAES::FAESKey::KeySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encrypt a plaintext key to AES block-padded bytes. Sets OutPlainLen to the
|
||||||
|
* original UTF-8 byte length so padding can be stripped on decrypt. */
|
||||||
|
TArray<uint8> EncryptElevenLabsKey(const FString& Plain, int32& OutPlainLen)
|
||||||
|
{
|
||||||
|
FTCHARToUTF8 Utf8(*Plain);
|
||||||
|
OutPlainLen = Utf8.Length();
|
||||||
|
|
||||||
|
TArray<uint8> Buffer;
|
||||||
|
Buffer.Append(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
|
||||||
|
if (Buffer.Num() == 0)
|
||||||
|
{
|
||||||
|
return Buffer; // empty key → empty blob
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad up to a multiple of the AES block size (required by FAES).
|
||||||
|
const int32 Remainder = Buffer.Num() % FAES::AESBlockSize;
|
||||||
|
if (Remainder != 0)
|
||||||
|
{
|
||||||
|
Buffer.AddZeroed(FAES::AESBlockSize - Remainder);
|
||||||
|
}
|
||||||
|
|
||||||
|
FAES::FAESKey Key;
|
||||||
|
GetObfuscationAESKey(Key);
|
||||||
|
FAES::EncryptData(Buffer.GetData(), Buffer.Num(), Key);
|
||||||
|
return Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decrypt AES block-padded bytes back to the original plaintext key. */
|
||||||
|
FString DecryptElevenLabsKey(const TArray<uint8>& Encrypted, int32 PlainLen)
|
||||||
|
{
|
||||||
|
if (Encrypted.Num() == 0 || PlainLen <= 0 || (Encrypted.Num() % FAES::AESBlockSize) != 0)
|
||||||
|
{
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<uint8> Buffer = Encrypted;
|
||||||
|
FAES::FAESKey Key;
|
||||||
|
GetObfuscationAESKey(Key);
|
||||||
|
FAES::DecryptData(Buffer.GetData(), Buffer.Num(), Key);
|
||||||
|
|
||||||
|
// Strip padding back to the original UTF-8 length, then null-terminate.
|
||||||
|
const int32 Len = FMath::Clamp(PlainLen, 0, Buffer.Num());
|
||||||
|
Buffer.SetNum(Len);
|
||||||
|
Buffer.Add(0);
|
||||||
|
return FString(FUTF8ToTCHAR(reinterpret_cast<const ANSICHAR*>(Buffer.GetData())).Get());
|
||||||
|
}
|
||||||
|
|
||||||
/** Load the API-key SaveGame from its slot, or null if none exists yet. */
|
/** Load the API-key SaveGame from its slot, or null if none exists yet. */
|
||||||
UPS_AI_ConvAgent_SaveGame_ElevenLabs* LoadElevenLabsKeySaveGame()
|
UPS_AI_ConvAgent_SaveGame_ElevenLabs* LoadElevenLabsKeySaveGame()
|
||||||
{
|
{
|
||||||
@@ -60,7 +124,9 @@ void UPS_AI_ConvAgent_BlueprintLibrary::SetElevenLabsAPIKey(const FString& Key)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Save->API_Key = Key;
|
int32 PlainLen = 0;
|
||||||
|
Save->EncryptedAPIKey = EncryptElevenLabsKey(Key, PlainLen);
|
||||||
|
Save->PlainKeyLength = PlainLen;
|
||||||
const bool bSaved = UGameplayStatics::SaveGameToSlot(
|
const bool bSaved = UGameplayStatics::SaveGameToSlot(
|
||||||
Save,
|
Save,
|
||||||
UPS_AI_ConvAgent_SaveGame_ElevenLabs::GetSlotName(),
|
UPS_AI_ConvAgent_SaveGame_ElevenLabs::GetSlotName(),
|
||||||
@@ -80,13 +146,13 @@ void UPS_AI_ConvAgent_BlueprintLibrary::SetElevenLabsAPIKey(const FString& Key)
|
|||||||
|
|
||||||
FString UPS_AI_ConvAgent_BlueprintLibrary::GetElevenLabsAPIKey()
|
FString UPS_AI_ConvAgent_BlueprintLibrary::GetElevenLabsAPIKey()
|
||||||
{
|
{
|
||||||
// Lazy-load once from the SaveGame, then serve from the in-memory cache.
|
// Lazy-load once from the SaveGame (decrypting), then serve from the cache.
|
||||||
if (!GElevenLabsKeyLoaded)
|
if (!GElevenLabsKeyLoaded)
|
||||||
{
|
{
|
||||||
GElevenLabsKeyLoaded = true;
|
GElevenLabsKeyLoaded = true;
|
||||||
if (const UPS_AI_ConvAgent_SaveGame_ElevenLabs* Save = LoadElevenLabsKeySaveGame())
|
if (const UPS_AI_ConvAgent_SaveGame_ElevenLabs* Save = LoadElevenLabsKeySaveGame())
|
||||||
{
|
{
|
||||||
GElevenLabsKeyCache = Save->API_Key;
|
GElevenLabsKeyCache = DecryptElevenLabsKey(Save->EncryptedAPIKey, Save->PlainKeyLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return GElevenLabsKeyCache;
|
return GElevenLabsKeyCache;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
#include "Misc/Base64.h"
|
#include "Misc/Base64.h"
|
||||||
|
|
||||||
|
#include "HttpModule.h"
|
||||||
|
#include "Interfaces/IHttpRequest.h"
|
||||||
|
#include "Interfaces/IHttpResponse.h"
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY_STATIC(LogPS_AI_ConvAgent_WS_ElevenLabs, Log, All);
|
DEFINE_LOG_CATEGORY_STATIC(LogPS_AI_ConvAgent_WS_ElevenLabs, Log, All);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -29,7 +33,36 @@ void UPS_AI_ConvAgent_WebSocket_ElevenLabsProxy::Connect(const FString& AgentIDO
|
|||||||
FModuleManager::LoadModuleChecked<FWebSocketsModule>("WebSockets");
|
FModuleManager::LoadModuleChecked<FWebSocketsModule>("WebSockets");
|
||||||
}
|
}
|
||||||
|
|
||||||
const FString URL = BuildWebSocketURL(AgentIDOverride, APIKeyOverride);
|
const UPS_AI_ConvAgent_Settings_ElevenLabs* Settings = FPS_AI_ConvAgentModule::Get().GetSettings();
|
||||||
|
const FString ResolvedKey = APIKeyOverride.IsEmpty()
|
||||||
|
? UPS_AI_ConvAgent_BlueprintLibrary::GetElevenLabsAPIKey()
|
||||||
|
: APIKeyOverride;
|
||||||
|
|
||||||
|
// Private agents (authentication enabled) require a Signed URL: a direct
|
||||||
|
// ?agent_id=... connection is rejected by ElevenLabs even with the xi-api-key
|
||||||
|
// header. So whenever we have a key (and no manual URL override), fetch a
|
||||||
|
// short-lived signed URL with that key and connect to it. This also works for
|
||||||
|
// public agents, and enforces "valid key required" — no valid key, no signed
|
||||||
|
// URL, no conversation.
|
||||||
|
if (!ResolvedKey.IsEmpty() && Settings->CustomWebSocketURL.IsEmpty())
|
||||||
|
{
|
||||||
|
if (AgentIDOverride.IsEmpty())
|
||||||
|
{
|
||||||
|
const FString Msg = TEXT("Cannot connect: no Agent ID configured. Set it in Project Settings or pass it to Connect().");
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Error, TEXT("%s"), *Msg);
|
||||||
|
ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Error;
|
||||||
|
OnError.Broadcast(Msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Connecting;
|
||||||
|
RequestSignedURLAndConnect(AgentIDOverride, ResolvedKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: direct connection. Used for the advanced custom-URL override, or
|
||||||
|
// for a public agent when no key is available (no authentication).
|
||||||
|
const FString URL = BuildWebSocketURL(AgentIDOverride, ResolvedKey);
|
||||||
if (URL.IsEmpty())
|
if (URL.IsEmpty())
|
||||||
{
|
{
|
||||||
const FString Msg = TEXT("Cannot connect: no Agent ID configured. Set it in Project Settings or pass it to Connect().");
|
const FString Msg = TEXT("Cannot connect: no Agent ID configured. Set it in Project Settings or pass it to Connect().");
|
||||||
@@ -39,21 +72,105 @@ void UPS_AI_ConvAgent_WebSocket_ElevenLabsProxy::Connect(const FString& AgentIDO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Log, TEXT("Connecting to ElevenLabs: %s"), *URL);
|
|
||||||
ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Connecting;
|
ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Connecting;
|
||||||
|
|
||||||
// Headers: the ElevenLabs Conversational AI WS endpoint accepts the
|
|
||||||
// xi-api-key header on the initial HTTP upgrade request.
|
|
||||||
// The key comes from the client-provided SaveGame (via the BlueprintLibrary);
|
|
||||||
// an explicit override passed to Connect() still takes precedence.
|
|
||||||
TMap<FString, FString> UpgradeHeaders;
|
TMap<FString, FString> UpgradeHeaders;
|
||||||
const FString ResolvedKey = APIKeyOverride.IsEmpty()
|
|
||||||
? UPS_AI_ConvAgent_BlueprintLibrary::GetElevenLabsAPIKey()
|
|
||||||
: APIKeyOverride;
|
|
||||||
if (!ResolvedKey.IsEmpty())
|
if (!ResolvedKey.IsEmpty())
|
||||||
{
|
{
|
||||||
UpgradeHeaders.Add(TEXT("xi-api-key"), ResolvedKey);
|
UpgradeHeaders.Add(TEXT("xi-api-key"), ResolvedKey);
|
||||||
}
|
}
|
||||||
|
OpenWebSocket(URL, UpgradeHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_AI_ConvAgent_WebSocket_ElevenLabsProxy::RequestSignedURLAndConnect(const FString& AgentID, const FString& APIKey)
|
||||||
|
{
|
||||||
|
const UPS_AI_ConvAgent_Settings_ElevenLabs* Settings = FPS_AI_ConvAgentModule::Get().GetSettings();
|
||||||
|
const FString RequestURL = FString::Printf(
|
||||||
|
TEXT("%s/v1/convai/conversation/get-signed-url?agent_id=%s"),
|
||||||
|
*Settings->GetAPIBaseURL(), *AgentID);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Log, TEXT("Requesting signed URL for agent %s"), *AgentID);
|
||||||
|
|
||||||
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
|
||||||
|
Request->SetURL(RequestURL);
|
||||||
|
Request->SetVerb(TEXT("GET"));
|
||||||
|
Request->SetHeader(TEXT("xi-api-key"), APIKey);
|
||||||
|
|
||||||
|
TWeakObjectPtr<UPS_AI_ConvAgent_WebSocket_ElevenLabsProxy> WeakThis(this);
|
||||||
|
Request->OnProcessRequestComplete().BindLambda(
|
||||||
|
[WeakThis](FHttpRequestPtr /*Req*/, FHttpResponsePtr Resp, bool bSuccess)
|
||||||
|
{
|
||||||
|
if (!WeakThis.IsValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UPS_AI_ConvAgent_WebSocket_ElevenLabsProxy* Self = WeakThis.Get();
|
||||||
|
|
||||||
|
const int32 Code = Resp.IsValid() ? Resp->GetResponseCode() : 0;
|
||||||
|
if (bSuccess && Code == 200 && Resp.IsValid())
|
||||||
|
{
|
||||||
|
FString SignedURL;
|
||||||
|
TSharedPtr<FJsonObject> Root;
|
||||||
|
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Resp->GetContentAsString());
|
||||||
|
if (FJsonSerializer::Deserialize(Reader, Root) && Root.IsValid())
|
||||||
|
{
|
||||||
|
Root->TryGetStringField(TEXT("signed_url"), SignedURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SignedURL.IsEmpty())
|
||||||
|
{
|
||||||
|
// Token is embedded in the signed URL — no auth header needed.
|
||||||
|
Self->OpenWebSocket(SignedURL, TMap<FString, FString>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Msg = TEXT("Signed URL request succeeded but the response had no 'signed_url'.");
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Error, TEXT("%s"), *Msg);
|
||||||
|
Self->ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Error;
|
||||||
|
Self->OnError.Broadcast(Msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Msg;
|
||||||
|
if (Code == 401 || Code == 403)
|
||||||
|
{
|
||||||
|
Msg = FString::Printf(TEXT("Authentication failed (HTTP %d): invalid or unauthorized ElevenLabs API key."), Code);
|
||||||
|
}
|
||||||
|
else if (Code == 0)
|
||||||
|
{
|
||||||
|
Msg = TEXT("Could not reach ElevenLabs to obtain a signed URL (network error).");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Msg = FString::Printf(TEXT("Failed to obtain a signed URL (HTTP %d)."), Code);
|
||||||
|
}
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Error, TEXT("%s"), *Msg);
|
||||||
|
Self->ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Error;
|
||||||
|
Self->OnError.Broadcast(Msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
Request->ProcessRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_AI_ConvAgent_WebSocket_ElevenLabsProxy::OpenWebSocket(const FString& URL, const TMap<FString, FString>& UpgradeHeaders)
|
||||||
|
{
|
||||||
|
if (URL.IsEmpty())
|
||||||
|
{
|
||||||
|
const FString Msg = TEXT("Cannot open WebSocket: empty URL.");
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Error, TEXT("%s"), *Msg);
|
||||||
|
ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Error;
|
||||||
|
OnError.Broadcast(Msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redact the query string (signed URLs carry an auth token there) before logging.
|
||||||
|
FString LogURL = URL;
|
||||||
|
int32 QueryIdx = INDEX_NONE;
|
||||||
|
if (URL.FindChar(TEXT('?'), QueryIdx))
|
||||||
|
{
|
||||||
|
LogURL = URL.Left(QueryIdx) + TEXT("?<params hidden>");
|
||||||
|
}
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_WS_ElevenLabs, Log, TEXT("Connecting to ElevenLabs: %s"), *LogURL);
|
||||||
|
|
||||||
WebSocket = FWebSocketsModule::Get().CreateWebSocket(URL, TEXT(""), UpgradeHeaders);
|
WebSocket = FWebSocketsModule::Get().CreateWebSocket(URL, TEXT(""), UpgradeHeaders);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
// - Packaged : <Build>/<Project>/Saved/SaveGames/PS_AI_ConvAgent_ElevenLabs.sav
|
// - Packaged : <Build>/<Project>/Saved/SaveGames/PS_AI_ConvAgent_ElevenLabs.sav
|
||||||
//
|
//
|
||||||
// Binary file → the key is never stored in plaintext, never committed, never
|
// Binary file → the key is never stored in plaintext, never committed, never
|
||||||
// baked into the shipped config. The key is entered once at runtime via the
|
// baked into the shipped config. The key is AES-encrypted at rest (see the codec
|
||||||
|
// in UPS_AI_ConvAgent_BlueprintLibrary) and entered once at runtime via the
|
||||||
// settings UI (BlueprintLibrary "Set ElevenLabs API Key").
|
// settings UI (BlueprintLibrary "Set ElevenLabs API Key").
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
UCLASS()
|
UCLASS()
|
||||||
@@ -24,9 +25,14 @@ class PS_AI_CONVAGENT_API UPS_AI_ConvAgent_SaveGame_ElevenLabs : public USaveGam
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Client-provided ElevenLabs API key. Empty until the user enters one. */
|
/** AES-encrypted ElevenLabs API key (UTF-8, block-padded). Empty until set.
|
||||||
|
* Encrypted/decrypted by UPS_AI_ConvAgent_BlueprintLibrary. */
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FString API_Key;
|
TArray<uint8> EncryptedAPIKey;
|
||||||
|
|
||||||
|
/** Plaintext (UTF-8) byte length, used to strip AES block padding on decrypt. */
|
||||||
|
UPROPERTY()
|
||||||
|
int32 PlainKeyLength = 0;
|
||||||
|
|
||||||
/** Fixed save slot name used for the key. */
|
/** Fixed save slot name used for the key. */
|
||||||
static const TCHAR* GetSlotName() { return TEXT("PS_AI_ConvAgent_ElevenLabs"); }
|
static const TCHAR* GetSlotName() { return TEXT("PS_AI_ConvAgent_ElevenLabs"); }
|
||||||
|
|||||||
@@ -234,6 +234,14 @@ private:
|
|||||||
/** Resolve the WebSocket URL from settings / parameters. */
|
/** Resolve the WebSocket URL from settings / parameters. */
|
||||||
FString BuildWebSocketURL(const FString& AgentID, const FString& APIKey) const;
|
FString BuildWebSocketURL(const FString& AgentID, const FString& APIKey) const;
|
||||||
|
|
||||||
|
/** Fetch a short-lived signed URL for a (private) agent using the API key,
|
||||||
|
* then open the WebSocket on it. Required for agents with authentication
|
||||||
|
* enabled — a direct ?agent_id=... connection is rejected by ElevenLabs. */
|
||||||
|
void RequestSignedURLAndConnect(const FString& AgentID, const FString& APIKey);
|
||||||
|
|
||||||
|
/** Create the WebSocket on the given URL, bind callbacks, and connect. */
|
||||||
|
void OpenWebSocket(const FString& URL, const TMap<FString, FString>& UpgradeHeaders);
|
||||||
|
|
||||||
TSharedPtr<IWebSocket> WebSocket;
|
TSharedPtr<IWebSocket> WebSocket;
|
||||||
EPS_AI_ConvAgent_ConnectionState_ElevenLabs ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Disconnected;
|
EPS_AI_ConvAgent_ConnectionState_ElevenLabs ConnectionState = EPS_AI_ConvAgent_ConnectionState_ElevenLabs::Disconnected;
|
||||||
FPS_AI_ConvAgent_ConversationInfo_ElevenLabs ConversationInfo;
|
FPS_AI_ConvAgent_ConversationInfo_ElevenLabs ConversationInfo;
|
||||||
|
|||||||
@@ -1822,6 +1822,20 @@ TSharedPtr<FJsonObject> FPS_AI_ConvAgent_AgentConfigCustomization_ElevenLabs::Bu
|
|||||||
}
|
}
|
||||||
Root->SetObjectField(TEXT("conversation_config"), ConvConfig);
|
Root->SetObjectField(TEXT("conversation_config"), ConvConfig);
|
||||||
|
|
||||||
|
// platform_settings.auth.enable_auth = true → force every created/updated
|
||||||
|
// agent to be PRIVATE (authentication required). Hardcoded on purpose (no
|
||||||
|
// toggle): a public agent can be used by anyone who knows the agent_id,
|
||||||
|
// running up the owner's ElevenLabs bill without a valid API key.
|
||||||
|
{
|
||||||
|
TSharedPtr<FJsonObject> AuthObj = MakeShareable(new FJsonObject());
|
||||||
|
AuthObj->SetBoolField(TEXT("enable_auth"), true);
|
||||||
|
|
||||||
|
TSharedPtr<FJsonObject> PlatformSettings = MakeShareable(new FJsonObject());
|
||||||
|
PlatformSettings->SetObjectField(TEXT("auth"), AuthObj);
|
||||||
|
|
||||||
|
Root->SetObjectField(TEXT("platform_settings"), PlatformSettings);
|
||||||
|
}
|
||||||
|
|
||||||
return Root;
|
return Root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user