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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ What actually happened (include logs or error messages if possible).

**Environment**
- OS: [e.g. Ubuntu 22.04, macOS 15.0, Windows 11]
- Language version: [e.g. 3.9, 3.10, 3.11, 3.12]
- Language version: [e.g. 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14]
- Framework: [e.g. Django, Flask, FastAPI]
- Environment: [local, Docker, cloud, etc.]
- Aikido Package version: [e.g. 1.2.3]
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/Dockerfile.qa
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13.2 AS builder
FROM python:3.14.6 AS builder

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
Expand All @@ -17,7 +17,7 @@ RUN .venv/bin/pip install -r requirements.txt && \
.venv/bin/pip install ./aikido_zen-*.whl
# === END OF MODIFIED SECTION ===

FROM python:3.13.2-slim
FROM python:3.14.6-slim
WORKDIR /app
RUN apt update && apt install -y gdb procps
COPY --from=builder /app/.venv .venv/
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -88,7 +88,10 @@ jobs:
fail-fast: false
matrix:
app: [starlette-postgres-uvicorn, flask-mysql-uwsgi, flask-mysql]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
exclude:
- app: flask-mysql-uwsgi
python-version: "3.14"
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/end2end.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- { name: fastapi-postgres-uvicorn, testfile: end2end/fastapi_postgres_uvicorn_test.py }
- { name: fastapi-postgres-uvicorn-latest, testfile: end2end/fastapi_postgres_uvicorn_latest_test.py }
- { name: fastapi-postgres-uvicorn-old, testfile: end2end/fastapi_postgres_uvicorn_old_test.py }
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Install packages
run: sudo apt update && sudo apt install python3-dev libmysqlclient-dev
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
python-version: '3.14'
- name: Setup safe-chain
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/qa-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
python-version: "3.14"
- name: Setup safe-chain
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
- name: Install poetry
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smoke-test-ffi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
python-version: 3.14

