Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
20c1c08
ci: add security-aware Dependabot updates for Python and GitHub Actions
HAYDEN-OAI Aug 17, 2026
4688d3b
fix: track Python lockfiles and pinned Actions securely
HAYDEN-OAI Aug 17, 2026
42e7f73
fix: preserve release marker in uv dependency lock
HAYDEN-OAI Aug 17, 2026
9eacb6d
fix: use Python 3.11 for dependency lock policy checks
HAYDEN-OAI Aug 17, 2026
05d03fc
Merge main and reconcile security dependency locks
HAYDEN-OAI Aug 18, 2026
253692a
Merge main and preserve uv-native dependency security
HAYDEN-OAI Aug 19, 2026
86b9b62
fix: reject untrusted uv lockfile dependency sources
HAYDEN-OAI Aug 19, 2026
6af5be9
fix: gate dependency installs on lock provenance
HAYDEN-OAI Aug 19, 2026
69acefe
fix: validate dependency artifacts and security update floors
HAYDEN-OAI Aug 19, 2026
70d1ffa
Merge main into dependency security policy updates
HAYDEN-OAI Aug 19, 2026
3e188b8
fix:enforce-strict-dependabot-security-dependency-floors
HAYDEN-OAI Aug 19, 2026
0b3f84b
fix: validate dependency floors against patched lock releases
HAYDEN-OAI Aug 19, 2026
d5e8f30
fix: reject weakened security floors with unchanged locks
HAYDEN-OAI Aug 19, 2026
05d1c29
fix: preserve contextual security floors and reject lock downgrades
HAYDEN-OAI Aug 19, 2026
2a4d89e
fix: validate root build provenance and preserve security constraints
HAYDEN-OAI Aug 19, 2026
dd2e769
fix: block dependency source builds and preserve marker-specific secu…
HAYDEN-OAI Aug 19, 2026
3860753
fix(ci): permit reviewed editable roots without building dependencies
HAYDEN-OAI Aug 19, 2026
27b0b83
fix(ci): reject direct dependency removals in security updates
HAYDEN-OAI Aug 19, 2026
2b16120
fix(ci): anchor fork build backends to the trusted base
HAYDEN-OAI Aug 19, 2026
4d25ed8
fix(ci): require protected floors to reach patched releases
HAYDEN-OAI Aug 19, 2026
f1f729e
fix(ci): preserve marker-specific security floors and trusted source …
HAYDEN-OAI Aug 19, 2026
eb134b8
fix(ci): defer source builds and support dependency marker membership
HAYDEN-OAI Aug 19, 2026
9071f2b
fix: preserve supported dependency security branches
HAYDEN-OAI Aug 19, 2026
05c414f
fix(security): require safe published dependency branches
HAYDEN-OAI Aug 19, 2026
f012570
fix(security): preserve bounds when dependency locks are unchanged
HAYDEN-OAI Aug 19, 2026
1ef8c69
fix(security): preserve all reviewed dependency bounds
HAYDEN-OAI Aug 20, 2026
a0049ec
fix(security): anchor dependency checks to immutable trusted bases
HAYDEN-OAI Aug 20, 2026
7a3b31a
fix(security): preserve reviewed exact dependency pins
HAYDEN-OAI Aug 20, 2026
95769f4
fix(security): require boundaries for transitive dependency patches
HAYDEN-OAI Aug 20, 2026
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
27 changes: 27 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,41 @@ updates:
default-days: 8
- package-ecosystem: uv
directory: /
versioning-strategy: increase-if-necessary
Comment thread
HAYDEN-OAI marked this conversation as resolved.
schedule:
interval: weekly
day: monday
time: "09:00"
timezone: Etc/UTC
# Version updates wait; Dependabot security updates are exempt.
cooldown:
default-days: 8
open-pull-requests-limit: 3
groups:
python-maintenance:
applies-to: version-updates
patterns:
- "*"
update-types:
- minor
- patch
python-security:
applies-to: security-updates
patterns:
- "*"

- package-ecosystem: github-actions
directory: /
Comment thread
HAYDEN-OAI marked this conversation as resolved.
schedule:
interval: weekly
day: monday
time: "09:30"
timezone: Etc/UTC
cooldown:
default-days: 8
open-pull-requests-limit: 5
groups:
# CodeQL init/analyze share versioned state and must change together.
codeql:
patterns:
- "github/codeql-action"
58 changes: 57 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,58 @@ on:
permissions:
contents: read

env:
UV_NO_BUILD: '1'
Comment thread
HAYDEN-OAI marked this conversation as resolved.
UV_NO_BINARY_PACKAGE: 'openai'
UNTRUSTED_BUILD_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && '1' || '0' }}
TRUSTED_BUILD_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}

