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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.pyd binary
*.so binary
*.dll binary
69 changes: 69 additions & 0 deletions .github/workflows/build-compiled-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Build Compiled Wheels

on:
push:
tags:
- "v*.*.*"
workflow_dispatch:

permissions:
contents: read

jobs:
build-wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"

- name: Install build tooling
run: python -m pip install cibuildwheel setuptools wheel cython

- name: Build wheels
env:
CIBW_BUILD: "cp312-* cp313-*"
CIBW_SKIP: "*musllinux*"
CIBW_BEFORE_BUILD: python -m pip install cython>=3.0 setuptools>=83
CIBW_BUILD_VERBOSITY: 1
run: python -m cibuildwheel --output-dir wheelhouse

- name: Store wheels
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: compiled-wheels-${{ matrix.os }}
path: wheelhouse/*.whl

publish-compiled:
name: Publish compiled wheels to PyPI
needs: build-wheels
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read

steps:
- name: Download all compiled wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: compiled-wheels-*
path: dist/
merge-multiple: true

- name: List collected artifacts
run: ls -la dist/

- name: Publish compiled wheels to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
3 changes: 1 addition & 2 deletions engraphis/cloud_license.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def machine_id() -> str:

# ── lease token (same ENGR1-style envelope, distinct prefix) ─────────────────────────

def compose_lease(payload: dict, secret: bytes) -> str:
def compose_lease(payload: object, secret: bytes) -> str:
"""Vendor-side: sign a lease payload and bind it to its signing-key id.

The payload copy keeps the caller's object immutable. A supplied, mismatched key id
Expand Down Expand Up @@ -658,7 +658,6 @@ def revalidate(lic, key_material: str, *, base_url: Optional[str] = None) -> str

# ── compilation integrity guard — runs at import time ────────────────────────────


def _verify_module_integrity():
"""Detect if this module was replaced with editable source after a compiled
extension was already installed (same check as licensing.py)."""
Expand Down
49 changes: 48 additions & 1 deletion engraphis/licensing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import math
import os
import re
import sys
import threading
import tempfile
import time
Expand Down Expand Up @@ -200,7 +201,7 @@ def upgrade_url(plan: Optional[str] = None) -> str:
# python -m scripts.license_admin keygen --key-file <secure-offline-path>/vendor_signing.key
# Pin the printed public key through the reviewed compatibility/reissue ceremony in
# docs/COMMERCIAL_OPERATIONS.md. Do not overwrite or discard the old seed first.
_VENDOR_PUBKEY_HEX = "0f9ede880d65184f4615221d03e8127c38e1b7a8f8d789a050780ae50c36421d"
_VENDOR_PUBKEY_HEX = "88b998850710f24b0626bc7a82fa9b5a841720102d291259dbf12696cf623d23"
# Previous production verify keys live here only during an audited rotation window.
# New issuance always uses ``_VENDOR_PUBKEY_HEX``; remove retired entries after every
# customer has received a replacement and the announced grace period has elapsed.
Expand Down Expand Up @@ -982,3 +983,49 @@ def production_warnings() -> list:
"upgrade link still points at the GitHub pricing anchor, not a real checkout. "
"Set ENGRAPHIS_UPGRADE_URL to your checkout page URL before charging.")
return warns


# ── compilation integrity guard — runs at import time ────────────────────────────

def _verify_module_integrity():
"""Detect if this module was replaced with editable source after a compiled
extension was already installed.

If a compiled native extension (.pyd/.so) exists alongside this file, then
Python's default import order should have loaded that instead — the fact that
we're running as .py means someone removed or renamed the extension, likely
to patch the source. If no compiled extension exists (pure-python install on
a platform without wheels), the check passes.

``ENGRAPHIS_DEV=1`` skips the check entirely for local development.
"""
mod = sys.modules.get(__name__)
if mod is None:
return
f = getattr(mod, "__file__", "")
if not f:
return
if os.environ.get("ENGRAPHIS_DEV", "").strip() in ("1", "true", "yes"):
return
if not f.endswith(".py"):
return # running as compiled extension — all good
# Running as .py — check if a compiled extension exists alongside.
# If it does, someone replaced the extension with editable source.
from importlib.machinery import EXTENSION_SUFFIXES
dirname = os.path.dirname(f)
basename = os.path.splitext(os.path.basename(f))[0]
for suffix in EXTENSION_SUFFIXES:
if os.path.exists(os.path.join(dirname, basename + suffix)):
raise LicenseError(
"Engraphis licensing integrity check failed: a compiled native "
"extension (.pyd/.so) exists but is not being loaded — the module "
"may have been replaced with editable source. Reinstall Engraphis "
"from the official distribution (pip install --force-reinstall "
"engraphis). For development, set ENGRAPHIS_DEV=1."
)
# No compiled extension found — pure-python install on a platform without
# compiled wheels. This is less secure (the source is plaintext and patchable),
# but it's the expected fallback for platforms we don't pre-compile for.


_verify_module_integrity()
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ requires = [
"setuptools>=83.0; python_version >= '3.10'",
"setuptools>=77.0; python_version < '3.10'",
"wheel",
# Cython is needed only when compiling licensing extensions for distribution.
# Dev installs (pip install -e .) use the plain .py sources and skip Cython.
"cython>=3.0",
]
build-backend = "setuptools.build_meta"

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def find_package_modules(self, package, package_dir):
return modules
return [
m for m in modules
if (package, m[1]) not in {
if (package, m[0]) not in {
("engraphis", "licensing"),
("engraphis", "cloud_license"),
}
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import os

# The licensing + cloud_license integrity guards (added 2026-07) refuse to import from
# editable .py source in production installs. The entire test suite is development mode —
# set ENGRAPHIS_DEV=1 BEFORE any import so the guards allow the plain .py source.
os.environ.setdefault("ENGRAPHIS_DEV", "1")

import pytest
from engraphis import cloud_license, licensing
from engraphis.config import settings
Expand Down
55 changes: 55 additions & 0 deletions tests/test_compiled_integrity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Tests for the licensing + cloud_license compilation integrity guards."""

import sys

import pytest

import engraphis.licensing as lic


# ── integrity guard ──

def test_guard_skipped_in_dev_mode():
"""ENGRAPHIS_DEV=1 is set by conftest.py — the guard does nothing."""
lic._verify_module_integrity()


def test_guard_passes_when_running_as_py_without_pyd():
"""On our dev machine (no compiled .pyd/.so alongside licensing.py), the guard
passes — it's the expected fallback for pure-python installs."""
lic._verify_module_integrity()


def test_guard_raises_when_pyd_exists_alongside(monkeypatch, tmp_path):
"""If a .pyd file exists next to licensing.py, the guard fires because the
compiled extension should have been loaded instead."""
from importlib.machinery import EXTENSION_SUFFIXES
monkeypatch.delenv("ENGRAPHIS_DEV", raising=False)
py_path = tmp_path / "licensing.py"
py_path.write_text("# stub")
# Create a file matching any extension suffix (e.g. .pyd or .cp311-win_amd64.pyd)
for suffix in EXTENSION_SUFFIXES:
(tmp_path / ("licensing" + suffix)).write_text("")
break # one is enough for the guard to fire

monkeypatch.setattr(sys.modules["engraphis.licensing"], "__file__", str(py_path))
with pytest.raises(lic.LicenseError, match="integrity check failed"):
lic._verify_module_integrity()


def test_guard_passes_when_compiled_extension(monkeypatch):
"""When __file__ ends in .pyd, the guard returns immediately without checking
for a sibling .py."""
monkeypatch.delenv("ENGRAPHIS_DEV", raising=False)
monkeypatch.setattr(lic, "__file__", "/path/engraphis/licensing.cp312-win_amd64.pyd")
lic._verify_module_integrity()


# ── production warnings ──

def test_production_warnings_returns_list():
assert isinstance(lic.production_warnings(), list)


def test_upgrade_url_not_empty():
assert len(lic.upgrade_url()) > 0