Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.10.2] - 2026-01-07

### Bug Fixes

- Fix SSL certificate verification for Nuitka binaries on macOS
- Fix macOS stdlib download URL (use PyPy 3.8 v7.3.11)

## [0.10.1] - 2026-01-07

### Enhancements
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "shannot"
version = "0.10.1"
version = "0.10.2"
description = "Sandboxed system administration for LLM agents"
readme = "README.md"
license = {text = "Apache-2.0"}
Expand Down
6 changes: 3 additions & 3 deletions shannot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ def _xdg_config_home() -> Path:
"sha256": "a23d21ca0de0f613732af4b4abb0b0db1cc56134b5bf0e33614eca87ab8805af",
},
"darwin": {
"version": "7.3.17", # PyPy 3.8
"url": "https://downloads.python.org/pypy/pypy3.8-v7.3.17-src.tar.bz2",
"sha256": "7491a669e3abc3420aca0cfb58cc69f8e0defda4469f503fd6cb415ec93d6b13",
"version": "7.3.11", # PyPy 3.8
"url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-src.tar.bz2",
"sha256": "4d6769bfca73734e8666fd70503b7ceb06a6e259110e617331bb3899ca4e6058",
},
}

Expand Down
4 changes: 3 additions & 1 deletion shannot/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
get_remote_deploy_dir,
get_version,
)
from .runtime import get_ssl_context
from .ssh import SSHConnection

if TYPE_CHECKING:
Expand Down Expand Up @@ -77,7 +78,8 @@ def _download_file(url: str, dest: Path, desc: str = "Downloading") -> None:

try:
request = urllib.request.Request(url, headers={"User-Agent": "shannot/1.0"})
with urllib.request.urlopen(request) as response:
ssl_context = get_ssl_context()
with urllib.request.urlopen(request, context=ssl_context) as response:
total_size = int(response.headers.get("Content-Length", 0))
downloaded = 0

Expand Down
34 changes: 33 additions & 1 deletion shannot/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import platform
import shutil
import ssl
import sys
import tarfile
import tempfile
Expand All @@ -32,6 +33,36 @@ class SetupError(Exception):
pass


def get_ssl_context() -> ssl.SSLContext:
"""Get SSL context that works on macOS with Nuitka binaries.

Nuitka-compiled binaries on macOS can't find system SSL certificates.
This function tries multiple certificate locations.
"""
# Try default context first (works on most systems)
ctx = ssl.create_default_context()

# On macOS, try known certificate locations if default fails verification
if platform.system() == "Darwin":
# Common certificate locations on macOS
cert_paths = [
"/etc/ssl/cert.pem", # Homebrew OpenSSL
"/opt/homebrew/etc/openssl@3/cert.pem", # Homebrew ARM64
"/usr/local/etc/openssl@3/cert.pem", # Homebrew x86_64
"/opt/homebrew/etc/openssl/cert.pem",
"/usr/local/etc/openssl/cert.pem",
]
for cert_path in cert_paths:
if os.path.exists(cert_path):
try:
ctx = ssl.create_default_context(cafile=cert_path)
break
except ssl.SSLError:
continue

return ctx


def is_runtime_installed() -> bool:
"""Check if runtime is installed and valid."""
return RUNTIME_LIB_PYTHON.is_dir() and RUNTIME_LIB_PYPY.is_dir()
Expand Down Expand Up @@ -114,8 +145,9 @@ def download_with_progress(
) -> None:
"""Download URL to dest with optional progress reporting."""
request = urllib.request.Request(url, headers={"User-Agent": "shannot/1.0"})
ssl_context = get_ssl_context()

with urllib.request.urlopen(request) as response:
with urllib.request.urlopen(request, context=ssl_context) as response:
total_size = int(response.headers.get("Content-Length", 0))
downloaded = 0

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading