Merge pull request #4416 from pipecat-ai/mb/pr-4333-aws-credentials-review

feat(aws): add shared credential resolver with boto3 chain fallback
This commit is contained in:
Mark Backman
2026-05-04 21:48:33 -04:00
committed by GitHub
8 changed files with 311 additions and 45 deletions

View File

@@ -12,7 +12,6 @@ Amazon Bedrock AgentCore Runtime and streams their responses as LLMTextFrames.
import asyncio
import json
import os
from collections.abc import Callable
import aioboto3
@@ -27,6 +26,7 @@ from pipecat.frames.frames import (
)
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.services.aws.utils import resolve_credentials
def default_context_to_payload_transformer(
@@ -122,8 +122,11 @@ class AWSAgentCoreProcessor(FrameProcessor):
Args:
agentArn: The Amazon Web Services Resource Name (ARN) of the agent.
aws_access_key: AWS access key ID. If None, uses default credentials.
aws_secret_key: AWS secret access key. If None, uses default credentials.
aws_access_key: AWS access key ID. If None, falls back to
environment variables and the default boto3 credential chain
(instance profiles, IRSA, ECS task roles, SSO, etc.).
aws_secret_key: AWS secret access key. Same fallback behaviour as
``aws_access_key``.
aws_session_token: AWS session token for temporary credentials.
aws_region: AWS region.
context_to_payload_transformer: Optional callable to transform
@@ -139,13 +142,13 @@ class AWSAgentCoreProcessor(FrameProcessor):
self._agentArn = agentArn
self._aws_session = aioboto3.Session()
# Store AWS session parameters for creating client in async context
self._aws_params = {
"aws_access_key_id": aws_access_key or os.getenv("AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": aws_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
"region_name": aws_region or os.getenv("AWS_REGION", "us-east-1"),
}
# Resolve credentials using the shared chain (explicit → env → boto3).
self._aws_params = resolve_credentials(
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
aws_session_token=aws_session_token,
region=aws_region,
).to_boto_kwargs()
# Set transformers with defaults
self._context_to_payload_transformer = (
@@ -204,7 +207,8 @@ class AWSAgentCoreProcessor(FrameProcessor):
# aioboto3's `client()` is an async context manager but its stubs don't
# advertise `__aenter__` / `__aexit__` in a way pyright can see.
async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues]
"bedrock-agentcore", **self._aws_params
"bedrock-agentcore",
**self._aws_params, # pyright: ignore[reportArgumentType]
) as client:
# Invoke the AgentCore agent
response = await client.invoke_agent_runtime(

View File

@@ -13,7 +13,6 @@ function calling.
import asyncio
import json
import os
import re
from dataclasses import dataclass, field
from typing import Any
@@ -36,6 +35,7 @@ from pipecat.frames.frames import (
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.aws.utils import resolve_credentials
from pipecat.services.llm_service import LLMService
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, assert_given
from pipecat.utils.tracing.service_decorators import traced_llm
@@ -135,8 +135,11 @@ class AWSBedrockLLMService(LLMService[AWSBedrockLLMAdapter]):
.. deprecated:: 0.0.105
Use ``settings=AWSBedrockLLMService.Settings(model=...)`` instead.
aws_access_key: AWS access key ID. If None, uses default credentials.
aws_secret_key: AWS secret access key. If None, uses default credentials.
aws_access_key: AWS access key ID. If None, falls back to
environment variables and the default boto3 credential chain
(instance profiles, IRSA, ECS task roles, SSO, etc.).
aws_secret_key: AWS secret access key. Same fallback behaviour as
``aws_access_key``.
aws_session_token: AWS session token for temporary credentials.
aws_region: AWS region for the Bedrock service.
params: Model parameters and configuration.
@@ -215,14 +218,14 @@ class AWSBedrockLLMService(LLMService[AWSBedrockLLMAdapter]):
self._aws_session = aioboto3.Session()
# Store AWS session parameters for creating client in async context
self._aws_params = {
"aws_access_key_id": aws_access_key or os.getenv("AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": aws_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
"region_name": aws_region or os.getenv("AWS_REGION", "us-east-1"),
"config": client_config,
}
# Resolve credentials using the shared chain (explicit → env → boto3).
resolved = resolve_credentials(
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
aws_session_token=aws_session_token,
region=aws_region,
)
self._aws_params = {**resolved.to_boto_kwargs(), "config": client_config}
self._retry_timeout_secs = retry_timeout_secs
self._retry_on_timeout = retry_on_timeout

View File

@@ -11,7 +11,6 @@ speech-to-text transcription with support for multiple languages and audio forma
"""
import json
import os
import random
import string
from collections.abc import AsyncGenerator
@@ -29,7 +28,12 @@ from pipecat.frames.frames import (
StartFrame,
TranscriptionFrame,
)
from pipecat.services.aws.utils import build_event_message, decode_event, get_presigned_url
from pipecat.services.aws.utils import (
build_event_message,
decode_event,
get_presigned_url,
resolve_credentials,
)
from pipecat.services.settings import STTSettings, assert_given
from pipecat.services.stt_latency import AWS_TRANSCRIBE_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
@@ -81,9 +85,12 @@ class AWSTranscribeSTTService(WebsocketSTTService):
"""Initialize the AWS Transcribe STT service.
Args:
api_key: AWS secret access key. If None, uses AWS_SECRET_ACCESS_KEY environment variable.
aws_access_key_id: AWS access key ID. If None, uses AWS_ACCESS_KEY_ID environment variable.
aws_session_token: AWS session token for temporary credentials. If None, uses AWS_SESSION_TOKEN environment variable.
api_key: AWS secret access key. If None, falls back to environment
variables and the default boto3 credential chain (instance
profiles, IRSA, ECS task roles, SSO, etc.).
aws_access_key_id: AWS access key ID. Same fallback behaviour as
``api_key``.
aws_session_token: AWS session token for temporary credentials.
region: AWS region for the service.
sample_rate: Audio sample rate in Hz. If None, uses the pipeline sample rate.
AWS Transcribe only supports 8000 or 16000 Hz; other values are
@@ -129,11 +136,19 @@ class AWSTranscribeSTTService(WebsocketSTTService):
self._show_speaker_label = False
self._enable_channel_identification = False
# Resolve credentials using the shared chain (explicit → env → boto3).
resolved = resolve_credentials(
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=api_key,
aws_session_token=aws_session_token,
region=region,
)
self._credentials = {
"aws_access_key_id": aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": api_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
"region": region or os.getenv("AWS_REGION", "us-east-1"),
"aws_access_key_id": resolved.access_key,
"aws_secret_access_key": resolved.secret_key,
"aws_session_token": resolved.session_token,
"region": resolved.region,
}
self._receive_task = None

View File

@@ -10,7 +10,6 @@ This module provides integration with Amazon Polly for text-to-speech synthesis,
supporting multiple languages, voices, and SSML features.
"""
import os
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
@@ -23,6 +22,7 @@ from pipecat.frames.frames import (
Frame,
TTSAudioRawFrame,
)
from pipecat.services.aws.utils import resolve_credentials
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language, resolve_language
@@ -191,8 +191,11 @@ class AWSPollyTTSService(TTSService):
"""Initializes the AWS Polly TTS service.
Args:
api_key: AWS secret access key. If None, uses AWS_SECRET_ACCESS_KEY environment variable.
aws_access_key_id: AWS access key ID. If None, uses AWS_ACCESS_KEY_ID environment variable.
api_key: AWS secret access key. If None, falls back to environment
variables and the default boto3 credential chain (instance
profiles, IRSA, ECS task roles, SSO, etc.).
aws_access_key_id: AWS access key ID. Same fallback behaviour as
``api_key``.
aws_session_token: AWS session token for temporary credentials.
region: AWS region for Polly service. Defaults to 'us-east-1'.
voice_id: Voice ID to use for synthesis. Defaults to 'Joanna'.
@@ -250,13 +253,13 @@ class AWSPollyTTSService(TTSService):
**kwargs,
)
# Get credentials from environment variables if not provided
self._aws_params = {
"aws_access_key_id": aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": api_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
"aws_session_token": aws_session_token or os.getenv("AWS_SESSION_TOKEN"),
"region_name": region or os.getenv("AWS_REGION", "us-east-1"),
}
# Resolve credentials using the shared chain (explicit → env → boto3).
self._aws_params = resolve_credentials(
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=api_key,
aws_session_token=aws_session_token,
region=region,
).to_boto_kwargs()
self._aws_session = aioboto3.Session()
@@ -348,7 +351,8 @@ class AWSPollyTTSService(TTSService):
# aioboto3's `client()` is an async context manager but its stubs
# don't advertise `__aenter__` / `__aexit__` to pyright.
async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues]
"polly", **self._aws_params
"polly",
**self._aws_params, # pyright: ignore[reportArgumentType]
) as polly:
response = await polly.synthesize_speech(**filtered_params)
if "AudioStream" in response:

View File

@@ -4,10 +4,11 @@
# SPDX-License-Identifier: BSD 2-Clause License
#
"""AWS Transcribe utility functions and classes for WebSocket streaming.
"""AWS utility functions for Pipecat services.
This module provides utilities for creating presigned URLs, building event messages,
and handling AWS event stream protocol for real-time transcription services.
This module provides shared credential resolution and AWS Transcribe utilities
for creating presigned URLs, building event messages, and handling AWS event
stream protocol for real-time transcription services.
"""
import binascii
@@ -15,8 +16,98 @@ import datetime
import hashlib
import hmac
import json
import os
import struct
import urllib.parse
from dataclasses import dataclass
from typing import Any
from loguru import logger
@dataclass
class AWSCredentials:
"""Resolved AWS credentials ready for use by any AWS service."""
access_key: str | None
secret_key: str | None
session_token: str | None
region: str
def to_boto_kwargs(self) -> dict[str, str | None]:
"""Return credentials as kwargs accepted by ``boto3``/``aioboto3`` clients."""
return {
"aws_access_key_id": self.access_key,
"aws_secret_access_key": self.secret_key,
"aws_session_token": self.session_token,
"region_name": self.region,
}
def resolve_credentials(
*,
aws_access_key_id: str | None = None,
aws_secret_access_key: str | None = None,
aws_session_token: str | None = None,
region: str | None = None,
) -> AWSCredentials:
"""Resolve AWS credentials using the standard fallback chain.
Resolution order:
1. Explicit parameters
2. Environment variables (``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``,
``AWS_SESSION_TOKEN``, ``AWS_REGION``)
3. Default boto3/botocore credential chain (instance profiles, IRSA,
ECS task roles, SSO, credential files, etc.)
The boto3 fallback (step 3) is only attempted when *both* access key and
secret key are still unresolved after steps 1-2. This avoids replacing
explicitly provided credentials with ambient ones.
Args:
aws_access_key_id: Explicit access key ID.
aws_secret_access_key: Explicit secret access key.
aws_session_token: Explicit session token.
region: Explicit AWS region.
Returns:
An :class:`AWSCredentials` instance. ``access_key`` and
``secret_key`` may still be ``None`` if no credentials could be
resolved (the caller should raise an appropriate error).
"""
access_key = aws_access_key_id or os.getenv("AWS_ACCESS_KEY_ID")
secret_key = aws_secret_access_key or os.getenv("AWS_SECRET_ACCESS_KEY")
session_token = aws_session_token or os.getenv("AWS_SESSION_TOKEN")
resolved_region = region or os.getenv("AWS_REGION", "us-east-1")
# Fall back to the boto3 credential provider chain (pod roles, IRSA,
# instance profiles, SSO, credential files, etc.) when explicit
# credentials were not supplied.
if not access_key and not secret_key:
try:
import boto3
session = boto3.Session(region_name=resolved_region)
creds = session.get_credentials()
if creds:
frozen = creds.get_frozen_credentials()
access_key = access_key or frozen.access_key
secret_key = secret_key or frozen.secret_key
session_token = session_token or frozen.token
except ImportError:
logger.debug(
"boto3 not available for credential chain fallback; "
"install pipecat-ai[aws] for full credential support."
)
except Exception as e:
logger.warning(f"Failed to resolve AWS credentials via boto3 chain: {e}")
return AWSCredentials(
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
region=resolved_region,
)
def get_presigned_url(