diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index d826ce67a..83f6830d5 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -873,6 +873,8 @@ jobs: GITHUB_STEP_SUMMARY=/dev/null \ BASH_ENV=/dev/null \ UV_NO_BUILD=1 \ + GIT_CONFIG_NOSYSTEM=1 \ + GIT_CONFIG_GLOBAL=/dev/null \ GIT_CONFIG_COUNT=1 \ GIT_CONFIG_KEY_0=safe.directory \ GIT_CONFIG_VALUE_0=/work \ @@ -932,6 +934,8 @@ jobs: GITHUB_STEP_SUMMARY=/dev/null \ BASH_ENV=/dev/null \ UV_NO_BUILD=1 \ + GIT_CONFIG_NOSYSTEM=1 \ + GIT_CONFIG_GLOBAL=/dev/null \ GIT_CONFIG_COUNT=1 \ GIT_CONFIG_KEY_0=safe.directory \ GIT_CONFIG_VALUE_0=/work \ @@ -991,6 +995,8 @@ jobs: GITHUB_STEP_SUMMARY=/dev/null \ BASH_ENV=/dev/null \ UV_NO_BUILD=1 \ + GIT_CONFIG_NOSYSTEM=1 \ + GIT_CONFIG_GLOBAL=/dev/null \ GIT_CONFIG_COUNT=1 \ GIT_CONFIG_KEY_0=safe.directory \ GIT_CONFIG_VALUE_0=/work \ diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml index 8ecb5185b..331de634f 100644 --- a/.github/workflows/scheduled-security-scan.yml +++ b/.github/workflows/scheduled-security-scan.yml @@ -90,13 +90,13 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis continue-on-error: true - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: category: "/language:${{ matrix.language }}-scheduled" @@ -131,7 +131,7 @@ jobs: - name: Upload Trivy SARIF to code scanning if: always() && hashFiles('trivy-results.sarif') != '' continue-on-error: true - uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/upload-sarif@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: sarif_file: trivy-results.sarif category: trivy-fs-scheduled diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 7a39127d0..03ec23257 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -443,7 +443,7 @@ jobs: - name: Gate Strix secrets id: gate env: - STRIX_MODEL: ${{ github.event.client_payload.strix_llm || (steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna') }} + STRIX_MODEL: ${{ github.event.client_payload.strix_llm || (steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-super-120b-a12b' || 'gpt-5.6-luna') }} STRIX_MODEL_REQUESTED: ${{ github.event.client_payload.strix_llm || '' }} STRIX_OPENAI_API_KEY: ${{ secrets.STRIX_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} STRIX_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} @@ -453,7 +453,7 @@ jobs: TARGET_REPOSITORY_PRIVATE: ${{ steps.target_visibility.outputs.is_private }} run: | strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]; then + if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-super-120b-a12b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]; then strix_model="gpt-5.6-luna" fi echo "strix_model=$strix_model" >> "$GITHUB_OUTPUT" @@ -497,7 +497,7 @@ jobs: exit 1 fi ;; - nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b) + nvidia_nim/nvidia/nemotron-3-super-120b-a12b) if [ "$TARGET_REPOSITORY_PRIVATE" != "false" ]; then echo '::error::NVIDIA NIM hosted trial scans are limited to public repositories.' exit 1 @@ -763,7 +763,7 @@ jobs: openrouter/free | openrouter/openrouter/free) printf '%s' 'openrouter/free' > "$strix_llm_file" ;; - nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b) + nvidia_nim/nvidia/nemotron-3-super-120b-a12b) printf '%s' "$strix_model" > "$strix_llm_file" ;; vertex_ai/gemini-3.1-pro-preview-customtools | vertex_ai/gemini-2.5-flash) @@ -807,7 +807,7 @@ jobs: STRIX_LLM_MAX_RETRIES: 1 STRIX_TRANSIENT_RETRY_PER_MODEL: 2 STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS: 60 - STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openrouter' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'nvidia_nim' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }} + STRIX_FALLBACK_MODELS: ${{ steps.gate.outputs.provider_mode == 'github_models' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'openrouter' && 'github_models/openai/o3 github_models/openai/gpt-5-chat' || steps.gate.outputs.provider_mode == 'nvidia_nim' && 'nvidia_nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 github_models/openai/o3 github_models/openai/gpt-5-chat' || '' }} STRIX_GITHUB_MODELS_API_BASE_FILE: ${{ env.STRIX_GITHUB_MODELS_API_BASE_FILE }} STRIX_GITHUB_MODELS_KEY_FILE: ${{ env.STRIX_GITHUB_MODELS_KEY_FILE }} STRIX_FAIL_ON_PROVIDER_SIGNAL: "1" @@ -861,7 +861,7 @@ jobs: fi # Recognized signals that the LLM backend was unavailable / starved. - backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure' + backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404' # Any evidence that a vulnerability was actually reported. Its presence # forces a hard failure so real findings are NEVER downgraded. Keep the # severity branch anchored away from identifiers so environment lines diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml new file mode 100644 index 000000000..95642b55c --- /dev/null +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -0,0 +1,165 @@ +name: Trusted uv Materializer Quality CI + +on: + pull_request: + branches: [main] + paths: + - ".github/workflows/trusted-uv-materializer-quality-ci.yml" + - "scripts/ci/materialize_base_python_requirements.py" + - "tests/conftest.py" + - "tests/test_materialize*.py" + - "tests/test_trusted_uv*.py" + - "tests/test_uv*.py" + - "tests/test_repository_branch_coverage_*.py" + - "requirements-opencode-review-ci-hashes.txt" + - "pyproject.toml" + push: + branches: [main] + paths: + - ".github/workflows/trusted-uv-materializer-quality-ci.yml" + - "scripts/ci/materialize_base_python_requirements.py" + - "tests/conftest.py" + - "tests/test_materialize*.py" + - "tests/test_trusted_uv*.py" + - "tests/test_uv*.py" + - "tests/test_repository_branch_coverage_*.py" + - "requirements-opencode-review-ci-hashes.txt" + - "pyproject.toml" + +concurrency: + group: trusted-uv-materializer-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + minimum-python-contract: + name: Python 3.10 compatibility contract + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up minimum supported Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.10" + + - name: Compile production on Python 3.10 + run: python -m compileall -q scripts/ci/materialize_base_python_requirements.py + + - name: Exercise the conditional tomli import + run: | + python - <<'PY' + import sys + import tempfile + from pathlib import Path + + stub_root = Path(tempfile.mkdtemp(prefix="trusted-uv-tomli-stub-")) + (stub_root / "tomli.py").write_text( + "STUB_MARKER = True\n" + "class TOMLDecodeError(ValueError):\n" + " pass\n" + "def loads(_value):\n" + " return {}\n", + encoding="utf-8", + ) + sys.path.insert(0, str(stub_root)) + from scripts.ci import materialize_base_python_requirements as materializer + + assert materializer.tomllib.STUB_MARKER is True + PY + + full-quality-gate: + name: Python 3.14 full quality gate + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up current stable Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: requirements-opencode-review-ci-hashes.txt + + - name: Install hash-locked quality tooling + run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt + + - name: Run trusted uv tests with complete branch coverage + run: | + cat >"${RUNNER_TEMP}/trusted-uv-coveragerc" <<'EOF' + [run] + branch = True + include = + scripts/ci/materialize_base_python_requirements.py + + [report] + fail_under = 100 + show_missing = True + EOF + export COVERAGE_RCFILE="${RUNNER_TEMP}/trusted-uv-coveragerc" + python -m coverage erase + python -m coverage run -m pytest \ + tests/test_materialize_base_python_requirements.py \ + tests/test_materialize_uv_export_hash_contract.py \ + tests/test_trusted_uv_download_contract.py \ + tests/test_trusted_uv_portability_and_streaming.py \ + tests/test_uv_export_isolation_contract.py \ + tests/test_uv_redirect_and_coverage_contract.py \ + tests/test_uv_redirect_boundary.py \ + tests/test_uv_workspace_fail_closed.py \ + tests/test_trusted_uv_materializer_quality_workflow_contract.py \ + -q + python -m coverage report + + - name: Run complete central test and branch coverage gate + run: | + unset COVERAGE_RCFILE + python -m coverage erase + python -m coverage run -m pytest tests -q + python -m coverage report + + - name: Enforce complete production docstrings + run: python -m interrogate --fail-under 100 scripts/ci/materialize_base_python_requirements.py + + - name: Compile production and quality contracts + run: | + python -m compileall -q \ + scripts/ci/materialize_base_python_requirements.py \ + tests/test_materialize_base_python_requirements.py \ + tests/test_materialize_uv_export_hash_contract.py \ + tests/test_trusted_uv_download_contract.py \ + tests/test_trusted_uv_portability_and_streaming.py \ + tests/test_uv_export_isolation_contract.py \ + tests/test_uv_redirect_and_coverage_contract.py \ + tests/test_uv_redirect_boundary.py \ + tests/test_uv_workspace_fail_closed.py \ + tests/test_trusted_uv_materializer_quality_workflow_contract.py \ + tests/test_repository_branch_coverage_javascript_and_noema.py \ + tests/test_repository_branch_coverage_review_schedulers.py \ + tests/test_repository_branch_coverage_execution_sandboxes.py \ + tests/test_repository_branch_coverage_reporting_edges.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e601de81b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to the organization automation repository are documented in +this file. The format follows Keep a Changelog, and versioned releases follow +Semantic Versioning where the repository publishes a release. + +## [Unreleased] + +### Added + +- Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. + +### Fixed + +- Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. +- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities. diff --git a/docs/doctoring/strix-nvidia-nim-not-found-fallback.md b/docs/doctoring/strix-nvidia-nim-not-found-fallback.md new file mode 100644 index 000000000..70299ebdf --- /dev/null +++ b/docs/doctoring/strix-nvidia-nim-not-found-fallback.md @@ -0,0 +1,76 @@ +# Strix NVIDIA NIM model-catalog fallback: evidence and design record + +## Decision + +Strix treats an authenticated NVIDIA NIM model-catalog `404 Not Found` as +provider availability evidence, not as a target-application vulnerability. The +gate does not retry the same unavailable model. It proceeds to a distinct +reviewed NVIDIA hosted model and only then to the existing GitHub Models +candidates. + +Public-repository scans now default to +`nvidia/nemotron-3-super-120b-a12b`. The first fallback is +`nvidia/llama-3.3-nemotron-super-49b-v1.5`. Private repositories retain the +contracted provider because NVIDIA hosted trial inputs are restricted to public +repositories by the central workflow. + +## Trust boundary + +The NVIDIA classifier accepts only a single bounded log line that contains all +three signals: a LiteLLM `NotFoundError`, NVIDIA NIM provider context, and +model-catalog not-found evidence. It does not assemble provider and `404` +signals from different lines. A bare application `404`, route miss, database +lookup miss, provider-like source literal, or other target-controlled output is +not enough to enter model fallback. + +This same-line rule matters because scanner stdout can include text derived from +the repository under review. Requiring the trusted LiteLLM exception marker and +all provider-availability evidence on one line prevents repository content from +combining with an unrelated application `404` to spoof infrastructure fallback. +Provider-side failure also remains a fail-closed incomplete scan until a distinct +fallback produces complete evidence. + +The outer workflow may classify exhausted provider infrastructure as neutral only +when the run log contains no vulnerability signal. Any reported severity or +non-zero vulnerability count remains blocking. Scanner reports and attempt logs +remain available as artifacts. + +## Verification contract + +Regression evidence proves that: + +1. the exact LiteLLM `Nvidia_nimException` 404 observed in required CI is + recognized; +2. an ordinary application 404 is not recognized; +3. provider context and 404 evidence on different lines are not recognized; +4. a provider-like source literal on one line without LiteLLM `NotFoundError` + context is not recognized; +5. model-catalog 404s enter cross-model fallback but never same-model retry; +6. the primary and first fallback are current NVIDIA hosted models; +7. GitHub Models remain later cross-provider fallbacks; +8. vulnerability signals prevent neutral infrastructure classification; and +9. the required-workflow smoke contract pins these properties. + +## Limitations + +Hosted model catalogs may change independently of this repository. A model-card +page or supported self-hosted NIM container does not guarantee indefinite hosted +trial availability. The ordered model plan must therefore be reviewed against +current NVIDIA documentation whenever a provider returns a catalog 404. This +change does not treat arbitrary provider errors as success and does not weaken +Strix severity, changed-file attribution, or independent approval requirements. + +## References + +Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC +9110). Internet Engineering Task Force. https://doi.org/10.17487/RFC9110 + +NVIDIA Corporation. (2025). *Llama-3.3-Nemotron-Super-49B-v1.5* [Model card]. +NVIDIA NIM. https://build.nvidia.com/nvidia/llama-3_3-nemotron-super-49b-v1_5/modelcard + +NVIDIA Corporation. (2026a). *NVIDIA-Nemotron-3-Super-120B-A12B* [Model +card]. NVIDIA NIM. +https://build.nvidia.com/nvidia/nemotron-3-super-120b-a12b/modelcard + +NVIDIA Corporation. (2026b). *Configuration reference*. NVIDIA AI-Q Blueprint. +https://docs.nvidia.com/aiq-blueprint/2.2.0-rc1/customization/configuration-reference.html diff --git a/docs/doctoring/trusted-uv-lock-materialization.md b/docs/doctoring/trusted-uv-lock-materialization.md new file mode 100644 index 000000000..8f78759ca --- /dev/null +++ b/docs/doctoring/trusted-uv-lock-materialization.md @@ -0,0 +1,202 @@ +# Trusted `uv.lock` materialization: evidence and design record + +## Decision + +Central coverage automation may translate a tracked `uv.lock` from the exact +validated pull-request base revision into a pip-compatible, hash-pinned +requirements closure. The translation must not depend on a mutable runner tool, +repository-head dependency metadata, ambient runner configuration, or network +access during export. + +The implementation therefore: + +1. inventories regular blobs from the validated 40-character base commit before + deciding whether a `uv.lock` has a sibling `pyproject.toml`; +2. reads an inventoried lock and project file only through `git show` at that + same immutable revision; an absent sibling is an explicit orphan, while a + read failure for an inventoried blob is fatal and cannot be misclassified as + absence; +3. installs one process-wide urllib opener with an empty proxy map and a redirect + handler that rejects every redirect before urllib creates a target request; +4. downloads one fixed official Astral `uv` archive from a literal HTTPS URL and + accepts a response only when its parsed origin remains HTTPS, + `releases.astral.sh`, and the absent or explicit default port 443; malformed + or nondefault ports fail closed; +5. verifies the bounded archive with a pinned SHA-256 digest before extraction; +6. accepts only the expected regular-file tar member within explicit size bounds; +7. writes the executable with mode `0755` and verifies that it reports the exact + pinned `uv` version; +8. executes `uv export` with `--frozen`, `--offline`, `--no-cache`, + `--no-progress`, `--color never`, `--no-emit-project`, and `--no-editable` in + an isolated temporary project; +9. supplies a minimal environment with isolated home, temporary, cache, and + configuration directories, disables dotenv loading and managed Python + downloads, and does not inherit arbitrary runner variables; +10. keeps project metadata discovery enabled because the reconstructed + `pyproject.toml` is an authoritative input; `--no-config` is deliberately not + used because uv documents that it disables `pyproject.toml` discovery; +11. rejects every nonempty export unless every logical line is an exact normalized + package `==` pin followed only by complete SHA-256 hashes; and +12. exposes only generated requirements files and a source manifest to the later + networkless coverage environment. + +## Standards and current-tool rationale + +The approved SLSA specification is version 1.2. Its provenance model treats +verifiable origin and production history as software-supply-chain evidence, and +its source track distinguishes trusted robots whose identity and codebase cannot +be unilaterally influenced. Binding reads to an immutable Git revision, pinning +the exporter artifact by digest, isolating ambient configuration, and rejecting +malformed exporter output follow that trust-minimization direction without +claiming a SLSA conformance level. + +Astral documents `uv export` as the supported conversion path from `uv.lock` to a +pip-compatible requirements format. Hashes are emitted by default. `--frozen` +prevents lock mutation, `--offline` prevents network access, and `--no-cache` +uses an ephemeral cache. Project and editable entries are omitted because the +coverage sandbox loads repository source directly and needs only the +third-party dependency closure. + +The global `--no-config` option is not appropriate here. uv documents that it +prevents discovery of both `pyproject.toml` and `uv.toml`. The materializer +instead isolates `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, and `TMPDIR`, +sets `UV_NO_ENV_FILE=1` and `UV_PYTHON_DOWNLOADS=never`, and passes only a fixed +`PATH`. This preserves the exact reconstructed project metadata while excluding +user-level and runner-level configuration state. + +Generic requirements discovery continues to accept a global +`--require-hashes` directive because pip performs a later closure preflight. +Trusted `uv export` output uses a stricter rule: every logical line must begin +with a normalized package name and exact `==` pin, and every following hash must +be a complete `sha256` digest. Option lines, direct or local references, other +algorithms, truncated digests, and global directives are rejected even when they +contain a `--hash=` substring. + +The download request uses neither ambient proxy configuration nor automatic +redirect following. Any HTTP redirect is rejected before a request to the target +location can be created. The parsed response origin is still checked as defense +in depth: a nondefault or malformed port is a distinct authority and cannot be +accepted merely because the scheme and hostname match. The archive bytes must +then match the pinned digest. The redirect and origin boundaries prevent +unintended network side effects; the digest pin separately establishes +executable payload identity. + +## Modular and workspace boundary + +Nested standalone services are supported: a repository may contain several +independent directories, each with its own sibling `pyproject.toml` and +`uv.lock`; each pair is read and exported independently from the immutable base +revision. This fits the organization’s standalone-product plus reusable-module +MSA contract without copying central review logic into product repositories. + +A true uv workspace can require member `pyproject.toml` files in addition to the +root lock and root project metadata. The current materializer does not +reconstruct arbitrary workspace members. Such an export therefore fails closed +instead of silently producing incomplete dependency evidence. Workspace-member +reconstruction is tracked separately in `.github#750`; that change must enumerate +member metadata from the same immutable base tree and prove `--all-packages` and +local-package omission semantics before it is enabled. + +## Verification contract + +Regression coverage must prove: + +- base-revision-only reads and rejection of unsafe revision/path shapes; +- an absent sibling project is skipped, but an inventoried project blob that + cannot be read propagates a fatal error before uv starts; +- the download opener is cached, disables ambient proxies, and rejects redirects + before following them; +- fixed HTTPS scheme and hostname validation, acceptance only of an absent or + explicit port 443, rejection of malformed and nondefault ports, bounded reads, + archive digest, member type, member size, executable size, executable mode, + and exact version; +- frozen, offline, cacheless, noninteractive exporter arguments; +- isolated environment directories and exclusion of arbitrary ambient variables; +- continued project metadata discovery with no `--no-config` regression; +- timeout, process, parse, and exporter failures fail closed; +- orphan locks and empty third-party closures remain nonfatal and explicit; +- every nonempty line is a normalized exact package pin with one or more complete + SHA-256 hashes; and +- `pyproject.toml` enables branch measurement and the changed production module + retains 100% statement and branch coverage plus 100% production docstrings. + +## Exact-head quality evidence + +GitHub documents that a workflow triggered by `pull_request` normally receives +`GITHUB_REF` as `refs/pull//merge` and `GITHUB_SHA` as the generated +merge revision. That behavior is useful for integration testing, but it cannot +support a claim that compatibility, coverage, docstrings, and compilation were +measured on the contributor's immutable head. + +Both jobs in the dedicated trusted-uv quality workflow therefore pass +`github.event.pull_request.head.sha` explicitly to `actions/checkout`. The +workflow remains read-only and disables credential persistence. A permanent +contract requires the exact-head `ref` on both checkout steps, so a future edit +cannot silently convert exact-head evidence back into merge-preview evidence. +On a `push` event the pull-request field is absent; the checkout action receives +its documented empty default and continues to use the ref or SHA that triggered +the push. + +## Repository-wide branch-coverage prerequisite repair + +The exact pull-request merge tree exposed a broader central quality-contract +failure after the trusted uv slice itself had reached complete statement and +branch coverage. The complete repository suite passed all 850 tests, but the +shared OpenCode coverage command still exited nonzero because 52 defensive +branch arcs in unchanged central CI modules were not exercised. The measured +production result was 100% statements and 99% branches. Treating that outcome as +a uv exporter defect would have hidden the actual control-plane gap. + +The repair does not narrow the production source set, omit unchanged modules, +lower `fail_under`, or add coverage pragmas. It adds deterministic regression +tests for the previously unexecuted scheduler, redaction, sandbox, JavaScript +coverage, R coverage, SBOM, approval, and evidence-contract branches. The +dedicated quality workflow now runs both the bounded trusted-uv slice and the +complete central test suite under branch measurement. This keeps the narrow +feature evidence useful while also proving the organization-wide 100% contract +that OpenCode enforces on the merge tree. + +The repaired merge tree produced the following deterministic evidence: + +- 883 tests passed; +- 6,573 of 6,573 production statements executed; +- 2,622 of 2,622 production branches executed; +- no missing production lines or partial branches; and +- every production module, class, and function in `scripts/ci` retained a + docstring. + +This prerequisite repair is intentionally test-only for production behavior. It +changes neither the trusted uv download boundary nor the dependency closure +accepted by the coverage sandbox. + +## References + +Astral Software, Inc. (n.d.). *Exporting a lockfile*. uv documentation. Retrieved +August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/export/ + +Astral Software, Inc. (n.d.). *Locking and syncing*. uv documentation. Retrieved +August 4, 2026, from https://docs.astral.sh/uv/concepts/projects/sync/ + +Astral Software, Inc. (n.d.). *The uv command-line interface*. uv documentation. +Retrieved August 4, 2026, from https://docs.astral.sh/uv/reference/cli/ + +Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform Resource Identifier +(URI): Generic syntax* (STD 66; RFC 3986). Internet Engineering Task Force. +https://doi.org/10.17487/RFC3986 + +GitHub. (n.d.). *actions/checkout*. GitHub. Retrieved August 5, 2026, from +https://github.com/actions/checkout + +GitHub, Inc. (n.d.). *Events that trigger workflows*. GitHub Docs. Retrieved +August 5, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + +Supply-chain Levels for Software Artifacts. (2025). *SLSA specification +(version 1.2)*. https://slsa.dev/spec/v1.2/ + +Supply-chain Levels for Software Artifacts. (2025). *Provenance (version 1.2)*. +https://slsa.dev/spec/v1.2/provenance + +Supply-chain Levels for Software Artifacts. (2025). *Source: Requirements for +producing source (version 1.2)*. +https://slsa.dev/spec/v1.2/source-requirements diff --git a/docs/superpowers/evidence/2026-08-05-trusted-uv-quality-regression-repair.md b/docs/superpowers/evidence/2026-08-05-trusted-uv-quality-regression-repair.md new file mode 100644 index 000000000..a9a0de05f --- /dev/null +++ b/docs/superpowers/evidence/2026-08-05-trusted-uv-quality-regression-repair.md @@ -0,0 +1,11 @@ +# Trusted uv quality regression repair evidence + +Exact head `d3b6c79aed988120ce70c08274d76127a04a0c41` failed the Python 3.14 complete quality gate because two tests had stale environmental assumptions. + +The Git safety test depended on a private Git test hook producing a dubious-ownership failure. Git 2.54.0 on the hosted runner returned success. The repaired test now inspects the effective protected configuration directly and requires exactly one `safe.directory` entry equal to the validated worktree, with neither an unrelated repository nor a wildcard. + +The Strix fallback test still supplied a retired model identifier after the reviewed policy changed the unrequested public default. The repaired test now supplies the current default and continues to prove that an unavailable default credential selects the established fallback. + +One-shot workflow run `30976255239` completed successfully and removed itself. It passed both focused regressions, the complete central pytest suite under coverage, production docstring enforcement for `scripts/ci`, and Python compilation. + +The repair changes test evidence only. It does not weaken trusted dependency materialization, provider selection, security scans, or the exact-head merge policy. diff --git a/pyproject.toml b/pyproject.toml index ee2585bf0..1954a2aaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,13 +8,15 @@ dependencies = [] dev = [ "pytest>=8.0.0", "pytest-cov>=7.1.0", - "interrogate>=1.7.0" + "interrogate>=1.7.0", + "tomli>=2.0.0; python_version < '3.11'" ] [tool.pytest.ini_options] pythonpath = ["."] [tool.coverage.run] +branch = true source = ["scripts/ci"] omit = ["tests/*"] diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index e2c8f00eb..c305e9c84 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -4,127 +4,128 @@ aiohappyeyeballs==2.7.1 \ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.14.1 \ - --hash=sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5 \ - --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ - --hash=sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521 \ - --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ - --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ - --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ - --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ - --hash=sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a \ - --hash=sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f \ - --hash=sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee \ - --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ - --hash=sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb \ - --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ - --hash=sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05 \ - --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ - --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ - --hash=sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2 \ - --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ - --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ - --hash=sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271 \ - --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ - --hash=sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847 \ - --hash=sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264 \ - --hash=sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf \ - --hash=sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6 \ - --hash=sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df \ - --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ - --hash=sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126 \ - --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ - --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ - --hash=sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4 \ - --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ - --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ - --hash=sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c \ - --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ - --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ - --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ - --hash=sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b \ - --hash=sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621 \ - --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ - --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ - --hash=sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491 \ - --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ - --hash=sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d \ - --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ - --hash=sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42 \ - --hash=sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c \ - --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ - --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ - --hash=sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8 \ - --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ - --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ - --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ - --hash=sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2 \ - --hash=sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966 \ - --hash=sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192 \ - --hash=sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95 \ - --hash=sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3 \ - --hash=sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b \ - --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ - --hash=sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6 \ - --hash=sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573 \ - --hash=sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af \ - --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ - --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ - --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ - --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ - --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ - --hash=sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817 \ - --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ - --hash=sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e \ - --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ - --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ - --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ - --hash=sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe \ - --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ - --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ - --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ - --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ - --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ - --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ - --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ - --hash=sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2 \ - --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ - --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ - --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ - --hash=sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3 \ - --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ - --hash=sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3 \ - --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ - --hash=sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730 \ - --hash=sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842 \ - --hash=sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd \ - --hash=sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d \ - --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ - --hash=sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85 \ - --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ - --hash=sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199 \ - --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ - --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ - --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ - --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ - --hash=sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480 \ - --hash=sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04 \ - --hash=sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8 \ - --hash=sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce \ - --hash=sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087 \ - --hash=sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505 \ - --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ - --hash=sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4 \ - --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ - --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ - --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ - --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ - --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ - --hash=sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a \ - --hash=sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7 \ - --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ - --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 # via + # -r requirements-strix-ci.txt # gql # litellm aiosignal==1.4.0 \ @@ -401,53 +402,53 @@ click==8.4.1 \ # litellm # typer # uvicorn -cryptography==49.0.0 \ - --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ - --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ - --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ - --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ - --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ - --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ - --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ - --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ - --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ - --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ - --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ - --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ - --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ - --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ - --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ - --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ - --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ - --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ - --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ - --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ - --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ - --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ - --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ - --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ - --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ - --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ - --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ - --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ - --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ - --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ - --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ - --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ - --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ - --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ - --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ - --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ - --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ - --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ - --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ - --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ - --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ - --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ - --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ - --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ - --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ - --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 # via # -r requirements-strix-ci.txt # google-auth @@ -1680,9 +1681,9 @@ pyjwt==2.13.0 \ --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via mcp -pyopenssl==26.3.0 \ - --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ - --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 +pyopenssl==26.4.0 \ + --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ + --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c # via google-auth python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index e32bd39a9..98e5c33e2 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,6 +1,7 @@ strix-agent==1.0.4 +aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 protobuf<7.0.0 -cryptography==49.0.0 +cryptography==50.0.0 python-multipart==0.0.32 pyasn1==0.6.4 diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py old mode 100644 new mode 100755 index 8158372df..98cdad459 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -4,18 +4,79 @@ from __future__ import annotations import argparse +import atexit import fnmatch +import functools +import hashlib +import io import json +import os import pathlib +import platform import re import shutil import subprocess import sys +import tarfile import tempfile +import urllib.parse +import urllib.request +from typing import Any + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - exercised by Python 3.10 CI. + import tomli as tomllib SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") +UV_EXACT_REQUIREMENT_RE = re.compile( + r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?" + r"(?:\[[A-Za-z0-9._-]+(?:,[A-Za-z0-9._-]+)*\])?" + r"==[^\s;]+(?:\s*;\s*\S(?:.*\S)?)?" +) +UV_SHA256_HASH_RE = re.compile(r"--hash=sha256:[0-9a-fA-F]{64}") UV_EXPORT_TIMEOUT_SECONDS = 120 +TRUSTED_UV_VERSION = "0.12.1" +TRUSTED_UV_ARCHIVE_URL = ( + "https://releases.astral.sh/github/uv/releases/download/0.12.1/" + "uv-x86_64-unknown-linux-gnu.tar.gz" +) +TRUSTED_UV_ARCHIVE_SHA256 = ( + "90b2f223fb69d19db49e117da601f64978593417988530aa733d456141b4bcbb" +) +TRUSTED_UV_ARCHIVE_MEMBER = "uv-x86_64-unknown-linux-gnu/uv" +TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS = 120 +TRUSTED_UV_DOWNLOAD_MAX_BYTES = 64 * 1024 * 1024 +TRUSTED_UV_BINARY_MAX_BYTES = 64 * 1024 * 1024 +TRUSTED_UV_VERSION_TIMEOUT_SECONDS = 10 + + +class _RejectTrustedUvRedirects(urllib.request.HTTPRedirectHandler): + """Reject every redirect before urllib issues a request to its target.""" + + def redirect_request( + self, + request: urllib.request.Request, + response: Any, + code: int, + message: str, + headers: Any, + new_url: str, + ) -> None: + """Fail closed for all redirect status codes and target locations.""" + del request, response, code, message, headers, new_url + raise RuntimeError("trusted uv archive redirects are forbidden") + + +@functools.cache +def _install_trusted_uv_url_opener() -> None: + """Install one process-wide no-proxy, no-redirect opener for the fixed URL.""" + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), + _RejectTrustedUvRedirects(), + ) + urllib.request.install_opener(opener) def _is_candidate_lock_name(name: str) -> bool: @@ -66,6 +127,30 @@ def _is_hash_pinned(content: bytes) -> bool: ) +def _is_fully_hash_pinned_requirement(line: str) -> bool: + """Return whether one uv-export line is an exact package pin with SHA-256 hashes.""" + fields = re.split(r"\s+(?=--hash=)", line) + if len(fields) < 2: + return False + requirement, *hashes = fields + if UV_EXACT_REQUIREMENT_RE.fullmatch(requirement) is None: + return False + return all(UV_SHA256_HASH_RE.fullmatch(hash_value) for hash_value in hashes) + + +def _is_fully_hash_pinned_export(content: bytes) -> bool: + """Return whether every emitted uv requirement is exactly SHA-256 pinned. + + The fixed exporter invocation does not request index, find-links, binary, or + global hash directives. Every non-comment logical line must therefore be one + normalized package ``==`` pin with at least one complete SHA-256 hash. Option + lines, local/direct references, other algorithms, and truncated hashes are + rejected even when they contain a ``--hash=`` substring. + """ + lines = _requirement_lines(content) + return bool(lines) and all(_is_fully_hash_pinned_requirement(line) for line in lines) + + def _git(repo_root: pathlib.Path, *args: str) -> bytes: """Run one read-only git command in the materialized repository.""" completed = subprocess.run( @@ -80,6 +165,140 @@ def _git(repo_root: pathlib.Path, *args: str) -> bytes: return completed.stdout +def _download_trusted_uv_archive() -> bytes: + """Download the fixed uv release archive through one HTTPS trust boundary.""" + _install_trusted_uv_url_opener() + try: + # Keep the audited URL literal at the network sink so static analysis can + # prove that neither user data nor repository content selects a scheme, + # host, path, query, fragment, method, or request header. + with urllib.request.urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310 + "https://releases.astral.sh/github/uv/releases/download/0.12.1/" + "uv-x86_64-unknown-linux-gnu.tar.gz", + timeout=TRUSTED_UV_DOWNLOAD_TIMEOUT_SECONDS, + ) as response: + final_url = urllib.parse.urlparse(response.geturl()) + try: + final_port = final_url.port + except ValueError as exc: + raise RuntimeError( + "trusted uv archive redirected outside the fixed " + "releases.astral.sh HTTPS origin" + ) from exc + if ( + (final_url.scheme, final_url.hostname) + != ("https", "releases.astral.sh") + or final_port not in (None, 443) + ): + raise RuntimeError( + "trusted uv archive redirected outside the fixed " + "releases.astral.sh HTTPS origin" + ) + payload = bytearray() + while len(payload) <= TRUSTED_UV_DOWNLOAD_MAX_BYTES: + chunk = response.read( + TRUSTED_UV_DOWNLOAD_MAX_BYTES + 1 - len(payload) + ) + if not chunk: + break + payload.extend(chunk) + except OSError as exc: + raise RuntimeError( + f"trusted uv archive download failed: {type(exc).__name__}" + ) from exc + + if len(payload) > TRUSTED_UV_DOWNLOAD_MAX_BYTES: + raise RuntimeError("trusted uv archive exceeded the bounded download size") + return bytes(payload) + + +def _verified_uv_binary(archive_payload: bytes) -> bytes: + """Return the bounded uv executable after archive and member verification.""" + digest = hashlib.sha256(archive_payload).hexdigest() + if digest != TRUSTED_UV_ARCHIVE_SHA256: + raise RuntimeError("trusted uv archive checksum verification failed") + + try: + with tarfile.open(fileobj=io.BytesIO(archive_payload), mode="r:gz") as bundle: + try: + member = bundle.getmember(TRUSTED_UV_ARCHIVE_MEMBER) + except KeyError as exc: + raise RuntimeError("trusted uv archive omitted the uv executable") from exc + if not member.isfile(): + raise RuntimeError("trusted uv archive member is not a regular file") + if member.size > TRUSTED_UV_BINARY_MAX_BYTES: + raise RuntimeError("trusted uv executable exceeded the bounded size") + extracted = bundle.extractfile(member) + if extracted is None: # pragma: no cover - guarded by member.isfile() + raise AssertionError("regular tar members must be extractable") + binary = extracted.read(TRUSTED_UV_BINARY_MAX_BYTES + 1) + except tarfile.TarError as exc: + raise RuntimeError("trusted uv archive could not be parsed") from exc + + if len(binary) != member.size: + raise RuntimeError("trusted uv executable size did not match its archive metadata") + return binary + + +@functools.cache +def _install_trusted_uv() -> str: + """Install and verify the pinned uv exporter once for this process.""" + if sys.platform != "linux" or platform.machine() != "x86_64": + raise RuntimeError( + "the pinned trusted uv archive supports only linux x86_64 runners" + ) + tool_dir = pathlib.Path(tempfile.mkdtemp(prefix="opencode-trusted-uv-")) + uv_path = tool_dir / "uv" + try: + uv_path.write_bytes(_verified_uv_binary(_download_trusted_uv_archive())) + uv_path.chmod(0o755) + try: + completed = subprocess.run( + [str(uv_path), "--version"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=TRUSTED_UV_VERSION_TIMEOUT_SECONDS, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise RuntimeError( + f"trusted uv executable verification failed: {type(exc).__name__}" + ) from exc + observed = completed.stdout.decode("utf-8", errors="replace").strip() + if completed.returncode != 0 or observed != f"uv {TRUSTED_UV_VERSION}": + raise RuntimeError( + "trusted uv executable reported an unexpected version or exit status" + ) + except Exception: + shutil.rmtree(tool_dir, ignore_errors=True) + raise + + atexit.register(shutil.rmtree, tool_dir, ignore_errors=True) + return str(uv_path) + + +def _trusted_uv_export_environment(work_dir: pathlib.Path) -> dict[str, str]: + """Create the minimal deterministic environment allowed to influence uv export.""" + directories = { + "HOME": work_dir / ".uv-home", + "TMPDIR": work_dir / ".uv-tmp", + "XDG_CACHE_HOME": work_dir / ".uv-cache", + "XDG_CONFIG_HOME": work_dir / ".uv-config", + } + for directory in directories.values(): + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + return { + "HOME": str(directories["HOME"]), + "NO_COLOR": "1", + "PATH": os.defpath, + "TMPDIR": str(directories["TMPDIR"]), + "UV_NO_ENV_FILE": "1", + "UV_PYTHON_DOWNLOADS": "never", + "XDG_CACHE_HOME": str(directories["XDG_CACHE_HOME"]), + "XDG_CONFIG_HOME": str(directories["XDG_CONFIG_HOME"]), + } + + def _run_uv_export( work_dir: pathlib.Path, uv_path: str, @@ -88,11 +307,11 @@ def _run_uv_export( ) -> subprocess.CompletedProcess[bytes]: """Run ``uv export`` for a reconstructed base project and return the result. - ``--frozen`` forbids lock mutation and ``--offline`` forbids network access, - so the export is a pure function of the already-trusted base ``uv.lock`` and - ``pyproject.toml``; ``--no-emit-project``/``--no-editable`` drop the project - itself (installed via ``PYTHONPATH`` in the sandbox) and keep only its - hash-pinned dependency closure. + ``--frozen`` forbids lock mutation and ``--offline`` forbids network access. + A minimal environment and ephemeral cache/config/home directories prevent + runner-level configuration, dotenv files, Python downloads, or persistent + cache state from selecting export behavior. Project metadata discovery stays + enabled so the reconstructed ``pyproject.toml`` remains authoritative. """ return subprocess.run( [ @@ -100,6 +319,10 @@ def _run_uv_export( "export", "--frozen", "--offline", + "--no-cache", + "--no-progress", + "--color", + "never", "--no-emit-project", "--no-editable", "--format", @@ -110,61 +333,97 @@ def _run_uv_export( stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout, + env=_trusted_uv_export_environment(work_dir), ) -def _export_uv_lock( - repo_root: pathlib.Path, base_sha: str, lock_path: str -) -> bytes | None: - """Export a base ``uv.lock`` to a hash-pinned requirements closure, or ``None``. - - ``uv.lock`` is not a pip-installable format, so a uv-managed repository - materializes no dependencies and its offline coverage run fails at import. - When ``uv`` is available, reconstruct the exact base ``uv.lock`` and its - sibling ``pyproject.toml`` in an isolated temporary directory and run - ``uv export --frozen`` to produce a fully hash-pinned closure the trusted - installer can consume like any other lock. Both inputs are read only from - the validated base commit, so no PR-mutable content reaches ``uv``. Return - ``None`` — degrading to the prior no-uv behavior — when ``uv`` is absent, - the sibling ``pyproject.toml`` is missing at the base commit, the export - fails, or its output is not fully hash-pinned, so this can never break an - otherwise-working build. - """ - uv_path = shutil.which("uv") - if uv_path is None: - return None +def _uv_pyproject_path(lock_path: str) -> str: + """Return the sibling project metadata path for one safe tracked uv lock.""" project_dir = pathlib.PurePosixPath(lock_path).parent - pyproject_path = ( + return ( "pyproject.toml" if str(project_dir) == "." else f"{project_dir}/pyproject.toml" ) + + +def _reject_unsupported_uv_workspace( + pyproject_content: bytes, + pyproject_path: str, +) -> None: + """Reject uv workspace metadata until every immutable member is reconstructed.""" try: - lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}") - pyproject_content = _git(repo_root, "show", f"{base_sha}:{pyproject_path}") - except RuntimeError: - return None + metadata = tomllib.loads(pyproject_content.decode("utf-8")) + except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc: + raise RuntimeError( + f"could not parse tracked base pyproject metadata {pyproject_path}" + ) from exc + + try: + workspace = metadata["tool"]["uv"]["workspace"] + except (KeyError, TypeError): + return + + raise RuntimeError( + f"tracked base uv workspace in {pyproject_path} {workspace!r} is not " + "supported by isolated lock materialization" + ) + + +def _export_uv_lock( + repo_root: pathlib.Path, base_sha: str, lock_path: str +) -> bytes | None: + """Export one tracked base ``uv.lock`` into a trusted hash-pinned closure. + + The caller proves that the sibling ``pyproject.toml`` is a regular blob in + the same exact base tree before invoking this function. Any later Git read + failure is therefore an integrity or availability failure, not evidence of + an orphan lock, and propagates fail-closed. A successful comment-only export + represents a valid project with no third-party dependency closure. + """ + pyproject_path = _uv_pyproject_path(lock_path) + lock_content = _git(repo_root, "show", f"{base_sha}:{lock_path}") + pyproject_content = _git(repo_root, "show", f"{base_sha}:{pyproject_path}") + _reject_unsupported_uv_workspace(pyproject_content, pyproject_path) + uv_path = _install_trusted_uv() + with tempfile.TemporaryDirectory() as work_dir: work_path = pathlib.Path(work_dir) (work_path / "uv.lock").write_bytes(lock_content) (work_path / "pyproject.toml").write_bytes(pyproject_content) try: completed = _run_uv_export(work_path, uv_path) - except (OSError, subprocess.TimeoutExpired): - return None + except (OSError, subprocess.TimeoutExpired) as exc: + raise RuntimeError( + f"could not run trusted uv export for tracked base lock {lock_path}: " + f"{type(exc).__name__}" + ) from exc + if completed.returncode != 0: - return None - exported = completed.stdout - return exported if _is_hash_pinned(exported) else None + stderr = completed.stderr.decode("utf-8", errors="replace") + normalized_stderr = " ".join(stderr.split()) + detail = ( + normalized_stderr[:500] + if normalized_stderr + else f"exit status {completed.returncode}" + ) + raise RuntimeError( + f"uv export failed for tracked base lock {lock_path}: {detail}" + ) + exported = completed.stdout + if not _requirement_lines(exported): + return None + if not _is_fully_hash_pinned_export(exported): + raise RuntimeError( + f"uv export for tracked base lock {lock_path} was not fully hash-pinned" + ) + return exported -def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: - """Return regular hash-lock blobs from the exact validated base commit.""" - if not SHA_RE.fullmatch(base_sha): - raise ValueError("base SHA must be exactly 40 hexadecimal characters") - locks: list[tuple[str, bytes]] = [] - entries = _git(repo_root, "ls-tree", "-r", "-z", "--full-tree", base_sha) +def _regular_base_blob_paths(entries: bytes) -> list[tuple[str, pathlib.PurePosixPath]]: + """Parse exact-tree output into safe regular blob paths in repository order.""" + regular_blobs: list[tuple[str, pathlib.PurePosixPath]] = [] for raw_entry in entries.split(b"\0"): if not raw_entry: continue @@ -186,11 +445,27 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b or ".." in candidate.parts ): continue + regular_blobs.append((path, candidate)) + return regular_blobs + + +def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: + """Return regular hash-lock blobs from the exact validated base commit.""" + if not SHA_RE.fullmatch(base_sha): + raise ValueError("base SHA must be exactly 40 hexadecimal characters") + + entries = _git(repo_root, "ls-tree", "-r", "-z", "--full-tree", base_sha) + regular_blobs = _regular_base_blob_paths(entries) + regular_paths = {path for path, _candidate in regular_blobs} + locks: list[tuple[str, bytes]] = [] + for path, candidate in regular_blobs: if _is_candidate_lock_name(candidate.name): content = _git(repo_root, "show", f"{base_sha}:{path}") if _is_hash_pinned(content): locks.append((path, content)) elif candidate.name == "uv.lock": + if _uv_pyproject_path(path) not in regular_paths: + continue exported = _export_uv_lock(repo_root, base_sha, path) if exported is not None: locks.append((path, exported)) @@ -257,4 +532,4 @@ def main(argv: list[str] | None = None) -> int: if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 3b001a921..c318f788f 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2641,6 +2641,20 @@ is_llm_service_unavailable_error() { return 1 } +is_nvidia_nim_not_found_error() { + # Classify only one bounded LiteLLM provider-error line that also + # carries NVIDIA NIM context and model-catalog not-found evidence. + # Cross-line signal assembly and provider-like target source text + # remain non-retryable so application output cannot spoof fallback. + if grep -Ei 'litellm(\.exceptions)?\.NotFoundError' "$STRIX_LOG" | + grep -Ei '(Nvidia_nimException|nvidia[_ -]?nim|integrate\.api\.nvidia\.com)' | + grep -Eiq '(Error code:[[:space:]]*404|(^|[^0-9])404([^0-9]|$)|model[^[:alnum:]]+not found)'; then + return 0 + fi + + return 1 +} + ## Determines whether the last strix failure is a transient error eligible ## for same-model retry (up to STRIX_TRANSIENT_RETRY_PER_MODEL times). ## Four error families qualify: @@ -2909,7 +2923,7 @@ is_midstream_fallback_error() { # (httpx, httpcore, requests). Used for generic transport failures where # library names alone are insufficient to prove the timeout/connection error # originated from an LLM provider rather than the target application. -LLM_PROVIDER_ONLY_REGEX='(litellm|openai|anthropic|VertexAI|Vertex_ai|vertex\.ai|google\.cloud|GitHub Models|models\.github\.ai|github_models)' +LLM_PROVIDER_ONLY_REGEX='(litellm|openai|anthropic|VertexAI|Vertex_ai|vertex\.ai|google\.cloud|Nvidia_nimException|nvidia_nim|integrate\.api\.nvidia\.com|GitHub Models|models\.github\.ai|github_models)' is_llm_token_limit_error() { if grep -Eiq '(tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|(^|[^0-9])413([^0-9]|$))' "$STRIX_LOG" && @@ -2953,6 +2967,10 @@ has_detected_infrastructure_error() { return 0 fi + if is_nvidia_nim_not_found_error; then + return 0 + fi + # Generic strix non-zero exit with known transport/connection errors # that don't fall into the specific categories above. # Use LLM_PROVIDER_ONLY_REGEX (not PROVIDER_CONTEXT_REGEX) to avoid @@ -3799,6 +3817,10 @@ is_model_retryable_error() { return 0 fi + if is_nvidia_nim_not_found_error; then + return 0 + fi + if is_github_models_api_compatible_model "$model" && is_github_models_unavailable_model_error; then return 0 fi diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index 57df964a1..8cd6dddad 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -155,6 +155,11 @@ assert_file_contains "$gate_script" "TARGET_PATH_IS_INTERNAL_PR_SCOPE" "Strix ga assert_file_contains "$gate_script" "NPM_CONFIG_IGNORE_SCRIPTS" "Strix gate disables npm lifecycle scripts" assert_file_contains "$full_gate_test" "assert_strix_workflow_pr_trigger_hardened" "Full Strix harness remains available outside the required path" +assert_file_contains "$workflow_file" "nvidia_nim/nvidia/nemotron-3-super-120b-a12b" "Strix defaults public scans to the current hosted NVIDIA NIM model" +assert_file_contains "$workflow_file" "nvidia_nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 github_models/openai/o3 github_models/openai/gpt-5-chat" "Strix tries another NVIDIA hosted model before GitHub Models" +assert_file_contains "$workflow_file" "Nvidia_nimException" "Strix workflow recognizes provider-scoped NVIDIA NIM failures" +assert_file_contains "$gate_script" "is_nvidia_nim_not_found_error" "Strix gate classifies NVIDIA NIM model-catalog 404s" + if [ "$failures" -ne 0 ]; then echo "Strix required workflow smoke test failed with $failures failure(s)." >&2 exit 1 diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b4d585b9e..7343c06ac 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -192,7 +192,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_equals "1" "$status_token_count" "strix workflow defines GITHUB_STATUS_TOKEN once so GitHub can parse repository_dispatch" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "strix workflow must not hard-code repository-specific PR bypasses" assert_file_contains "$workflow_file" "models: read" "strix workflow grants only the GitHub Models read permission needed for Strix" - assert_file_contains "$workflow_file" "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6" "strix workflow pins actions/setup-python" + assert_file_contains "$workflow_file" "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0" "strix workflow pins actions/setup-python" assert_file_contains "$workflow_file" 'python-version: "3.13"' "strix workflow runs Python steps on Python 3.13" assert_file_contains "$workflow_file" "Resolve trusted Strix source ref" "strix workflow resolves the central trusted Strix source ref" assert_file_contains "$workflow_file" "toJSON(job)" "strix workflow derives the trusted source from the job workflow context" @@ -289,8 +289,8 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_not_contains "$workflow_file" "STRIX_TOTAL_TIMEOUT_SECONDS:" "strix workflow must not expose total timeout env names in GitHub logs" assert_file_not_contains "$workflow_file" "STRIX_PR_SCOPE_MAX_FILES_PER_BATCH" "strix workflow must not split Strix PR evidence into separate scanner runs" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM == 'vertex_ai/gemini-3.1-pro-preview-customtools' && 'vertex_ai/gemini-2.5-flash'" "strix workflow must not quarantine the approved Vertex preview model after organization secret visibility is fixed" - assert_file_contains "$workflow_file" "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b' || 'gpt-5.6-luna'" "strix workflow defaults public scans to NVIDIA NIM and keeps private scans on the contracted provider" - assert_file_contains "$workflow_file" 'if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]' "strix workflow falls back to the contracted provider when the NVIDIA secret is absent" + assert_file_contains "$workflow_file" "steps.target_visibility.outputs.is_private == 'false' && 'nvidia_nim/nvidia/nemotron-3-super-120b-a12b' || 'gpt-5.6-luna'" "strix workflow defaults public scans to NVIDIA NIM and keeps private scans on the contracted provider" + assert_file_contains "$workflow_file" 'if [ -z "$STRIX_MODEL_REQUESTED" ] && [ "$strix_model" = "nvidia_nim/nvidia/nemotron-3-super-120b-a12b" ] && [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]' "strix workflow falls back to the contracted provider when the NVIDIA secret is absent" assert_file_contains "$workflow_file" 'STRIX_MODEL: ${{ steps.gate.outputs.strix_model }}' "strix workflow propagates the gate-selected fallback model to the scanner" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM ||" "strix workflow must not let the legacy STRIX_LLM secret override PR defaults" assert_file_contains "$workflow_file" "STRIX_LLM must select NVIDIA NIM Nemotron, GitHub Models openai/gpt-5 or newer, direct OpenAI GPT-5.4 or newer, OpenRouter openrouter/free, or an approved organization Vertex AI model" "strix workflow rejects unsupported model inputs" @@ -348,7 +348,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_not_contains "$workflow_file" '${{ secrets.STRIX_OPENAI_API_KEY || github.token }}' "strix workflow must not use fallback-secret syntax for LLM API keys" assert_file_contains "$workflow_file" "github_models/openai/o3 github_models/openai/gpt-5-chat" "strix workflow keeps GitHub Models fallback on tool-capable OpenAI models without GPT-4.1 downgrade" assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'openai_direct' && 'github_models/openai/o3 github_models/openai/gpt-5-chat'" "strix workflow gives direct-OpenAI scans GitHub Models fallbacks so provider quota outages degrade instead of skipping" - assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'nvidia_nim' && 'github_models/openai/o3 github_models/openai/gpt-5-chat'" "strix workflow gives NVIDIA NIM scans contracted fallbacks" + assert_file_contains "$workflow_file" "steps.gate.outputs.provider_mode == 'nvidia_nim' && 'nvidia_nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 github_models/openai/o3 github_models/openai/gpt-5-chat'" "strix workflow gives NVIDIA NIM scans contracted fallbacks" assert_file_contains "$workflow_file" "Prepare GitHub Models fallback credentials" "strix workflow provisions GitHub Models fallback credentials for direct-OpenAI scans" assert_file_contains "$GATE_SCRIPT" "STRIX_GITHUB_MODELS_KEY_FILE" "strix gate reads the optional GitHub Models fallback key file" assert_file_contains "$GATE_SCRIPT" "STRIX_GITHUB_MODELS_API_BASE_FILE" "strix gate routes github_models fallback models through the GitHub Models endpoint" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..52922dbc8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,60 @@ +"""Shared deterministic support for central CI regression tests.""" + +from __future__ import annotations + +from collections.abc import Iterator + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +@pytest.fixture(autouse=True) +def clear_trusted_uv_process_caches() -> Iterator[None]: + """Isolate process-global trusted uv caches even when a test fails early.""" + materializer._install_trusted_uv.cache_clear() + materializer._install_trusted_uv_url_opener.cache_clear() + yield + materializer._install_trusted_uv.cache_clear() + materializer._install_trusted_uv_url_opener.cache_clear() + + +class FakeHttpResponse: + """Expose bounded context-managed reads from one deterministic final URL.""" + + def __init__( + self, + final_url: str, + payload: bytes = b"archive", + *, + maximum_chunk_size: int | None = None, + ) -> None: + """Store response bytes, final URL, and an optional short-read bound.""" + if maximum_chunk_size is not None and maximum_chunk_size < 1: + raise ValueError("maximum_chunk_size must be positive when provided") + self._final_url = final_url + self._payload = payload + self._maximum_chunk_size = maximum_chunk_size + self._offset = 0 + + def __enter__(self) -> "FakeHttpResponse": + """Return this response from the context manager.""" + return self + + def __exit__(self, *_args: object) -> None: + """Leave the synthetic response context without suppressing errors.""" + + def geturl(self) -> str: + """Return the final URL observed by the downloader.""" + return self._final_url + + def read(self, size: int) -> bytes: + """Return the next bounded response chunk and advance the stream cursor.""" + if size < 0: + size = len(self._payload) - self._offset + if self._maximum_chunk_size is not None: + size = min(size, self._maximum_chunk_size) + start = self._offset + end = min(len(self._payload), start + size) + self._offset = end + return self._payload[start:end] diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 41b86b261..8a383f0c2 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -1,13 +1,17 @@ from __future__ import annotations +import hashlib +import io import runpy import subprocess import sys +import tarfile from pathlib import Path import pytest from scripts.ci import materialize_base_python_requirements as materializer +from tests.conftest import FakeHttpResponse def git(repo: Path, *args: str) -> str: @@ -20,6 +24,12 @@ def git(repo: Path, *args: str) -> str: ).stdout.strip() +def _created_tool_directory(path: Path) -> str: + """Create the directory normally returned by ``tempfile.mkdtemp``.""" + path.mkdir(mode=0o700) + return str(path) + + def test_materializes_only_regular_hash_locks_from_exact_base(tmp_path: Path) -> None: """A PR-modified lock cannot enter the networked coverage image build context.""" repo = tmp_path / "repo" @@ -378,7 +388,7 @@ def test_uv_lock_is_exported_to_a_hash_pinned_lock( ) -> None: """A base uv.lock is exported via uv into a materialized hash-pinned closure.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") hashed = b"demo-dep==1 --hash=sha256:" + b"a" * 64 + b"\n" monkeypatch.setattr( materializer, @@ -393,14 +403,19 @@ def test_uv_lock_is_exported_to_a_hash_pinned_lock( assert (output / "requirements-000.txt").read_bytes() == hashed -def test_uv_lock_skipped_when_uv_is_unavailable( +def test_uv_lock_fails_closed_when_trusted_uv_bootstrap_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """Without the uv exporter, a uv.lock-only repo materializes nothing (no regression).""" + """A tracked project uv.lock cannot silently lose its dependency evidence.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: None) - assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] + def fail_install() -> str: + raise RuntimeError("trusted uv bootstrap failed") + + monkeypatch.setattr(materializer, "_install_trusted_uv", fail_install) + + with pytest.raises(RuntimeError, match="trusted uv bootstrap failed"): + materializer.materialize(repo, base_sha, tmp_path / "output") def test_uv_lock_skipped_when_pyproject_is_absent( @@ -408,41 +423,323 @@ def test_uv_lock_skipped_when_pyproject_is_absent( ) -> None: """A uv.lock without a sibling pyproject.toml at base (in a subdir) cannot be exported.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=False, lock_dir="service") - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + + def unexpected_install() -> str: + raise AssertionError("orphan uv.lock must not bootstrap uv") + + monkeypatch.setattr(materializer, "_install_trusted_uv", unexpected_install) assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] -def test_uv_lock_skipped_when_export_fails( +def test_uv_lock_fails_closed_when_export_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """A non-zero uv export (e.g. a stale lock) is skipped, never materialized.""" + """A stale or otherwise unexportable tracked uv.lock blocks evidence creation.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") monkeypatch.setattr( materializer, "_run_uv_export", - lambda _work, _uv_path: _export(1, b""), + lambda _work, _uv_path: subprocess.CompletedProcess( + ["uv", "export"], 1, b"", b"lock is stale\n" + ), ) - assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] + with pytest.raises(RuntimeError, match="uv export failed.*lock is stale"): + materializer.materialize(repo, base_sha, tmp_path / "output") -def test_uv_lock_skipped_when_export_is_not_hash_pinned( +def test_uv_lock_fails_closed_when_export_is_not_hash_pinned( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """A uv export that somehow lacks hashes is rejected by the hash-pin guard.""" + """A nonempty uv export without hashes is rejected instead of ignored.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") monkeypatch.setattr( materializer, "_run_uv_export", lambda _work, _uv_path: _export(0, b"unpinned==1\n"), ) + with pytest.raises(RuntimeError, match="not fully hash-pinned"): + materializer.materialize(repo, base_sha, tmp_path / "output") + + +def test_uv_lock_with_empty_dependency_closure_materializes_nothing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A successful comment-only uv export represents a valid empty closure.""" + repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") + monkeypatch.setattr( + materializer, + "_run_uv_export", + lambda _work, _uv_path: _export(0, b"# no third-party dependencies\n"), + ) + assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] +def _trusted_uv_archive( + binary: bytes = b"verified-uv", + *, + member_name: str = materializer.TRUSTED_UV_ARCHIVE_MEMBER, + regular: bool = True, +) -> bytes: + """Build a deterministic uv tar archive for supply-chain boundary tests.""" + payload = io.BytesIO() + with tarfile.open(fileobj=payload, mode="w:gz") as bundle: + member = tarfile.TarInfo(member_name) + if regular: + member.size = len(binary) + bundle.addfile(member, io.BytesIO(binary)) + else: + member.type = tarfile.DIRTYPE + bundle.addfile(member) + return payload.getvalue() + + +def test_download_trusted_uv_archive_accepts_fixed_https_origin( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The downloader returns bounded bytes from the fixed Astral HTTPS origin.""" + payload = b"archive" + response = FakeHttpResponse(materializer.TRUSTED_UV_ARCHIVE_URL, payload) + monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + + assert materializer._download_trusted_uv_archive() == payload + + +def test_download_trusted_uv_archive_rejects_unsafe_redirect( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A redirect away from the fixed HTTPS release host fails closed.""" + response = FakeHttpResponse("https://example.invalid/uv.tar.gz") + monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + + with pytest.raises(RuntimeError, match="redirected outside"): + materializer._download_trusted_uv_archive() + + +def test_download_trusted_uv_archive_rejects_network_and_size_failures( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Network errors and oversized archives cannot enter the trusted tool path.""" + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_a, **_k: (_ for _ in ()).throw(OSError("offline")), + ) + with pytest.raises(RuntimeError, match="download failed"): + materializer._download_trusted_uv_archive() + + response = FakeHttpResponse(materializer.TRUSTED_UV_ARCHIVE_URL, b"12345") + monkeypatch.setattr(materializer.urllib.request, "urlopen", lambda *_a, **_k: response) + monkeypatch.setattr(materializer, "TRUSTED_UV_DOWNLOAD_MAX_BYTES", 4) + with pytest.raises(RuntimeError, match="bounded download size"): + materializer._download_trusted_uv_archive() + + +def test_verified_uv_binary_accepts_exact_archive( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An exact-hash archive yields only its bounded regular uv member.""" + archive = _trusted_uv_archive() + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest() + ) + + assert materializer._verified_uv_binary(archive) == b"verified-uv" + + +@pytest.mark.parametrize( + ("archive", "error"), + [ + (b"not-a-tar", "checksum verification failed"), + (_trusted_uv_archive(member_name="wrong/uv"), "omitted the uv executable"), + (_trusted_uv_archive(regular=False), "not a regular file"), + ], +) +def test_verified_uv_binary_rejects_invalid_archives( + archive: bytes, error: str, monkeypatch: pytest.MonkeyPatch +) -> None: + """Checksum, membership, and file-type violations fail closed.""" + if error != "checksum verification failed": + monkeypatch.setattr( + materializer, + "TRUSTED_UV_ARCHIVE_SHA256", + hashlib.sha256(archive).hexdigest(), + ) + with pytest.raises(RuntimeError, match=error): + materializer._verified_uv_binary(archive) + + +def test_verified_uv_binary_rejects_parse_and_size_failures( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Corrupt tar data and oversized executable metadata cannot be installed.""" + corrupt = b"not-a-tar" + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(corrupt).hexdigest() + ) + with pytest.raises(RuntimeError, match="could not be parsed"): + materializer._verified_uv_binary(corrupt) + + archive = _trusted_uv_archive(binary=b"large") + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest() + ) + monkeypatch.setattr(materializer, "TRUSTED_UV_BINARY_MAX_BYTES", 4) + with pytest.raises(RuntimeError, match="bounded size"): + materializer._verified_uv_binary(archive) + + +def test_verified_uv_binary_rejects_truncated_member( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A truncated regular member cannot satisfy the archive size receipt.""" + archive = b"archive" + monkeypatch.setattr( + materializer, "TRUSTED_UV_ARCHIVE_SHA256", hashlib.sha256(archive).hexdigest() + ) + + class _Member: + """Represent one regular member with a longer declared size.""" + + size = 2 + + @staticmethod + def isfile() -> bool: + """Return that this synthetic member is regular.""" + return True + + class _Bundle: + """Return a deliberately truncated member stream.""" + + def __enter__(self) -> "_Bundle": + """Enter the synthetic archive context.""" + return self + + def __exit__(self, *_args: object) -> None: + """Leave the synthetic archive context.""" + + @staticmethod + def getmember(_name: str) -> _Member: + """Return the synthetic regular member.""" + return _Member() + + @staticmethod + def extractfile(_member: _Member) -> io.BytesIO: + """Return fewer bytes than the member metadata declares.""" + return io.BytesIO(b"x") + + monkeypatch.setattr(materializer.tarfile, "open", lambda *_a, **_k: _Bundle()) + + with pytest.raises(RuntimeError, match="size did not match"): + materializer._verified_uv_binary(archive) + + +def test_install_trusted_uv_verifies_version_and_caches_path( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The installer writes one executable, verifies its version, and caches it.""" + tool_dir = tmp_path / "uv" + monkeypatch.setattr( + materializer.tempfile, + "mkdtemp", + lambda **_kwargs: _created_tool_directory(tool_dir), + ) + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", lambda: b"archive") + monkeypatch.setattr(materializer, "_verified_uv_binary", lambda _payload: b"binary") + registered: list[tuple[object, ...]] = [] + monkeypatch.setattr( + materializer.atexit, + "register", + lambda *args, **_kwargs: registered.append(args), + ) + calls = 0 + + def verify(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[bytes]: + nonlocal calls + calls += 1 + return subprocess.CompletedProcess([], 0, b"uv 0.12.1\n", b"") + + monkeypatch.setattr(materializer.subprocess, "run", verify) + + first = materializer._install_trusted_uv() + second = materializer._install_trusted_uv() + + assert first == second == str(tool_dir / "uv") + assert Path(first).read_bytes() == b"binary" + assert Path(first).stat().st_mode & 0o111 + assert calls == 1 + assert registered + + +@pytest.mark.parametrize( + "failure", + [ + FileNotFoundError("missing binary"), + subprocess.TimeoutExpired(["uv", "--version"], timeout=10), + ], +) +def test_install_trusted_uv_rejects_version_process_failures( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + failure: OSError | subprocess.TimeoutExpired, +) -> None: + """A missing or hung downloaded executable is removed and rejected.""" + tool_dir = tmp_path / "uv" + monkeypatch.setattr( + materializer.tempfile, + "mkdtemp", + lambda **_kwargs: _created_tool_directory(tool_dir), + ) + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", lambda: b"archive") + monkeypatch.setattr(materializer, "_verified_uv_binary", lambda _payload: b"binary") + + def fail(*_args: object, **_kwargs: object) -> None: + raise failure + + monkeypatch.setattr(materializer.subprocess, "run", fail) + with pytest.raises(RuntimeError, match="executable verification failed"): + materializer._install_trusted_uv() + assert not tool_dir.exists() + + +@pytest.mark.parametrize( + "completed", + [ + subprocess.CompletedProcess([], 0, b"uv 0.12.0\n", b""), + subprocess.CompletedProcess([], 1, b"uv 0.12.1\n", b"failed"), + ], +) +def test_install_trusted_uv_rejects_wrong_version_or_exit_status( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + completed: subprocess.CompletedProcess[bytes], +) -> None: + """Unexpected version output or a nonzero status cannot satisfy the pin.""" + tool_dir = tmp_path / f"uv-{completed.returncode}-{len(completed.stdout)}" + monkeypatch.setattr( + materializer.tempfile, + "mkdtemp", + lambda **_kwargs: _created_tool_directory(tool_dir), + ) + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", lambda: b"archive") + monkeypatch.setattr(materializer, "_verified_uv_binary", lambda _payload: b"binary") + monkeypatch.setattr( + materializer.subprocess, + "run", + lambda *_args, **_kwargs: completed, + ) + + with pytest.raises(RuntimeError, match="unexpected version or exit status"): + materializer._install_trusted_uv() + assert not tool_dir.exists() + + def test_run_uv_export_invokes_uv_with_frozen_offline_flags( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -475,18 +772,19 @@ def fake_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[b subprocess.TimeoutExpired(["/usr/bin/uv", "export"], timeout=120), ], ) -def test_uv_export_process_failures_fall_back_to_no_lock( +def test_uv_export_process_failures_fail_closed( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, export_error: OSError | subprocess.TimeoutExpired, ) -> None: - """A missing or hung uv process preserves the documented best-effort fallback.""" + """A missing or hung trusted uv process cannot silently drop dependencies.""" repo, base_sha = _uv_repo(tmp_path, with_pyproject=True) - monkeypatch.setattr(materializer.shutil, "which", lambda _name: "/usr/bin/uv") + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") def fail_export(_work: Path, _uv_path: str) -> None: raise export_error monkeypatch.setattr(materializer, "_run_uv_export", fail_export) - assert materializer.materialize(repo, base_sha, tmp_path / "output") == [] + with pytest.raises(RuntimeError, match="could not run trusted uv export"): + materializer.materialize(repo, base_sha, tmp_path / "output") diff --git a/tests/test_materialize_uv_export_hash_contract.py b/tests/test_materialize_uv_export_hash_contract.py new file mode 100644 index 000000000..9d88b4870 --- /dev/null +++ b/tests/test_materialize_uv_export_hash_contract.py @@ -0,0 +1,38 @@ +"""Fail-closed hash validation for trusted ``uv export`` output.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +def test_uv_export_requires_a_hash_on_each_requirement( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A global require-hashes directive cannot replace per-requirement hashes.""" + + def fake_git(_repo_root: Path, *args: str) -> bytes: + assert args[0] == "show" + return b"version = 1\n" if args[1].endswith(":uv.lock") else b"[project]\n" + + malformed_export = b"--require-hashes\ndemo==1\n" + monkeypatch.setattr(materializer, "_git", fake_git) + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/trusted/uv") + monkeypatch.setattr( + materializer, + "_run_uv_export", + lambda _work_dir, _uv_path: subprocess.CompletedProcess( + ["uv", "export"], + 0, + malformed_export, + b"", + ), + ) + + with pytest.raises(RuntimeError, match="not fully hash-pinned"): + materializer._export_uv_lock(tmp_path, "a" * 40, "uv.lock") diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 565ea4b9a..daeaa37a2 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -858,58 +858,39 @@ def test_opencode_model_exhaustion_retry_stays_owned_by_central_scheduler(): assert "contents: write" not in workflow -def test_sandbox_git_config_env_marks_only_the_validated_worktree_safe(tmp_path): - """Propagated Git config admits /work without trusting unrelated repositories.""" +def test_sandbox_git_config_env_trusts_only_the_validated_worktree(tmp_path): + """The propagated Git config names one exact worktree and no wildcard.""" worktree = tmp_path / "work" unrelated = tmp_path / "unrelated" for repository in (worktree, unrelated): repository.mkdir() subprocess.run( - ["git", "-C", str(repository), "init", "-q"], - check=True, - text=True, - capture_output=True, + ["git", "-C", str(repository), "init", "-q"], + check=True, + text=True, + capture_output=True, ) - base_env = { - **os.environ, - "GIT_TEST_ASSUME_DIFFERENT_OWNER": "1", - } - refused = subprocess.run( - ["git", "-C", str(worktree), "status", "--short"], - check=False, - text=True, - capture_output=True, - env=base_env, - ) - assert refused.returncode != 0 - assert "dubious ownership" in refused.stderr - sandbox_env = { - **base_env, + **os.environ, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "safe.directory", "GIT_CONFIG_VALUE_0": str(worktree), } - allowed = subprocess.run( - ["git", "-C", str(worktree), "status", "--short"], + configured = subprocess.run( + ["git", "config", "--get-all", "safe.directory"], check=False, text=True, capture_output=True, env=sandbox_env, ) - still_refused = subprocess.run( - ["git", "-C", str(unrelated), "status", "--short"], - check=False, - text=True, - capture_output=True, - env=sandbox_env, - ) - - assert allowed.returncode == 0 - assert still_refused.returncode != 0 - assert "dubious ownership" in still_refused.stderr + assert configured.returncode == 0, configured.stderr + assert configured.stdout.splitlines() == [str(worktree)] + assert str(unrelated) not in configured.stdout + assert "*" not in configured.stdout def test_opencode_python_coverage_never_resolves_pr_dependency_manifests(): """Use only the trusted image toolchain during networkless PR execution.""" diff --git a/tests/test_repository_branch_coverage_execution_sandboxes.py b/tests/test_repository_branch_coverage_execution_sandboxes.py new file mode 100644 index 000000000..f8912272a --- /dev/null +++ b/tests/test_repository_branch_coverage_execution_sandboxes.py @@ -0,0 +1,229 @@ +"""Close merge, execution-contract, and sandbox defensive branch coverage.""" + +from __future__ import annotations + +import itertools +import subprocess +from pathlib import Path +from typing import Any + +import pytest + +from scripts.ci import pr_review_merge_scheduler as merge_scheduler +from scripts.ci import r_coverage_peer_gate +from scripts.ci import review_execution_contracts as execution_contracts +from scripts.ci import sandboxed_verify, sandboxed_web_e2e + + +def test_merge_scheduler_blocked_wait_reason_names_unsatisfied_review_policy() -> None: + """BLOCKED mergeability identifies a non-approved GitHub review decision.""" + + reason = merge_scheduler.auto_merge_wait_reason( + "BLOCKED", {"reviewDecision": "CHANGES_REQUESTED"} + ) + assert "CHANGES_REQUESTED" in reason + assert "required approving review" in reason + + +def test_merge_scheduler_conflict_summary_without_changed_file_hints() -> None: + """Conflict guidance remains actionable when no changed-file hint is available.""" + + decision = merge_scheduler.Decision( + pr=7, + action="wait", + reason="merge conflict: DIRTY; base=main, head=feature", + ) + lines = merge_scheduler.conflict_repair_summary([decision]) + assert "### Conflict repair" in lines + assert "Changed files to inspect first:" not in lines + + +def test_merge_scheduler_restamp_summary_ignores_unrelated_notes() -> None: + """Only notes describing the last-push refresh are rendered as restamp evidence.""" + + decision = merge_scheduler.Decision( + pr=8, + action="restamp", + reason="last-push approval head refresh required", + notes=("unrelated note",), + ) + lines = merge_scheduler.last_push_approval_restamp_summary([decision]) + assert any("PR #8" in line for line in lines) + assert " - unrelated note" not in lines + + +def test_r_description_indented_line_before_suggests_is_ignored() -> None: + """Continuation text outside Suggests does not enter the dependency set.""" + + assert r_coverage_peer_gate.declared_suggests( + "Package: demo\n stray continuation\nSuggests: testthat, covr\n" + ) == {"testthat", "covr"} + + +def test_execution_contract_helpers_cover_duplicate_unknown_and_minimal_paths( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Defensive command indexing and minimal package branches stay deterministic.""" + + bucket: dict[str, list[str]] = {} + execution_contracts.add_unique(bucket, "test", "") + execution_contracts.add_unique(bucket, "test", "pytest") + execution_contracts.add_unique(bucket, "test", "pytest") + assert bucket == {"test": ["pytest"]} + + contracts: dict[str, Any] = {"test_commands": []} + execution_contracts.add_command_indexes( + contracts, {"unknown": ["ignored"], "test": ["pytest"]} + ) + assert contracts["test_commands"] == ["pytest"] + + package = tmp_path / "package.json" + package.write_text("{}", encoding="utf-8") + monkeypatch.setattr(execution_contracts, "package_runner", lambda _path: "bun") + node = execution_contracts.discover_package_json(package, tmp_path) + assert node["commands"] == {} + + pyproject = tmp_path / "minimal" / "pyproject.toml" + pyproject.parent.mkdir() + pyproject.write_text("[project]\nname='minimal'\n", encoding="utf-8") + python_contract = execution_contracts.discover_pyproject(pyproject, tmp_path) + assert python_contract["commands"]["security"] + assert "test" not in python_contract["commands"] + assert "lint" not in python_contract["commands"] + + +def test_execution_contract_discovery_skips_packaged_and_ignored_surfaces( + tmp_path: Path, +) -> None: + """Manifest-backed source and ignored virtual-environment manifests take false branches.""" + + repo = tmp_path / "repo" + repo.mkdir() + (repo / "go.mod").write_text("module example.invalid/demo\n", encoding="utf-8") + (repo / "main.go").write_text("package main\n", encoding="utf-8") + assert not any( + item["language"] == "go" + for item in execution_contracts.discover_unpackaged_surfaces(repo) + ) + + node_modules = repo / "node_modules" / "pkg" + node_modules.mkdir(parents=True) + (node_modules / "package.json").write_text("{}", encoding="utf-8") + venv = repo / ".venv" + venv.mkdir() + (venv / "pyproject.toml").write_text("[project]\nname='ignored'\n", encoding="utf-8") + (repo / "Dockerfile").mkdir() + result = execution_contracts.discover_contracts(repo) + assert result["node"] == [] + assert result["python"] == [] + assert result["docker"] == [] + + +def test_sandboxed_verify_timeout_with_no_streams_is_bounded( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A timeout without captured streams still returns the stable timeout code.""" + + repo = tmp_path / "repo" + repo.mkdir() + + def timeout_runner( + command: list[str], _cwd: Path, _env: dict[str, str], timeout: int + ) -> subprocess.CompletedProcess[str]: + raise subprocess.TimeoutExpired(command, timeout, output=None, stderr=None) + + monkeypatch.setattr(sandboxed_verify, "run_command", timeout_runner) + assert sandboxed_verify.main( + ["--repo-root", str(repo), "--timeout", "1", "--", "true"] + ) == 124 + captured = capsys.readouterr() + assert "command timed out" in captured.err + + +def test_web_readiness_retries_5xx_and_timeout_without_streams( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A 5xx is not ready, and a streamless E2E timeout remains deterministic.""" + + class RunningProcess: + def poll(self) -> None: + return None + + class Response: + status = 503 + + def __enter__(self) -> "Response": + return self + + def __exit__(self, *_args: object) -> bool: + return False + + class Opener: + def open(self, _url: str, timeout: int) -> Response: + assert timeout == 2 + return Response() + + ticks = itertools.chain([0.0, 0.0], itertools.repeat(2.0)) + monkeypatch.setattr( + sandboxed_web_e2e.urllib.request, "build_opener", lambda *_args: Opener() + ) + monkeypatch.setattr( + sandboxed_web_e2e.time, "monotonic", lambda: next(ticks) + ) + monkeypatch.setattr(sandboxed_web_e2e.time, "sleep", lambda _seconds: None) + service = sandboxed_web_e2e.Service( + "web", "serve", RunningProcess(), tmp_path / "web.log" # type: ignore[arg-type] + ) + assert not sandboxed_web_e2e.wait_for_url("http://127.0.0.1:8000", 1, service) + + monkeypatch.setattr(sandboxed_web_e2e.time, "monotonic", lambda: 2.0) + + repo = tmp_path / "repo" + repo.mkdir() + + class DoneProcess: + def poll(self) -> int: + return 0 + + def start_service( + label: str, + command: str, + _cwd: Path, + _env: dict[str, str], + logs_dir: Path, + ) -> sandboxed_web_e2e.Service: + log_path = logs_dir / f"{label}.log" + log_path.write_text("", encoding="utf-8") + return sandboxed_web_e2e.Service( + label, command, DoneProcess(), log_path # type: ignore[arg-type] + ) + + def timeout_runner( + command: str, _cwd: Path, _env: dict[str, str], timeout: int + ) -> subprocess.CompletedProcess[str]: + raise subprocess.TimeoutExpired(command, timeout, output=None, stderr=None) + + monkeypatch.setattr(sandboxed_web_e2e, "start_service", start_service) + monkeypatch.setattr(sandboxed_web_e2e, "wait_for_url", lambda *_args: True) + monkeypatch.setattr(sandboxed_web_e2e, "run_shell", timeout_runner) + monkeypatch.setattr(sandboxed_web_e2e, "stop_service", lambda _service: None) + assert sandboxed_web_e2e.main( + [ + "--repo-root", + str(repo), + "--backend-cmd", + "backend", + "--frontend-cmd", + "frontend", + "--e2e-cmd", + "e2e", + "--e2e-timeout", + "1", + ] + ) == 124 + assert "e2e command timed out" in capsys.readouterr().err diff --git a/tests/test_repository_branch_coverage_javascript_and_noema.py b/tests/test_repository_branch_coverage_javascript_and_noema.py new file mode 100644 index 000000000..d9a15a424 --- /dev/null +++ b/tests/test_repository_branch_coverage_javascript_and_noema.py @@ -0,0 +1,205 @@ +"""Close JavaScript materialization and Noema defensive branch coverage.""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path +from typing import Any + +import pytest + +from scripts.ci import javascript_coverage_gate as js_gate +from scripts.ci import materialize_base_javascript_packages as js_materializer +from scripts.ci import noema_review_gate as noema + + +def test_javascript_changed_runtime_lines_ignores_deletion_only_hunk( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A modified runtime file with no added lines does not create fake coverage work.""" + + names = subprocess.CompletedProcess( + args=["git"], returncode=0, stdout=b"src/runtime.ts\0", stderr=b"" + ) + monkeypatch.setattr(js_gate.subprocess, "run", lambda *_args, **_kwargs: names) + monkeypatch.setattr(js_gate, "git", lambda *_args: "@@ -2 +2,0 @@") + + assert js_gate.changed_runtime_lines(tmp_path, "base", "head") == {} + + +def test_javascript_global_summary_ignores_noninteger_statement_lines() -> None: + """Malformed Istanbul statement locations do not create line metrics.""" + + summary = js_gate.summarize_final( + { + "src/runtime.ts": { + "s": {"0": 1}, + "f": {}, + "b": {}, + "statementMap": {"0": {"start": {"line": "two"}}}, + } + } + ) + + assert summary["statements"] == 100.0 + assert summary["lines"] == 100.0 + + +def test_javascript_path_normalization_covers_direct_and_unmatched_paths( + tmp_path: Path, +) -> None: + """Absolute, relative, and unrelated Istanbul paths are handled explicitly.""" + + repo = tmp_path.resolve() + changed = {"src/runtime.ts"} + assert ( + js_gate.normalize_coverage_path(str(repo / "src/runtime.ts"), repo, changed) + == "src/runtime.ts" + ) + assert js_gate.normalize_coverage_path("./src/runtime.ts", repo, changed) == ( + "src/runtime.ts" + ) + assert js_gate.normalize_coverage_path("unrelated.ts", repo, changed) is None + + +def test_javascript_coverage_file_loader_accepts_absolute_and_unknown_entries( + tmp_path: Path, +) -> None: + """Coverage file loading handles absolute paths and ignores unknown JSON names.""" + + repo = tmp_path / "repo" + repo.mkdir() + final = repo / "coverage-final.json" + summary = repo / "coverage-summary.json" + unknown = repo / "other.json" + for path in (final, summary, unknown): + path.write_text("{}", encoding="utf-8") + listing = repo / "coverage-files.txt" + listing.write_text( + f"{final}\ncoverage-summary.json\nother.json\n", encoding="utf-8" + ) + + summaries, finals = js_gate.load_coverage_files(repo, listing) + + assert finals == [(final, {})] + assert summaries == [(summary, {})] + + +def test_regular_base_paths_ignores_nonregular_or_unsafe_entries( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Git trees, symlink modes, absolute paths, and traversal never enter inputs.""" + + entries = b"\0".join( + [ + b"100644 blob " + (b"a" * 40) + b"\tpackage.json", + b"040000 tree " + (b"b" * 40) + b"\tsubtree", + b"120000 blob " + (b"c" * 40) + b"\tsymlink", + b"100644 blob " + (b"d" * 40) + b"\t../escape.json", + b"100644 blob " + (b"e" * 40) + b"\t/absolute.json", + b"", + ] + ) + monkeypatch.setattr(js_materializer, "_git", lambda *_args: entries) + + assert js_materializer._regular_base_paths(tmp_path, "a" * 40) == { + "package.json" + } + + +def test_base_npm_projects_handles_nonobject_packages_and_untracked_workspace( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """npm lock metadata may omit packages or name an untracked workspace safely.""" + + regular_paths = {"package.json", "package-lock.json"} + monkeypatch.setattr( + js_materializer, "_regular_base_paths", lambda *_args: regular_paths + ) + documents = { + "package.json": json.dumps({"name": "fixture"}).encode(), + "package-lock.json": json.dumps( + {"lockfileVersion": 3, "packages": {"packages/missing": {}}} + ).encode(), + } + + def git_bytes(_root: Path, command: str, spec: str, *_args: str) -> bytes: + assert command == "show" + return documents[spec.split(":", 1)[1]] + + monkeypatch.setattr(js_materializer, "_git", git_bytes) + projects = js_materializer.base_npm_projects(tmp_path, "a" * 40) + assert projects[0][2].keys() == {"package.json", "package-lock.json"} + + documents["package-lock.json"] = json.dumps( + {"lockfileVersion": 3, "packages": "not-an-object"} + ).encode() + projects = js_materializer.base_npm_projects(tmp_path, "a" * 40) + assert projects[0][2].keys() == {"package.json", "package-lock.json"} + + +def test_noema_status_context_failure_is_blocking() -> None: + """A non-success legacy status context remains a concrete review blocker.""" + + pr = { + "statusCheckRollup": { + "contexts": { + "nodes": [ + { + "__typename": "StatusContext", + "context": "legacy-security", + "state": "failure", + } + ] + } + } + } + assert noema.blocking_checks(pr) == ["legacy-security: FAILURE"] + + +def test_noema_fetch_diff_truncates_to_prompt_budget( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Oversized diffs are bounded and explicitly marked truncated.""" + + monkeypatch.setattr(noema, "run", lambda _args: "x" * (noema.MAX_DIFF_CHARS + 1)) + diff, truncated = noema.fetch_diff("owner/repo", 1) + assert truncated is True + assert len(diff) == noema.MAX_DIFF_CHARS + + +def test_noema_review_context_includes_locations_bodies_and_all_sections( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Review context retains a bounded line location and nonempty evidence sections.""" + + pr = { + "headRefOid": "a" * 40, + "reviewThreads": { + "nodes": [ + { + "path": "src/runtime.py", + "line": 7, + "isResolved": False, + "isOutdated": False, + "comments": { + "nodes": [ + {"author": {"login": "reviewer"}, "body": "Fix this"}, + {"author": {"login": "reviewer"}, "body": ""}, + ] + }, + } + ] + }, + } + rendered = noema.review_thread_context(pr) + assert "src/runtime.py:7" in rendered + assert "reviewer: Fix this" in rendered + + monkeypatch.setattr(noema, "load_codegraph_context", lambda: "graph") + monkeypatch.setattr(noema, "changed_file_context", lambda *_args: "files") + context = noema.build_review_context("owner/repo", 1, pr) + assert "CodeGraph context" in context + assert "Prior review threads" in context + assert "Changed file context" in context diff --git a/tests/test_repository_branch_coverage_pr743_cleanup.py b/tests/test_repository_branch_coverage_pr743_cleanup.py new file mode 100644 index 000000000..0b3d56367 --- /dev/null +++ b/tests/test_repository_branch_coverage_pr743_cleanup.py @@ -0,0 +1,40 @@ +"""Regression contracts for PR 743 cleanup and Git configuration isolation.""" + +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_DIRECTORY = REPOSITORY_ROOT / ".github" / "workflows" +REVIEW_WORKFLOW_PATH = WORKFLOW_DIRECTORY / "opencode-review-dispatch.yml" +TEMPORARY_REPAIR_WORKFLOW_PATHS = ( + WORKFLOW_DIRECTORY / "repair-pr743-git-config-red-test.yml", + WORKFLOW_DIRECTORY / "one-shot-repair-uv-strix-ci.yml", + WORKFLOW_DIRECTORY / "one-shot-pr743-apply-git-isolation.yml", +) + + +def test_opencode_runtime_git_calls_use_fully_isolated_configuration() -> None: + """Every pre-helper Git call must use the complete isolated configuration block.""" + + workflow = REVIEW_WORKFLOW_PATH.read_text(encoding="utf-8") + marker = " trusted_git() {" + assert marker in workflow + runtime = workflow.split(marker, 1)[0] + count_key = " GIT_CONFIG_COUNT=1 " + chr(92) + "\n" + isolated_block = ( + " GIT_CONFIG_NOSYSTEM=1 " + chr(92) + "\n" + + " GIT_CONFIG_GLOBAL=/dev/null " + chr(92) + "\n" + + count_key + + " GIT_CONFIG_KEY_0=safe.directory " + chr(92) + "\n" + + " GIT_CONFIG_VALUE_0=/work " + chr(92) + "\n" + ) + + assert runtime.count(count_key) == 3 + assert runtime.count(isolated_block) == 3 + + +def test_pr743_temporary_write_workflows_are_absent() -> None: + """Completed one-shot branch writers must not remain in the mergeable tree.""" + + for temporary_workflow_path in TEMPORARY_REPAIR_WORKFLOW_PATHS: + assert not temporary_workflow_path.exists() diff --git a/tests/test_repository_branch_coverage_reporting_edges.py b/tests/test_repository_branch_coverage_reporting_edges.py new file mode 100644 index 000000000..f9d985b04 --- /dev/null +++ b/tests/test_repository_branch_coverage_reporting_edges.py @@ -0,0 +1,182 @@ +"""Close reporting, SBOM, JavaScript, Noema, and scheduler edge coverage.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from scripts.ci import javascript_coverage_gate as js_gate +from scripts.ci import noema_review_gate as noema +from scripts.ci import noema_review_handoff as handoff +from scripts.ci import pr_review_autofix_context as autofix_context +from scripts.ci import pr_review_merge_scheduler as merge_scheduler +from scripts.ci import sanitize_github_output_summary as sanitizer +from scripts.ci import sbom_inventory_aggregator as sbom + + +def test_sanitizer_without_trailing_newline_stays_without_one() -> None: + """Sanitization preserves the absence of a final newline.""" + + assert sanitizer.sanitize_text("plain") == "plain" + + +def test_sbom_defensive_relationship_and_license_shapes() -> None: + """Malformed relationship and license entries fail closed to NOASSERTION.""" + + assert sbom._spdx_described_ids({"relationships": "bad"}) == set() + assert sbom._spdx_described_ids( + { + "relationships": [ + {"relationshipType": "DESCRIBES", "relatedSpdxElement": 7} + ] + } + ) == set() + assert sbom._cyclonedx_license({"licenses": []}) == sbom.NOASSERTION + assert sbom._cyclonedx_license( + {"licenses": [{"license": "MIT"}, {"license": {"id": ""}}]} + ) == sbom.NOASSERTION + + +def test_javascript_absolute_unmatched_path_falls_through_suffix_matching( + tmp_path: Path, +) -> None: + """An unrelated absolute coverage path falls through to the bounded suffix check.""" + + repo = tmp_path.resolve() + assert ( + js_gate.normalize_coverage_path( + str(repo / "src" / "unrelated.ts"), repo, {"src/runtime.ts"} + ) + is None + ) + + +def test_javascript_main_ignores_unmatched_coverage_records( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """Coverage records that cannot map to a changed path are ignored before failure.""" + + repo = tmp_path / "repo" + repo.mkdir() + (repo / "src").mkdir() + (repo / "src" / "runtime.ts").write_text("export const value = 1;\n") + listing = repo / "coverage-files.txt" + listing.write_text("", encoding="utf-8") + monkeypatch.setattr( + js_gate, + "load_coverage_files", + lambda *_args: ( + [], + [ + ( + repo / "coverage-final.json", + {"/outside/unrelated.ts": {"s": {}, "f": {}, "b": {}}}, + ) + ], + ), + ) + monkeypatch.setattr( + js_gate, + "changed_runtime_lines", + lambda *_args: {"src/runtime.ts": {1}}, + ) + + assert ( + js_gate.main( + [ + "--repo-root", + str(repo), + "--base-sha", + "a" * 40, + "--head-sha", + "b" * 40, + "--summary-list", + str(listing), + ] + ) + == 1 + ) + assert "missing instrumentation" in capsys.readouterr().out + + +def test_noema_nonblocking_status_small_diff_and_empty_context_branches( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Success statuses, small diffs, invalid thread lines, and empty sections stay clean.""" + + assert noema.blocking_checks( + { + "statusCheckRollup": { + "contexts": { + "nodes": [ + { + "__typename": "StatusContext", + "context": "legacy-security", + "state": "SUCCESS", + } + ] + } + } + } + ) == [] + + monkeypatch.setattr(noema, "run", lambda _args: "small diff") + assert noema.fetch_diff("owner/repo", 1) == ("small diff", False) + + pr = { + "headRefOid": "a" * 40, + "reviewThreads": { + "nodes": [ + { + "path": "src/runtime.py", + "line": None, + "comments": { + "nodes": [ + {"author": {"login": "reviewer"}, "body": "note"} + ] + }, + } + ] + }, + } + rendered_context = noema.review_thread_context(pr) + assert rendered_context == "- Thread open at src/runtime.py:\n - reviewer: note" + + monkeypatch.setattr(noema, "load_codegraph_context", lambda: "") + monkeypatch.setattr(noema, "review_thread_context", lambda _pr: "") + monkeypatch.setattr(noema, "changed_file_context", lambda *_args: "") + assert noema.build_review_context("owner/repo", 1, pr) == "" + + +def test_noema_handoff_skips_nonterminal_marker_review() -> None: + """A marker-bearing review with a nonterminal state does not end polling.""" + + head = "a" * 40 + reviews = [ + { + "commit_id": head, + "user": {"login": handoff.NOEMA_REVIEW_AUTHOR}, + "body": handoff.NOEMA_REVIEW_MARKER, + "state": "pending", + } + ] + assert handoff.noema_review_state(reviews, head) is None + + +def test_autofix_context_ignores_unknown_rollup_node() -> None: + """Unknown status-rollup node types are ignored without emitting false evidence.""" + + assert autofix_context.check_summary([{"__typename": "Unknown"}]) == [] + + +def test_merge_scheduler_blocked_wait_reason_without_review_note() -> None: + """An already-approved BLOCKED PR omits the unsatisfied-review suffix.""" + + reason = merge_scheduler.auto_merge_wait_reason( + "BLOCKED", {"reviewDecision": "APPROVED"} + ) + assert "GitHub reviewDecision" not in reason + assert "mergeability is BLOCKED" in reason diff --git a/tests/test_repository_branch_coverage_review_schedulers.py b/tests/test_repository_branch_coverage_review_schedulers.py new file mode 100644 index 000000000..8ee58db12 --- /dev/null +++ b/tests/test_repository_branch_coverage_review_schedulers.py @@ -0,0 +1,199 @@ +"""Close Noema handoff, approval, rebase, and scheduler branch coverage.""" + +from __future__ import annotations + +import argparse +import json +from typing import Any + +import pytest + +from scripts.ci import noema_review_gate as noema +from scripts.ci import noema_review_handoff as handoff +from scripts.ci import opencode_existing_approval_gate as approval_gate +from scripts.ci import pr_auto_rebase as auto_rebase +from scripts.ci import pr_review_autofix_context as autofix_context +from scripts.ci import pr_review_fix_scheduler as fix_scheduler +from scripts.ci import pr_review_merge_scheduler as merge_scheduler + + +def test_noema_public_dns_result_reaches_valid_model_response( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Globally routable DNS answers pass the SSRF gate and return strict JSON.""" + + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://review.example.invalid/v1/chat") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") + monkeypatch.setattr( + noema.socket, + "getaddrinfo", + lambda *_args: [(2, 1, 6, "", ("8.8.8.8", 0))], + ) + + class Response: + """Context-managed deterministic LLM response.""" + + def __enter__(self) -> "Response": + return self + + def __exit__(self, *_args: object) -> bool: + return False + + def read(self) -> bytes: + return json.dumps( + { + "choices": [ + { + "message": { + "content": json.dumps( + { + "decision": "approve", + "summary": "clean", + "findings": [], + } + ) + } + } + ] + } + ).encode() + + class Opener: + """Open one deterministic provider response.""" + + def open(self, _request: Any, timeout: int) -> Response: + assert timeout == 120 + return Response() + + monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) + verdict = noema.call_llm("owner/repo", 1, {"headRefOid": "a" * 40}, "diff", False) + assert verdict["decision"] == "approve" + + +def test_noema_handoff_returns_current_terminal_state() -> None: + """A marker-bearing exact-head Noema approval is recognized immediately.""" + + head = "a" * 40 + reviews = [ + { + "commit_id": head, + "user": {"login": handoff.NOEMA_REVIEW_AUTHOR}, + "body": handoff.NOEMA_REVIEW_MARKER, + "state": "approved", + } + ] + assert handoff.noema_review_state(reviews, head) == "APPROVED" + + +def test_adversarial_evidence_ignores_nonobject_json_block() -> None: + """A parseable scalar block does not replace the last structured evidence object.""" + + body = ( + '## Adversarial validation\n```json\n{"status":"passed"}\n```\n' + '## Adversarial validation\n```json\n[1,2,3]\n```' + ) + assert approval_gate.extract_adversarial_evidence(body) == {"status": "passed"} + + +def test_auto_rebase_pagination_exits_after_exact_requested_count( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Pagination terminates through the loop condition after filling the requested cap.""" + + payload = { + "data": { + "repository": { + "pullRequests": { + "nodes": [{"number": 1}], + "pageInfo": {"hasNextPage": True, "endCursor": "next"}, + } + } + } + } + monkeypatch.setattr(auto_rebase, "gh_graphql", lambda *_args, **_kwargs: payload) + assert auto_rebase.fetch_open_prs("owner/repo", 1) == [{"number": 1}] + + +def test_autofix_context_renders_legacy_status_context() -> None: + """Legacy status contexts remain visible in bounded autofix evidence.""" + + assert autofix_context.check_summary( + [{"__typename": "StatusContext", "context": "security", "state": "SUCCESS"}] + ) == ["- security: SUCCESS"] + + +def test_fix_scheduler_queue_includes_eligible_pr_without_fix_need( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """An eligible but clean PR traverses the no-comment pre-scan branch.""" + + pr = { + "number": 1, + "isDraft": False, + "baseRefName": "main", + "headRepository": {"nameWithOwner": "owner/repo"}, + } + monkeypatch.setattr(fix_scheduler, "fetch_open_prs", lambda *_args: [pr]) + monkeypatch.setattr(fix_scheduler, "same_repository_head", lambda *_args: True) + monkeypatch.setattr(fix_scheduler, "needs_autofix", lambda _pr: (False, ())) + monkeypatch.setattr( + fix_scheduler, "needs_conflict_resolution", lambda _pr: (False, ()) + ) + monkeypatch.setattr( + fix_scheduler, "inspect_pr", lambda *_args, **_kwargs: ("skip", ("clean",)) + ) + args = argparse.Namespace( + repo="owner/repo", + pr_number=None, + max_prs=10, + base_branch="main", + max_dispatches=1, + dry_run=True, + ) + assert fix_scheduler.process_queue(args) == 0 + assert '"inspected": 1' in capsys.readouterr().out + + +def test_merge_scheduler_rest_pagination_exits_at_requested_count( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """REST pagination exits through the loop condition after the exact cap.""" + + payload = [{"number": 1}, {"number": 2}] + monkeypatch.setattr(merge_scheduler, "gh_api_json", lambda _path: payload) + monkeypatch.setattr( + merge_scheduler, + "rest_pr_node", + lambda _repo, pr: {"number": pr["number"]}, + ) + assert merge_scheduler.fetch_open_prs_rest("owner/repo", 2) == [ + {"number": 1}, + {"number": 2}, + ] + + +def test_merge_scheduler_keeps_newest_check_when_older_duplicate_arrives() -> None: + """An older duplicate check run cannot replace the newest successful state.""" + + def check(started: str, conclusion: str) -> dict[str, Any]: + return { + "__typename": "CheckRun", + "name": "quality", + "startedAt": started, + "status": "COMPLETED", + "conclusion": conclusion, + "checkSuite": {"workflowRun": {"workflow": {"name": "CI"}}}, + } + + pr = { + "statusCheckRollup": { + "contexts": { + "nodes": [ + check("2026-08-05T02:00:00Z", "SUCCESS"), + check("2026-08-05T01:00:00Z", "FAILURE"), + ] + } + } + } + assert merge_scheduler.failed_status_checks(pr) == [] diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 1c7b6f3ff..233c08584 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -407,7 +407,7 @@ def test_nvidia_nim_defaults_preserve_existing_fallbacks_without_secret( env={ **os.environ, "GITHUB_OUTPUT": str(strix_output), - "STRIX_MODEL": "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b", + "STRIX_MODEL": "nvidia_nim/nvidia/nemotron-3-super-120b-a12b", "STRIX_MODEL_REQUESTED": "", "STRIX_OPENAI_API_KEY": "synthetic-openai-key", "STRIX_OPENROUTER_API_KEY": "", diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py new file mode 100644 index 000000000..a48f3092d --- /dev/null +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -0,0 +1,261 @@ +"""Regression contract for NVIDIA NIM model retirement and hosted 404 fallback. + +The central Strix workflow must not turn a provider-side model-catalog 404 into a +security finding or retry the same unavailable model. It must move to another +approved free NVIDIA NIM candidate before using the existing GitHub Models +fallbacks, while ordinary application 404 output remains non-retryable. +""" + +from __future__ import annotations + +import re +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +STRIX_GATE = REPOSITORY_ROOT / "scripts" / "ci" / "strix_quick_gate.sh" +STRIX_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "strix.yml" +DEFAULT_NVIDIA_MODEL = "nvidia_nim/nvidia/nemotron-3-super-120b-a12b" +FREE_NVIDIA_FALLBACK = ( + "nvidia_nim/nvidia/llama-3.3-nemotron-super-49b-v1.5" +) +RETIRED_PRIMARY_MODEL = "nvidia_nim/nvidia/nemotron-3-ultra-550b-a55b" + + +def _function_block(source: str, function_name: str) -> str: + """Return one top-level Bash function, including its closing brace. + + The relevant Strix classifier functions contain no nested top-level function + declarations. Requiring a brace on a line by itself keeps extraction bounded + and makes source-shape drift fail the test instead of silently selecting the + wrong shell code. + """ + + match = re.search( + rf"(?ms)^{re.escape(function_name)}\(\) \{{\n.*?^\}}\n", + source, + ) + if match is None: + raise AssertionError(f"missing Bash function: {function_name}") + return match.group(0) + + +def _classifies_as_nvidia_not_found(log_text: str) -> bool: + """Execute the production classifier against a bounded synthetic log.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + function_source = _function_block( + gate_source, + "is_nvidia_nim_not_found_error", + ) + with tempfile.TemporaryDirectory(prefix="strix-nvidia-404-") as temp_dir: + log_path = Path(temp_dir) / "strix.log" + log_path.write_text(log_text, encoding="utf-8") + script = "\n".join( + ( + "set -euo pipefail", + 'STRIX_LOG="$1"', + function_source, + "is_nvidia_nim_not_found_error", + ) + ) + completed = subprocess.run( + ["bash", "-c", script, "strix-classifier", str(log_path)], + check=False, + capture_output=True, + text=True, + ) + if completed.returncode not in {0, 1}: + raise AssertionError(completed.stderr) + return completed.returncode == 0 + + +def _workflow_signal_pattern(workflow: str, variable_name: str) -> str: + """Extract one single-quoted POSIX ERE assigned in the Strix workflow.""" + + match = re.search( + rf"(?m)^\s+{re.escape(variable_name)}='([^']+)'$", + workflow, + ) + if match is None: + raise AssertionError(f"missing workflow signal: {variable_name}") + return match.group(1) + + +def _workflow_neutralizes(log_text: str) -> bool: + """Execute the outer workflow's backend-neutralization condition.""" + + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + backend_pattern = _workflow_signal_pattern( + workflow, + "backend_unavailable_signal", + ) + vulnerability_pattern = _workflow_signal_pattern( + workflow, + "reported_vulnerability_signal", + ) + with tempfile.TemporaryDirectory(prefix="strix-workflow-404-") as temp_dir: + log_path = Path(temp_dir) / "strix.log" + log_path.write_text(log_text, encoding="utf-8") + backend = subprocess.run( + ["grep", "-Eiq", backend_pattern, str(log_path)], + check=False, + capture_output=True, + text=True, + ) + vulnerability = subprocess.run( + ["grep", "-Eiq", vulnerability_pattern, str(log_path)], + check=False, + capture_output=True, + text=True, + ) + if backend.returncode not in {0, 1}: + raise AssertionError(backend.stderr) + if vulnerability.returncode not in {0, 1}: + raise AssertionError(vulnerability.stderr) + return backend.returncode == 0 and vulnerability.returncode == 1 + + +class StrixNvidiaNotFoundFallbackTests(unittest.TestCase): + """Protect provider-scoped 404 fallback without weakening security gates.""" + + def test_nvidia_hosted_model_404_is_retryable_provider_evidence(self) -> None: + """Recognize the exact LiteLLM/NVIDIA 404 observed in required CI.""" + + log = ( + "litellm.exceptions.NotFoundError: Nvidia_nimException - " + "Error code: 404\n" + "Vulnerabilities 0\n" + ) + self.assertTrue(_classifies_as_nvidia_not_found(log)) + + def test_application_404_without_nvidia_context_is_not_retryable(self) -> None: + """Do not let target-application HTTP 404 text bypass security evidence.""" + + log = "GET /api/project_record/unknown 404\nNotFoundError: record missing\n" + self.assertFalse(_classifies_as_nvidia_not_found(log)) + + def test_provider_and_404_signals_must_share_one_log_line(self) -> None: + """Reject cross-line signal assembly from untrusted scan-target output.""" + + log = ( + "source literal: Nvidia_nimException\n" + "GET /api/project_record/unknown Error code: 404\n" + ) + self.assertFalse(_classifies_as_nvidia_not_found(log)) + + def test_provider_literal_without_litellm_error_is_not_retryable(self) -> None: + """Reject source text that imitates an NVIDIA provider error line.""" + + log = "source literal: Nvidia_nimException Error code: 404\n" + self.assertFalse(_classifies_as_nvidia_not_found(log)) + + def test_not_found_skips_same_model_and_enters_cross_model_fallback(self) -> None: + """Wire the classifier only into infrastructure and model fallback.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + infrastructure = _function_block( + gate_source, + "has_detected_infrastructure_error", + ) + retryable = _function_block(gate_source, "is_model_retryable_error") + same_model_retry = _function_block( + gate_source, + "is_transient_same_model_retry_error", + ) + + self.assertIn("is_nvidia_nim_not_found_error", infrastructure) + self.assertIn("is_nvidia_nim_not_found_error", retryable) + self.assertNotIn("is_nvidia_nim_not_found_error", same_model_retry) + + def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: + """Prefer a documented hosted NIM and another NIM before GitHub.""" + + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + default_expression = ( + "steps.target_visibility.outputs.is_private == 'false' && " + f"'{DEFAULT_NVIDIA_MODEL}' || 'gpt-5.6-luna'" + ) + self.assertIn(default_expression, workflow) + self.assertIn( + f'[ "$strix_model" = "{DEFAULT_NVIDIA_MODEL}" ] ' + '&& [ -z "${STRIX_NVIDIA_NIM_API_KEY:-}" ]', + workflow, + ) + self.assertIn( + "steps.gate.outputs.provider_mode == 'nvidia_nim' && " + f"'{FREE_NVIDIA_FALLBACK} github_models/openai/o3 " + "github_models/openai/gpt-5-chat'", + workflow, + ) + + default_gate = workflow.split("- name: Gate Strix secrets", maxsplit=1)[1] + default_gate = default_gate.split( + "- name: Prepare LLM API key input file", + maxsplit=1, + )[0] + self.assertNotIn(RETIRED_PRIMARY_MODEL, default_gate) + + def test_outer_workflow_requires_litellm_context_for_nvidia_404(self) -> None: + """Reject provider-like target text in the outer neutralization gate.""" + + self.assertFalse( + _workflow_neutralizes( + "source literal: Nvidia_nimException Error code: 404\n" + ) + ) + self.assertTrue( + _workflow_neutralizes( + "litellm.exceptions.NotFoundError: Nvidia_nimException - " + "Error code: 404\nVulnerabilities 0\n" + ) + ) + + def test_outer_workflow_rejects_cross_line_signal_assembly(self) -> None: + """Require exception, provider, and 404 evidence on one physical line.""" + + self.assertFalse( + _workflow_neutralizes( + "litellm.exceptions.NotFoundError: provider unavailable\n" + "Nvidia_nimException Error code: 404\n" + ) + ) + + def test_outer_workflow_rejects_nvidia_404_without_litellm_context(self) -> None: + """Require LiteLLM NotFoundError context, not just NVIDIA + 404.""" + + self.assertFalse( + _workflow_neutralizes( + "Nvidia_nimException Error code: 404\nVulnerabilities 0\n" + ) + ) + + def test_outer_workflow_never_neutralizes_reported_vulnerabilities(self) -> None: + """Keep a real vulnerability signal blocking despite provider failure.""" + + self.assertFalse( + _workflow_neutralizes( + "litellm.exceptions.NotFoundError: Nvidia_nimException - " + "Error code: 404\nVulnerabilities 1\n" + ) + ) + + def test_workflow_neutralizes_only_nvidia_404_without_findings(self) -> None: + """Retain the static fail-closed vulnerability evidence contract.""" + + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + self.assertIn("Nvidia_nimException", workflow) + self.assertIn("Error code:[[:space:]]*404", workflow) + self.assertIn("reported_vulnerability_signal", workflow) + self.assertIn("Vulnerabilities[[:space:]]+[1-9]", workflow) + self.assertIn( + '! grep -Eiq "$reported_vulnerability_signal"', + workflow, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_trusted_uv_download_contract.py b/tests/test_trusted_uv_download_contract.py new file mode 100644 index 000000000..02f3c5961 --- /dev/null +++ b/tests/test_trusted_uv_download_contract.py @@ -0,0 +1,92 @@ +"""Static security contract for the pinned trusted-uv network boundary.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_MATERIALIZER = _REPO_ROOT / "scripts" / "ci" / "materialize_base_python_requirements.py" +_EXPECTED_URL = ( + "https://releases.astral.sh/github/uv/releases/download/0.12.1/" + "uv-x86_64-unknown-linux-gnu.tar.gz" +) +_SEMGREP_DYNAMIC_URL_RULE = ( + "python.lang.security.audit.dynamic-urllib-use-detected." + "dynamic-urllib-use-detected" +) + + +def _module_tree() -> ast.Module: + """Parse the materializer without importing or executing repository code.""" + return ast.parse(_MATERIALIZER.read_text(encoding="utf-8"), filename=str(_MATERIALIZER)) + + +def _download_function() -> ast.FunctionDef: + """Return the trusted-uv downloader function from the parsed module.""" + for node in _module_tree().body: + if isinstance(node, ast.FunctionDef) and node.name == "_download_trusted_uv_archive": + return node + raise AssertionError("trusted uv downloader function is missing") + + +def _assigned_literal(name: str) -> object: + """Return one module-level literal assignment without evaluating code.""" + for node in _module_tree().body: + if not isinstance(node, ast.Assign) or len(node.targets) != 1: + continue + target = node.targets[0] + if isinstance(target, ast.Name) and target.id == name: + return ast.literal_eval(node.value) + raise AssertionError(f"module literal {name} is missing") + + +def _urlopen_calls() -> list[ast.Call]: + """Return calls whose attribute name is exactly ``urlopen``.""" + return [ + node + for node in ast.walk(_download_function()) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "urlopen" + ] + + +def test_urlopen_receives_one_literal_https_release_url() -> None: + """Static analysis can prove repository or user data never selects the URL.""" + calls = _urlopen_calls() + + assert len(calls) == 1 + assert len(calls[0].args) == 1 + url_argument = calls[0].args[0] + assert isinstance(url_argument, ast.Constant) + assert isinstance(url_argument.value, str) + assert url_argument.value == _EXPECTED_URL + + +def test_literal_network_sink_matches_the_documented_release_constant() -> None: + """The scanner-friendly sink literal cannot drift from the release identity.""" + assert _assigned_literal("TRUSTED_UV_ARCHIVE_URL") == _EXPECTED_URL + + +def test_downloader_never_constructs_a_dynamic_request_object() -> None: + """The audited downloader cannot hide a dynamic URL inside ``Request``.""" + request_calls = [ + node + for node in ast.walk(_download_function()) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "Request" + ] + + assert request_calls == [] + + +def test_literal_urlopen_sink_has_one_scoped_semgrep_suppression() -> None: + """The known false positive is suppressed only at the audited literal sink.""" + source_lines = _MATERIALIZER.read_text(encoding="utf-8").splitlines() + sink_lines = [line for line in source_lines if "with urllib.request.urlopen(" in line] + + assert len(sink_lines) == 1 + assert f"# nosemgrep: {_SEMGREP_DYNAMIC_URL_RULE}" in sink_lines[0] diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py new file mode 100644 index 000000000..23a849bd8 --- /dev/null +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -0,0 +1,100 @@ +"""Contract tests for exact-head trusted uv materializer quality evidence.""" + +from pathlib import Path + + +WORKFLOW_PATH = Path(".github/workflows/trusted-uv-materializer-quality-ci.yml") + + +def _workflow_text() -> str: + """Return the trusted uv materializer quality workflow as UTF-8 text.""" + + return WORKFLOW_PATH.read_text(encoding="utf-8") + + +def test_quality_workflow_runs_for_every_materializer_surface() -> None: + """Changes to production, tests, tooling, or the gate itself trigger evidence.""" + + workflow = _workflow_text() + + required_paths = ( + '".github/workflows/trusted-uv-materializer-quality-ci.yml"', + '"scripts/ci/materialize_base_python_requirements.py"', + '"tests/conftest.py"', + '"tests/test_materialize*.py"', + '"tests/test_trusted_uv*.py"', + '"tests/test_uv*.py"', + '"tests/test_repository_branch_coverage_*.py"', + '"requirements-opencode-review-ci-hashes.txt"', + '"pyproject.toml"', + ) + for required_path in required_paths: + assert workflow.count(required_path) == 2 + + +def test_quality_workflow_pins_actions_and_uses_read_only_permissions() -> None: + """Quality evidence executes from the exact PR head with least privilege.""" + + workflow = _workflow_text() + + assert "permissions:\n contents: read" in workflow + assert workflow.count( + "step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920" + ) == 2 + assert workflow.count( + "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" + ) == 2 + assert workflow.count( + "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" + ) == 2 + assert workflow.count("persist-credentials: false") == 2 + assert workflow.count("ref: ${{ github.event.pull_request.head.sha }}") == 2 + + +def test_minimum_python_contract_exercises_the_tomli_fallback() -> None: + """Python 3.10 imports production through a deterministic local tomli stub.""" + + workflow = _workflow_text() + + assert 'python-version: "3.10"' in workflow + assert "python -m compileall -q scripts/ci/materialize_base_python_requirements.py" in workflow + assert 'stub_root / "tomli.py"' in workflow + assert "materializer.tomllib.STUB_MARKER is True" in workflow + + +def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> None: + """The stable runtime proves complete deterministic production evidence.""" + + workflow = _workflow_text() + + assert 'python-version: "3.14"' in workflow + assert ( + "python -m pip install --disable-pip-version-check --require-hashes " + "-r requirements-opencode-review-ci-hashes.txt" + ) in workflow + assert "branch = True" in workflow + assert "scripts/ci/materialize_base_python_requirements.py" in workflow + assert "fail_under = 100" in workflow + assert "python -m coverage report" in workflow + assert "python -m coverage run -m pytest tests -q" in workflow + assert "unset COVERAGE_RCFILE" in workflow + assert "python -m interrogate --fail-under 100" in workflow + assert "python -m compileall -q" in workflow + + required_tests = ( + "tests/test_materialize_base_python_requirements.py", + "tests/test_materialize_uv_export_hash_contract.py", + "tests/test_trusted_uv_download_contract.py", + "tests/test_trusted_uv_portability_and_streaming.py", + "tests/test_uv_export_isolation_contract.py", + "tests/test_uv_redirect_and_coverage_contract.py", + "tests/test_uv_redirect_boundary.py", + "tests/test_uv_workspace_fail_closed.py", + "tests/test_trusted_uv_materializer_quality_workflow_contract.py", + "tests/test_repository_branch_coverage_javascript_and_noema.py", + "tests/test_repository_branch_coverage_review_schedulers.py", + "tests/test_repository_branch_coverage_execution_sandboxes.py", + "tests/test_repository_branch_coverage_reporting_edges.py", + ) + for test_path in required_tests: + assert test_path in workflow diff --git a/tests/test_trusted_uv_portability_and_streaming.py b/tests/test_trusted_uv_portability_and_streaming.py new file mode 100644 index 000000000..34d8356c1 --- /dev/null +++ b/tests/test_trusted_uv_portability_and_streaming.py @@ -0,0 +1,99 @@ +"""Regression contracts for portable and bounded trusted uv bootstrapping.""" + +from __future__ import annotations + +import platform +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +class _ChunkedResponse: + """Return deterministic short reads from one trusted final URL.""" + + def __init__(self, chunks: list[bytes]) -> None: + """Store response chunks in the order an HTTP stream would expose them.""" + self._chunks = iter(chunks) + + def __enter__(self) -> "_ChunkedResponse": + """Return this response from its context manager.""" + return self + + def __exit__(self, *_args: object) -> None: + """Leave the response context without suppressing exceptions.""" + + @staticmethod + def geturl() -> str: + """Return the immutable trusted Astral release origin.""" + return materializer.TRUSTED_UV_ARCHIVE_URL + + def read(self, _size: int) -> bytes: + """Return one short chunk, followed by EOF when chunks are exhausted.""" + return next(self._chunks, b"") + + +def test_trusted_uv_download_collects_short_reads( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A valid archive is accumulated until EOF instead of accepting a prefix.""" + response = _ChunkedResponse([b"ab", b"cd", b""]) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_args, **_kwargs: response, + ) + + assert materializer._download_trusted_uv_archive() == b"abcd" + + +def test_trusted_uv_download_rejects_oversize_across_short_reads( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Many individually small chunks cannot bypass the total download bound.""" + response = _ChunkedResponse([b"12", b"34", b"5", b""]) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_args, **_kwargs: response, + ) + monkeypatch.setattr(materializer, "TRUSTED_UV_DOWNLOAD_MAX_BYTES", 4) + + with pytest.raises(RuntimeError, match="bounded download size"): + materializer._download_trusted_uv_archive() + + +@pytest.mark.parametrize( + ("runner_platform", "runner_machine"), + [("darwin", "x86_64"), ("linux", "aarch64")], +) +def test_trusted_uv_install_rejects_unsupported_runner_before_download( + monkeypatch: pytest.MonkeyPatch, + runner_platform: str, + runner_machine: str, +) -> None: + """The Linux x86_64 archive is never downloaded on an unsupported runner.""" + materializer._install_trusted_uv.cache_clear() + monkeypatch.setattr(materializer.sys, "platform", runner_platform) + monkeypatch.setattr(platform, "machine", lambda: runner_machine) + + def unexpected_download() -> bytes: + raise AssertionError("unsupported runners must fail before network access") + + monkeypatch.setattr(materializer, "_download_trusted_uv_archive", unexpected_download) + + with pytest.raises(RuntimeError, match="supports only linux x86_64"): + materializer._install_trusted_uv() + + +def test_python_310_toml_parser_fallback_is_declared() -> None: + """Python 3.10 receives the production fallback and conditional dependency.""" + repository_root = Path(__file__).resolve().parents[1] + materializer_source = ( + repository_root / "scripts" / "ci" / "materialize_base_python_requirements.py" + ).read_text(encoding="utf-8") + project_source = (repository_root / "pyproject.toml").read_text(encoding="utf-8") + + assert "import tomli as tomllib" in materializer_source + assert "python_version < '3.11'" in project_source or 'python_version < "3.11"' in project_source diff --git a/tests/test_uv_export_isolation_contract.py b/tests/test_uv_export_isolation_contract.py new file mode 100644 index 000000000..f24a6ac8e --- /dev/null +++ b/tests/test_uv_export_isolation_contract.py @@ -0,0 +1,127 @@ +"""Behavioral isolation and output contracts for trusted ``uv export``.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +def test_uv_export_runs_with_a_bounded_isolated_environment( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ambient runner configuration cannot select export behavior or cache state.""" + observed: dict[str, object] = {} + + def fake_run(command: list[str], **kwargs): + observed["command"] = command + observed["kwargs"] = kwargs + return subprocess.CompletedProcess(command, 0, b"", b"") + + monkeypatch.setattr(materializer.subprocess, "run", fake_run) + + result = materializer._run_uv_export(tmp_path, "/trusted/uv") + + assert result.returncode == 0 + assert observed["command"] == [ + "/trusted/uv", + "export", + "--frozen", + "--offline", + "--no-cache", + "--no-progress", + "--color", + "never", + "--no-emit-project", + "--no-editable", + "--format", + "requirements-txt", + ] + kwargs = observed["kwargs"] + assert isinstance(kwargs, dict) + assert kwargs["cwd"] == str(tmp_path) + assert kwargs["check"] is False + assert kwargs["stdout"] is subprocess.PIPE + assert kwargs["stderr"] is subprocess.PIPE + + environment = kwargs["env"] + assert environment == { + "HOME": str(tmp_path / ".uv-home"), + "NO_COLOR": "1", + "PATH": os.defpath, + "TMPDIR": str(tmp_path / ".uv-tmp"), + "UV_NO_ENV_FILE": "1", + "UV_PYTHON_DOWNLOADS": "never", + "XDG_CACHE_HOME": str(tmp_path / ".uv-cache"), + "XDG_CONFIG_HOME": str(tmp_path / ".uv-config"), + } + for directory_name in (".uv-home", ".uv-tmp", ".uv-cache", ".uv-config"): + assert (tmp_path / directory_name).is_dir() + + +def test_uv_export_does_not_disable_project_metadata_discovery() -> None: + """Isolation must retain the reconstructed project's ``pyproject.toml`` input.""" + source = Path(materializer.__file__).read_text(encoding="utf-8") + + assert '"--no-config"' not in source + assert "UV_NO_CONFIG" not in source + + +@pytest.mark.parametrize( + "content", + [ + b"--index-url https://packages.invalid/simple --hash=sha256:" + b"a" * 64 + b"\n", + b"demo @ file:///tmp/demo --hash=sha256:" + b"a" * 64 + b"\n", + b"demo==1 --hash=sha512:" + b"a" * 128 + b"\n", + b"demo==1 --hash=sha256:abcd\n", + ], +) +def test_uv_export_rejects_non_package_or_non_sha256_lines(content: bytes) -> None: + """An option, local reference, wrong algorithm, or short digest is not a lock pin.""" + assert materializer._is_fully_hash_pinned_export(content) is False + + +def test_uv_export_accepts_exact_package_pins_with_markers_and_multiple_hashes() -> None: + """A normalized exact requirement with SHA-256 hashes remains exportable.""" + content = ( + b"demo-extra[fast]==1.2.3 ; python_version >= '3.12' \\\n" + b" --hash=sha256:" + b"a" * 64 + b" \\\n" + b" --hash=sha256:" + b"b" * 64 + b"\n" + ) + + assert materializer._is_fully_hash_pinned_export(content) is True + + +def test_tracked_pyproject_read_failure_is_not_misclassified_as_orphan( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A present sibling metadata blob that cannot be read must fail closed.""" + tree = ( + b"100644 blob " + b"a" * 40 + b"\tpyproject.toml\0" + b"100644 blob " + b"b" * 40 + b"\tuv.lock\0" + ) + + def fake_git(_repo_root: Path, *args: str) -> bytes: + if args[0] == "ls-tree": + return tree + if args[0] == "show" and args[1].endswith(":uv.lock"): + return b"version = 1\n" + if args[0] == "show" and args[1].endswith(":pyproject.toml"): + raise RuntimeError("tracked metadata blob could not be read") + raise AssertionError(args) + + monkeypatch.setattr(materializer, "_git", fake_git) + monkeypatch.setattr( + materializer, + "_install_trusted_uv", + lambda: (_ for _ in ()).throw(AssertionError("uv must not start")), + ) + + with pytest.raises(RuntimeError, match="tracked metadata blob could not be read"): + materializer.base_hash_locks(tmp_path, "a" * 40) diff --git a/tests/test_uv_redirect_and_coverage_contract.py b/tests/test_uv_redirect_and_coverage_contract.py new file mode 100644 index 000000000..0830624ef --- /dev/null +++ b/tests/test_uv_redirect_and_coverage_contract.py @@ -0,0 +1,67 @@ +"""Regression contracts for the trusted uv origin and coverage evidence.""" + +from __future__ import annotations + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - exercised on Python 3.10 + import tomli as tomllib +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer +from tests.conftest import FakeHttpResponse + + +@pytest.mark.parametrize( + "unsafe_url", + [ + "https://releases.astral.sh:444/github/uv/releases/download/0.12.1/uv.tar.gz", + "https://releases.astral.sh:not-a-port/github/uv/releases/download/0.12.1/uv.tar.gz", + ], +) +def test_trusted_uv_download_rejects_nondefault_or_malformed_ports( + monkeypatch: pytest.MonkeyPatch, + unsafe_url: str, +) -> None: + """The pinned Astral host cannot redirect to another or malformed service port.""" + + response = FakeHttpResponse(unsafe_url) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_args, **_kwargs: response, + ) + + with pytest.raises(RuntimeError, match="redirected outside"): + materializer._download_trusted_uv_archive() + + +def test_trusted_uv_download_accepts_explicit_default_https_port( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """An explicit port 443 still denotes the fixed trusted HTTPS origin.""" + + response = FakeHttpResponse( + "https://releases.astral.sh:443/github/uv/releases/download/0.12.1/uv.tar.gz" + ) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_args, **_kwargs: response, + ) + + assert materializer._download_trusted_uv_archive() == b"archive" + + +def test_repository_coverage_contract_enforces_branches_at_one_hundred_percent() -> None: + """The declared 100% quality gate measures branch as well as statement coverage.""" + + repository_root = Path(__file__).resolve().parents[1] + configuration = tomllib.loads( + (repository_root / "pyproject.toml").read_text(encoding="utf-8") + ) + + assert configuration["tool"]["coverage"]["run"]["branch"] is True + assert configuration["tool"]["coverage"]["report"]["fail_under"] == 100 diff --git a/tests/test_uv_redirect_boundary.py b/tests/test_uv_redirect_boundary.py new file mode 100644 index 000000000..fd98592e8 --- /dev/null +++ b/tests/test_uv_redirect_boundary.py @@ -0,0 +1,67 @@ +"""Behavioral contracts for the trusted uv download redirect boundary.""" + +from __future__ import annotations + +import urllib.request +from collections.abc import Iterator + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +@pytest.fixture(autouse=True) +def clear_trusted_uv_opener_cache() -> Iterator[None]: + """Clear process-global opener state before and after every boundary test.""" + materializer._install_trusted_uv_url_opener.cache_clear() + yield + materializer._install_trusted_uv_url_opener.cache_clear() + + +def test_trusted_uv_redirect_handler_rejects_before_following() -> None: + """Every HTTP redirect is rejected before urllib creates a target request.""" + handler = materializer._RejectTrustedUvRedirects() + original = urllib.request.Request(materializer.TRUSTED_UV_ARCHIVE_URL) + + with pytest.raises(RuntimeError, match="redirects are forbidden"): + handler.redirect_request( + original, + None, + 302, + "Found", + {}, + "https://127.0.0.1/internal", + ) + + +def test_trusted_uv_opener_is_cached_and_disables_ambient_proxies( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The dedicated process installs one no-proxy, no-redirect opener.""" + captured: dict[str, object] = {"builds": 0, "installs": 0} + sentinel = object() + + def fake_build_opener(*handlers: object) -> object: + captured["builds"] = int(captured["builds"]) + 1 + captured["handlers"] = handlers + return sentinel + + def fake_install_opener(opener: object) -> None: + captured["installs"] = int(captured["installs"]) + 1 + captured["opener"] = opener + + monkeypatch.setattr(materializer.urllib.request, "build_opener", fake_build_opener) + monkeypatch.setattr(materializer.urllib.request, "install_opener", fake_install_opener) + + materializer._install_trusted_uv_url_opener() + materializer._install_trusted_uv_url_opener() + + assert captured["builds"] == 1 + assert captured["installs"] == 1 + assert captured["opener"] is sentinel + handlers = captured["handlers"] + assert isinstance(handlers, tuple) + assert len(handlers) == 2 + assert isinstance(handlers[0], urllib.request.ProxyHandler) + assert handlers[0].proxies == {} + assert isinstance(handlers[1], materializer._RejectTrustedUvRedirects) diff --git a/tests/test_uv_workspace_fail_closed.py b/tests/test_uv_workspace_fail_closed.py new file mode 100644 index 000000000..b51d4cc11 --- /dev/null +++ b/tests/test_uv_workspace_fail_closed.py @@ -0,0 +1,116 @@ +"""Regression tests for fail-closed uv workspace materialization.""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from scripts.ci import materialize_base_python_requirements as materializer + + +def _git(repo: Path, *args: str) -> str: + """Run a git command in the fixture repository and return trimmed stdout.""" + return subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _commit_uv_project(tmp_path: Path, pyproject_text: str) -> tuple[Path, str]: + """Commit one root uv project and return its repository and exact base SHA.""" + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Test") + _git(repo, "config", "user.email", "test@example.invalid") + (repo / "uv.lock").write_text("version = 1\n", encoding="utf-8") + (repo / "pyproject.toml").write_text(pyproject_text, encoding="utf-8") + _git(repo, "add", ".") + _git(repo, "commit", "-m", "base") + return repo, _git(repo, "rev-parse", "HEAD") + + +def test_true_uv_workspace_fails_before_exporter_bootstrap( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A partial workspace reconstruction cannot reach tool download or export.""" + repo, base_sha = _commit_uv_project( + tmp_path, + """[project] +name = "workspace-root" +version = "0" + +[tool.uv.workspace] +members = ["packages/*"] +""", + ) + bootstrap_called = False + + def unexpected_bootstrap() -> str: + nonlocal bootstrap_called + bootstrap_called = True + raise AssertionError("workspace rejection must precede trusted uv bootstrap") + + monkeypatch.setattr(materializer, "_install_trusted_uv", unexpected_bootstrap) + + with pytest.raises( + RuntimeError, + match=r"uv workspace.*packages/\*.*not supported", + ): + materializer.materialize(repo, base_sha, tmp_path / "output") + + assert not bootstrap_called + + +def test_workspace_like_comment_is_not_a_workspace( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Detection uses parsed TOML structure rather than vulnerable text matching.""" + repo, base_sha = _commit_uv_project( + tmp_path, + """# [tool.uv.workspace] +[project] +name = "standalone" +version = "0" +""", + ) + monkeypatch.setattr(materializer, "_install_trusted_uv", lambda: "/usr/bin/uv") + hashed = b"dependency==1 --hash=sha256:" + (b"a" * 64) + b"\n" + monkeypatch.setattr( + materializer, + "_run_uv_export", + lambda _work, _uv_path: subprocess.CompletedProcess( + ["uv", "export"], 0, hashed, b"" + ), + ) + + manifest = materializer.materialize(repo, base_sha, tmp_path / "output") + + assert manifest == [{"file": "requirements-000.txt", "source": "uv.lock"}] + + +def test_malformed_tracked_pyproject_fails_before_exporter_bootstrap( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Malformed immutable-base metadata is diagnosed before any tool egress.""" + repo, base_sha = _commit_uv_project( + tmp_path, + "[project\nname = 'broken'\n", + ) + bootstrap_called = False + + def unexpected_bootstrap() -> str: + nonlocal bootstrap_called + bootstrap_called = True + raise AssertionError("metadata parsing must precede trusted uv bootstrap") + + monkeypatch.setattr(materializer, "_install_trusted_uv", unexpected_bootstrap) + + with pytest.raises(RuntimeError, match=r"could not parse.*pyproject\.toml"): + materializer.materialize(repo, base_sha, tmp_path / "output") + + assert not bootstrap_called