From 8d92ef9480380f302fd0429695dc71463a2bd8d8 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 23 Jul 2026 11:08:18 -0700 Subject: [PATCH 1/8] fix(security): prepare neural-sdk 0.4.2 contact hotfix --- .bumpversion.cfg | 4 +-- CHANGELOG.md | 6 ++++ CODE_OF_CONDUCT.md | 4 +-- CONTRIBUTING.md | 6 ++-- docs/mint.json | 2 +- docs/openapi/authentication-schemes.yaml | 4 +-- docs/openapi/data-collection-apis.yaml | 4 +-- docs/openapi/data-models.yaml | 4 +-- docs/openapi/fix-protocol.yaml | 4 +-- docs/openapi/kalshi-trading-api.yaml | 4 +-- docs/openapi/websocket-api.yaml | 4 +-- neural/_version.py | 2 +- pyproject.toml | 7 ++--- scripts/generate_openapi_specs.py | 2 +- scripts/package_smoke.py | 6 ++-- tests/test_contact_security.py | 37 ++++++++++++++++++++++++ 16 files changed, 71 insertions(+), 29 deletions(-) create mode 100644 tests/test_contact_security.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 34347ad8..306764d7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.1 +current_version = 0.4.2 commit = True tag = True tag_name = v{new_version} @@ -9,6 +9,6 @@ message = Bump version: {current_version} → {new_version} search = version = "{current_version}" replace = version = "{new_version}" -[bumpversion:file:neural/__init__.py] +[bumpversion:file:neural/_version.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d9079d..849e1231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver ## [Unreleased] +## [0.4.2] - 2026-07-23 + +### Security +- Replaced public contact addresses on domains not controlled by the maintainers with the accountable maintainer address. +- Removed the uncontrolled contributor-domain address from package author and maintainer metadata. + ## [0.4.1] - 2026-04-12 ### Added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 7c9bb7ac..1bdb35a3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -27,7 +27,7 @@ Community leaders are responsible for clarifying and enforcing our standards of This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at contributors@neural-sdk.dev. All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at hudson@intelip.co. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. @@ -42,4 +42,4 @@ Community leaders will follow these Community Impact Guidelines in determining t ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. \ No newline at end of file +For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8d524ea..c3459990 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ This document provides guidelines and instructions for contributing to Neural SD ## Code of Conduct -This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to contributors@neural-sdk.dev. +This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to hudson@intelip.co. ## Getting Started @@ -430,6 +430,6 @@ Contributors are recognized in: - **Documentation**: https://neural-sdk.mintlify.app - **Discussions**: https://github.com/IntelIP/Neural/discussions -- **Email**: contributors@neural-sdk.dev +- **Email**: hudson@intelip.co -Thank you for contributing to Neural SDK! \ No newline at end of file +Thank you for contributing to Neural SDK! diff --git a/docs/mint.json b/docs/mint.json index e3ab967f..db1eee66 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -19,7 +19,7 @@ "topbarLinks": [ { "name": "Support", - "url": "mailto:support@neural-sdk.com" + "url": "mailto:hudson@intelip.co" } ], "topbarCtaButton": { diff --git a/docs/openapi/authentication-schemes.yaml b/docs/openapi/authentication-schemes.yaml index ed953d3f..d8a77912 100644 --- a/docs/openapi/authentication-schemes.yaml +++ b/docs/openapi/authentication-schemes.yaml @@ -30,7 +30,7 @@ info: version: 1.0.0 contact: name: Neural SDK Support - email: support@neural-sdk.com + email: hudson@intelip.co url: https://github.com/IntelIP/Neural license: name: MIT @@ -495,4 +495,4 @@ tags: - name: Authorization description: Authorization flows and permissions - name: Security - description: Security best practices and error handling \ No newline at end of file + description: Security best practices and error handling diff --git a/docs/openapi/data-collection-apis.yaml b/docs/openapi/data-collection-apis.yaml index cdd9543a..af05c264 100644 --- a/docs/openapi/data-collection-apis.yaml +++ b/docs/openapi/data-collection-apis.yaml @@ -27,7 +27,7 @@ info: version: 1.0.0 contact: name: Neural SDK Support - email: support@neural-sdk.com + email: hudson@intelip.co url: https://github.com/IntelIP/Neural license: name: MIT @@ -806,4 +806,4 @@ tags: - name: ESPN Sports Data description: Real-time sports scores and team information from ESPN API - name: Twitter API - description: Social media data and sentiment analysis from Twitter API \ No newline at end of file + description: Social media data and sentiment analysis from Twitter API diff --git a/docs/openapi/data-models.yaml b/docs/openapi/data-models.yaml index 43d84250..5dc0ccfe 100644 --- a/docs/openapi/data-models.yaml +++ b/docs/openapi/data-models.yaml @@ -25,7 +25,7 @@ info: version: 1.0.0 contact: name: Neural SDK Support - email: support@neural-sdk.com + email: hudson@intelip.co url: https://github.com/IntelIP/Neural license: name: MIT @@ -1022,4 +1022,4 @@ components: ErrorCategory: type: string enum: [validation, authentication, authorization, rate_limit, server, network] - description: Error category enumeration \ No newline at end of file + description: Error category enumeration diff --git a/docs/openapi/fix-protocol.yaml b/docs/openapi/fix-protocol.yaml index 1a239aa9..8c6316ea 100644 --- a/docs/openapi/fix-protocol.yaml +++ b/docs/openapi/fix-protocol.yaml @@ -33,7 +33,7 @@ info: version: 5.0.2 contact: name: Neural SDK Support - email: support@neural-sdk.com + email: hudson@intelip.co url: https://github.com/IntelIP/Neural license: name: MIT @@ -889,4 +889,4 @@ tags: - name: Market Data description: Real-time market data subscriptions and updates - name: System Messages - description: System-level messages and error handling \ No newline at end of file + description: System-level messages and error handling diff --git a/docs/openapi/kalshi-trading-api.yaml b/docs/openapi/kalshi-trading-api.yaml index 6f07b9ca..485c7ba8 100644 --- a/docs/openapi/kalshi-trading-api.yaml +++ b/docs/openapi/kalshi-trading-api.yaml @@ -18,7 +18,7 @@ info: version: 2.0.0 contact: name: Neural SDK Support - email: support@neural-sdk.com + email: hudson@intelip.co url: https://github.com/IntelIP/Neural license: name: MIT @@ -922,4 +922,4 @@ tags: - name: Order Management description: Order creation, modification, and cancellation - name: Historical Data - description: Historical market data for analysis \ No newline at end of file + description: Historical market data for analysis diff --git a/docs/openapi/websocket-api.yaml b/docs/openapi/websocket-api.yaml index 74f8e519..86dbf4db 100644 --- a/docs/openapi/websocket-api.yaml +++ b/docs/openapi/websocket-api.yaml @@ -26,7 +26,7 @@ info: version: 2.0.0 contact: name: Neural SDK Support - email: support@neural-sdk.com + email: hudson@intelip.co url: https://github.com/IntelIP/Neural servers: @@ -615,4 +615,4 @@ tags: - name: Trading description: Order and position update channels - name: System - description: System messages and errors \ No newline at end of file + description: System messages and errors diff --git a/neural/_version.py b/neural/_version.py index a7c1ee88..5af37041 100644 --- a/neural/_version.py +++ b/neural/_version.py @@ -1,3 +1,3 @@ """Single-source package version metadata.""" -__version__ = "0.4.1" +__version__ = "0.4.2" diff --git a/pyproject.toml b/pyproject.toml index b69c09ce..7ab963ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "neural-sdk" -version = "0.4.1" +version = "0.4.2" description = "Professional-grade SDK for algorithmic trading on prediction markets (Beta - Core features stable, advanced modules experimental)" readme = "README.md" requires-python = ">=3.10" @@ -19,11 +19,10 @@ keywords = [ ] authors = [ { name = "Hudson Aikins", email = "hudson@intelip.co" }, - { name = "Neural Contributors", email = "contributors@neural-sdk.dev" } + { name = "Neural Contributors" } ] maintainers = [ - { name = "Advanced Intellectual Labs LLC", email = "hudson@intelip.co" }, - { name = "Neural Contributors", email = "contributors@neural-sdk.dev" } + { name = "Advanced Intellectual Labs LLC", email = "hudson@intelip.co" } ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/scripts/generate_openapi_specs.py b/scripts/generate_openapi_specs.py index 89fa0d70..ff9a87af 100644 --- a/scripts/generate_openapi_specs.py +++ b/scripts/generate_openapi_specs.py @@ -19,7 +19,7 @@ def __init__(self, output_dir: Path = Path("docs/openapi")): "title": "Neural SDK API", "version": "0.3.0", "description": "REST API for Neural SDK trading and data collection functionality", - "contact": {"name": "Neural SDK Team", "email": "support@neural-sdk.com"}, + "contact": {"name": "Neural SDK Team", "email": "hudson@intelip.co"}, "license": { "name": "MIT", "url": "https://github.com/IntelIP/Neural/blob/main/LICENSE", diff --git a/scripts/package_smoke.py b/scripts/package_smoke.py index 060c6179..6cf9e99c 100644 --- a/scripts/package_smoke.py +++ b/scripts/package_smoke.py @@ -66,7 +66,7 @@ def _assert_clean_cli() -> None: text=True, check=True, ) - assert version.stdout.strip() == "0.4.1" + assert version.stdout.strip() == "0.4.2" assert version.stderr == "" doctor = subprocess.run( @@ -77,7 +77,7 @@ def _assert_clean_cli() -> None: ) assert doctor.stderr == "" payload = json.loads(doctor.stdout) - assert payload["version"] == "0.4.1" + assert payload["version"] == "0.4.2" assert "credentials" in payload assert "optional_dependencies" in payload @@ -89,7 +89,7 @@ def _assert_import_surface() -> None: import neural.data_collection as data_collection import neural.trading as trading - assert neural.__version__ == "0.4.1" + assert neural.__version__ == "0.4.2" assert neural.cli.main assert auth.__all__ and data_collection.__all__ and trading.__all__ assert "site-packages" in neural.__file__ diff --git a/tests/test_contact_security.py b/tests/test_contact_security.py new file mode 100644 index 00000000..675bc861 --- /dev/null +++ b/tests/test_contact_security.py @@ -0,0 +1,37 @@ +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +CONTACT_SURFACES = ( + "pyproject.toml", + "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md", + "docs/mint.json", + "docs/openapi/authentication-schemes.yaml", + "docs/openapi/data-collection-apis.yaml", + "docs/openapi/data-models.yaml", + "docs/openapi/fix-protocol.yaml", + "docs/openapi/kalshi-trading-api.yaml", + "docs/openapi/websocket-api.yaml", + "scripts/generate_openapi_specs.py", +) +UNCONTROLLED_CONTACT_DOMAINS = ("neural-sdk.dev", "neural-sdk.com") + + +def test_public_contact_surfaces_do_not_use_uncontrolled_domains() -> None: + violations = [] + + for relative_path in CONTACT_SURFACES: + content = (REPO_ROOT / relative_path).read_text(encoding="utf-8") + for domain in UNCONTROLLED_CONTACT_DOMAINS: + if domain in content: + violations.append(f"{relative_path}: {domain}") + + assert not violations, "Uncontrolled contact domains found: " + ", ".join(violations) + + +def test_package_metadata_uses_accountable_maintainer_contact() -> None: + metadata = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8") + + assert '{ name = "Hudson Aikins", email = "hudson@intelip.co" }' in metadata + assert '{ name = "Neural Contributors" }' in metadata + assert '{ name = "Advanced Intellectual Labs LLC", email = "hudson@intelip.co" }' in metadata From e232f140ac10a5c30279785b9affbf7e04ef1099 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 23 Jul 2026 11:15:32 -0700 Subject: [PATCH 2/8] test(security): reject mixed-case contact domains --- tests/test_contact_security.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/test_contact_security.py b/tests/test_contact_security.py index 675bc861..61734663 100644 --- a/tests/test_contact_security.py +++ b/tests/test_contact_security.py @@ -17,18 +17,31 @@ UNCONTROLLED_CONTACT_DOMAINS = ("neural-sdk.dev", "neural-sdk.com") +def _find_uncontrolled_domains(relative_path: str, content: str) -> list[str]: + normalized_content = content.casefold() + return [ + f"{relative_path}: {domain}" + for domain in UNCONTROLLED_CONTACT_DOMAINS + if domain.casefold() in normalized_content + ] + + def test_public_contact_surfaces_do_not_use_uncontrolled_domains() -> None: violations = [] for relative_path in CONTACT_SURFACES: content = (REPO_ROOT / relative_path).read_text(encoding="utf-8") - for domain in UNCONTROLLED_CONTACT_DOMAINS: - if domain in content: - violations.append(f"{relative_path}: {domain}") + violations.extend(_find_uncontrolled_domains(relative_path, content)) assert not violations, "Uncontrolled contact domains found: " + ", ".join(violations) +def test_uncontrolled_domain_detection_is_case_insensitive() -> None: + assert _find_uncontrolled_domains("synthetic.txt", "Contact EVIL@NEURAL-SDK.DEV") == [ + "synthetic.txt: neural-sdk.dev" + ] + + def test_package_metadata_uses_accountable_maintainer_contact() -> None: metadata = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8") From 6bd49b0e7ecf7db8f2dc90a346043abf52fde48e Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 23 Jul 2026 11:23:42 -0700 Subject: [PATCH 3/8] test(release): exercise optional import smoke symbols --- scripts/package_smoke.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/package_smoke.py b/scripts/package_smoke.py index 6cf9e99c..564db9da 100644 --- a/scripts/package_smoke.py +++ b/scripts/package_smoke.py @@ -46,7 +46,12 @@ def fake_import(name, globals=None, locals=None, fromlist=(), level=0): KalshiWebSocketClient, ) - for symbol in (WebSocketSource, FIXConnectionConfig, KalshiFIXClient, KalshiWebSocketClient): + for symbol in ( + WebSocketSource, + FIXConnectionConfig, + KalshiFIXClient, + KalshiWebSocketClient, + ): try: symbol() except ImportError as exc: @@ -107,7 +112,18 @@ def _assert_import_surface() -> None: TradingClient, ) - assert AuthClient and Strategy and DataSource and TradingClient + assert all( + ( + AuthClient, + Strategy, + DataSource, + WebSocketSource, + TradingClient, + FIXConnectionConfig, + KalshiFIXClient, + KalshiWebSocketClient, + ) + ) _assert_optional_dependency_contract() From 7aa6c856f5ad18b42375254fe8b52bc50cc868d4 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 23 Jul 2026 14:05:21 -0700 Subject: [PATCH 4/8] fix(release): harden package publication gates --- .github/workflows/publish.yml | 50 ++--- .github/workflows/release-dry-run.yml | 5 + scripts/validate_release.py | 259 ++++++++++++++++++++++++++ tests/test_contact_security.py | 41 +--- tests/test_release_validation.py | 175 +++++++++++++++++ 5 files changed, 461 insertions(+), 69 deletions(-) create mode 100644 scripts/validate_release.py create mode 100644 tests/test_release_validation.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 207b958f..0fccea85 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,17 @@ jobs: with: python-version: "3.11" + - name: Classify release tag + id: release + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python scripts/validate_release.py classify-tag "${RELEASE_TAG}" >> "${GITHUB_OUTPUT}" + + - name: Validate public contact surfaces + run: | + python scripts/validate_release.py validate-source . + - name: Install build tools run: | python -m pip install --upgrade pip @@ -26,6 +37,11 @@ jobs: run: | python -m build --no-isolation + - name: Validate release artifacts + run: | + twine check dist/* + python scripts/validate_release.py validate-artifacts dist/* + - name: Verify source distribution installs cleanly run: | python -m venv /tmp/neural-publish-sdist-venv @@ -36,47 +52,17 @@ jobs: /tmp/neural-publish-sdist-venv/bin/python "${GITHUB_WORKSPACE}/scripts/package_smoke.py" - name: Publish to PyPI + if: steps.release.outputs.channel == 'pypi' env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | - twine check dist/* twine upload --non-interactive --skip-existing dist/* - publish-testpypi: - runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-') - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install build tools - run: | - python -m pip install --upgrade pip - pip install build twine wheel "setuptools>=77" - - - name: Build package - run: | - python -m build --no-isolation - - - name: Verify source distribution installs cleanly - run: | - python -m venv /tmp/neural-testpypi-sdist-venv - /tmp/neural-testpypi-sdist-venv/bin/python -m pip install --upgrade pip - /tmp/neural-testpypi-sdist-venv/bin/python -m pip install "setuptools>=77" wheel - /tmp/neural-testpypi-sdist-venv/bin/python -m pip install --no-build-isolation dist/*.tar.gz - cd /tmp - /tmp/neural-testpypi-sdist-venv/bin/python "${GITHUB_WORKSPACE}/scripts/package_smoke.py" - - name: Publish to TestPyPI + if: steps.release.outputs.channel == 'testpypi' env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TESTPYPI_API_TOKEN }} run: | - twine check dist/* twine upload --repository testpypi --non-interactive --skip-existing dist/* diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index dc13af2c..57bbc935 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -19,6 +19,10 @@ jobs: with: python-version: "3.11" + - name: Validate public contact surfaces + run: | + python scripts/validate_release.py validate-source . + - name: Install build tools run: | python -m pip install --upgrade pip @@ -31,6 +35,7 @@ jobs: - name: Check package metadata run: | twine check dist/* + python scripts/validate_release.py validate-artifacts dist/* - name: Validate wheel install path run: | diff --git a/scripts/validate_release.py b/scripts/validate_release.py new file mode 100644 index 00000000..76c3a0df --- /dev/null +++ b/scripts/validate_release.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import tarfile +import zipfile +from email.parser import BytesParser +from email.policy import default +from pathlib import Path +from typing import BinaryIO + +import tomllib + +_VERSION_CORE = r"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)" +_SEMVER_IDENTIFIER = r"(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)" +_STABLE_VERSION = re.compile(rf"^{_VERSION_CORE}$") +_TEST_VERSION = re.compile( + rf"^{_VERSION_CORE}(?:(?:a|b|rc|dev)\d+|\.dev\d+" + rf"|-{_SEMVER_IDENTIFIER}(?:\.{_SEMVER_IDENTIFIER})*)$" +) + +_BLOCKED_DOMAINS = tuple(f"neural-sdk.{suffix}".encode("ascii") for suffix in ("dev", "com")) +_EXPECTED_METADATA = { + "Author-email": "Hudson Aikins ", + "Maintainer-email": "Advanced Intellectual Labs LLC ", +} +_IGNORED_SOURCE_PARTS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".venv", + "__pycache__", + "build", + "dist", + "tests", + "venv", +} + + +class ReleaseValidationError(ValueError): + """Release input failed a deterministic safety check.""" + + +def project_version(project_file: Path) -> str: + """Read the package version used to build release artifacts.""" + try: + with project_file.open("rb") as stream: + version = tomllib.load(stream)["project"]["version"] + except (OSError, KeyError, tomllib.TOMLDecodeError) as exc: + raise ReleaseValidationError( + f"cannot read project version from {project_file}: {exc}" + ) from exc + if not isinstance(version, str) or not version: + raise ReleaseValidationError(f"invalid project version in {project_file}") + return version + + +def classify_tag(tag: str, expected_version: str) -> str: + """Return the only package index allowed for a release tag.""" + if tag != f"v{expected_version}": + raise ReleaseValidationError( + f"release tag {tag!r} does not match project version {expected_version!r}" + ) + if _STABLE_VERSION.fullmatch(expected_version): + return "pypi" + if _TEST_VERSION.fullmatch(expected_version): + return "testpypi" + raise ReleaseValidationError(f"invalid project release version: {expected_version!r}") + + +def _blocked_domains_in_stream(stream: BinaryIO) -> set[str]: + found: set[str] = set() + overlap = max(map(len, _BLOCKED_DOMAINS)) - 1 + tail = b"" + + while chunk := stream.read(64 * 1024): + normalized = (tail + chunk).lower() + found.update(domain.decode("ascii") for domain in _BLOCKED_DOMAINS if domain in normalized) + tail = normalized[-overlap:] + + return found + + +def _source_files(root: Path): + for path in root.rglob("*"): + if not path.is_file(): + continue + relative = path.relative_to(root) + if any(part in _IGNORED_SOURCE_PARTS for part in relative.parts): + continue + yield relative, path + + +def validate_source(root: Path) -> None: + """Reject uncontrolled contact domains from public source surfaces.""" + violations = [] + for relative, path in _source_files(root): + with path.open("rb") as stream: + domains = _blocked_domains_in_stream(stream) + violations.extend(f"{relative}: {domain}" for domain in sorted(domains)) + + if violations: + raise ReleaseValidationError( + "uncontrolled contact domains in source: " + ", ".join(violations) + ) + + +def _validate_wheel(path: Path) -> list[str]: + violations = [] + try: + with zipfile.ZipFile(path) as archive: + for member in archive.infolist(): + if member.is_dir(): + continue + with archive.open(member) as stream: + domains = _blocked_domains_in_stream(stream) + violations.extend( + f"{path.name}:{member.filename}: {domain}" for domain in sorted(domains) + ) + except (OSError, zipfile.BadZipFile) as exc: + raise ReleaseValidationError(f"invalid wheel {path}: {exc}") from exc + return violations + + +def _validate_sdist(path: Path) -> list[str]: + violations = [] + try: + with tarfile.open(path, mode="r:*") as archive: + for member in archive: + if not member.isfile(): + continue + stream = archive.extractfile(member) + if stream is None: + continue + with stream: + domains = _blocked_domains_in_stream(stream) + violations.extend( + f"{path.name}:{member.name}: {domain}" for domain in sorted(domains) + ) + except (OSError, tarfile.TarError) as exc: + raise ReleaseValidationError(f"invalid sdist {path}: {exc}") from exc + return violations + + +def _wheel_metadata(path: Path) -> bytes: + try: + with zipfile.ZipFile(path) as archive: + names = [ + member.filename + for member in archive.infolist() + if not member.is_dir() and member.filename.endswith(".dist-info/METADATA") + ] + if len(names) != 1: + raise ReleaseValidationError( + f"expected one wheel METADATA file in {path}; found {len(names)}" + ) + return archive.read(names[0]) + except (OSError, zipfile.BadZipFile) as exc: + raise ReleaseValidationError(f"invalid wheel {path}: {exc}") from exc + + +def _sdist_metadata(path: Path) -> bytes: + try: + with tarfile.open(path, mode="r:*") as archive: + members = [ + member + for member in archive + if member.isfile() + and member.name.endswith("/PKG-INFO") + and member.name.count("/") == 1 + ] + if len(members) != 1: + raise ReleaseValidationError( + f"expected one top-level sdist PKG-INFO file in {path}; found {len(members)}" + ) + stream = archive.extractfile(members[0]) + if stream is None: + raise ReleaseValidationError(f"cannot read sdist metadata from {path}") + with stream: + return stream.read() + except (OSError, tarfile.TarError) as exc: + raise ReleaseValidationError(f"invalid sdist {path}: {exc}") from exc + + +def _validate_core_metadata(path: Path, content: bytes, expected_version: str) -> None: + metadata = BytesParser(policy=default).parsebytes(content) + expected = {"Version": expected_version, **_EXPECTED_METADATA} + mismatches = [ + f"{header}: expected {value!r}, found {metadata.get(header)!r}" + for header, value in expected.items() + if metadata.get(header) != value + ] + if mismatches: + raise ReleaseValidationError( + f"invalid core metadata in {path.name}: " + "; ".join(mismatches) + ) + + +def validate_artifacts(paths: list[Path], expected_version: str) -> None: + """Require one wheel and one sdist, both free of uncontrolled contacts.""" + violations = [] + wheels = [] + sdists = [] + + for path in paths: + if path.suffix == ".whl": + wheels.append(path) + violations.extend(_validate_wheel(path)) + elif path.name.endswith((".tar.gz", ".tgz")): + sdists.append(path) + violations.extend(_validate_sdist(path)) + else: + raise ReleaseValidationError(f"unsupported release artifact: {path}") + + if len(wheels) != 1 or len(sdists) != 1: + raise ReleaseValidationError( + f"expected one wheel and one sdist; found {len(wheels)} wheel(s) " + f"and {len(sdists)} sdist(s)" + ) + if violations: + raise ReleaseValidationError( + "uncontrolled contact domains in artifacts: " + ", ".join(violations) + ) + _validate_core_metadata(wheels[0], _wheel_metadata(wheels[0]), expected_version) + _validate_core_metadata(sdists[0], _sdist_metadata(sdists[0]), expected_version) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate Neural release inputs") + subparsers = parser.add_subparsers(dest="command", required=True) + + classify_parser = subparsers.add_parser("classify-tag") + classify_parser.add_argument("tag") + classify_parser.add_argument("--project-file", type=Path, default=Path("pyproject.toml")) + + source_parser = subparsers.add_parser("validate-source") + source_parser.add_argument("root", type=Path) + + artifact_parser = subparsers.add_parser("validate-artifacts") + artifact_parser.add_argument("artifacts", nargs="+", type=Path) + artifact_parser.add_argument("--project-file", type=Path, default=Path("pyproject.toml")) + + args = parser.parse_args() + try: + if args.command == "classify-tag": + print(f"channel={classify_tag(args.tag, project_version(args.project_file))}") + elif args.command == "validate-source": + validate_source(args.root) + else: + validate_artifacts(args.artifacts, project_version(args.project_file)) + except ReleaseValidationError as exc: + parser.exit(2, f"release validation failed: {exc}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_contact_security.py b/tests/test_contact_security.py index 61734663..19014f44 100644 --- a/tests/test_contact_security.py +++ b/tests/test_contact_security.py @@ -1,45 +1,12 @@ from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[1] -CONTACT_SURFACES = ( - "pyproject.toml", - "CONTRIBUTING.md", - "CODE_OF_CONDUCT.md", - "docs/mint.json", - "docs/openapi/authentication-schemes.yaml", - "docs/openapi/data-collection-apis.yaml", - "docs/openapi/data-models.yaml", - "docs/openapi/fix-protocol.yaml", - "docs/openapi/kalshi-trading-api.yaml", - "docs/openapi/websocket-api.yaml", - "scripts/generate_openapi_specs.py", -) -UNCONTROLLED_CONTACT_DOMAINS = ("neural-sdk.dev", "neural-sdk.com") - - -def _find_uncontrolled_domains(relative_path: str, content: str) -> list[str]: - normalized_content = content.casefold() - return [ - f"{relative_path}: {domain}" - for domain in UNCONTROLLED_CONTACT_DOMAINS - if domain.casefold() in normalized_content - ] - +from scripts.validate_release import validate_source -def test_public_contact_surfaces_do_not_use_uncontrolled_domains() -> None: - violations = [] - - for relative_path in CONTACT_SURFACES: - content = (REPO_ROOT / relative_path).read_text(encoding="utf-8") - violations.extend(_find_uncontrolled_domains(relative_path, content)) - - assert not violations, "Uncontrolled contact domains found: " + ", ".join(violations) +REPO_ROOT = Path(__file__).resolve().parents[1] -def test_uncontrolled_domain_detection_is_case_insensitive() -> None: - assert _find_uncontrolled_domains("synthetic.txt", "Contact EVIL@NEURAL-SDK.DEV") == [ - "synthetic.txt: neural-sdk.dev" - ] +def test_public_source_does_not_use_uncontrolled_domains() -> None: + validate_source(REPO_ROOT) def test_package_metadata_uses_accountable_maintainer_contact() -> None: diff --git a/tests/test_release_validation.py b/tests/test_release_validation.py new file mode 100644 index 00000000..d66da640 --- /dev/null +++ b/tests/test_release_validation.py @@ -0,0 +1,175 @@ +import io +import tarfile +import zipfile +from pathlib import Path + +import pytest + +from scripts.validate_release import ( + ReleaseValidationError, + classify_tag, + project_version, + validate_artifacts, + validate_source, +) + + +def _uncontrolled_contact(suffix: str) -> str: + return f"EVIL@NEURAL-SDK.{suffix}" + + +@pytest.mark.parametrize("tag", ["v0.4.2", "v1.0.0", "v10.20.30"]) +def test_stable_tags_route_only_to_pypi(tag: str) -> None: + assert classify_tag(tag, tag.removeprefix("v")) == "pypi" + + +@pytest.mark.parametrize( + "tag", + [ + "v0.4.2rc1", + "v0.4.2dev1", + "v0.4.2.dev1", + "v0.4.2-alpha.1", + "v0.4.2-beta.2", + "v0.4.2-dev.3", + ], +) +def test_prerelease_and_dev_tags_route_only_to_testpypi(tag: str) -> None: + assert classify_tag(tag, tag.removeprefix("v")) == "testpypi" + + +@pytest.mark.parametrize( + "tag", + [ + "0.4.2", + "v0.4", + "v01.4.2", + "v0.4.2-01", + "v0.4.2+build", + "v0.4.2.post1", + "release-v0.4.2", + ], +) +def test_invalid_tags_are_rejected(tag: str) -> None: + with pytest.raises(ReleaseValidationError): + classify_tag(tag, tag.removeprefix("v")) + + +@pytest.mark.parametrize("tag", ["v99.0.0", "v0.4.2rc1"]) +def test_tag_must_match_project_version(tag: str) -> None: + with pytest.raises(ReleaseValidationError, match="does not match project version"): + classify_tag(tag, "0.4.2") + + +def test_project_version_reads_pyproject(tmp_path: Path) -> None: + project_file = tmp_path / "pyproject.toml" + project_file.write_text('[project]\nversion = "1.2.3rc1"\n', encoding="utf-8") + + assert project_version(project_file) == "1.2.3rc1" + + +def test_source_scan_is_recursive_and_ignores_test_fixtures(tmp_path: Path) -> None: + public_doc = tmp_path / "docs" / "nested" / "contact.md" + public_doc.parent.mkdir(parents=True) + public_doc.write_text("Contact hudson@intelip.co", encoding="utf-8") + + intentional_fixture = tmp_path / "tests" / "fixtures" / "malicious.txt" + intentional_fixture.parent.mkdir(parents=True) + intentional_fixture.write_text(_uncontrolled_contact("DEV"), encoding="utf-8") + + validate_source(tmp_path) + + public_doc.write_text(_uncontrolled_contact("DEV"), encoding="utf-8") + with pytest.raises(ReleaseValidationError, match="docs/nested/contact.md"): + validate_source(tmp_path) + + +def _metadata( + *, + version: str = "0.4.2", + author: str = "Hudson Aikins ", + maintainer: str = "Advanced Intellectual Labs LLC ", +) -> bytes: + return ( + f"Metadata-Version: 2.4\nVersion: {version}\n" + f"Author-email: {author}\nMaintainer-email: {maintainer}\n" + ).encode() + + +def _write_wheel(path: Path, content: bytes) -> None: + with zipfile.ZipFile(path, mode="w") as archive: + archive.writestr("neural_sdk-0.4.2.dist-info/METADATA", content) + + +def _write_sdist(path: Path, content: bytes) -> None: + with tarfile.open(path, mode="w:gz") as archive: + member = tarfile.TarInfo("neural_sdk-0.4.2/PKG-INFO") + member.size = len(content) + archive.addfile(member, io.BytesIO(content)) + + +def test_clean_wheel_and_sdist_pass(tmp_path: Path) -> None: + wheel = tmp_path / "neural_sdk-0.4.2-py3-none-any.whl" + sdist = tmp_path / "neural_sdk-0.4.2.tar.gz" + _write_wheel(wheel, _metadata()) + _write_sdist(sdist, _metadata()) + + validate_artifacts([wheel, sdist], "0.4.2") + + +@pytest.mark.parametrize("artifact_type", ["wheel", "sdist"]) +def test_mixed_case_uncontrolled_contacts_block_artifacts( + tmp_path: Path, artifact_type: str +) -> None: + wheel = tmp_path / "neural_sdk-0.4.2-py3-none-any.whl" + sdist = tmp_path / "neural_sdk-0.4.2.tar.gz" + _write_wheel( + wheel, + _metadata() + + ( + f"Contact: {_uncontrolled_contact('DEV')}".encode() if artifact_type == "wheel" else b"" + ), + ) + _write_sdist( + sdist, + _metadata() + + ( + f"Contact: {_uncontrolled_contact('COM')}".encode() if artifact_type == "sdist" else b"" + ), + ) + + with pytest.raises(ReleaseValidationError, match="uncontrolled contact domains"): + validate_artifacts([wheel, sdist], "0.4.2") + + +@pytest.mark.parametrize( + ("wheel_metadata", "sdist_metadata", "match"), + [ + (_metadata(version="9.9.9"), _metadata(), "Version"), + (_metadata(author="Attacker "), _metadata(), "Author-email"), + (_metadata(), _metadata(maintainer="Attacker "), "Maintainer-email"), + (_metadata(author=""), _metadata(), "Author-email"), + ], +) +def test_artifact_metadata_must_match_release_contract( + tmp_path: Path, + wheel_metadata: bytes, + sdist_metadata: bytes, + match: str, +) -> None: + wheel = tmp_path / "neural_sdk-0.4.2-py3-none-any.whl" + sdist = tmp_path / "neural_sdk-0.4.2.tar.gz" + _write_wheel(wheel, wheel_metadata) + _write_sdist(sdist, sdist_metadata) + + with pytest.raises(ReleaseValidationError, match=match): + validate_artifacts([wheel, sdist], "0.4.2") + + +def test_publish_workflow_has_mutually_exclusive_uploads() -> None: + workflow = Path(".github/workflows/publish.yml").read_text(encoding="utf-8") + + assert "steps.release.outputs.channel == 'pypi'" in workflow + assert "steps.release.outputs.channel == 'testpypi'" in workflow + assert workflow.count("twine upload") == 2 + assert workflow.index("validate-artifacts") < workflow.index("twine upload") From c4377a42b717229d49d600a25502584a6a2ac370 Mon Sep 17 00:00:00 2001 From: hudsonaikins-crown Date: Thu, 23 Jul 2026 18:16:15 -0700 Subject: [PATCH 5/8] fix(release): address candidate review findings --- MANIFEST.in | 4 +++- scripts/validate_release.py | 25 ++++++++++++++++++++----- tests/test_release_validation.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 1f6eb194..79eb2f52 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,6 @@ include CHANGELOG.md recursive-include docs * recursive-include neural *.py include neural/py.typed -global-exclude *.py[cod] __pycache__ *.so *.dylib \ No newline at end of file +exclude tests/test_contact_security.py +exclude tests/test_release_validation.py +global-exclude *.py[cod] __pycache__ *.so *.dylib diff --git a/scripts/validate_release.py b/scripts/validate_release.py index 76c3a0df..3d50ca6c 100644 --- a/scripts/validate_release.py +++ b/scripts/validate_release.py @@ -10,15 +10,20 @@ from pathlib import Path from typing import BinaryIO -import tomllib +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - exercised by Python 3.10 CI + import tomli as tomllib _VERSION_CORE = r"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)" -_SEMVER_IDENTIFIER = r"(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)" _STABLE_VERSION = re.compile(rf"^{_VERSION_CORE}$") _TEST_VERSION = re.compile( - rf"^{_VERSION_CORE}(?:(?:a|b|rc|dev)\d+|\.dev\d+" - rf"|-{_SEMVER_IDENTIFIER}(?:\.{_SEMVER_IDENTIFIER})*)$" + rf"^{_VERSION_CORE}(?:(?:a|b|rc)\d+|(?:\.?dev)\d+" rf"|-(?:alpha|beta|rc|dev)\.(?:0|[1-9]\d*))$" ) +_VERSION_ALIAS = re.compile( + rf"^(?P{_VERSION_CORE})-(?P