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
7 changes: 7 additions & 0 deletions ai_agents/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,16 @@ tasks:
test:
desc: run tests
cmds:
- task: test-security
- task: test-agent-extensions
- task: test-server

test-security:
desc: run security invariant tests
internal: true
cmds:
- python -m pytest tests {{ .CLI_ARGS }}

test-server:
desc: test server
dir: ./server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ def __init__(self, ten_env, uri, service_id):
self.ten_env = ten_env

async def connect(self):
# pylint: disable=protected-access
ssl_context = ssl._create_unverified_context()
ssl_context = ssl.create_default_context()
self.websocket = await websockets.connect(self.uri, ssl=ssl_context)
asyncio.create_task(
self.listen()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,6 @@ async def _process_websocket(self) -> None:
# Establish connection
headers = {"Authorization": f"Bearer {self.config.key}"}
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

session_start_time = time.time()
if self.ten_env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ async def _process_websocket(self) -> None:
# Establish connection
headers = {"Authorization": f"Bearer {self.config.api_key}"}
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

session_start_time = time.time()
if self.ten_env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,8 @@ async def start(self, timeout=10):
ws_url = self._create_url()
self._log_debug(f"Connecting to: {ws_url}")

# Create SSL context that doesn't verify certificates (similar to original)
# Create an SSL context with standard certificate verification.
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

# Connect to WebSocket with timeout
self.websocket = await websockets.connect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,8 @@ async def start(self, timeout=10):
ws_url = self._create_url()
self._log_debug(f"Connecting to: {ws_url}")

# Create SSL context that doesn't verify certificates (similar to original)
# Create an SSL context with standard certificate verification.
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

# Connect to WebSocket with timeout
self.websocket = await websockets.connect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,8 @@ async def start(self, timeout=10):
ws_url = self._create_url()
self.ten_env.log_info(f"Connecting to: {ws_url}")

# Create SSL context that doesn't verify certificates (similar to original)
# Create an SSL context with standard certificate verification.
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE

# Connect to WebSocket with timeout
self.websocket = await websockets.connect(
Expand Down
67 changes: 67 additions & 0 deletions ai_agents/tests/test_tls_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations

import ast
from pathlib import Path

EXTENSIONS_ROOT = (
Path(__file__).resolve().parents[1]
/ "agents"
/ "ten_packages"
/ "extension"
)


def _attribute_name(node: ast.AST) -> str | None:
if not isinstance(node, ast.Attribute):
return None
if isinstance(node.value, ast.Name):
return f"{node.value.id}.{node.attr}"
return node.attr


def _find_insecure_tls_settings(path: Path) -> list[str]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
violations = []

for node in ast.walk(tree):
if isinstance(node, ast.Call):
if _attribute_name(node.func) == "ssl._create_unverified_context":
violations.append(
f"{path.relative_to(EXTENSIONS_ROOT)}:{node.lineno} "
"uses an unverified SSL context"
)

if not isinstance(node, ast.Assign):
continue

for target in node.targets:
if not isinstance(target, ast.Attribute):
continue
if target.attr == "check_hostname" and isinstance(
node.value, ast.Constant
):
if node.value.value is False:
violations.append(
f"{path.relative_to(EXTENSIONS_ROOT)}:{node.lineno} "
"disables hostname verification"
)
if (
target.attr == "verify_mode"
and _attribute_name(node.value) == "ssl.CERT_NONE"
):
violations.append(
f"{path.relative_to(EXTENSIONS_ROOT)}:{node.lineno} "
"disables certificate verification"
)

return violations


def test_production_extensions_do_not_disable_tls_verification():
violations = []
for path in EXTENSIONS_ROOT.rglob("*.py"):
if "tests" in path.parts:
continue
violations.extend(_find_insecure_tls_settings(path))

assert violations == []
8 changes: 8 additions & 0 deletions docs/ai/L1/08_security.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ def to_str(self, sensitive_handling: bool = True) -> str:

Never log raw API keys, tokens, or credentials.

## TLS Verification

- Use `ssl.create_default_context()` for outbound TLS and WebSocket clients.
- Never set `check_hostname` to `False` or `verify_mode` to `ssl.CERT_NONE`.
- Never use `ssl._create_unverified_context()` for production connections.
- Add trusted private certificate authorities to the default context when a
deployment requires them; do not disable verification globally.

## Server-Side Protections

The Go server (`http_server.go`) implements:
Expand Down
Loading