jobs:
dependency-locks:
Comment thread
HAYDEN-OAI marked this conversation as resolved.
timeout-minutes: 10
name: dependency lock freshness
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Verify dependency source provenance before installing tools
run: |
python -I -c 'import ast, os, pathlib, re, subprocess, tomllib; config = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); fork = os.environ.get("UNTRUSTED_BUILD_FORK", "0") == "1"; base = os.environ.get("TRUSTED_BUILD_BASE_SHA", ""); assert not fork or re.fullmatch(r"[0-9a-f]{40}", base), "Untrusted fork build base commit"; assert not fork or subprocess.check_output(["git", "remote", "get-url", "origin"], text=True).strip() in {"https://github.com/openai/openai-python", "https://github.com/openai/openai-python.git"}, "Untrusted fork build base origin"; subprocess.run(["git", "fetch", "--no-tags", "--depth=1", "origin", base], check=True) if fork else None; trusted = tomllib.loads(subprocess.check_output(["git", "show", base + ":pyproject.toml"], text=True)) if fork else config; project = config["project"]; lock = tomllib.loads(pathlib.Path("uv.lock").read_text()); trusted_lock = tomllib.loads(subprocess.check_output(["git", "show", base + ":uv.lock"], text=True)) if fork else lock; roots = [package for package in lock["package"] if package.get("name") == project["name"] and package.get("version") == project["version"] and package.get("source") == {"editable": "."}]; build = config.get("build-system", {}); uv = config.get("tool", {}).get("uv", {}); requires = build.get("requires", []); constraints = uv.get("build-constraint-dependencies", []); reviewed = config.get("dependency-groups", {}).get("build", []); canonical = lambda value: re.sub(r"[-_.]+", "-", value).lower(); package_identity = lambda package: (canonical(package["name"]), package["version"], package.get("source", {}).get("registry", ""), tuple(sorted((artifact["url"], artifact["hash"]) for artifact in (([package["sdist"]] if "sdist" in package else []) + package.get("wheels", []))))); normalize = lambda value: re.sub(r"\s+", "", value); identity = lambda value: (canonical((item := re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s*==\s*([0-9]+(?:\.[0-9]+)*(?:\.post[0-9]+)?)(?:\s*;\s*(.+))?", value)).group(1)), item.group(2), ast.dump(ast.parse(item.group(3), mode="eval")) if item.group(3) is not None else ""); assert len(roots) == 1 and canonical(project["name"]) == "openai" and sum(canonical(package.get("name", "")) == "openai" for package in lock["package"]) == 1 and all(package is roots[0] or package.get("source") == {"registry": "https://pypi.org/simple"} and isinstance(package.get("wheels"), list) and bool(package.get("wheels")) and (artifacts := ([package["sdist"]] if "sdist" in package else []) + package.get("wheels", [])) and all(isinstance(artifact, dict) and re.fullmatch(r"https://files\.pythonhosted\.org/packages/[^\s?#]+", artifact.get("url", "")) and re.fullmatch(r"sha256:[0-9a-f]{64}", artifact.get("hash", "")) for artifact in artifacts) for package in lock["package"]) and (not fork or sorted(package_identity(package) for package in lock["package"] if package is not roots[0]) == sorted(package_identity(package) for package in trusted_lock["package"] if package.get("source") != {"editable": "."})) and build.get("build-backend") == "hatchling.build" and "backend-path" not in build and isinstance(requires, list) and bool(requires) and isinstance(constraints, list) and bool(constraints) and isinstance(reviewed, list) and bool(reviewed) and not any(uv.get(key) for key in ("sources", "index", "indexes", "index-url", "extra-index-url", "default-index", "find-links", "index_url", "extra_index_url", "default_index", "find_links", "no-binary", "no-binary-package", "no_binary", "no_binary_package")) and not any(pathlib.Path(path).exists() for path in ("uv.toml", ".uv.toml")) and all(isinstance(requirement, str) and (match := re.fullmatch(r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s*==\s*([0-9]+(?:\.[0-9]+)*(?:\.post[0-9]+)?)(?:\s*;\s*(.+))?", requirement)) is not None and (match.group(3) is None or isinstance(marker := ast.parse(match.group(3), mode="eval").body, ast.Compare) and isinstance(marker.left, ast.Name) and len(marker.ops) == 1 and type(marker.ops[0]) in {ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn} and len(marker.comparators) == 1 and isinstance(marker.comparators[0], ast.Constant) and isinstance(marker.comparators[0].value, str)) and any(canonical(package.get("name", "")) == canonical(match.group(1)) and package.get("version") == match.group(2) and package.get("source") == {"registry": "https://pypi.org/simple"} for package in lock["package"]) for requirement in requires + constraints + reviewed) and {normalize(requirement) for requirement in constraints} == {normalize(requirement) for requirement in reviewed} and {normalize(requirement) for requirement in requires}.issubset({normalize(requirement) for requirement in constraints}) and any(canonical(requirement.split("==", 1)[0].strip()) == "hatchling" for requirement in requires) and build.get("build-backend") == trusted.get("build-system", {}).get("build-backend") and {identity(requirement) for requirement in requires} == {identity(requirement) for requirement in trusted.get("build-system", {}).get("requires", [])} and {identity(requirement) for requirement in constraints} == {identity(requirement) for requirement in trusted.get("tool", {}).get("uv", {}).get("build-constraint-dependencies", [])} and {identity(requirement) for requirement in reviewed} == {identity(requirement) for requirement in trusted.get("dependency-groups", {}).get("build", [])}, "Use only the public PyPI registry, reviewed artifacts, the exact editable root project, and locked reviewed build requirements in uv.lock"'

