Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/pipecatcloud/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#

import json
import os
import ssl
from functools import wraps
from typing import Callable, List, Optional, Union

Expand All @@ -16,6 +18,28 @@
from pipecatcloud.exception import AgentStartError


def _get_ssl_context() -> ssl.SSLContext:
"""Get SSL context with proper certificate verification.

Attempts to use certifi's certificates as a fallback if system certificates
are not available. This fixes SSL verification issues on macOS and other systems.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more specific about what OS versions of macos this affects?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using Tahoe 26.0 but it can also be tested on different versions to see if can be replicated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @moechehabb ! What Python version are you using? Is it the one from the system or from somewhere else? It's weird that Python doesn't have access to system certificates by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after a little research, seems this might have to do with how Python was installed on macos (more than OS version).
ie, pyenv / conda, downloaded from python.org vs. homebrew

I might reword it to something like This fixes SSL verification issues encountered by some python installations on macOS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for posterity, claude's debugger script:

import ssl
import certifi
import os

print(f"Default verify paths: {ssl.get_default_verify_paths()}")
print(f"Certifi location: {certifi.where()}")
print(f"SSL version: {ssl.OPENSSL_VERSION}")
print(f"Python executable: {os.path.realpath(sys.executable)}")

"""
ssl_context = ssl.create_default_context()

# If SSL_CERT_FILE is not set, try to use certifi's certificates
if not os.environ.get('SSL_CERT_FILE'):
try:
import certifi
ssl_context.load_verify_locations(certifi.where())
logger.debug(f"Using certifi certificates from: {certifi.where()}")
except ImportError:
logger.trace("certifi not available, using system certificates")
except Exception as e:
logger.debug(f"Could not load certifi certificates: {e}")

return ssl_context


def api_method(func):
@wraps(func)
async def wrapper(self, *args, live=None, **kwargs):
Expand Down Expand Up @@ -61,7 +85,11 @@ async def _base_request(
not_found_is_empty: bool = False,
override_token: Optional[str] = None,
) -> Optional[dict]:
async with aiohttp.ClientSession() as session:
# Create SSL context with certifi fallback
ssl_context = _get_ssl_context()
connector = aiohttp.TCPConnector(ssl=ssl_context)

async with aiohttp.ClientSession(connector=connector) as session:
logger.debug(f"Request: {method} {url} {params} {json}")

response = await session.request(
Expand Down
67 changes: 60 additions & 7 deletions src/pipecatcloud/cli/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import itertools
import webbrowser
import os
import ssl
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Optional, Tuple

Expand All @@ -32,30 +33,78 @@
auth_cli = typer.Typer(name="auth", help="Manage Pipecat Cloud credentials", no_args_is_help=True)


def _get_ssl_context() -> ssl.SSLContext:
"""Get SSL context with proper certificate verification.

Attempts to use certifi's certificates as a fallback if system certificates
are not available. This fixes SSL verification issues on macOS and other systems.
"""
ssl_context = ssl.create_default_context()

# If SSL_CERT_FILE is not set, try to use certifi's certificates
if not os.environ.get('SSL_CERT_FILE'):
try:
import certifi
ssl_context.load_verify_locations(certifi.where())
logger.debug(f"Using certifi certificates from: {certifi.where()}")
except ImportError:
logger.trace("certifi not available, using system certificates")
except Exception as e:
logger.debug(f"Could not load certifi certificates: {e}")

return ssl_context


class _AuthFlow:
def __init__(self):
pass

@asynccontextmanager
async def start(self) -> AsyncGenerator[Tuple[Optional[str], Optional[str]], None]:
try:
async with aiohttp.ClientSession() as session:
# Create SSL context with certifi fallback
ssl_context = _get_ssl_context()
connector = aiohttp.TCPConnector(ssl=ssl_context)

async with aiohttp.ClientSession(connector=connector) as session:
url = f"{API.construct_api_url('login_path')}"
logger.debug(url)
async with session.post(url) as resp:
logger.debug(f"Starting auth flow at: {url}")
async with session.post(
url, timeout=aiohttp.ClientTimeout(total=10.0)
) as resp:
if resp.status != 200:
raise Exception(f"Failed to start auth flow: {resp.status}")
error_msg = f"Failed to start auth flow: HTTP {resp.status}"
try:
error_data = await resp.json()
if "error" in error_data:
error_msg += f" - {error_data['error']}"
except Exception:
pass
logger.error(error_msg)
raise Exception(error_msg)
data = await resp.json()
self.token_flow_id = data["token_flow_id"]
self.wait_secret = data["wait_secret"]
web_url = data["web_url"]
yield (self.token_flow_id, web_url)
except Exception:
except asyncio.TimeoutError:
logger.error("Connection timeout while starting auth flow")
yield (None, None)
except aiohttp.ClientError as e:
logger.error(f"Network error during auth flow: {type(e).__name__}: {e}")
yield (None, None)
except Exception as e:
logger.error(f"Unexpected error during auth flow: {type(e).__name__}: {e}")
yield (None, None)

async def finish(self, timeout: float = 40.0, network_timeout: float = 5.0) -> Optional[str]:
start_time = asyncio.get_event_loop().time()
async with aiohttp.ClientSession() as session:

# Create SSL context with certifi fallback
ssl_context = _get_ssl_context()
connector = aiohttp.TCPConnector(ssl=ssl_context)

async with aiohttp.ClientSession(connector=connector) as session:
while (asyncio.get_event_loop().time() - start_time) < timeout:
try:
async with session.get(
Expand Down Expand Up @@ -103,7 +152,11 @@ def _open_url(url: str) -> bool:
async def _get_account_org(
token: str, active_org: Optional[str] = None
) -> Tuple[Optional[str], Optional[str]]:
async with aiohttp.ClientSession() as session:
# Create SSL context with certifi fallback
ssl_context = _get_ssl_context()
connector = aiohttp.TCPConnector(ssl=ssl_context)

async with aiohttp.ClientSession(connector=connector) as session:
async with session.get(
f"{API.construct_api_url('organization_path')}",
headers={"Authorization": f"Bearer {token}"},
Expand Down