diff --git a/examples/memory/encrypted_session_example.py b/examples/memory/encrypted_session_example.py new file mode 100644 index 000000000..d3d9a9e74 --- /dev/null +++ b/examples/memory/encrypted_session_example.py @@ -0,0 +1,109 @@ +""" +Example demonstrating encrypted session memory functionality. + +This example shows how to use encrypted session memory to maintain conversation history +across multiple agent runs with automatic encryption and TTL-based expiration. +The EncryptedSession wrapper provides transparent encryption over any underlying session. +""" + +import asyncio +from typing import cast + +from agents import Agent, Runner, SQLiteSession +from agents.extensions.memory import EncryptedSession +from agents.extensions.memory.encrypt_session import EncryptedEnvelope + + +async def main(): + # Create an agent + agent = Agent( + name="Assistant", + instructions="Reply very concisely.", + ) + + # Create an underlying session (SQLiteSession in this example) + session_id = "conversation_123" + underlying_session = SQLiteSession(session_id) + + # Wrap with encrypted session for automatic encryption and TTL + session = EncryptedSession( + session_id=session_id, + underlying_session=underlying_session, + encryption_key="my-secret-encryption-key", + ttl=3600, # 1 hour TTL for messages + ) + + print("=== Encrypted Session Example ===") + print("The agent will remember previous messages automatically with encryption.\n") + + # First turn + print("First turn:") + print("User: What city is the Golden Gate Bridge in?") + result = await Runner.run( + agent, + "What city is the Golden Gate Bridge in?", + session=session, + ) + print(f"Assistant: {result.final_output}") + print() + + # Second turn - the agent will remember the previous conversation + print("Second turn:") + print("User: What state is it in?") + result = await Runner.run(agent, "What state is it in?", session=session) + print(f"Assistant: {result.final_output}") + print() + + # Third turn - continuing the conversation + print("Third turn:") + print("User: What's the population of that state?") + result = await Runner.run( + agent, + "What's the population of that state?", + session=session, + ) + print(f"Assistant: {result.final_output}") + print() + + print("=== Conversation Complete ===") + print("Notice how the agent remembered the context from previous turns!") + print("All conversation history was automatically encrypted and stored securely.") + + # Demonstrate the limit parameter - get only the latest 2 items + print("\n=== Latest Items Demo ===") + latest_items = await session.get_items(limit=2) + print("Latest 2 items (automatically decrypted):") + for i, msg in enumerate(latest_items, 1): + role = msg.get("role", "unknown") + content = msg.get("content", "") + print(f" {i}. {role}: {content}") + + print(f"\nFetched {len(latest_items)} out of total conversation history.") + + # Get all items to show the difference + all_items = await session.get_items() + print(f"Total items in session: {len(all_items)}") + + # Show that underlying storage is encrypted + print("\n=== Encryption Demo ===") + print("Checking underlying storage to verify encryption...") + raw_items = await underlying_session.get_items() + print("Raw encrypted items in underlying storage:") + for i, item in enumerate(raw_items, 1): + if isinstance(item, dict) and item.get("__enc__") == 1: + enc_item = cast(EncryptedEnvelope, item) + print( + f" {i}. Encrypted envelope: __enc__={enc_item['__enc__']}, " + f"payload length={len(enc_item['payload'])}" + ) + else: + print(f" {i}. Unencrypted item: {item}") + + print(f"\nAll {len(raw_items)} items are stored encrypted with TTL-based expiration.") + + # Clean up + underlying_session.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index bf05b2b4e..d91a5c55a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ viz = ["graphviz>=0.17"] litellm = ["litellm>=1.67.4.post1, <2"] realtime = ["websockets>=15.0, <16"] sqlalchemy = ["SQLAlchemy>=2.0", "asyncpg>=0.29.0"] +encrypt = ["cryptography>=45.0, <46"] [dependency-groups] dev = [ @@ -65,6 +66,7 @@ dev = [ "eval-type-backport>=0.2.2", "fastapi >= 0.110.0, <1", "aiosqlite>=0.21.0", + "cryptography>=45.0, <46", ] [tool.uv.workspace] diff --git a/src/agents/extensions/memory/__init__.py b/src/agents/extensions/memory/__init__.py index 13fa36e75..4e7bad61f 100644 --- a/src/agents/extensions/memory/__init__.py +++ b/src/agents/extensions/memory/__init__.py @@ -1,15 +1,42 @@ -"""Session memory backends living in the extensions namespace. - -This package contains optional, production-grade session implementations that -introduce extra third-party dependencies (database drivers, ORMs, etc.). They -conform to the :class:`agents.memory.session.Session` protocol so they can be -used as a drop-in replacement for :class:`agents.memory.session.SQLiteSession`. -""" - -from __future__ import annotations - -from .sqlalchemy_session import SQLAlchemySession # noqa: F401 - -__all__: list[str] = [ - "SQLAlchemySession", -] +"""Session memory backends living in the extensions namespace. + +This package contains optional, production-grade session implementations that +introduce extra third-party dependencies (database drivers, ORMs, etc.). They +conform to the :class:`agents.memory.session.Session` protocol so they can be +used as a drop-in replacement for :class:`agents.memory.session.SQLiteSession`. +""" + +from __future__ import annotations + +from typing import Any + +__all__: list[str] = [ + "EncryptedSession", + "SQLAlchemySession", +] + + +def __getattr__(name: str) -> Any: + if name == "EncryptedSession": + try: + from .encrypt_session import EncryptedSession # noqa: F401 + + return EncryptedSession + except ModuleNotFoundError as e: + raise ImportError( + "EncryptedSession requires the 'cryptography' extra. " + "Install it with: pip install openai-agents[encrypt]" + ) from e + + if name == "SQLAlchemySession": + try: + from .sqlalchemy_session import SQLAlchemySession # noqa: F401 + + return SQLAlchemySession + except ModuleNotFoundError as e: + raise ImportError( + "SQLAlchemySession requires the 'sqlalchemy' extra. " + "Install it with: pip install openai-agents[sqlalchemy]" + ) from e + + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/src/agents/extensions/memory/encrypt_session.py b/src/agents/extensions/memory/encrypt_session.py new file mode 100644 index 000000000..1fc032e47 --- /dev/null +++ b/src/agents/extensions/memory/encrypt_session.py @@ -0,0 +1,185 @@ +"""Encrypted Session wrapper for secure conversation storage. + +This module provides transparent encryption for session storage with automatic +expiration of old data. When TTL expires, expired items are silently skipped. + +Usage:: + + from agents.extensions.memory import EncryptedSession, SQLAlchemySession + + # Create underlying session (e.g. SQLAlchemySession) + underlying_session = SQLAlchemySession.from_url( + session_id="user-123", + url="postgresql+asyncpg://app:secret@db.example.com/agents", + create_tables=True, + ) + + # Wrap with encryption and TTL-based expiration + session = EncryptedSession( + session_id="user-123", + underlying_session=underlying_session, + encryption_key="your-encryption-key", + ttl=600, # 10 minutes + ) + + await Runner.run(agent, "Hello", session=session) +""" + +from __future__ import annotations + +import base64 +import json +from typing import Any, cast + +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from typing_extensions import Literal, TypedDict, TypeGuard + +from ...items import TResponseInputItem +from ...memory.session import SessionABC + + +class EncryptedEnvelope(TypedDict): + """TypedDict for encrypted message envelopes stored in the underlying session.""" + + __enc__: Literal[1] + v: int + kid: str + payload: str + + +def _ensure_fernet_key_bytes(master_key: str) -> bytes: + """ + Accept either a Fernet key (urlsafe-b64, 32 bytes after decode) or a raw string. + Returns raw bytes suitable for HKDF input. + """ + if not master_key: + raise ValueError("encryption_key not set; required for EncryptedSession.") + try: + key_bytes = base64.urlsafe_b64decode(master_key) + if len(key_bytes) == 32: + return key_bytes + except Exception: + pass + return master_key.encode("utf-8") + + +def _derive_session_fernet_key(master_key_bytes: bytes, session_id: str) -> Fernet: + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=session_id.encode("utf-8"), + info=b"agents.session-store.hkdf.v1", + ) + derived = hkdf.derive(master_key_bytes) + return Fernet(base64.urlsafe_b64encode(derived)) + + +def _to_json_bytes(obj: Any) -> bytes: + return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), default=str).encode("utf-8") + + +def _from_json_bytes(data: bytes) -> Any: + return json.loads(data.decode("utf-8")) + + +def _is_encrypted_envelope(item: object) -> TypeGuard[EncryptedEnvelope]: + """Type guard to check if an item is an encrypted envelope.""" + return ( + isinstance(item, dict) + and item.get("__enc__") == 1 + and "payload" in item + and "kid" in item + and "v" in item + ) + + +class EncryptedSession(SessionABC): + """Encrypted wrapper for Session implementations with TTL-based expiration. + + This class wraps any SessionABC implementation to provide transparent + encryption/decryption of stored items using Fernet encryption with + per-session key derivation and automatic expiration of old data. + + When items expire (exceed TTL), they are silently skipped during retrieval. + + Note: Expired tokens are rejected based on the system clock of the application server. + To avoid valid tokens being rejected due to clock drift, ensure all servers in + your environment are synchronized using NTP. + """ + + def __init__( + self, + session_id: str, + underlying_session: SessionABC, + encryption_key: str, + ttl: int = 600, + ): + """ + Args: + session_id: ID for this session + underlying_session: The real session store (e.g. SQLiteSession, SQLAlchemySession) + encryption_key: Master key (Fernet key or raw secret) + ttl: Token time-to-live in seconds (default 10 min) + """ + self.session_id = session_id + self.underlying_session = underlying_session + self.ttl = ttl + + master = _ensure_fernet_key_bytes(encryption_key) + self.cipher = _derive_session_fernet_key(master, session_id) + self._kid = "hkdf-v1" + self._ver = 1 + + def __getattr__(self, name): + return getattr(self.underlying_session, name) + + def _wrap(self, item: TResponseInputItem) -> EncryptedEnvelope: + if isinstance(item, dict): + payload = item + elif hasattr(item, "model_dump"): + payload = item.model_dump() + elif hasattr(item, "__dict__"): + payload = item.__dict__ + else: + payload = dict(item) + + token = self.cipher.encrypt(_to_json_bytes(payload)).decode("utf-8") + return {"__enc__": 1, "v": self._ver, "kid": self._kid, "payload": token} + + def _unwrap(self, item: TResponseInputItem | EncryptedEnvelope) -> TResponseInputItem | None: + if not _is_encrypted_envelope(item): + return cast(TResponseInputItem, item) + + try: + token = item["payload"].encode("utf-8") + plaintext = self.cipher.decrypt(token, ttl=self.ttl) + return cast(TResponseInputItem, _from_json_bytes(plaintext)) + except (InvalidToken, KeyError): + return None + + async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]: + encrypted_items = await self.underlying_session.get_items(limit) + valid_items: list[TResponseInputItem] = [] + for enc in encrypted_items: + item = self._unwrap(enc) + if item is not None: + valid_items.append(item) + return valid_items + + async def add_items(self, items: list[TResponseInputItem]) -> None: + wrapped: list[EncryptedEnvelope] = [self._wrap(it) for it in items] + await self.underlying_session.add_items(cast(list[TResponseInputItem], wrapped)) + + async def pop_item(self) -> TResponseInputItem | None: + while True: + enc = await self.underlying_session.pop_item() + if not enc: + return None + item = self._unwrap(enc) + if item is not None: + return item + + async def clear_session(self) -> None: + await self.underlying_session.clear_session() diff --git a/tests/extensions/memory/test_encrypt_session.py b/tests/extensions/memory/test_encrypt_session.py new file mode 100644 index 000000000..5eb1d9b53 --- /dev/null +++ b/tests/extensions/memory/test_encrypt_session.py @@ -0,0 +1,333 @@ +from __future__ import annotations + +import tempfile +import time +from pathlib import Path + +import pytest + +pytest.importorskip("cryptography") # Skip tests if cryptography is not installed + +from cryptography.fernet import Fernet + +from agents import Agent, Runner, SQLiteSession, TResponseInputItem +from agents.extensions.memory.encrypt_session import EncryptedSession +from tests.fake_model import FakeModel +from tests.test_responses import get_text_message + +# Mark all tests in this file as asyncio +pytestmark = pytest.mark.asyncio + + +@pytest.fixture +def agent() -> Agent: + """Fixture for a basic agent with a fake model.""" + return Agent(name="test", model=FakeModel()) + + +@pytest.fixture +def encryption_key() -> str: + """Fixture for a valid Fernet encryption key.""" + return str(Fernet.generate_key().decode("utf-8")) + + +@pytest.fixture +def underlying_session(): + """Fixture for an underlying SQLite session.""" + temp_dir = tempfile.mkdtemp() + db_path = Path(temp_dir) / "test_encrypt.db" + return SQLiteSession("test_session", db_path) + + +async def test_encrypted_session_basic_functionality( + agent: Agent, encryption_key: str, underlying_session: SQLiteSession +): + """Test basic encryption/decryption functionality.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ttl=600, + ) + + items: list[TResponseInputItem] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + await session.add_items(items) + + retrieved = await session.get_items() + assert len(retrieved) == 2 + assert retrieved[0].get("content") == "Hello" + assert retrieved[1].get("content") == "Hi there!" + + encrypted_items = await underlying_session.get_items() + assert encrypted_items[0].get("__enc__") == 1 + assert "payload" in encrypted_items[0] + assert encrypted_items[0].get("content") != "Hello" + + underlying_session.close() + + +async def test_encrypted_session_with_runner( + agent: Agent, encryption_key: str, underlying_session: SQLiteSession +): + """Test that EncryptedSession works with Runner.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ) + + assert isinstance(agent.model, FakeModel) + agent.model.set_next_output([get_text_message("San Francisco")]) + result1 = await Runner.run( + agent, + "What city is the Golden Gate Bridge in?", + session=session, + ) + assert result1.final_output == "San Francisco" + + agent.model.set_next_output([get_text_message("California")]) + result2 = await Runner.run(agent, "What state is it in?", session=session) + assert result2.final_output == "California" + + last_input = agent.model.last_turn_args["input"] + assert len(last_input) > 1 + assert any("Golden Gate Bridge" in str(item.get("content", "")) for item in last_input) + + underlying_session.close() + + +async def test_encrypted_session_pop_item(encryption_key: str, underlying_session: SQLiteSession): + """Test pop_item functionality.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ) + + items: list[TResponseInputItem] = [ + {"role": "user", "content": "First"}, + {"role": "assistant", "content": "Second"}, + ] + await session.add_items(items) + + popped = await session.pop_item() + assert popped is not None + assert popped.get("content") == "Second" + + remaining = await session.get_items() + assert len(remaining) == 1 + assert remaining[0].get("content") == "First" + + underlying_session.close() + + +async def test_encrypted_session_clear(encryption_key: str, underlying_session: SQLiteSession): + """Test clear_session functionality.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ) + + await session.add_items([{"role": "user", "content": "Test"}]) + await session.clear_session() + + items = await session.get_items() + assert len(items) == 0 + + underlying_session.close() + + +async def test_encrypted_session_ttl_expiration( + encryption_key: str, underlying_session: SQLiteSession +): + """Test TTL expiration - expired items are silently skipped.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ttl=1, # 1 second TTL + ) + + items: list[TResponseInputItem] = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + ] + await session.add_items(items) + + time.sleep(2) + + retrieved = await session.get_items() + assert len(retrieved) == 0 + + underlying_items = await underlying_session.get_items() + assert len(underlying_items) == 2 + + underlying_session.close() + + +async def test_encrypted_session_pop_expired( + encryption_key: str, underlying_session: SQLiteSession +): + """Test pop_item with expired data.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ttl=1, + ) + + await session.add_items([{"role": "user", "content": "Test"}]) + time.sleep(2) + + popped = await session.pop_item() + assert popped is None + + underlying_session.close() + + +async def test_encrypted_session_pop_mixed_expired_valid( + encryption_key: str, underlying_session: SQLiteSession +): + """Test pop_item auto-retry with mixed expired and valid items.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ttl=2, # 2 second TTL + ) + + await session.add_items( + [ + {"role": "user", "content": "Old message 1"}, + {"role": "assistant", "content": "Old response 1"}, + ] + ) + + time.sleep(3) + + await session.add_items( + [ + {"role": "user", "content": "New message"}, + {"role": "assistant", "content": "New response"}, + ] + ) + + popped = await session.pop_item() + assert popped is not None + assert popped.get("content") == "New response" + + popped2 = await session.pop_item() + assert popped2 is not None + assert popped2.get("content") == "New message" + + popped3 = await session.pop_item() + assert popped3 is None + + underlying_session.close() + + +async def test_encrypted_session_raw_string_key(underlying_session: SQLiteSession): + """Test using raw string as encryption key (not base64).""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key="my-secret-password", # Raw string, not Fernet key + ) + + await session.add_items([{"role": "user", "content": "Test"}]) + items = await session.get_items() + assert len(items) == 1 + assert items[0].get("content") == "Test" + + underlying_session.close() + + +async def test_encrypted_session_get_items_limit( + encryption_key: str, underlying_session: SQLiteSession +): + """Test get_items with limit parameter.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ) + + items: list[TResponseInputItem] = [ + {"role": "user", "content": f"Message {i}"} for i in range(5) + ] + await session.add_items(items) + + limited = await session.get_items(limit=2) + assert len(limited) == 2 + assert limited[0].get("content") == "Message 3" # Latest 2 + assert limited[1].get("content") == "Message 4" + + underlying_session.close() + + +async def test_encrypted_session_unicode_content( + encryption_key: str, underlying_session: SQLiteSession +): + """Test encryption of international text content.""" + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ) + + items: list[TResponseInputItem] = [ + {"role": "user", "content": "Hello world"}, + {"role": "assistant", "content": "Special chars: áéíóú"}, + {"role": "user", "content": "Numbers and symbols: 123!@#"}, + ] + await session.add_items(items) + + retrieved = await session.get_items() + assert retrieved[0].get("content") == "Hello world" + assert retrieved[1].get("content") == "Special chars: áéíóú" + assert retrieved[2].get("content") == "Numbers and symbols: 123!@#" + + underlying_session.close() + + +class CustomSession(SQLiteSession): + """Mock custom session with additional methods for testing delegation.""" + + def get_stats(self) -> dict[str, int]: + """Custom method that should be accessible through delegation.""" + return {"custom_method_calls": 42, "test_value": 123} + + async def custom_async_method(self) -> str: + """Custom async method for testing delegation.""" + return "custom_async_result" + + +async def test_encrypted_session_delegation(): + """Test that custom methods on underlying session are accessible through delegation.""" + temp_dir = tempfile.mkdtemp() + db_path = Path(temp_dir) / "test_delegation.db" + underlying_session = CustomSession("test_session", db_path) + + encryption_key = str(Fernet.generate_key().decode("utf-8")) + session = EncryptedSession( + session_id="test_session", + underlying_session=underlying_session, + encryption_key=encryption_key, + ) + + stats = session.get_stats() + assert stats == {"custom_method_calls": 42, "test_value": 123} + + result = await session.custom_async_method() + assert result == "custom_async_result" + + await session.add_items([{"role": "user", "content": "Test delegation"}]) + items = await session.get_items() + assert len(items) == 1 + assert items[0].get("content") == "Test delegation" + + underlying_session.close() diff --git a/uv.lock b/uv.lock index ec64f6924..30d05c7cc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.11'", @@ -566,6 +566,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, ] +[[package]] +name = "cryptography" +version = "45.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, + { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, + { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, + { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, + { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442, upload-time = "2025-09-01T11:14:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233, upload-time = "2025-09-01T11:14:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202, upload-time = "2025-09-01T11:14:43.047Z" }, + { url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900, upload-time = "2025-09-01T11:14:45.089Z" }, + { url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562, upload-time = "2025-09-01T11:14:47.166Z" }, + { url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781, upload-time = "2025-09-01T11:14:48.747Z" }, + { url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634, upload-time = "2025-09-01T11:14:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533, upload-time = "2025-09-01T11:14:52.091Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557, upload-time = "2025-09-01T11:14:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023, upload-time = "2025-09-01T11:14:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722, upload-time = "2025-09-01T11:14:57.319Z" }, + { url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908, upload-time = "2025-09-01T11:14:58.78Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -1829,6 +1876,9 @@ dependencies = [ ] [package.optional-dependencies] +encrypt = [ + { name = "cryptography" }, +] litellm = [ { name = "litellm" }, ] @@ -1852,6 +1902,7 @@ voice = [ dev = [ { name = "aiosqlite" }, { name = "coverage" }, + { name = "cryptography" }, { name = "eval-type-backport" }, { name = "fastapi" }, { name = "graphviz" }, @@ -1877,6 +1928,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "asyncpg", marker = "extra == 'sqlalchemy'", specifier = ">=0.29.0" }, + { name = "cryptography", marker = "extra == 'encrypt'", specifier = ">=45.0,<46" }, { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.17" }, { name = "griffe", specifier = ">=1.5.6,<2" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.67.4.post1,<2" }, @@ -1891,12 +1943,13 @@ requires-dist = [ { name = "websockets", marker = "extra == 'realtime'", specifier = ">=15.0,<16" }, { name = "websockets", marker = "extra == 'voice'", specifier = ">=15.0,<16" }, ] -provides-extras = ["voice", "viz", "litellm", "realtime", "sqlalchemy"] +provides-extras = ["voice", "viz", "litellm", "realtime", "sqlalchemy", "encrypt"] [package.metadata.requires-dev] dev = [ { name = "aiosqlite", specifier = ">=0.21.0" }, { name = "coverage", specifier = ">=7.6.12" }, + { name = "cryptography", specifier = ">=45.0,<46" }, { name = "eval-type-backport", specifier = ">=0.2.2" }, { name = "fastapi", specifier = ">=0.110.0,<1" }, { name = "graphviz" },