- name: Require published minimums for direct security updates
if: >-
github.event_name == 'pull_request'
&& github.event.pull_request.user.login == 'dependabot[bot]'
&& contains(github.event.pull_request.head.ref, 'python-security')
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
[[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]]
origin="$(git remote get-url origin)"
[[ "$origin" == "https://github.com/openai/openai-python" || "$origin" == "https://github.com/openai/openai-python.git" ]]
git fetch --no-tags --depth=1 origin "$BASE_SHA"
git show "$BASE_SHA:scripts/check-dependency-security.py" | python -I -

Comment on lines +52 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run the guard from a base-controlled workflow

Fresh evidence beyond loading the checker from BASE_SHA is that the invocation itself remains part of the submitted pull_request workflow. A maintainer update to a Dependabot-authored security branch can delete this step or weaken its if expression while the PR author remains dependabot[bot], so the immutable checker is never loaded and uv lock --check alone accepts weakened published security boundaries. Make the required check originate from trusted base-controlled automation rather than relying on a guard that the guarded revision can modify.

AGENTS.md reference: AGENTS.md:L21-L24

Useful? React with 👍 / 👎.

- name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: '0.12.5'
enable-cache: true

- name: Verify dependency groups and committed lock
run: |
uv run --no-project --python '>=3.11' python -c 'import pathlib, tomllib; project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); assert project["dependency-groups"]["dev"] and "dev" in project["tool"]["uv"]["default-groups"], "Keep the uv development dependency group enabled"'
uv run --no-project --python '>=3.11' python -c 'import pathlib, tomllib; project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()); text = pathlib.Path("uv.lock").read_text(); lock = tomllib.loads(text); version = project["project"]["version"]; assert next(package["version"] for package in lock["package"] if package["name"] == "openai") == version and "version = " + chr(34) + version + chr(34) + " # x-release-please-version" in text, "Preserve the uv.lock release-please marker and project version"'
uv lock --check

lint:
timeout-minutes: 10
name: lint
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -44,6 +91,7 @@ jobs:
run: ./scripts/lint

build:
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
timeout-minutes: 10
name: build
Expand All @@ -65,7 +113,9 @@ jobs:
run: uv sync --locked --all-extras

- name: Run build
run: ./scripts/build
run: |
external_packages="$(python -c 'import importlib, re, sys; parser = importlib.import_module("tomllib" if sys.version_info >= (3, 11) else "tomli"); packages = parser.loads(open("uv.lock").read())["package"]; roots = [package for package in packages if package.get("source") == {"editable": "."}]; external = [package for package in packages if package not in roots]; assert len(roots) == 1 and roots[0].get("name") == "openai" and external and all(package.get("source") == {"registry": "https://pypi.org/simple"} and isinstance(package.get("name"), str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", package["name"]) for package in external), "Refuse malformed external build package restrictions"; denied = sorted({re.sub(r"[-_.]+", "-", package["name"]).lower() for package in external}); assert denied and "openai" not in denied, "Refuse incomplete external build package restrictions"; print(" ".join(denied))')"
env -u UV_NO_BUILD UV_NO_BUILD_PACKAGE="$external_packages" ./scripts/build

- name: Validate Python version metadata
run: uv run --locked --all-extras python scripts/utils/validate-python-version-wheel.py
Expand Down Expand Up @@ -96,6 +146,7 @@ jobs:
timeout-minutes: 15
name: test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
strategy:
fail-fast: false
Expand Down Expand Up @@ -132,6 +183,7 @@ jobs:
timeout-minutes: 20
name: test (HTTPX2)
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'push' || github.event_name == 'merge_group' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand Down Expand Up @@ -167,6 +219,7 @@ jobs:
name: examples
environment: ci
runs-on: ubuntu-latest
needs: dependency-locks
if: github.repository == 'openai/openai-python' && github.event_name == 'push' && github.ref == 'refs/heads/main'

steps:
Expand Down Expand Up @@ -209,8 +262,11 @@ jobs:
timeout-minutes: 20
name: compatibility (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
needs: dependency-locks
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
continue-on-error: ${{ matrix.experimental }}
env:
UV_NO_BUILD: ${{ matrix.experimental && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '0' || '1' }}
strategy:
fail-fast: false
matrix:
Expand Down
Loading
Loading