- name: Display system info
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
# Don't cancel jobs if one fails
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Zen instruments the following AI SDKs to track which models are used and how man
* ✅ [`boto3`](https://pypi.org/project/boto3) (AWS Bedrock)
* ✅ [`groq`](https://pypi.org/project/groq)

Zen is compatible with Python 3.8-3.13 and can run on Windows, Linux, and Mac OS X.
Zen is compatible with Python 3.8-3.14 and can run on Windows, Linux, and Mac OS X.

## Reporting to your Aikido Security dashboard

Expand Down
3 changes: 3 additions & 0 deletions aikido_zen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# Re-export functions :
from aikido_zen.context.users import set_user
from aikido_zen.helpers.check_gevent import check_gevent
from aikido_zen.helpers.gil_not_enabled import gil_not_enabled
from aikido_zen.helpers.python_version_not_supported import python_version_not_supported
from aikido_zen.middleware import should_block_request
from aikido_zen.middleware.set_rate_limit_group import set_rate_limit_group
Expand Down Expand Up @@ -37,6 +38,8 @@ def protect(mode="daemon", token=""):
return
if python_version_not_supported():
return
if gil_not_enabled():
return
if not test_uds_file_access():
return # Unable to start background process
if check_gevent():
Expand Down
26 changes: 24 additions & 2 deletions aikido_zen/background_process/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
and listen for data sent by our sources and sinks
"""

import multiprocessing
import os
from multiprocessing import Process
import platform
import sys

from aikido_zen.helpers.token import get_token_from_env
from aikido_zen.helpers.get_temp_dir import get_temp_dir
Expand All @@ -20,6 +21,27 @@
from .aikido_background_process import AikidoBackgroundProcess


def get_process_factory():
"""
Return a process factory that is safe to start while an app is importing.

Python 3.14 changed the default POSIX start method from fork to forkserver.
Forkserver re-imports the application's main module, but Zen starts its
background process while that module is still importing.

Inspect the configured start method without setting multiprocessing's
process-wide default. If it is unset, get_all_start_methods() reports the
platform default as its first entry.
"""
if sys.version_info >= (3, 14):
start_method = multiprocessing.get_start_method(allow_none=True)
if start_method is None:
start_method = multiprocessing.get_all_start_methods()[0]
if start_method == "forkserver":
return multiprocessing.get_context("fork").Process
return multiprocessing.Process


def start_background_process():
"""
Starts a process to handle incoming/outgoing data
Expand Down Expand Up @@ -50,7 +72,7 @@ def start_background_process():
pass

# Daemon is set to True so that the process kills itself when the main process dies
background_process = Process(
background_process = get_process_factory()(
target=AikidoBackgroundProcess,
args=(comms.address, comms.key),
name="zen-agent-process",
Expand Down
93 changes: 92 additions & 1 deletion aikido_zen/background_process/init_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,99 @@
import subprocess
import sys

import aikido_zen.background_process as background_process


def test_python314_configured_forkserver_uses_fork_context(monkeypatch, mocker):
monkeypatch.setattr(background_process.sys, "version_info", (3, 14))
get_start_method = mocker.patch.object(
background_process.multiprocessing,
"get_start_method",
return_value="forkserver",
)
get_all_start_methods = mocker.patch.object(
background_process.multiprocessing,
"get_all_start_methods",
)
fork_context = mocker.patch.object(
background_process.multiprocessing,
"get_context",
).return_value

assert background_process.get_process_factory() == fork_context.Process
get_start_method.assert_called_once_with(allow_none=True)
get_all_start_methods.assert_not_called()
background_process.multiprocessing.get_context.assert_called_once_with("fork")


def test_python314_unset_forkserver_default_uses_fork_context(monkeypatch, mocker):
monkeypatch.setattr(background_process.sys, "version_info", (3, 14))
get_start_method = mocker.patch.object(
background_process.multiprocessing,
"get_start_method",
return_value=None,
)
get_all_start_methods = mocker.patch.object(
background_process.multiprocessing,
"get_all_start_methods",
return_value=["forkserver", "spawn", "fork"],
)
fork_context = mocker.patch.object(
background_process.multiprocessing,
"get_context",
).return_value

assert background_process.get_process_factory() == fork_context.Process
get_start_method.assert_called_once_with(allow_none=True)
get_all_start_methods.assert_called_once_with()
background_process.multiprocessing.get_context.assert_called_once_with("fork")


def test_python314_configured_spawn_uses_configured_context(monkeypatch, mocker):
monkeypatch.setattr(background_process.sys, "version_info", (3, 14))
get_start_method = mocker.patch.object(
background_process.multiprocessing,
"get_start_method",
return_value="spawn",
)
get_all_start_methods = mocker.patch.object(
background_process.multiprocessing,
"get_all_start_methods",
)
get_context = mocker.patch.object(
background_process.multiprocessing,
"get_context",
)

assert (
background_process.get_process_factory()
== background_process.multiprocessing.Process
)
get_start_method.assert_called_once_with(allow_none=True)
get_all_start_methods.assert_not_called()
get_context.assert_not_called()


def test_get_process_factory_does_not_set_global_start_method():
script = """
import multiprocessing

assert multiprocessing.get_start_method(allow_none=True) is None

from aikido_zen.background_process import get_process_factory

assert multiprocessing.get_start_method(allow_none=True) is None
get_process_factory()
assert multiprocessing.get_start_method(allow_none=True) is None
"""

subprocess.run([sys.executable, "-c", script], check=True)


def test_stale_socket_removed_by_another_worker(monkeypatch, mocker):
process = mocker.patch("aikido_zen.background_process.Process")
process = mocker.patch(
"aikido_zen.background_process.get_process_factory"
).return_value
monkeypatch.setenv("AIKIDO_TOKEN", "AIK_RUNTIME_TEST")
monkeypatch.setattr(background_process.platform, "system", lambda: "Linux")
monkeypatch.setattr(
Expand Down
11 changes: 11 additions & 0 deletions aikido_zen/helpers/gil_not_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sys

from aikido_zen.helpers.logging import logger


def gil_not_enabled() -> bool:
is_gil_enabled = getattr(sys, "_is_gil_enabled", None)
if is_gil_enabled is not None and not is_gil_enabled():
logger.error("Zen does not support running Python with the GIL disabled")
return True
return False
30 changes: 30 additions & 0 deletions aikido_zen/helpers/gil_not_enabled_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from aikido_zen.helpers.gil_not_enabled import gil_not_enabled


def test_gil_enabled(monkeypatch):
monkeypatch.setattr(
"aikido_zen.helpers.gil_not_enabled.sys._is_gil_enabled",
lambda: True,
raising=False,
)

assert gil_not_enabled() is False


def test_gil_disabled(monkeypatch, caplog):
monkeypatch.setattr(
"aikido_zen.helpers.gil_not_enabled.sys._is_gil_enabled",
lambda: False,
raising=False,
)

assert gil_not_enabled() is True
assert "does not support running Python with the GIL disabled" in caplog.text


def test_python_without_runtime_gil_check(monkeypatch):
monkeypatch.delattr(
"aikido_zen.helpers.gil_not_enabled.sys._is_gil_enabled", raising=False
)

assert gil_not_enabled() is False
4 changes: 2 additions & 2 deletions aikido_zen/helpers/python_version_not_supported.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def python_version_not_supported() -> bool:
if major != 3:
logger.error("This version of Zen only supports Python 3")
return True
if minor > 13:
logger.error("This version of Zen doesn't support versions above Python 3.13")
if minor > 14:
logger.error("This version of Zen doesn't support versions above Python 3.14")
return True
return False
34 changes: 34 additions & 0 deletions aikido_zen/helpers/python_version_not_supported_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from types import SimpleNamespace

from aikido_zen.helpers.python_version_not_supported import (
python_version_not_supported,
)


def test_python_3_14_is_supported(monkeypatch):
monkeypatch.setattr(
"aikido_zen.helpers.python_version_not_supported.sys.version_info",
SimpleNamespace(major=3, minor=14),
)

assert python_version_not_supported() is False


def test_python_3_15_is_not_supported(monkeypatch, caplog):
monkeypatch.setattr(
"aikido_zen.helpers.python_version_not_supported.sys.version_info",
SimpleNamespace(major=3, minor=15),
)

assert python_version_not_supported() is True
assert "doesn't support versions above Python 3.14" in caplog.text


def test_non_python_3_version_is_not_supported(monkeypatch, caplog):
monkeypatch.setattr(
"aikido_zen.helpers.python_version_not_supported.sys.version_info",
SimpleNamespace(major=4, minor=0),
)

assert python_version_not_supported() is True
assert "only supports Python 3" in caplog.text
14 changes: 14 additions & 0 deletions aikido_zen/init_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest.mock import patch

import pytest

import aikido_zen
Expand All @@ -17,3 +19,15 @@ def test_protect_with_django(monkeypatch, caplog):
def test_protect_sets_token():
aikido_zen.protect(token="MY_TOKEN_1")
assert get_token_from_env().token == "MY_TOKEN_1"


def test_protect_does_not_start_without_gil(monkeypatch):
monkeypatch.setattr("aikido_zen.gil_not_enabled", lambda: True)

with patch("aikido_zen.test_uds_file_access") as test_uds_file_access, patch(
"aikido_zen.start_background_process"
) as start_background_process:
protect()

test_uds_file_access.assert_not_called()
start_background_process.assert_not_called()
Loading
Loading