diff --git a/.Jules/palette.md b/.Jules/palette.md index 159662d06c..9864e9e356 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,5 @@ -## 2024-05-20 - Repository without UI Codebase -**Learning:** This repository is a GitHub organization profile consisting entirely of Markdown documentation and static assets, and does not contain an active UI or frontend application codebase. -**Action:** Since there is no UI, no UX enhancements can be applied. Aborting UX enhancements and PR creation as per instructions. +## 2026-09-29 - Product UI boundary confirmed + +**Learning:** This repository owns the ContextualWisdomLab GitHub Actions control plane, including production CI scripts, tests, workflows, and documentation. It does not own an interactive product UI or frontend application. + +**Action:** Do not invent product UI work in this repository. Improve the control-plane artifacts here and route reusable product UI work to its canonical product owner. diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index dc3baea170..87fd633573 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -22,6 +22,7 @@ on: - "scripts/ci/ensure_rust_llvm19.sh" - "tests/test_opencode_rust_coverage_toolchain_contract.py" - "tests/test_rust_coverage_timeout_not_measured.py" + - "tests/test_coverage_incomplete_summary.py" - "scripts/ci/resolve_base_rust_toolchain.py" - "tests/test_resolve_base_rust_toolchain.py" - "scripts/ci/place_maturin_extension.py" @@ -211,6 +212,7 @@ jobs: scripts/ci/ensure_rust_llvm19.sh|\ tests/test_opencode_rust_coverage_toolchain_contract.py|\ tests/test_rust_coverage_timeout_not_measured.py|\ + tests/test_coverage_incomplete_summary.py|\ scripts/ci/resolve_base_rust_toolchain.py|\ tests/test_resolve_base_rust_toolchain.py|\ scripts/ci/place_maturin_extension.py|\ @@ -391,8 +393,8 @@ jobs: if: steps.affected_suites.outputs.opencode == 'true' run: | set -euo pipefail - python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_rust_coverage_timeout_not_measured.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py - python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py + python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_rust_coverage_timeout_not_measured.py tests/test_coverage_incomplete_summary.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py + python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py tests/test_coverage_incomplete_summary.py tests/test_resolve_base_rust_toolchain.py tests/test_place_maturin_extension.py - name: Verify JavaScript materializer documentation contract if: steps.affected_suites.outputs.opencode == 'true' diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 09bbf8181a..4aed284603 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -2441,7 +2441,12 @@ jobs: if [ "$not_measured" -ne 0 ]; then append "- Not measured: ${not_measured} Rust coverage command(s) exceeded the 900 s per-command cap; line coverage for that code is unproven, not failed." fi - if [ "$failures" -eq 0 ]; then + if [ "$failures" -eq 0 ] && [ "$not_measured" -ne 0 ]; then + append "- Result: NOT MEASURED" + append "- Test evidence: incomplete; timed-out Rust suites are not proven passing." + append "- Coverage thresholds: not proven satisfied for unmeasured Rust code." + append "- Review handling: advisory only; this result is not approval evidence and does not replace required verification." + elif [ "$failures" -eq 0 ]; then append "- Result: PASS" if [ "$measured_any" -eq 0 ]; then append "- Test coverage: not applicable (no supported changed source files or package manifests)" @@ -7052,6 +7057,12 @@ jobs: grep -Fq -- "- R test evidence: deferred package-load failures require a successful current-head peer R CMD check" } + coverage_decision_is_pass() { + local coverage_decision + coverage_decision="$(printf '%s\n' "${COVERAGE_EVIDENCE_SUMMARY:-}" | grep '^- Result:' || true)" + [ "$coverage_decision" = '- Result: PASS' ] + } + collect_successful_r_cmd_check_evidence() { local output_file="$1" if ! gh pr checks "$PR_NUMBER" \ @@ -7149,7 +7160,8 @@ jobs: publish_blockers_after_model_unavailable() { local pending_wait_status body - if [ "${COVERAGE_EVIDENCE_RESULT:-skipped}" != "success" ]; then + if [ "${COVERAGE_EVIDENCE_RESULT:-skipped}" != "success" ] || + ! coverage_decision_is_pass; then return 1 fi @@ -7278,6 +7290,7 @@ jobs: printf '%s\n' "$reviews_json" | python3 scripts/ci/opencode_existing_approval_gate.py \ --head "$HEAD_SHA" \ + --coverage-summary "$COVERAGE_EVIDENCE_SUMMARY" \ --require-opencode-app } @@ -7481,6 +7494,10 @@ jobs: echo "::endgroup::" exit 1 fi + if ! coverage_decision_is_pass; then + stop_approval_without_review "COVERAGE_NOT_MEASURED" \ + "Coverage measurement is incomplete or its decision is missing. A successful advisory job is not approval evidence and does not replace required verification." + fi if request_changes_for_merge_conflict_if_present; then echo "::endgroup::" exit 0 @@ -7862,6 +7879,7 @@ jobs: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} OPENCODE_MODEL_POOL_OUTCOME: ${{ steps.opencode_review_model_pool.outputs.review_status }} COVERAGE_EVIDENCE_RESULT: ${{ needs.coverage-evidence.result }} + COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_STATUS_TOKEN_SOURCE: ${{ needs.validate-pr-metadata.outputs.target_repository == github.repository && 'github-token' || secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }} @@ -7895,6 +7913,7 @@ jobs: python3 scripts/ci/opencode_dispatch_status.py \ --model-outcome "${OPENCODE_MODEL_POOL_OUTCOME:-missing}" \ --coverage-result "${COVERAGE_EVIDENCE_RESULT:-missing}" \ + --coverage-summary "${COVERAGE_EVIDENCE_SUMMARY:-}" \ --expected-head "$PR_HEAD_SHA" \ --pull-request-file "$pull_request_file" \ --reviews-file "$reviews_file" @@ -7954,6 +7973,7 @@ jobs: PR_BASE_REF: ${{ needs.validate-pr-metadata.outputs.base_ref }} PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} + COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }} OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head @@ -7985,6 +8005,7 @@ jobs: if printf '%s\n' "$reviews_json" | python3 scripts/ci/opencode_existing_approval_gate.py \ --head "$PR_HEAD_SHA" \ + --coverage-summary "$COVERAGE_EVIDENCE_SUMMARY" \ --require-opencode-app \ 2>"$gate_error_file"; then approval_visible=1 diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index e05a8b155a..a092515a14 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -42,6 +42,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 persist-credentials: false - name: Verify exact revision shell: bash diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index db70ec324c..bcf5a898b6 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -98,6 +98,7 @@ jobs: with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Set up current stable Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/CHANGELOG.d/20260930-coverage-incomplete-approval.md b/CHANGELOG.d/20260930-coverage-incomplete-approval.md new file mode 100644 index 0000000000..4052abf75f --- /dev/null +++ b/CHANGELOG.d/20260930-coverage-incomplete-approval.md @@ -0,0 +1,9 @@ +### Incomplete Rust coverage cannot supply formal approval evidence + +- A Rust coverage timeout now reports `NOT MEASURED`, never `PASS` or a + claim that the timed-out suite passed. The advisory coverage job may finish + successfully, but formal approval requires one unambiguous `PASS` decision + from that same dispatch. Missing, unknown, malformed, and contradictory + decisions withhold approval without manufacturing a source finding. Existing + exact-head review and required-check gates remain in force; unmeasured + coverage does not satisfy or replace required verification. diff --git a/CHANGELOG.d/20260930-github-api-http-error-close.md b/CHANGELOG.d/20260930-github-api-http-error-close.md new file mode 100644 index 0000000000..df2feb4a78 --- /dev/null +++ b/CHANGELOG.d/20260930-github-api-http-error-close.md @@ -0,0 +1,7 @@ +## Fixed + +- Close file-like `HTTPError` responses in the central CodeQL, Strix, Noema, + Pingora, review-preflight, Pages, and sandbox-readiness clients after bounded + status/telemetry extraction, preventing Python 3.14 resource leaks without + permitting redirects, suppressing warnings, or weakening bearer-token + authority checks; cap CodeQL diagnostic error-body reads at 400 bytes. diff --git a/CHANGELOG.d/20260930-pyjwt-recursion-security-lock.md b/CHANGELOG.d/20260930-pyjwt-recursion-security-lock.md new file mode 100644 index 0000000000..6859e10f2e --- /dev/null +++ b/CHANGELOG.d/20260930-pyjwt-recursion-security-lock.md @@ -0,0 +1,7 @@ +# Security + +- Pin the shared Strix CI runtime to PyJWT 2.15.1. This retains the 2.15.0 + security correction that converts malicious deeply nested JWT payload + recursion into a bounded `DecodeError`, while also carrying the signed + 2.15.1 Base64URL-padding correction. The source input and generated hash + lock now carry the same exact release. diff --git a/CHANGELOG.d/20260930-repository-metadata-evidence-ancestry.md b/CHANGELOG.d/20260930-repository-metadata-evidence-ancestry.md new file mode 100644 index 0000000000..3d68a778dc --- /dev/null +++ b/CHANGELOG.d/20260930-repository-metadata-evidence-ancestry.md @@ -0,0 +1,6 @@ +### Repository metadata validation receives published evidence ancestry + +- The Repository Metadata Reconcile validation job now fetches complete Git + history before the repository-wide contract suite checks whether documented + evidence commits belong to the current exact-head ancestry. Exact revision + verification and credential isolation remain unchanged. diff --git a/CHANGELOG.d/20260930-urllib3-security-lock.md b/CHANGELOG.d/20260930-urllib3-security-lock.md new file mode 100644 index 0000000000..4e22487120 --- /dev/null +++ b/CHANGELOG.d/20260930-urllib3-security-lock.md @@ -0,0 +1,6 @@ +# Security + +- Pin the shared pip-audit and Strix CI runtimes to urllib3 2.8.0, which fixes + the HTTPS-proxy TLS-policy crossover and two streaming denial-of-service + vulnerabilities present in 2.7.0. Both source inputs and generated hash locks + now carry the same exact version. diff --git a/CHANGELOG.d/20261001-coverage-approval-reuse-gate.md b/CHANGELOG.d/20261001-coverage-approval-reuse-gate.md new file mode 100644 index 0000000000..d874f043af --- /dev/null +++ b/CHANGELOG.d/20261001-coverage-approval-reuse-gate.md @@ -0,0 +1,24 @@ +### Approval reuse requires a unique passing coverage decision + +- OpenCode's existing exact-head approval path, merge-scheduler reuse gate, and + repository-dispatch status publisher now inspect the coverage evidence + summary as well as the coverage job conclusion. They accept exactly one + `- Result: PASS` line and fail closed for missing, `NOT MEASURED`, non-passing, + or duplicate decisions. A successful advisory coverage job therefore cannot + reuse a predecessor approval when the current dispatch did not measure and + pass coverage. +- The existing-approval CLI regression executes the non-passing decision branch + directly, preserving the repository's 100% statement/branch coverage gate + instead of excluding or suppressing the new fail-closed path. +- Existing approval reuse now evaluates the latest exact-head OpenCode + publication decision instead of skipping a newer `CHANGES_REQUESTED` or + otherwise invalid decision and resurrecting an older `APPROVED` review. +- The canonical Noema document-reader lock now selects `fast-uri` 3.1.8 and + `ip-address` 10.7.2, removing CVE-2026-86472, CVE-2026-101911, and + CVE-2026-101912 from the exact runtime installed by the hosted review lane. + The bundle contract pins both transitive security versions so a later lock + regeneration cannot silently restore the vulnerable releases. +- The central Strix input and generated hash lock now select LiteLLM 1.94.3, + the patched 1.94 release for CVE-2026-84377. This closes the authenticated + provider-credential forwarding and SSRF boundary exposed by 1.94.1 while + preserving the existing Strix package and override set. diff --git a/CHANGELOG.md b/CHANGELOG.md index d90fa0c899..a4f0ddac1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +### Central coverage owner preserves concurrent repairs and restores the 100% gate + +- The `.github#2521` coverage owner now carries the complete valid deltas from + `.github#2530` (hash-pinned full-suite parser dependencies) and `.github#2532` + (explicit GitHub `HTTPError` response closure) through ordinary two-parent + merges. Behavior-level contracts replace dummy/live-CLI coverage for the + OpenCode queue and Strix manifest scanners, close release dependency trust + boundaries, and prove the runtime-archive prescreener's reachable state space; + one redundant unreachable postcondition was removed without weakening any + earlier fail-closed validation. Python 3.10 TOML fallback selection is now a + directly testable compatibility boundary. On the integrated tree, + warnings-as-errors produced 5,228 passed, 5 skipped, and 40 subtests passed; + all 18,232 owned production statements and 7,488 branches are covered. + Hosted exact-head Checks and qualifying independent review remain required. + +### Full-suite quality environments install their collection parsers + +- The common OpenCode quality input now owns the existing hash-pinned + `defusedxml` document parser and `PyYAML` workflow parser used during complete + repository test collection. Its Python 3.14/Linux lock was regenerated by + the repository compiler without manual hash edits. Local verification + reproduced the lock byte-for-byte, installed the common and Noema locks + together, imported both parsers, and passed 73 focused contracts with two + optional skips. Hosted exact-head Checks and qualifying independent review + remain required before protected merge. + +### Shared security fixtures use patched PyJWT and PyO3 releases + +- The Strix hash lock now takes PyJWT `2.14.0` as an explicit source input, + closing CVE-2026-102274 without hiding the dependency in the cryptography-only + override file. The offline Rust coverage fixture advances from PyO3 `0.22.6` + to `0.29.2`, beyond the `0.29.0` fixes for GHSA-36hh-v3qg-5jq4 and + GHSA-chgr-c6px-7xpp. Source/lock parity tests prevent either generated lock + from silently returning to the vulnerable versions. Protected integration, + immutable consumer-pin advancement, and fresh exact-head hosted security + Checks remain required before release admission. + ### Intel macOS native archives are bound to x86_64 bytes - The release prescreener now requires every native member in an Intel macOS diff --git a/docs/doctoring/central-coverage-owner-stack-2521.md b/docs/doctoring/central-coverage-owner-stack-2521.md new file mode 100644 index 0000000000..aa0f614bb1 --- /dev/null +++ b/docs/doctoring/central-coverage-owner-stack-2521.md @@ -0,0 +1,43 @@ +# Central coverage owner stack RCA + +## Incident and ownership + +`ContextualWisdomLab/.github#2521` owns the repository-wide Python statement +and branch coverage repair. Concurrent PRs carried valid prerequisites rather +than competing implementations: #2530 owns the hash-pinned parser inputs used +by complete test collection, and #2532 owns explicit closure of file-like +GitHub `HTTPError` responses. Both were preserved as ordinary merge parents; +neither failure was treated as a reason to discard a PR. + +## Root causes + +The first warning-fatal integrated run separated three causes: + +1. the predecessor tree lacked parser dependencies required during collection; +2. synthetic GitHub redirect/error paths leaked response objects under Python + 3.14 and therefore failed when `ResourceWarning` was promoted to an error; +3. four central scripts retained executable paths that their tests did not + reach, while two tests invoked a live `gh` boundary or asserted nothing. + +The remaining coverage work was test-first and behavior-bound. It exercises +queue admission/cancellation, bounded repository scans, exact Git blobs, +Cargo workspace and development-lock identity, runtime receipt architecture, +release fanout limits, and Python 3.10 TOML parser fallback. The runtime archive +prescreener's final repeated count check was proven unreachable because its +earlier exact-cardinality, uniqueness, and per-archive validation already +reject every false case; only that redundant branch was removed. + +## Evidence and acceptance + +- RED integrated evidence: 5,196 passed, 6 skipped; 90 statements missed and + 29 partial branches across the four remaining owner modules. +- Focused release dependency evidence: 950 passed with warnings treated as + errors. +- GREEN integrated evidence after re-fetching and ordinarily merging the moved + #2530 prerequisite head: 5,228 passed, 5 skipped, 40 subtests passed; + 18,232/18,232 production statements and 7,488/7,488 branches covered. +- No warning, security result, or fail-closed input validation was suppressed. + +This local result is not merge authority. Acceptance requires publication to +the re-fetched #2521 branch without force, fresh hosted Checks bound to that +exact commit, qualifying independent review, and ordinary protected merge. diff --git a/docs/doctoring/full-suite-parser-locks-20260930.md b/docs/doctoring/full-suite-parser-locks-20260930.md new file mode 100644 index 0000000000..8454481059 --- /dev/null +++ b/docs/doctoring/full-suite-parser-locks-20260930.md @@ -0,0 +1,81 @@ +# Full-suite parser dependency admission + +## Incident + +At protected `main@37b10243cec3d160ecc9c1be75c71428b160a703`, the common +OpenCode quality lock did not install the parsers imported by repository test +collection. Agent Mention Router Quality run +[36443475158](https://github.com/ContextualWisdomLab/.github/actions/runs/36443475158) +and source-repair run +[36443475290](https://github.com/ContextualWisdomLab/.github/actions/runs/36443475290) +reported `ModuleNotFoundError: No module named 'defusedxml'` and 13 collection +errors. The newer main additionally has four workflow tests importing `yaml`. +These are environment failures before test execution, not product findings or +passing coverage measurements. + +## Repair boundary + +The shared tooling input reuses `requirements-noema-document-ci.txt` for the +existing `defusedxml==0.7.1` pin and adds the existing security-tooling +`PyYAML==6.0.3` pin. The generated lock is rebuilt only with +`./scripts/ci/compile_opencode_review_lock.sh` (Python 3.14, Linux target, +`--upgrade`, hashes). The required upgrade also resolves Hypothesis 6.168.3 +instead of 6.168.0. No hashes are edited by hand. + +Installing the entire Bandit lock alongside this lock is not the repair: their +Pygments pins differ. The common quality environment instead resolves its own +complete, consistent dependency closure. Hash checking remains mandatory. +PyYAML's repository reports MIT. GitHub reports NOASSERTION for defusedxml, +so its installed hash-pinned 0.7.1 wheel's LICENSE was read directly and confirms +Python Software Foundation License Version 2; a missing SPDX detection is not +interpreted as missing permission. + +A contract regression requires both parser packages in the installed lock. +RED: one failing test because both packages were absent. GREEN: 36 tests passed +and two optional document-format tests skipped after hash-locked installation. +The full repository suite is a separate verification step, not implied by that +focused result. On code head `41e95af9dd1ac95dea716d53c44df1eacbd81d9d`, the final +isolated run finished with 5,165 passed, five skipped and 40 subtests passed in +356.08 seconds. It used only the common quality lock plus the project-pinned +pip seed, disabled incidental pytest plugin autoload and cleared live event/token +environment variables. An inherited ResourceWarning from a synthetic HTTPError +fixture remains visible; it is not suppressed or treated as a product failure. + +## Verification environment + +The first YAML-present full run on source-repair head `deb47f8db` finished with +5,232 passed, seven failed, five skipped and 40 subtests passed. Two failures +were a locally unseeded virtualenv lacking pip; five used pytest temporary +paths beneath the host home, which the sandbox correctly refuses. Installing +project-pinned pip 26.2.1 and using an isolated `/tmp` basetemp made all seven +reproductions pass. These local environment corrections do not weaken sandbox +validation and are not changes to production code. + +An initial clean-lock run had one further environment failure: macOS inherited +`/tmp` group 0, which this user does not belong to, and silently cleared the +setgid bit when the test requested mode 2600. A direct mode probe observed 600, +while a staff-owned directory retained 2600. The final run used a unique +staff-owned parent outside the host home; the security check itself was not +changed. Both the targeted permission test and the complete suite passed there. + +Source-repair's own two scripts separately measured 100% statement/branch +coverage and 100% docstrings. Neither those measurements nor the parser repair +constitute hosted exact-head approval or merge authorization. + +## Consumers and limits + +Quality workflows install `requirements-opencode-review-ci-hashes.txt` directly. +The central OpenCode coverage image also consumes it in +`.github/workflows/opencode-review-dispatch.yml`, together with the existing +Noema document lock. No exact-head fast-mlsirm Noema HTTP 400 or Strix report +scope failure is claimed resolved by this dependency change. Those incidents +require their own current-head transport/report evidence and independent review. + +## References + +Python Packaging Authority. (n.d.). *Secure installs*. Retrieved September 30, +2026, from https://pip.pypa.io/en/stable/topics/secure-installs/ + +The pip documentation requires every dependency in hash-checking mode to be +pinned and hashed. The repair preserves that contract rather than adding an +unhashed installation fallback. diff --git a/docs/doctoring/github-api-http-error-response-lifecycle.md b/docs/doctoring/github-api-http-error-response-lifecycle.md new file mode 100644 index 0000000000..e26110c0b8 --- /dev/null +++ b/docs/doctoring/github-api-http-error-response-lifecycle.md @@ -0,0 +1,86 @@ +# GitHub API HTTP error response lifecycle + +Status: Proposed source repair; exact-head hosted security Checks and independent review remain mandatory. + +## Incident and exact owner + +Protected `ContextualWisdomLab/.github` `main` at +`37b10243cec3d160ecc9c1be75c71428b160a703` reproduced eight failures in +`tests/test_github_api_url_boundary.py` under Python 3.14.7 with warnings promoted +to errors. Both authenticated GitHub REST clients correctly refused a synthetic +302 without sending a second request, but the resulting `HTTPError` response was +left open and Python reported `ResourceWarning: Implicitly cleaning up +`. + +The exact RED command was: + +```console +GITHUB_ACTIONS=true python -m pytest tests/test_github_api_url_boundary.py -q -W error +``` + +Result on the protected revision: `8 failed, 26 passed`. The same result in a +clean protected-revision worktree proves this is a pre-existing central supplier +defect rather than a consumer or `.github#2040` branch-only failure. + +## Root cause and repair + +Python documents `urllib.error.HTTPError` as both an exception and the same kind +of file-like response returned by `urlopen()`, with a readable error-body file +pointer. The clients converted the exception into their domain error but did not +close that response object. Success responses already used context managers; the +HTTP error path violated the same lifecycle boundary. + +The repair keeps the existing fail-closed redirect policy and error mapping. +The first exact-tree verification also exposed the same lifecycle defect in +other central HTTP boundaries, so the owner repair now covers every observed +path rather than leaving warning-fatal failures for a successor: + +- `codeql_ghas_configuration_identity._request_json` reads at most 400 bytes of + diagnostic body and closes the `HTTPError` in `finally`, including decode/read + failures; +- `strix_evidence_binding.default_github_opener` snapshots the status code, + closes the response, and then raises `EvidenceBindingError`; +- `noema_review_gate.call_llm` extracts only bounded allowlisted gateway + telemetry and then closes the response in `finally`; +- the Pingora artifact client closes JSON and raw-blob HTTP errors after + preserving its existing 404/domain-error mapping; +- the review preflight closes provider HTTP errors after recording only the + safe status and retry fields; +- Pages publication and sandbox readiness probes close rejected redirects + before returning their existing fail-closed result; +- the production-opener regression now asserts that the single synthetic 302 + response is closed as well as proving no redirected bearer request occurs, + and each newly discovered caller has an equivalent close contract. + +The 400-byte intake bound matches the pre-existing 400-character public +diagnostic limit without reading an arbitrarily large untrusted response first. +No warning filter, security suppression, redirect allowance, timeout, or check +threshold changed. + +## Acceptance + +1. The protected-revision RED becomes GREEN under Python 3.14.7 and `-W error`. +2. CodeQL identity, Strix evidence, Noema, Pingora, review-preflight, Pages, + sandbox readiness, and URL-authority suites remain GREEN with warnings + fatal. +3. Changed production behavior retains contract coverage and public docs. +4. Hosted Semgrep, Bandit, CodeQL, dependency, and independent review Checks pass + on the exact PR head before ordinary protected merge. +5. `.github#2040` then merges the protected repair normally and regenerates its + own exact-head evidence; stale predecessor failures are not rerun as proof. + +Local repair evidence on Python 3.14.7 is `35 passed` for the URL-authority +suite after adding the bounded-intake regression (`1 failed` before the repair), +`220 passed` for the original eight-file CodeQL/Strix impact suite, `69 passed` +for the sandbox E2E suite, and `5161 passed, 10 skipped, 40 subtests passed` +for the complete warning-fatal test tree. The original two production modules +each report 100% statement/branch coverage, while the repository-wide report +remains at its pre-existing 99% because unrelated production files outside this +delta retain uncovered branches. Hosted exact-head Checks, rather than this +local evidence, remain the merge authority. + +## Primary reference + +Python Software Foundation. (2026). *urllib.error — Exception classes raised by +urllib.request* (Python 3.14.7 documentation). +https://docs.python.org/3.14/library/urllib.error.html diff --git a/docs/doctoring/opencode-coverage-approval-reuse-20261001.md b/docs/doctoring/opencode-coverage-approval-reuse-20261001.md new file mode 100644 index 0000000000..8ebeaf56f8 --- /dev/null +++ b/docs/doctoring/opencode-coverage-approval-reuse-20261001.md @@ -0,0 +1,70 @@ +# OpenCode coverage approval-reuse gate + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; fresh exact-head +hosted Checks and qualifying independent approval remain mandatory. + +## Failure evidence and root cause + +CodeRabbit review thread `PRRT_kwDOS_C14s6nmf3Q` identified a fail-open edge in +the current PR head: `publish_blockers_after_model_unavailable` and +`scripts/ci/opencode_dispatch_status.py` required only the coverage job result +`success` before reusing an existing same-head OpenCode approval. A later audit +also found the merge-scheduler's direct approval-gate invocation omitted the +coverage summary. The coverage +producer intentionally completes successfully for an honest `NOT MEASURED` +result so that it can publish diagnostics without fabricating a source defect. +Consequently, job success alone is not proof that the current dispatch measured +and passed coverage. + +The causal owner is the central `.github` OpenCode review boundary, not a +consumer repository or the model provider. The defect was an incomplete +evidence contract between the coverage producer and the approval/status +consumers. + +## Test-first repair + +The RED regression added missing, `NOT MEASURED`, malformed, and duplicate +coverage-decision cases and failed because no decision validator existed. The +repair introduces one shared Python validator that accepts exactly one +`- Result: PASS` line, applies it before existing-approval reuse and status +publication, and mirrors the same exact rule in the workflow shell path. The +workflow now supplies the current coverage summary to both consumers. + +The focused approval, security-boundary, workflow-contract, executable shell, +and reviewed-blob suites pass 186 tests with 1 optional LLVM-platform skip. +This includes concurrent exact-head commits +`87ffafa2f6b19080c01f6ee24b987b37cb92dcb8`, +`0bcded6b08af4554541223438d046bc412c4b093`, and +`feb88e34c9197275a58310306492069e25eb0b67`; their additional environment, +prefixed-result, contradictory-result, implementation, and coverage cases were +preserved rather than overwritten. After integration, the +warnings-as-errors repository suite passes 5,255 tests, 5 optional skips, and 40 +subtests. No timeout, coverage threshold, exact-head rule, independent-review +rule, or required Check is relaxed. Missing or ambiguous evidence remains a +failure, while an honest unmeasured result remains diagnostic rather than being +relabeled as a code finding. + +Fresh exact-head Trusted uv run +[`36751696675`](https://github.com/ContextualWisdomLab/.github/actions/runs/36751696675), +job `110011676248`, then exposed a test-contract omission: the suite passed, but +`scripts/ci/opencode_existing_approval_gate.py:241-242` remained unexecuted, so +the repository coverage gate reported 18,252 statements with 2 misses and +7,498 branches with 1 partial branch (99%). The repair executes the real CLI +with `NOT MEASURED` and asserts its fail-closed diagnostic. It does not exclude +the branch or lower the 100% threshold. The exact hosted command reproduced +locally with 5,255 passed, 5 optional skips, and 40 subtests, covering all +18,252 production statements and 7,498 branches at 100% with zero misses or +partial branches. + +## Operational consequence + +Existing approvals remain reusable only when all of the following are true: + +1. the approval is valid for the exact live head; +2. the coverage evidence job completed successfully; and +3. its current summary contains exactly one authoritative `PASS` decision; and +4. every approval consumer, including merge scheduling, receives that summary. + +Fresh hosted execution on the repaired head is still required. Predecessor +GREEN, skipped CodeQL, pending dispatch verdicts, or this local result do not +authorize merge. diff --git a/docs/doctoring/repository-metadata-shallow-ancestry.md b/docs/doctoring/repository-metadata-shallow-ancestry.md new file mode 100644 index 0000000000..03e0f61eb1 --- /dev/null +++ b/docs/doctoring/repository-metadata-shallow-ancestry.md @@ -0,0 +1,45 @@ +# Repository Metadata Reconcile shallow-ancestry RCA + +## Status and exact evidence + +Status: Proposed on `ContextualWisdomLab/.github#2536`. Exact-head run +[`36720930491`](https://github.com/ContextualWisdomLab/.github/actions/runs/36720930491), +job `109905558240`, checked out +`737fc6fd3b536495a7d5f8bbbae9d0474771d21f` and failed the repository-wide +suite at +`tests/test_github_api_url_boundary.py::test_documented_opener_lineage_references_published_commits`. +The exact assertion reported evidence commit +`57477289ebec5631b0c48f0bc419f336dbe19deb` as an invalid object. + +## Root cause + +The workflow verified the exact requested revision but left the pinned +`actions/checkout` input `fetch-depth` at its default value of `1`. The G-17 +contract intentionally calls `git cat-file` and `git merge-base --is-ancestor` +for published commits named by the product-gap baseline. A depth-one object +database cannot answer that ancestry question after an ordinary merge even +when the evidence commit is genuinely reachable. The earlier local full-history +run therefore did not reproduce the hosted runner's incomplete Git object +database. + +This is a workflow-fixture defect, not a product-source defect and not stale +evidence. Exact-head checkout alone and history availability are distinct +contracts. + +## RED, repair, and verification + +The new workflow regression failed first because the validation checkout did +not contain `fetch-depth: 0`. Commit +`3bc859c73ed67074df13b2e01aa89dff2159e260` adds that single checkout input and +the regression. The focused workflow and ancestry suites then passed 37 tests. +The exact revision assertion and `persist-credentials: false` remain in place; +the apply job's credential and write boundaries are unchanged. + +Hosted exact-head revalidation remains required after publication. A queued, +skipped, pending, cancelled, or predecessor result is not passing evidence. + +## Reference + +actions/checkout contributors. (2026). *Checkout V7: Fetch all history for all +tags and branches* [Computer software documentation]. GitHub. +https://github.com/actions/checkout/blob/main/README.md#fetch-all-history-for-all-tags-and-branches diff --git a/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md new file mode 100644 index 0000000000..c6c3ff67b6 --- /dev/null +++ b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md @@ -0,0 +1,103 @@ +# Shared PyJWT and PyO3 security baseline repair + +## Status and decision + +**Proposed; release HOLD.** The central `.github` repository owns both affected +dependency surfaces, so the repair belongs on a protected-main foundation PR. +It must not be copied into PR #1026, which changes neither lock. The owner repair +must pass exact-head review and hosted security Checks, merge ordinarily, and +then reach #1026 through a non-force merge from protected `main`. + +## Exact incident evidence + +PR [#1026](https://github.com/ContextualWisdomLab/.github/pull/1026) was observed +at exact head `6f645a73502e159d5a229805afa34868ad9bb851` against protected +`main@37b10243cec3d160ecc9c1be75c71428b160a703`. + +- [Security Scan run 36495499815](https://github.com/ContextualWisdomLab/.github/actions/runs/36495499815), + job `109380628690`, reported PyO3 `0.22.6` in + `tests/fixtures/coverage-cargo/Cargo.lock`: GHSA-36hh-v3qg-5jq4 (High, 8.0) + and GHSA-chgr-c6px-7xpp (Medium, 5.5). Both advisories fix the defect in + PyO3 `0.29.0`; this repair selects and locally verifies `0.29.2`. +- [Python Security run 36495499871](https://github.com/ContextualWisdomLab/.github/actions/runs/36495499871), + job `109380725819`, reported PyJWT `2.13.0` in + `requirements-strix-ci-hashes.txt` as affected by CVE-2026-102274. PyJWT + `2.14.0` is the fixed release. +- The exact #1026 diff changes neither vulnerable file. The same lock bytes + were present on protected `main`, establishing a shared baseline defect rather + than a PR-specific regression. + +## Root cause and operational scenarios + +PyJWT was only a transitive MCP dependency, so the generated Strix lock could +select a newly vulnerable release without an explicit reviewed source pin. A +malformed RSA JWK can raise a plain `ValueError` and abort processing of the +whole JWK Set. An operator can therefore lose otherwise valid signing keys and +fail authentication or review-agent startup because one untrusted key is bad. + +The offline Rust coverage fixture intentionally pins exact crate releases, but +its PyO3 pin was not advanced when the two 2026 advisories were published. One +defect permits an out-of-bounds read from iterator methods; the other omits a +required `Sync` bound and permits a data race. Even though this is a test +fixture, the central scanner correctly treats its lock as executable supply +chain material. + +## RED to GREEN contract + +RED commit `cd84d887` introduced two fail-closed contracts: + +1. `requirements-strix-ci.txt` must explicitly select PyJWT `2.14.0`, and the + generated hash lock must contain the same version. +2. The Rust coverage fixture manifest and lock must both select PyO3 `0.29.2`. + +The implementation adds the direct PyJWT input, regenerates the Python 3.13 +manylinux hash lock with the repository command, advances the exact PyO3 +manifest pin, and regenerates the Cargo lock with Rust `1.97.1`. The +cryptography override remains single-purpose; it does not become a general +security-version overlay. + +## Ownership, release, and failure recovery + +The fixed-source release workflows consume `requirements-strix-ci-hashes.txt` +from immutable central revisions. This PR repairs the canonical owner bytes but +does not rewrite those workflows to an open branch. After ordinary protected +merge, a separate consumer change must advance their exact source commit and +rerun API/schema, security, SBOM, and provenance evidence. If any exact-head +scanner, build, or independent review fails, the PR remains HOLD and the root +cause is repaired here; no bypass or mutable source reference is permitted. + +## Local verification on the repaired tree + +- Focused dependency, fixed-source, Maturin asset, and Rust toolchain contracts: + 48 passed, 1 skipped. +- Repository regression suite: 5,160 passed, 11 skipped, 40 subtests passed. +- Rust `1.97.1` `cargo check --locked`: passed for the coverage fixture. +- Python lock regeneration with `uv 0.12.18`, seeded with the existing reviewed + output, was byte-identical. Input SHA-256 values were `c3812261…` for + `requirements-strix-ci.txt` and `3b745514…` for the override; the output was + `8f8318d4…`. A hash-enforced installation loaded PyJWT `2.14.0`. A fresh + unseeded solve is intentionally not claimed to be byte-identical because it + may select newer allowed transitive releases. +- `pip-audit`: no known vulnerabilities in the Strix lock. OSV's direct + `pyo3@0.29.2` query returned no vulnerability records. + +No production Python module changes in this repair. The repository-wide +coverage and docstring commands expose separate protected-main baseline debt: +coverage is 99% (178 statements missing) and `interrogate scripts/ci` is 97% +(43 docstrings missing). Those failures are not waived or called green here; +they require their own bounded owner repair before the repository can claim the +100% global gates. + +## References + +GitHub. (2026, June 12). *Out-of-bounds read in PyO3 iterator methods* +(GHSA-36hh-v3qg-5jq4). GitHub Advisory Database. +https://github.com/advisories/GHSA-36hh-v3qg-5jq4 + +GitHub. (2026, June 12). *PyO3 missing Sync bound can lead to a data race* +(GHSA-chgr-c6px-7xpp). GitHub Advisory Database. +https://github.com/advisories/GHSA-chgr-c6px-7xpp + +Open Source Vulnerabilities. (2026). *CVE-2026-102274: PyJWT RSA JWK Set +availability failure*. +https://osv.dev/vulnerability/CVE-2026-102274 diff --git a/docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md b/docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md new file mode 100644 index 0000000000..adebc5c8b2 --- /dev/null +++ b/docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md @@ -0,0 +1,48 @@ +# Shared Strix PyJWT recursion security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; hosted exact-head +revalidation and qualifying independent review remain mandatory. + +## Failure scene and causal owner + +Exact-head Security Scan run +[`36740858208`](https://github.com/ContextualWisdomLab/.github/actions/runs/36740858208), +job `109974634074`, evaluated +`d76ab4238591cc33b329881782e2759f3f5d51be`. Its dependency-review support +check reached GitHub successfully and the other scanner jobs passed. The +dependency-review action then rejected `requirements-strix-ci.txt` because +PyJWT 2.14.0 is affected by GHSA-42vr-xj54-vc7v, an unauthenticated +`RecursionError` denial of service in pre-verification payload parsing. + +This is a shared Strix CI-runtime lock, so the central `.github` security and +review bounded context is the causal owner. It is not a transient network +failure, a consumer defect, or stale predecessor evidence. The dependency +review remains fail-closed; no severity threshold or workflow gate changes. + +## RED to repair + +The retained `test_strix_pyjwt_security_pin_is_an_explicit_lock_input` +contract first failed with one failure and four passing dependency tests when +it required PyJWT 2.15.1 but both the source and lock still selected 2.14.0. +The repair advances the explicit source pin and its two PyPI artifact hashes to +2.15.1. PyJWT's upstream changelog records the recursion hardening in 2.15.0; +the signed 2.15.1 release includes that fix and adds a Base64URL-padding +correction. + +The focused dependency contract passes five tests after the repair. pip-audit +2.10.1's strict exact-pin audit reports no known vulnerabilities. The final +local tree passes 5,236 tests, 5 optional skips, and 40 subtests in the +warnings-as-errors repository suite; Ruff and `git diff --check` also pass. +Completion still requires fresh hosted Security Scan, Python Security, and +every other applicable exact-head check plus an independent review. + +## References + +PyJWT maintainers. (2026, September 28). *PyJWT 2.15.1* [Software release]. +GitHub. https://github.com/jpadilla/pyjwt/releases/tag/2.15.1 + +PyJWT maintainers. (2026). *Changelog* [Software documentation]. GitHub. +https://github.com/jpadilla/pyjwt/blob/2.15.1/CHANGELOG.rst + +Python Package Index. (2026, September 28). *PyJWT 2.15.1* [Package release]. +https://pypi.org/project/PyJWT/2.15.1/ diff --git a/docs/doctoring/shared-security-baseline-urllib3-20260930.md b/docs/doctoring/shared-security-baseline-urllib3-20260930.md new file mode 100644 index 0000000000..f18070e1aa --- /dev/null +++ b/docs/doctoring/shared-security-baseline-urllib3-20260930.md @@ -0,0 +1,48 @@ +# Shared Python CI urllib3 2.8.0 security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; hosted exact-head +revalidation and qualifying independent review remain mandatory. + +## Failure scene and causal owner + +After `ContextualWisdomLab/.github#2536` became Ready, exact-head Python +Security run +[`36737059681`](https://github.com/ContextualWisdomLab/.github/actions/runs/36737059681), +job `109961499214`, audited the pull-request merge revision whose head was +`88143f95d36be9b08550b52b86bd2baeefa0fe86`. The job found urllib3 2.7.0 in +both `requirements-pip-audit-ci-hashes.txt` and +`requirements-strix-ci-hashes.txt`. pip-audit reported CVE-2026-97687, +CVE-2026-97688, and CVE-2026-97689, each fixed in 2.8.0. The first can apply +target TLS policy or credentials to an HTTPS proxy; the latter two permit CPU +or memory denial of service through hostile chunked streaming responses. + +These are shared `.github` CI-runtime locks, so the central security/review +bounded context is the causal owner. No product repository may suppress the +audit or copy a mutable proposed lock. + +## RED to repair + +The retained regression +`test_python_security_inputs_pin_patched_urllib3` first failed because neither +source input constrained urllib3. The repair adds the exact `urllib3==2.8.0` +constraint to both source inputs, regenerates both locks with their recorded +`uv pip compile --generate-hashes` commands, and requires source/lock parity in +both runtime graphs. The generated artifact hashes are +`0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3` +and `63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63`. + +The change does not alter pip-audit severity, failure classification, network +policy, or any workflow gate. Both regenerated locks returned `No known +vulnerabilities found` under pip-audit 2.10.1's strict exact-pin audit. The +focused dependency contract passes 5 tests, and the exact local tree passes +5,236 tests with 5 optional skips and 40 subtests while warnings are errors. +`git diff --check` passes. Completion still requires a fresh exact-head Python +Security result plus all other applicable checks and independent review. + +## References + +urllib3 maintainers. (2026, September 15). *urllib3 2.8.0* [Software +release]. GitHub. https://github.com/urllib3/urllib3/releases/tag/2.8.0 + +urllib3 maintainers. (2026, September 15). *Security advisories* [Security +advisory index]. GitHub. https://github.com/urllib3/urllib3/security/advisories diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e5f1c549a..8f4d5cdd39 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,42 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-10-01 OpenCode approval-order delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-OPENCODE-LATEST-REVIEW-01 | **Proposed — exact head `ba55414b…`; local RED→GREEN; hosted exact-head acceptance pending** | `.github#2536@18c886cb…`에서 existing-approval gate가 같은 head의 최신 `CHANGES_REQUESTED`를 건너뛰고 과거 validated `APPROVED`를 재사용하는 RED를 재현했다. Gate를 dispatch-status와 동일한 latest-decision authority로 정렬한 focused suite는 44 passed다. 후속 coverage 회귀가 stale-head와 unknown-actor skip 분기를 실제 실행하며 최종 warning-fatal coverage suite는 5,257 passed, 5 optional skips, 40 subtests, 18,252/18,252 statements와 7,498/7,498 branches다. 첫 수리는 ordinary one-parent commit `ba55414b…`로 force 없이 게시됐다. | Canonical owner는 중앙 `scripts/ci/opencode_existing_approval_gate.py`다. 후속 보안 delta를 ordinary descendant로 게시하고 hosted quality/review Checks와 qualifying independent approval을 새 exact head에서 확인한다. queued/skipped/pending은 acceptance evidence가 아니다. | +| CONTROL-NOEMA-DOCUMENT-LOCK-01 | **Proposed — hosted Security RED repaired and integrated locally; republish pending** | exact-head Security Scan run `36779214593`, Trivy job `110104871060`이 중앙 Noema document-reader lock의 `fast-uri` 3.1.7(CVE-2026-86472)과 `ip-address` 10.7.0(CVE-2026-101911, CVE-2026-101912)을 검출했다. 회귀는 vulnerable lock에서 1 failed / 11 passed / 2 skipped였고, `fast-uri` 3.1.8 및 `ip-address` 10.7.2로 재생성한 lock에서 12 passed / 2 skipped다. `npm audit --omit=dev --audit-level=moderate`는 109 production dependencies와 vulnerability 0을 보고했고 통합 full coverage도 100%다. | Canonical owner는 중앙 `scripts/ci/noema-document-reader/package-lock.json`이다. ordinary descendant로 #2536을 갱신한 뒤 fresh exact-head Security/quality/review Checks를 수집한다. | +| CONTROL-STRIX-LITELLM-LOCK-01 | **Proposed — hosted Python Security RED repaired and integrated locally; republish pending** | exact-head Python Security run `36779214017`, pip-audit job `110104825199`이 Strix hash lock의 LiteLLM 1.94.1에서 CVE-2026-84377을 검출했다. Source input에 fixed 1.94-line release `litellm==1.94.3`을 명시하고 기존 uv command/override로 hash lock을 재생성했다. 새 source/lock parity regression은 old lock에서 RED였다. strict exact-pin pip-audit는 known vulnerability 0이며 통합 full coverage도 100%다. | Canonical owner는 중앙 `requirements-strix-ci.txt`와 생성 `requirements-strix-ci-hashes.txt`다. ordinary descendant를 게시하고 fresh exact-head Python Security 및 review Checks를 수집한다. | + +### 2026-09-30 central coverage owner stack delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-CENTRAL-COVERAGE-OWNER-01 | **Proposed — complete local integration GREEN; hosted exact-head acceptance pending** | `.github#2521@f62bb7ec…` 위에 parser/security/response integration successor `.github#2530@2510618f…`와 GitHub API response-lifecycle owner `.github#2532@9d3ec75d…`를 ordinary two-parent merge로 보존했다. 첫 integrated warning-fatal run은 `5196 passed, 6 skipped` 뒤 queue/Strix/release prescreen/release dependency의 실제 미실행 분기 90개와 partial branch 29개를 드러냈다. Dummy/live-CLI tests를 bounded behavior contracts로 교체하고, exact Git blob·Cargo development lock·runtime receipt·final fanout cap·Python 3.10 TOML fallback을 검증했으며, 앞선 필수조건 때문에 도달 불가능했던 prescreener postcondition만 제거했다. 이동한 #2530 head까지 재수집·일반 병합한 최종 exact local tree는 `5228 passed, 5 skipped, 40 subtests passed`, owned production `18232/18232` statements 및 `7488/7488` branches, warning 0이다. | Canonical owner는 중앙 `ContextualWisdomLab/.github`이며 source delta는 #2521 stack에만 통합한다. #2530의 live head 이동을 재수집해 force 없이 merge했고 새 integrated tree 전체를 재검증했다. 게시된 #2521 exact head의 hosted security/quality Checks 및 qualifying independent approval을 새로 확인한다. queued/skipped/pending을 성공으로 간주하지 않고 #2530/#2532를 단순 Close하지 않는다. [RCA와 검증 근거](doctoring/central-coverage-owner-stack-2521.md). | + +### 2026-09-30 full-suite parser-lock incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-QUALITY-FULL-SUITE-PARSER-LOCK-01 | **Proposed — `.github#2530@ef28f6bc…` integration successor (tree `76ec51bb…`) preserves security prerequisite `.github#2531@d1aa3659…` and response-lifecycle prerequisite `.github#2532@9d3ec75d…`; hosted acceptance pending** | Protected `main@37b10243…`의 common quality lock만 설치하는 전체 suite가 `defusedxml`을 찾지 못해 collection error 13건으로 중단됐고, 같은 suite의 신규 workflow 계약은 `yaml`을 import한다. #2530의 parser-lock head `bc40de51…`에 #2531과 #2532의 exact owner heads를 두 ordinary merge commit의 부모로 보존해 순환 full-suite 의존성을 해소했다. 통합 tree `76ec51bb…`에서 Python 3.14 warning-fatal 전체 suite는 5,173 passed, 6 skipped, 40 subtests passed였고, focused cross-owner regression은 524 passed, 2 skipped였다. ref는 force 없이 fast-forward됐다. 이 행은 live Project #1 상태나 merge authorization을 주장하지 않는다. | Canonical owner는 중앙 `.github`의 `requirements-opencode-review-ci.txt`, 생성 hash lock, 직접 소비 quality workflows다. #2530은 Draft로 유지하고 새 exact head의 hosted Checks, 미해결 thread 0, qualifying independent approval을 다시 수집한다. #2531/#2532는 protected integration과 complete carryover가 검증되기 전 닫지 않으며, #2531 ordinary merge 뒤 #2530 base를 current `main` ancestry에 맞춰 비강제 retarget한다. full-quality의 honest 99% coverage failure는 별도 coverage owner #2521에서 통합·수리하며 이 stack으로 waive하지 않는다. fast-mlsirm의 Noema HTTP 400·Strix linkage·제품 coverage 문제의 완료 증거로 재사용하지 않는다. | + +### 2026-09-30 공유 보안 기준 exact-head delta + +이 delta는 아래 2026-08-26 인벤토리를 덮어쓰지 않는다. 2026-09-30 재수집한 +보호 `main`은 `37b10243cec3d160ecc9c1be75c71428b160a703`이고, live API의 첫 +페이지에는 열린 PR 50개가 있었다. 페이지 전체를 조직의 총 PR 수로 추론하지 않는다. + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-SHARED-SECURITY-LOCK-01 | **Source repair in progress — release HOLD** | `.github#1026@6f645a73502e159d5a229805afa34868ad9bb851`의 Security Scan run `36495499815`는 공통 Rust fixture의 PyO3 `0.22.6`에서 GHSA-36hh-v3qg-5jq4와 GHSA-chgr-c6px-7xpp를 검출했고, Python Security run `36495499871`은 공통 Strix hash lock의 PyJWT `2.13.0`에서 CVE-2026-102274를 검출했다. 두 파일은 #1026 변경 범위 밖이며 보호 `main`에도 동일하게 남아 있었다. RED commit `cd84d887`는 PyO3 `0.29.2`와 PyJWT `2.14.0` source/lock parity를 요구한다. | 중앙 `.github`가 공통 fixture와 Strix lock을 소유한다. [RCA와 검증 계약](doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md)에 따라 owner PR의 exact-head Checks와 독립 승인, ordinary protected merge, immutable consumer source pin 갱신, 그리고 #1026의 비강제 main merge-forward가 순서대로 필요하다. 어떤 실패도 #1026 전용 패치나 bypass로 처리하지 않는다. | + +### 2026-09-30 GitHub API response lifecycle incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-GITHUB-API-HTTP-ERROR-CLOSE-01 | **Proposed — protected-main RED reproduced; source repair under hosted exact-head verification** | 보호된 `.github/main@37b10243cec3d160ecc9c1be75c71428b160a703`의 Python 3.14.7 `tests/test_github_api_url_boundary.py -W error`가 실제 CodeQL/Strix opener의 synthetic 302 여덟 경우에서 `ResourceWarning: Implicitly cleaning up `로 `8 failed, 26 passed`였다. 첫 repair의 warning-fatal full suite가 동일 defect를 Noema/Pingora/preflight/Pages/sandbox readiness에서 추가로 드러냈다. | Canonical owner는 중앙 `.github`이다. 각 caller가 기존 bounded status/telemetry와 fail-closed mapping을 보존한 뒤 file-like error response를 명시적으로 닫는다. `5161 passed, 10 skipped, 40 subtests passed`로 complete warning-fatal local tree가 GREEN이다. [RCA와 acceptance](doctoring/github-api-http-error-response-lifecycle.md)를 따라 exact-head hosted security, independent review, ordinary protected merge를 완료한 뒤 `.github#2040`과 review-transport stack이 새 protected head를 정상 병합해 downstream 증거를 재생성해야 한다. | + ### 2026-09-19 exact-head incident delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | @@ -3648,3 +3684,139 @@ verdict-shape acceptance, and qualifying independent approval. **Gap / failure scene.** The v2 handler names a run with `head/base/required-run/producer-source`, but its required-workflow fallback looked up only `head/base/required-run`. When authenticated status publication is unavailable, a completed clean handler job could not be found and a rerun ended false RED. Omitting the producer source would also allow a regenerated live merge revision to reuse predecessor evidence. **Action / evidence.** Correct the fallback lookup to include the live merge source and retain fail-closed base, head, required-run, workflow-path, job-name, GHAS-identity, and SARIF checks. The test-first repair reproduced two failures, then passed 96 focused workflow-contract tests; the new edge case rejects a stale merge-source title. Ruff E9/F/I on the changed dispatch-contract file and `git diff --check` pass. Fresh hosted Checks and a qualifying independent approval are still required on the unchanged executable delta before merge. + +## 2026-09-30 Repository Metadata Reconcile shallow-ancestry fixture + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; hosted exact-head +revalidation and qualifying independent review remain mandatory. + +**Context Map / owner.** The central `.github` metadata-maintenance bounded +context owns its workflow and repository-wide evidence contracts. Git object +ancestry is local runner evidence; no product repository may fabricate or copy +that result. + +**Gap / RCA.** Exact-head run +[`36720930491`](https://github.com/ContextualWisdomLab/.github/actions/runs/36720930491), +job `109905558240`, checked out +`737fc6fd3b536495a7d5f8bbbae9d0474771d21f` at depth one. The full suite then +failed because documented G-17 evidence commit +`57477289ebec5631b0c48f0bc419f336dbe19deb` was absent from that shallow object +database. This was a workflow-fixture defect: the test deliberately proves +reachability with `git cat-file` and `git merge-base --is-ancestor`, while the +workflow supplied only the exact tip object. The earlier local full-history run +masked the hosted condition. + +**RED → GREEN / action.** A new contract first failed on the missing complete- +history input. Commit `3bc859c73ed67074df13b2e01aa89dff2159e260` +sets `fetch-depth: 0` only on the validation checkout; exact revision +verification, `persist-credentials: false`, apply credentials, and all gates +remain unchanged. The focused workflow plus G-17 ancestry suites pass 37 tests. +Publication must trigger fresh checks on the new exact head; predecessor GREEN +and queued/skipped/pending conclusions do not authorize merge. + +## 2026-09-30 Shared Python CI urllib3 security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; release and merge +remain HOLD pending fresh exact-head hosted Checks and qualifying independent +review. + +**Context Map / owner.** Central `.github` owns the pip-audit and Strix +hash-locked CI runtimes. urllib3 and its security advisories are upstream +evidence; product repositories consume only an ordinarily integrated central +workflow revision. + +**Gap / RCA.** Exact-head Python Security run +[`36737059681`](https://github.com/ContextualWisdomLab/.github/actions/runs/36737059681), +job `109961499214`, found urllib3 2.7.0 vulnerable to CVE-2026-97687, +CVE-2026-97688, and CVE-2026-97689 in both the pip-audit installer lock and +the Strix runtime lock. The upstream 2.8.0 release fixes the corresponding +HTTPS-proxy TLS-policy crossover and chunked-stream CPU/memory denial-of-service +issues. Because neither source input constrained urllib3, unrelated dependency +resolution could retain the vulnerable transitive version. + +**RED → repair.** The retained regression first failed because the two source +inputs had no urllib3 row. Both sources now require `urllib3==2.8.0`; both +generated locks carry the same exact version and PyPI artifact hashes. The +detailed evidence and APA 7th references are in +[`docs/doctoring/shared-security-baseline-urllib3-20260930.md`](doctoring/shared-security-baseline-urllib3-20260930.md). +Both exact-pin audits return no known vulnerabilities; the focused dependency +contract passes 5 tests and the warnings-as-errors full suite passes 5,236 +tests, 5 optional skips, and 40 subtests. No audit threshold, failure mode, or +workflow gate changes. Fresh exact-head Python Security and the remaining +applicable checks are mandatory; the failed predecessor and any skipped or +pending result are not acceptance evidence. + +## 2026-09-30 Shared Strix PyJWT recursion security refresh + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; release and merge +remain HOLD pending fresh exact-head hosted Checks and qualifying independent +review. + +**Context Map / owner.** Central `.github` owns the hash-locked Strix CI +runtime. PyJWT and its signed package artifacts are upstream evidence; product +repositories consume only an ordinarily integrated central workflow revision. + +**Gap / RCA.** Exact-head Security Scan run +[`36740858208`](https://github.com/ContextualWisdomLab/.github/actions/runs/36740858208), +job `109974634074`, found PyJWT 2.14.0 affected by GHSA-42vr-xj54-vc7v in +`requirements-strix-ci.txt`. The dependency-review support probe succeeded and +the run's Gitleaks, Scorecard, Trivy, and OSV jobs passed, isolating the actual +failure to PyJWT's unauthenticated nested-payload recursion path rather than a +permissions or network condition. + +**RED → repair.** The existing source/lock parity contract first failed with +the old 2.14.0 pin. Both surfaces now select PyJWT 2.15.1 with the signed PyPI +artifact hashes. Upstream documents the recursion hardening in 2.15.0; 2.15.1 +retains it and fixes Base64URL padding. Detailed evidence and APA 7th references +are in +[`docs/doctoring/shared-security-baseline-pyjwt-recursion-20260930.md`](doctoring/shared-security-baseline-pyjwt-recursion-20260930.md). +No dependency-review threshold, fail-closed behavior, or workflow gate changes. +The focused dependency contract passes 5 tests; pip-audit 2.10.1's strict +exact-pin audit reports no known vulnerabilities; and the warnings-as-errors +repository suite passes 5,236 tests, 5 optional skips, and 40 subtests. Ruff and +`git diff --check` pass. Fresh exact-head hosted security evidence remains +mandatory. + +## 2026-10-01 OpenCode coverage approval-reuse evidence contract + +**Status:** Proposed on `ContextualWisdomLab/.github#2536`; release and merge +remain HOLD pending fresh exact-head hosted Checks, resolution of the actionable +review thread, and qualifying independent approval. + +**Context Map / owner.** Central `.github` owns the OpenCode coverage producer, +existing-approval gate, and repository-dispatch status publisher. Consumer +repositories receive only the released workflow contract and must not reinterpret +advisory job success as a passing coverage decision. + +**Gap / RCA.** CodeRabbit review thread `PRRT_kwDOS_C14s6nmf3Q` showed that two +approval-reuse paths checked only whether the coverage job concluded `success`; +a subsequent call-site audit found that the merge-scheduler approval gate did +not receive the summary at all. +The producer deliberately uses a successful job to publish an honest +`NOT MEASURED` diagnostic, so this result is necessary but not sufficient +approval evidence. A same-head approval could therefore be reused without a +current unique `PASS` decision. + +**RED → repair.** New regressions reject missing, `NOT MEASURED`, malformed, +and duplicate decisions. A shared validator and the workflow shell guard now +require exactly one `- Result: PASS` line before either existing-approval reuse +or success-status publication; the current summary is passed explicitly across +all approval consumers. The integrated focused suite passes 186 tests with 1 optional +LLVM-platform skip, including concurrent exact-head test commit +`87ffafa2f6b19080c01f6ee24b987b37cb92dcb8` and implementation commit +`0bcded6b08af4554541223438d046bc412c4b093`. Detailed evidence is in +[`docs/doctoring/opencode-coverage-approval-reuse-20261001.md`](doctoring/opencode-coverage-approval-reuse-20261001.md). +The warnings-as-errors repository suite passes 5,255 tests, 5 optional skips, +and 40 subtests. No threshold or required gate changed. Fresh exact-head hosted +evidence remains mandatory before integration. + +**Hosted follow-up.** Exact-head Trusted uv run +[`36751696675`](https://github.com/ContextualWisdomLab/.github/actions/runs/36751696675), +job `110011676248`, passed the tests but correctly failed the 100% repository +coverage gate because the new CLI rejection at +`scripts/ci/opencode_existing_approval_gate.py:241-242` had no direct caller. +The added regression executes a `NOT MEASURED` decision and verifies the +fail-closed diagnostic. No coverage exclusion or threshold reduction was used; +the exact hosted command now covers all 18,252 production statements and 7,498 +branches at 100% locally, with 5,255 passed, 5 optional skips, and 40 subtests. +Fresh exact-head hosted revalidation is required. diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 116009874b..f58dc7f4d3 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -137,88 +137,96 @@ coverage==7.15.4 \ # via # -r requirements-opencode-review-ci.txt # pytest-cov -hypothesis==6.168.0 \ - --hash=sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669 \ - --hash=sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9 \ - --hash=sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a \ - --hash=sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4 \ - --hash=sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc \ - --hash=sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76 \ - --hash=sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1 \ - --hash=sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d \ - --hash=sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099 \ - --hash=sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f \ - --hash=sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29 \ - --hash=sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c \ - --hash=sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01 \ - --hash=sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377 \ - --hash=sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7 \ - --hash=sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f \ - --hash=sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c \ - --hash=sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0 \ - --hash=sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d \ - --hash=sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517 \ - --hash=sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131 \ - --hash=sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507 \ - --hash=sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09 \ - --hash=sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9 \ - --hash=sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2 \ - --hash=sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67 \ - --hash=sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a \ - --hash=sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3 \ - --hash=sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063 \ - --hash=sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab \ - --hash=sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4 \ - --hash=sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b \ - --hash=sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6 \ - --hash=sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda \ - --hash=sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc \ - --hash=sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795 \ - --hash=sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475 \ - --hash=sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2 \ - --hash=sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e \ - --hash=sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f \ - --hash=sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4 \ - --hash=sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128 \ - --hash=sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7 \ - --hash=sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28 \ - --hash=sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236 \ - --hash=sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782 \ - --hash=sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8 \ - --hash=sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79 \ - --hash=sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e \ - --hash=sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780 \ - --hash=sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20 \ - --hash=sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27 \ - --hash=sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349 \ - --hash=sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef \ - --hash=sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9 \ - --hash=sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8 \ - --hash=sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93 \ - --hash=sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63 \ - --hash=sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8 \ - --hash=sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b \ - --hash=sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04 \ - --hash=sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887 \ - --hash=sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c \ - --hash=sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149 \ - --hash=sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0 \ - --hash=sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5 \ - --hash=sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1 \ - --hash=sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f \ - --hash=sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd \ - --hash=sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193 \ - --hash=sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7 \ - --hash=sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc \ - --hash=sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc \ - --hash=sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571 \ - --hash=sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d \ - --hash=sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213 \ - --hash=sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68 \ - --hash=sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441 \ - --hash=sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad \ - --hash=sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d \ - --hash=sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843 +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via -r requirements-noema-document-ci.txt +hypothesis==6.168.3 \ + --hash=sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408 \ + --hash=sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89 \ + --hash=sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62 \ + --hash=sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897 \ + --hash=sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef \ + --hash=sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b \ + --hash=sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102 \ + --hash=sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575 \ + --hash=sha256:140fe3cc7ba98a4ffeeb6492c318b43c326110a4d7b4f56bb759d5c264e2dc1c \ + --hash=sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa \ + --hash=sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7 \ + --hash=sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595 \ + --hash=sha256:26084ef31653108da9e5eb171278a1d930e90d8631604391f828219f5c1f6cb2 \ + --hash=sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef \ + --hash=sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f \ + --hash=sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7 \ + --hash=sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16 \ + --hash=sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2 \ + --hash=sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96 \ + --hash=sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074 \ + --hash=sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90 \ + --hash=sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4 \ + --hash=sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a \ + --hash=sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766 \ + --hash=sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462 \ + --hash=sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca \ + --hash=sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5 \ + --hash=sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd \ + --hash=sha256:558625ff28e415f2f770cad618c2c9e05a045a6586949397f178d537bec2f083 \ + --hash=sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef \ + --hash=sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc \ + --hash=sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026 \ + --hash=sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f \ + --hash=sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd \ + --hash=sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5 \ + --hash=sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c \ + --hash=sha256:7311d8ff296806ea41513d52edc832fcf43a1dc8d5c5f308cc386fc31b253a05 \ + --hash=sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa \ + --hash=sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e \ + --hash=sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d \ + --hash=sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa \ + --hash=sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b \ + --hash=sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a \ + --hash=sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d \ + --hash=sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018 \ + --hash=sha256:836eda971f25ddfce274107ed113297e8acede8d9b267775742b8ba8a404d0f0 \ + --hash=sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b \ + --hash=sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24 \ + --hash=sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5 \ + --hash=sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696 \ + --hash=sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b \ + --hash=sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b \ + --hash=sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75 \ + --hash=sha256:aac5889ea7b0b86cf2bf5709250b7576c44859dd249d1be23c009a7d6fb9f742 \ + --hash=sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2 \ + --hash=sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11 \ + --hash=sha256:b1872d2dc3f3758dfbf17cc39838b8e6b6a5c39a37811c067245e87b12d50f74 \ + --hash=sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba \ + --hash=sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a \ + --hash=sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128 \ + --hash=sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed \ + --hash=sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95 \ + --hash=sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068 \ + --hash=sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96 \ + --hash=sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7 \ + --hash=sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb \ + --hash=sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957 \ + --hash=sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38 \ + --hash=sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86 \ + --hash=sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499 \ + --hash=sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4 \ + --hash=sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248 \ + --hash=sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2 \ + --hash=sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd \ + --hash=sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c \ + --hash=sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483 \ + --hash=sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0 \ + --hash=sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580 \ + --hash=sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe \ + --hash=sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675 \ + --hash=sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6 \ + --hash=sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09 \ + --hash=sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab \ + --hash=sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e \ + --hash=sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837 # via -r requirements-opencode-review-ci.txt iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ @@ -272,6 +280,81 @@ pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 # via -r requirements-opencode-review-ci.txt +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via -r requirements-opencode-review-ci.txt sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index bf2112ed68..452ce2f074 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -12,4 +12,7 @@ interrogate==1.7.0 maturin==1.15.0 pytest==9.1.1 pytest-cov==7.1.0 +# Full-suite collection imports the Noema document parser and workflow YAML. +-r requirements-noema-document-ci.txt +PyYAML==6.0.3 uv==0.12.7 diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index 7a41a3d2d8..9a331a0d62 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -391,7 +391,9 @@ tomli-w==1.2.0 \ --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 # via pip-audit -urllib3==2.7.0 \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 - # via requests +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 + # via + # -r requirements-pip-audit-ci.txt + # requests diff --git a/requirements-pip-audit-ci.txt b/requirements-pip-audit-ci.txt index 684087ba51..bec9276178 100644 --- a/requirements-pip-audit-ci.txt +++ b/requirements-pip-audit-ci.txt @@ -1 +1,2 @@ pip-audit==2.10.1 +urllib3==2.8.0 diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index eb83beda17..171878f750 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -144,6 +144,7 @@ anyio==4.14.2 \ --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via + # -r requirements-strix-ci.txt # google-genai # gql # httpx @@ -1119,24 +1120,45 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -litellm==1.94.1 \ - --hash=sha256:001be1cde7950f2ae484e450ab2f8e93ab8791e5e8d4da560d21f2fb456b0b47 \ - --hash=sha256:07c1771315d7d26e242ef90b9336bcbc49a52158ff72ee640b4f8160cc963147 \ - --hash=sha256:156c62022320bccab7c3507b6b13400b26e55b74c799e5a4a2d5bf904a77368f \ - --hash=sha256:1b0bc4a2373e54f2bd4c13f8ef9fda3839bfb2e1173fb4bcea3150b07d4c59bc \ - --hash=sha256:2103e9b155d6545b48936d2ac2e614661613adb9e3d081c58c7303ca5dd6c656 \ - --hash=sha256:44e55a55270dee8bb85e063940c368d32040e6db66765c55db4b884fc002d4ef \ - --hash=sha256:66bc95498af3ab687ce7570704cb274bcf1d78049afa87a9f5f64db45b72847d \ - --hash=sha256:a6f5274876f20dd5c9e53ba3da502e94f5b3c681c5027a0398231d0caae4aacd \ - --hash=sha256:af37356cf5b325a2887c40ff772b39e1e0865b988297c544b29123ffb13fd1b9 \ - --hash=sha256:b0145d6b9fb718d12b7242ce5c975123f4dbfecd7b8ed1eb6a6939b0e506c946 \ - --hash=sha256:c5c9247d9fea8fe7cda851f7b15db560ee547a8325a0af86048967edf3ccfa15 \ - --hash=sha256:cfef0468bda9c1ba8f554bebc2966f08436f1ead98017e7ed2d7663ece77f1c2 \ - --hash=sha256:d14e5812b5f36af2ab45461ee0c925251bc07daf65c33b8f2ce3fd3ec1235eae \ - --hash=sha256:e9b6d92e305d96bdadb8a5ccd343b1ac188de142fbd6c91f72c75416b8c25c48 \ - --hash=sha256:e9effe4c1e9206740b4bb4c98142ea1f71bae57e49df007cd25ef24b0ce4563f \ - --hash=sha256:ffa9a6cd9b6205d60b02ffc0b7f077a03693d835b06d2a34bfeaabb4f073c08a +litellm==1.94.3 \ + --hash=sha256:0ddb60a439cc361e69bb4f550a78d1656c8edad6753f8fece14f60bbd0fcd054 \ + --hash=sha256:1017f29a6f72a16ef1b026208b3e52c7197b91ae848b396c817bfb051f86c300 \ + --hash=sha256:136960922878cbc66065346583a60fd0d35f3b1dbe853d636d10da39f049399f \ + --hash=sha256:14f06f1486aa4eef0684a2088539548e3647495fa119edc2b8145c5cfee3ca24 \ + --hash=sha256:1594d231e005f6a99e319310f2e3c0d33f26d8c746e90ec42fedc2b413f16573 \ + --hash=sha256:1ec0ff6e81acbfa85b4af90f9460c46a7872a28d53aaeee956ad7d289f47edc4 \ + --hash=sha256:2098c818ce5d8139eb46a851f26d1ac5caac1502e19d5223af4930b31682c461 \ + --hash=sha256:45b9f56f02a5417b249ecaf6f01f2531e744b17b701aaea441741ed9431ba35e \ + --hash=sha256:4a21e3f5cc72b18bd424009e5ad9addcee5ebdd726c7b26cadb4c87f865f5f1b \ + --hash=sha256:56bb6b303e2e341eb55570fd0cfa5734e5d89e8a1a5ee0096e7bb0bd9f27f9e2 \ + --hash=sha256:57a82dbbf1528aa530f021f1e6fe7528331c7d5f95fa755a17f984623eaf29ae \ + --hash=sha256:584b705ece98e7b7a2fc3cf4b720711d0b786e4d131de5c4469c34cabc3f55e0 \ + --hash=sha256:5b7ef13f56e167c76ca5dfc2e6bba8e52faf6371a540c00585ee7cff81af4ba0 \ + --hash=sha256:68b5b8c4ec8d51e4a2d02cf5405b6dd85be06b8ccf0205b7b0b30c1d3c02aebe \ + --hash=sha256:6ae267172e1c615346523faf6de6cac2f21937bda2e15e0af8c5d3291476a4e5 \ + --hash=sha256:87a343c86304daa6be5f96493f1eb0796d57142886f21f8d0815e1712d2bfc18 \ + --hash=sha256:93e13c1919f8eaeb3bee9a9160c6339d1fc3913254981a1e152023c5815667cc \ + --hash=sha256:9d0fabd73ca571771773a892de1c57e208349c99a3bbc51cd049a326f7a6f6e2 \ + --hash=sha256:9e339bea866f313487b1d7e150374360700f7ed80cac41722f7ddb072b9b685e \ + --hash=sha256:9f0529505cd5cceed220f89b311f4403fa076beae18069e9df31c4e59b226871 \ + --hash=sha256:a18386278e02a2b7f8d73bf836f1c7945133becca7894ee0641dbacc6473322d \ + --hash=sha256:ac40df3e6f412a2e87f61553640922d63c8114e433c85992461317b5f67ce78f \ + --hash=sha256:b6984b3b8032ab6807aa7c56423e961a5c4220d6b93a348dbf37ec1162b68e2d \ + --hash=sha256:b6ba10b699165a9a46811f2d3abe92967dc1c2d16f972d1057dae5c5c9cd460b \ + --hash=sha256:c0eee30a6d7c38fd4702339a5a53648f140ed8e5307baa321215d0d10dd9e535 \ + --hash=sha256:c526656e85ee1be21038c75774c094b97e7439e649dc13f61cb434700199990a \ + --hash=sha256:ccf58062430fdfd53d1ebb02d55d2316ad1068005dbb4186a522039077489f9b \ + --hash=sha256:d2027a4e492fd85702ffdbcc43be770a450c521b8c8c0c995e6f7d2fb6df1d49 \ + --hash=sha256:d8eedd2dc2e504ea8452392c4c66c102ed7ccd204307d72420fd073552b59ea9 \ + --hash=sha256:de2b3cc1a5cb5b9c7e393b55a0da58cdbe46f0b373b215d4422e30b2d5dec29b \ + --hash=sha256:deac3f3efdb5373729f37f612d2469696bbc928baef5f7c80ae7a2b0940b7939 \ + --hash=sha256:e05cabfc94633e47b725bed0851ecf18809117d23e8d6ef9abaa6059c2712261 \ + --hash=sha256:e4255bc09a27d99e8c1526baf76ff307e16d3543f51fda9c87c51a71c99c8ff7 \ + --hash=sha256:f8913c9f10ef3fb1baf4ab16fc77e110f92c1c0a9a16fa0f86d1b90b417e063f \ + --hash=sha256:f8c9a92d6165570065349b8d324e7b58800cb993c39b85a5f3c7458b5d926f3d \ + --hash=sha256:fafb59618a85f3c96d38a123ac53187c75e722a6b4e34d305063661262f62ea8 # via + # -r requirements-strix-ci.txt # openai-agents # strix-agent markdown-it-py==4.2.0 \ @@ -1811,10 +1833,12 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich -pyjwt==2.13.0 \ - --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ - --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 - # via mcp +pyjwt==2.15.1 \ + --hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \ + --hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8 + # via + # -r requirements-strix-ci.txt + # mcp pyopenssl==26.4.0 \ --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c @@ -2346,10 +2370,11 @@ typing-inspection==0.4.4 \ # mcp # pydantic # pydantic-settings -urllib3==2.7.0 \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 # via + # -r requirements-strix-ci.txt # docker # requests uvicorn==0.49.0 \ diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 50e8a05f9b..76a28b18b0 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,5 +1,7 @@ strix-agent==1.5.3 +litellm==1.94.3 anyio==4.14.2 +pyjwt==2.15.1 openai[httpx2]==2.54.0 aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 @@ -7,3 +9,4 @@ protobuf<8.0.0 cryptography==50.0.0 python-multipart==0.0.32 pyasn1==0.6.4 +urllib3==2.8.0 diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py index 53e00c41c6..295ecf544e 100644 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ b/scripts/ci/codeql_ghas_configuration_identity.py @@ -29,6 +29,7 @@ DEFAULT_SETUP_ANALYSIS_KEY = "dynamic/github-code-scanning/codeql:analyze" CODEQL_TOOL_NAME = "CodeQL" GITHUB_API_AUTHORITY = "api.github.com" +MAX_HTTP_ERROR_DIAGNOSTIC_BYTES = 400 class ConfigurationIdentityError(RuntimeError): @@ -199,7 +200,12 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: with _GITHUB_API_OPENER.open(request, timeout=timeout_seconds) as response: payload = response.read().decode("utf-8") except urllib.error.HTTPError as exc: - body = exc.read().decode("utf-8", errors="replace")[-400:] + try: + body = exc.read(MAX_HTTP_ERROR_DIAGNOSTIC_BYTES).decode( + "utf-8", errors="replace" + ) + finally: + exc.close() raise ConfigurationIdentityError( f"GitHub API GET failed with HTTP {exc.code}: {body}" ) from exc diff --git a/scripts/ci/contextual_orchestrator_review_launcher.py b/scripts/ci/contextual_orchestrator_review_launcher.py index 811dc7d3f8..58c019ffa9 100644 --- a/scripts/ci/contextual_orchestrator_review_launcher.py +++ b/scripts/ci/contextual_orchestrator_review_launcher.py @@ -33,6 +33,7 @@ import sys from pathlib import Path from typing import Any, Callable +from urllib.error import HTTPError from scripts.ci.contextual_orchestrator_review_policy import ( FREE_POOL_CREDENTIAL_NAMES, @@ -329,10 +330,15 @@ def _safe_retry_after_seconds(exc: Exception) -> int | None: # runs inside the probe walk's exception handler, so a ValueError here # would escape ``_preflight_review_agents`` -- whose callers catch only # ``ReviewPreflightError`` -- and kill the boot before any evidence file - # is written. Every ``isdecimal`` string is accepted by ``int``. - if not isinstance(raw, str) or not raw.strip().isdecimal(): + # is written. Reject more than five decimal characters before conversion: + # the accepted ceiling is 86400, and Python deliberately rejects very + # long integer strings before ``int`` can return a value. + if not isinstance(raw, str): return None - seconds = int(raw.strip()) + normalized = raw.strip() + if len(normalized) > 5 or not normalized.isdecimal(): + return None + seconds = int(normalized) return seconds if 0 <= seconds <= 86400 else None @@ -389,19 +395,25 @@ def _record_provider_exception(row: dict[str, object], exc: Exception) -> None: row: The in-progress per-route evidence row to update. exc: The exception a probe attempt raised. """ - row["status"] = "rejected" - error_type = type(exc).__name__ - row["error_type"] = ( - error_type if error_type.isidentifier() and len(error_type) <= 64 else "provider_error" - ) - http_status = _safe_http_status(exc) - if http_status is not None: - row["http_status"] = http_status - retry_after = _safe_retry_after_seconds(exc) - if retry_after is not None: - row["retry_after_s"] = retry_after - row.pop("finish_reason", None) - row.pop("reasoning_without_content", None) + try: + row["status"] = "rejected" + error_type = type(exc).__name__ + row["error_type"] = ( + error_type + if error_type.isidentifier() and len(error_type) <= 64 + else "provider_error" + ) + http_status = _safe_http_status(exc) + if http_status is not None: + row["http_status"] = http_status + retry_after = _safe_retry_after_seconds(exc) + if retry_after is not None: + row["retry_after_s"] = retry_after + row.pop("finish_reason", None) + row.pop("reasoning_without_content", None) + finally: + if isinstance(exc, HTTPError): + exc.close() def _demote_agent(agent: object, penalty: int) -> object: diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 3ddc126604..99d228d5e1 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -22,6 +22,7 @@ import urllib.parse import urllib.request from typing import Any +from urllib.error import HTTPError try: import tomllib @@ -376,8 +377,11 @@ def _download_trusted_uv_archive() -> bytes: break payload.extend(chunk) except OSError as exc: + error_type = type(exc).__name__ + if isinstance(exc, HTTPError): + exc.close() raise RuntimeError( - f"trusted uv archive download failed: {type(exc).__name__}" + f"trusted uv archive download failed: {error_type}" ) from exc if len(payload) > TRUSTED_UV_DOWNLOAD_MAX_BYTES: diff --git a/scripts/ci/noema-document-reader/package-lock.json b/scripts/ci/noema-document-reader/package-lock.json index 1026fd79a3..2a0b8215b0 100644 --- a/scripts/ci/noema-document-reader/package-lock.json +++ b/scripts/ci/noema-document-reader/package-lock.json @@ -458,9 +458,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -669,9 +669,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 380ee22675..c08df174a3 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -1719,12 +1719,15 @@ def call_llm( http_status: int | None = None retry_after_seconds: int | None = None if isinstance(exc, urllib.error.HTTPError): - active_phase = "response_error" - http_status = exc.code if type(exc.code) is int else None - retry_after_seconds = parse_http_retry_after_seconds(exc.headers) - gateway_telemetry = _extract_http_error_telemetry(exc) - model_value = gateway_telemetry.get("served_model") - served_model = model_value if isinstance(model_value, str) else None + try: + active_phase = "response_error" + http_status = exc.code if type(exc.code) is int else None + retry_after_seconds = parse_http_retry_after_seconds(exc.headers) + gateway_telemetry = _extract_http_error_telemetry(exc) + model_value = gateway_telemetry.get("served_model") + served_model = model_value if isinstance(model_value, str) else None + finally: + exc.close() elapsed = time.monotonic() - attempt_started current_failure = _stable_failure_diagnostic(exc) model_note = served_model or "unknown" diff --git a/scripts/ci/opencode_dispatch_status.py b/scripts/ci/opencode_dispatch_status.py index 25cbb75b7a..bbb2a26a51 100644 --- a/scripts/ci/opencode_dispatch_status.py +++ b/scripts/ci/opencode_dispatch_status.py @@ -11,11 +11,13 @@ try: from opencode_existing_approval_gate import ( OPENCODE_APP_APPROVAL_AUTHORS, + coverage_summary_rejection_reason, review_rejection_reason, ) except ModuleNotFoundError: # pragma: no cover - package import path from scripts.ci.opencode_existing_approval_gate import ( OPENCODE_APP_APPROVAL_AUTHORS, + coverage_summary_rejection_reason, review_rejection_reason, ) @@ -43,6 +45,7 @@ def decide_status( *, model_outcome: str, coverage_result: str, + coverage_summary: str, expected_head: str, pull_request: dict[str, Any], reviews: Sequence[dict[str, Any]], @@ -51,6 +54,8 @@ def decide_status( live_head = str((pull_request.get("head") or {}).get("sha") or "") if coverage_result != "success": reason = "OpenCode coverage evidence did not pass for the current head." + elif coverage_summary_rejection_reason(coverage_summary): + reason = "OpenCode coverage decision is missing, non-passing, or ambiguous." elif not expected_head or live_head.lower() != expected_head.lower(): reason = "OpenCode status target is stale or the live PR head is unavailable." elif not _has_current_approval(reviews, expected_head): @@ -71,6 +76,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--model-outcome", required=True) parser.add_argument("--coverage-result", required=True) + parser.add_argument("--coverage-summary", required=True) parser.add_argument("--expected-head", required=True) parser.add_argument("--pull-request-file", required=True, type=Path) parser.add_argument("--reviews-file", required=True, type=Path) @@ -89,6 +95,7 @@ def main(argv: Sequence[str] | None = None) -> int: decide_status( model_outcome=args.model_outcome, coverage_result=args.coverage_result, + coverage_summary=args.coverage_summary, expected_head=args.expected_head, pull_request=pull_request, reviews=reviews, diff --git a/scripts/ci/opencode_existing_approval_gate.py b/scripts/ci/opencode_existing_approval_gate.py index 6712c02282..075716a048 100644 --- a/scripts/ci/opencode_existing_approval_gate.py +++ b/scripts/ci/opencode_existing_approval_gate.py @@ -5,6 +5,7 @@ import argparse import json +import os import re import sys from typing import Any, TextIO @@ -46,6 +47,23 @@ ) +def coverage_summary_rejection_reason(summary: str) -> str | None: + """Explain why a coverage summary cannot authorize approval reuse.""" + decisions = [line for line in summary.splitlines() if line.startswith("- Result:")] + if not decisions: + return "coverage decision is missing" + if len(decisions) != 1: + return "coverage decision is duplicated or contradictory" + if decisions[0] != "- Result: PASS": + return "coverage decision is not PASS" + return None + + +def coverage_decision_is_pass(summary: str) -> bool: + """Return whether a summary has one authoritative PASS decision.""" + return coverage_summary_rejection_reason(summary) is None + + def flatten_reviews(document: object) -> list[dict[str, Any]]: """Flatten REST pagination output while rejecting malformed review entries.""" if not isinstance(document, list): @@ -156,13 +174,12 @@ def has_reusable_real_model_approval( log: TextIO, approval_authors: frozenset[str] = APPROVAL_AUTHORS, ) -> bool: - """Return whether reviews contain a real-model approval for the exact head.""" + """Return whether the latest exact-head OpenCode decision is reusable.""" candidate_count = 0 for review in reversed(reviews): - state = str(review.get("state") or "").upper() commit_id = str(review.get("commit_id") or "") login = str((review.get("user") or {}).get("login") or "") - if state != "APPROVED" or commit_id.lower() != head_sha.lower(): + if commit_id.lower() != head_sha.lower(): continue if login not in KNOWN_PUBLICATION_ACTORS: continue @@ -182,9 +199,11 @@ def has_reusable_real_model_approval( return True print( "existing-approval gate rejected same-head review " - f"id={review_id} author={login}: {reason}", + f"id={review_id} author={login}; latest same-head review is authoritative: " + f"{reason}", file=log, ) + break print( "existing-approval gate found no reusable real-model approval " @@ -198,6 +217,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace: """Parse existing-approval gate command-line arguments.""" parser = argparse.ArgumentParser() parser.add_argument("--head", required=True) + parser.add_argument( + "--coverage-summary", + default=os.environ.get("COVERAGE_EVIDENCE_SUMMARY", ""), + ) parser.add_argument( "--require-opencode-app", action="store_true", @@ -214,6 +237,10 @@ def main(argv: list[str]) -> int: "existing-approval gate requires a 40-character head SHA", file=sys.stderr ) return 2 + coverage_error = coverage_summary_rejection_reason(args.coverage_summary) + if coverage_error: + print(f"existing-approval gate rejected evidence: {coverage_error}", file=sys.stderr) + return 1 try: reviews = flatten_reviews(json.load(sys.stdin)) except (json.JSONDecodeError, ValueError) as exc: diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 5c1e39d9e3..4c9697f1d7 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -467,11 +467,17 @@ def _github_open_json(url: str, token: str) -> object: with github_opener.open(request, timeout=30) as response: payload = response.read(MAX_RESPONSE_BYTES + 1) except (HTTPError, URLError, TimeoutError) as exc: - if isinstance(exc, HTTPError) and exc.code == 404: - raise ArtifactDeclarationNotFoundError( - f"GitHub API reported no resource for policy evidence at {url}" + try: + if isinstance(exc, HTTPError) and exc.code == 404: + raise ArtifactDeclarationNotFoundError( + f"GitHub API reported no resource for policy evidence at {url}" + ) from exc + raise PolicyError( + f"GitHub API request failed for policy evidence: {type(exc).__name__}" ) from exc - raise PolicyError(f"GitHub API request failed for policy evidence: {type(exc).__name__}") from exc + finally: + if isinstance(exc, HTTPError): + exc.close() if len(payload) > MAX_RESPONSE_BYTES: raise PolicyError("GitHub API policy response exceeded the bounded response size") try: @@ -497,7 +503,13 @@ def _github_open_raw_bytes(url: str, token: str, max_bytes: int) -> bytes: with github_opener.open(request, timeout=30) as response: raw = response.read(max_bytes + 1) except (HTTPError, URLError, TimeoutError) as exc: - raise PolicyError(f"GitHub raw blob request failed: {type(exc).__name__}") from exc + try: + raise PolicyError( + f"GitHub raw blob request failed: {type(exc).__name__}" + ) from exc + finally: + if isinstance(exc, HTTPError): + exc.close() if len(raw) > max_bytes: raise PolicyError("GitHub raw blob exceeded the bounded response size") return raw @@ -614,8 +626,6 @@ def _load_raw_file_bytes( return raw if encoding == "none": raise PolicyError(f"GitHub content evidence for {path} has no inline content and no verifiable oversized size") - if encoding != "base64": - raise PolicyError(f"GitHub content evidence for {path} is not a regular base64 file") encoded = payload.get("content") if not isinstance(encoded, str): raise PolicyError(f"GitHub content evidence for {path} has a malformed size or content field") diff --git a/scripts/ci/prescreen_release_runtime_archives.py b/scripts/ci/prescreen_release_runtime_archives.py index 7631a38a06..9969d5887e 100644 --- a/scripts/ci/prescreen_release_runtime_archives.py +++ b/scripts/ci/prescreen_release_runtime_archives.py @@ -391,11 +391,6 @@ def prescreen(scope: Any, root: Path) -> dict[str, list[dict[str, Any]]]: "native_properties": native_properties, "fixture": fixture, "fixture_sha256": gate.fixture_digest(fixture), "legs": [leg]} - if (len(seen_legs) != 13 or "sdist" not in seen_legs - or seen_variants != {f"universal2-apple-darwin-py{version}" - for version in ("3.12", "3.13", "3.14")} - or not rows): - raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "runtime archive coverage is incomplete") return {"archives": sorted(rows.values(), key=lambda row: (row["key"], row["source_sha256"])), "build_packages": sorted(build_rows.values(), key=lambda row: row["key"]), "build_tools": sorted(tool_rows.values(), key=lambda row: row["key"])} diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 1570455818..a78bd09725 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -16,7 +16,7 @@ import sys from pathlib import Path from typing import Any -from urllib.error import URLError +from urllib.error import HTTPError, URLError from urllib.request import HTTPRedirectHandler, Request, build_opener @@ -38,8 +38,6 @@ class _NoPagesRedirects(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): """Raise an HTTPError instead of following the redirect.""" - from urllib.error import HTTPError - raise HTTPError(req.full_url, code, msg, headers, fp) @@ -247,7 +245,13 @@ def _pages_publication_ready(repository: str, current: dict[str, Any]) -> None: if not response.read(1): raise RuntimeError(f"GitHub Pages returned empty content for {repository}") except (URLError, TimeoutError, OSError) as exc: - raise RuntimeError(f"GitHub Pages is not reachable for {repository}") from exc + try: + raise RuntimeError( + f"GitHub Pages is not reachable for {repository}" + ) from exc + finally: + if isinstance(exc, HTTPError): + exc.close() def _repository_file_exists(repository: str, default_branch: str, path: str) -> bool: diff --git a/scripts/ci/release_dependency_gate.py b/scripts/ci/release_dependency_gate.py index 96347278f6..aca0307201 100644 --- a/scripts/ci/release_dependency_gate.py +++ b/scripts/ci/release_dependency_gate.py @@ -48,6 +48,7 @@ import ast import email.parser import hashlib +import importlib import io import json import os @@ -63,10 +64,15 @@ from pathlib import Path, PurePosixPath from typing import Any, Iterable, Mapping, Sequence -try: - import tomllib -except ModuleNotFoundError: # Python 3.10; already declared in the dev group. - import tomli as tomllib +def _import_toml_parser(): + """Return the stdlib TOML parser, or the declared Python 3.10 backport.""" + try: + return importlib.import_module("tomllib") + except ModuleNotFoundError: # Python 3.10; already declared in the dev group. + return importlib.import_module("tomli") + + +tomllib = _import_toml_parser() try: from scripts.ci.spdx_license_policy import ( diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index b0376c0822..fb5702f0c2 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -584,6 +584,9 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: if 200 <= response.status < 500: return True time.sleep(1) + except urllib.error.HTTPError as exc: + exc.close() + time.sleep(1) except (urllib.error.URLError, TimeoutError): time.sleep(1) return False diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py index 7319040df2..aed54ded1a 100644 --- a/scripts/ci/strix_evidence_binding.py +++ b/scripts/ci/strix_evidence_binding.py @@ -305,8 +305,10 @@ def default_github_opener(url: str, token: str) -> Any: with _GITHUB_API_OPENER.open(request, timeout=30) as response: payload = response.read() except HTTPError as exc: + status_code = exc.code + exc.close() raise EvidenceBindingError( - f"GitHub changed-file request failed with HTTP {exc.code}" + f"GitHub changed-file request failed with HTTP {status_code}" ) from exc except URLError as exc: raise EvidenceBindingError( diff --git a/tests/fixtures/coverage-cargo/Cargo.lock b/tests/fixtures/coverage-cargo/Cargo.lock index 9a54521941..20666db700 100644 --- a/tests/fixtures/coverage-cargo/Cargo.lock +++ b/tests/fixtures/coverage-cargo/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - [[package]] name = "coverage-cargo-fixtures" version = "0.0.0" @@ -29,15 +17,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "itoa" version = "1.0.15" @@ -50,15 +29,6 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -82,37 +52,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -120,9 +85,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -132,13 +97,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -152,12 +116,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - [[package]] name = "ryu" version = "1.0.20" @@ -177,18 +135,12 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/tests/fixtures/coverage-cargo/Cargo.toml b/tests/fixtures/coverage-cargo/Cargo.toml index dbe13ab8ab..67af5592ca 100644 --- a/tests/fixtures/coverage-cargo/Cargo.toml +++ b/tests/fixtures/coverage-cargo/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] itoa = "=1.0.15" ryu = "=1.0.20" -pyo3 = { version = "=0.22.6", features = ["extension-module", "abi3-py310"] } +pyo3 = { version = "=0.29.2", features = ["extension-module", "abi3-py310"] } diff --git a/tests/test_agent_mention_workflow_contract.py b/tests/test_agent_mention_workflow_contract.py index e640121ad1..1cce19993e 100644 --- a/tests/test_agent_mention_workflow_contract.py +++ b/tests/test_agent_mention_workflow_contract.py @@ -52,6 +52,19 @@ def test_workflow_uses_local_event_and_central_sweep_with_job_scoped_writes() -> assert "agent_mention_sweep.py" in sweep +def test_full_suite_tooling_lock_includes_collection_dependencies() -> None: + """A quality install must provide both parsers imported during suite collection.""" + lock = (ROOT / "requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) + requirements = { + line.split("==", 1)[0].casefold() + for line in lock.splitlines() + if line and not line.startswith(("#", " ", "-")) and "==" in line + } + assert {"defusedxml", "pyyaml"} <= requirements + + def test_quality_workflow_measures_exact_files_without_module_name_warnings() -> None: """Coverage includes the two script paths instead of treating paths as modules.""" diff --git a/tests/test_codeql_ghas_configuration_identity.py b/tests/test_codeql_ghas_configuration_identity.py index 817cd56497..84a04a4de9 100644 --- a/tests/test_codeql_ghas_configuration_identity.py +++ b/tests/test_codeql_ghas_configuration_identity.py @@ -430,8 +430,8 @@ def test_request_json_maps_http_and_transport_failures(monkeypatch): """HTTP and transport failures become ConfigurationIdentityError.""" class _HTTPError(identity.urllib.error.HTTPError): - def read(self) -> bytes: - return b"denied" + def read(self, size: int = -1) -> bytes: + return b"denied"[:size] def raise_http(request, timeout=30): del request, timeout diff --git a/tests/test_coverage_configuration.py b/tests/test_coverage_configuration.py new file mode 100644 index 0000000000..db960e6a8c --- /dev/null +++ b/tests/test_coverage_configuration.py @@ -0,0 +1,31 @@ +"""Contracts for the repository-wide production coverage boundary.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: # pragma: no cover - exercised by the Python 3.10 CI lane + import tomli as tomllib + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +ALLOWED_COVERAGE_OMISSIONS = { + "tests/*", + "scripts/ci/contextual_orchestrator_review_launcher.py", +} + + +def test_production_coverage_omissions_are_explicitly_allowlisted() -> None: + """Reject production exclusions that manufacture a 100% coverage result.""" + + with (REPOSITORY_ROOT / "pyproject.toml").open("rb") as config_file: + project_configuration = tomllib.load(config_file) + + configured_omissions = set( + project_configuration["tool"]["coverage"]["run"]["omit"] + ) + + assert configured_omissions == ALLOWED_COVERAGE_OMISSIONS diff --git a/tests/test_coverage_incomplete_summary.py b/tests/test_coverage_incomplete_summary.py new file mode 100644 index 0000000000..0163175d9f --- /dev/null +++ b/tests/test_coverage_incomplete_summary.py @@ -0,0 +1,83 @@ +"""Execute the actual decision block: missing evidence must not be called PASS.""" + +from pathlib import Path +import subprocess + +WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" + + +def summary(failures: int, not_measured: int) -> str: + text = WORKFLOW.read_text(encoding="utf-8") + start = text.index(' append "## Coverage Decision"') + end = text.index(' coverage_output_file=', start) + block = "\n".join(line[10:] for line in text[start:end].splitlines()) + script = ( + f"set -eu\nfailures={failures}; not_measured={not_measured}; " + "measured_any=1; r_peer_check_required=0\n" + 'append(){ printf "%s\\n" "$*"; }\n' + block + ) + return subprocess.run(["bash", "-c", script], check=True, capture_output=True, text=True).stdout + + +def test_timeout_is_incomplete_not_success_evidence() -> None: + output = summary(0, 1) + assert "- Result: NOT MEASURED" in output + assert "- Result: PASS" not in output + assert "supported repository test suites passed" not in output + assert "does not replace required verification" in output + + +def test_real_failure_wins_over_incomplete_measurements() -> None: + assert "- Result: FAIL" in summary(1, 1) + + +def test_complete_success_still_passes() -> None: + assert "- Result: PASS" in summary(0, 0) + + +def approval_prefix(decision: str) -> subprocess.CompletedProcess[str]: + """Run the real APPROVE preconditions with a successful advisory job.""" + text = WORKFLOW.read_text(encoding="utf-8") + helper_start = text.index(" coverage_decision_is_pass() {") + helper_end = text.index("\n }", helper_start) + len("\n }") + helper = "\n".join( + line[10:] for line in text[helper_start:helper_end].splitlines() + ) + start = text.index(" APPROVE)") + len(" APPROVE)") + end = text.index(" if request_changes_for_merge_conflict_if_present", start) + block = "\n".join(line[14:] for line in text[start:end].splitlines()) + script = ( + "set -eu\nCOVERAGE_EVIDENCE_RESULT=success\n" + 'COVERAGE_EVIDENCE_SUMMARY="$1"\n' + 'stop_approval_without_review(){ printf "%s\\n" "$1"; exit 1; }\n' + + helper + + "\n" + + block + + '\nprintf "approval_allowed\\n"\n' + ) + return subprocess.run(["bash", "-c", script, "test", decision], capture_output=True, text=True) + + +def test_successful_advisory_job_cannot_approve_unmeasured_coverage() -> None: + result = approval_prefix(summary(0, 1)) + assert result.returncode == 1 + assert "COVERAGE_NOT_MEASURED" in result.stdout + assert "approval_allowed" not in result.stdout + + +def test_missing_or_ambiguous_decision_fails_closed_before_approval() -> None: + for decision in ( + "", + "- Result: UNKNOWN", + "prefix - Result: PASS", + "- Result: PASS (assumed)", + "- Result: PASS\n- Result: NOT MEASURED", + "- Result: PASS\n- Result: PASS", + ): + assert approval_prefix(decision).returncode == 1, decision + + +def test_complete_decision_can_reach_remaining_approval_checks() -> None: + result = approval_prefix(summary(0, 0)) + assert result.returncode == 0 + assert "approval_allowed" in result.stdout diff --git a/tests/test_github_api_url_boundary.py b/tests/test_github_api_url_boundary.py index a9050584fd..1c856c3bfa 100644 --- a/tests/test_github_api_url_boundary.py +++ b/tests/test_github_api_url_boundary.py @@ -8,6 +8,7 @@ import re import subprocess from typing import Any +from urllib.error import HTTPError from urllib.request import Request from urllib.response import addinfourl @@ -44,6 +45,7 @@ def __init__(self, target: str) -> None: """Store the redirect target and initialize the observed request ledger.""" self.target = target self.calls: list[tuple[str, str | None]] = [] + self.responses: list[Any] = [] def https_open(self, request: Request) -> Any: """Return a synthetic redirect response without contacting a network target.""" @@ -52,6 +54,7 @@ def https_open(self, request: Request) -> Any: headers["Location"] = self.target response = addinfourl(BytesIO(b""), headers, request.full_url, code=302) response.msg = "Found" + self.responses.append(response) return response @@ -71,6 +74,20 @@ def read(self) -> bytes: return b"[]" +class _ReadSizeRecordingBody(BytesIO): + """Record the requested byte limit for one synthetic HTTP error body.""" + + def __init__(self, payload: bytes) -> None: + """Store the payload and initialize the read-size ledger.""" + super().__init__(payload) + self.read_sizes: list[int] = [] + + def read(self, size: int = -1) -> bytes: + """Record the caller's bound before returning response bytes.""" + self.read_sizes.append(size) + return super().read(size) + + def _unexpected_open(*_args: Any, **_kwargs: Any) -> Any: """Fail if a rejected authority reaches the network/file opener boundary.""" pytest.fail("rejected GitHub API authority reached opener") @@ -165,6 +182,8 @@ def test_production_openers_reject_redirect_without_forwarding_bearer( assert transport.calls == [ (CANONICAL_GITHUB_API_URL, "Bearer test-token"), ] + assert len(transport.responses) == 1 + assert transport.responses[0].closed @pytest.mark.parametrize("target", REDIRECT_TARGETS) @@ -231,6 +250,37 @@ def strix_open(request: Any, **_kwargs: Any) -> _JsonResponse: assert strix_calls == [CANONICAL_GITHUB_API_URL] +def test_codeql_identity_client_bounds_http_error_diagnostic_read( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A hostile GitHub error body cannot force an unbounded diagnostic read.""" + body = _ReadSizeRecordingBody(b"x" * 8_192) + error = HTTPError( + CANONICAL_GITHUB_API_URL, + 502, + "Bad Gateway", + Message(), + body, + ) + + def raise_http_error(*_args: Any, **_kwargs: Any) -> Any: + """Raise the synthetic response at the authenticated opener boundary.""" + raise error + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_http_error) + + with pytest.raises(identity.ConfigurationIdentityError, match="HTTP 502"): + identity._request_json( + CANONICAL_GITHUB_API_URL, + token="test-token", + timeout_seconds=1, + ) + + assert body.read_sizes + assert all(0 < size <= 400 for size in body.read_sizes) + assert body.closed + + def test_documented_opener_lineage_references_published_commits() -> None: """Owner evidence must name the published commits that carry each repair.""" doctoring = Path( diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index a2da04ae25..06b66953ac 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -9,6 +9,7 @@ import tarfile import zipfile from pathlib import Path +from urllib.error import HTTPError import pytest @@ -811,6 +812,30 @@ def test_download_trusted_uv_archive_rejects_network_and_size_failures( materializer._download_trusted_uv_archive() +def test_download_trusted_uv_archive_closes_transformed_http_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The downloader owns and closes an HTTP response once it transforms the error.""" + error_body = io.BytesIO(b"private body") + provider_error = HTTPError( + materializer.TRUSTED_UV_ARCHIVE_URL, + 503, + "private body", + {}, + error_body, + ) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_a, **_k: (_ for _ in ()).throw(provider_error), + ) + + with pytest.raises(RuntimeError, match="download failed: HTTPError"): + materializer._download_trusted_uv_archive() + + assert error_body.closed + + def test_verified_uv_binary_accepts_exact_archive( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/test_materialize_base_rust_dependencies.py b/tests/test_materialize_base_rust_dependencies.py index d8b409d342..e7364897e6 100644 --- a/tests/test_materialize_base_rust_dependencies.py +++ b/tests/test_materialize_base_rust_dependencies.py @@ -489,6 +489,30 @@ def test_run_cargo_vendor_propagates_missing_binary(tmp_path: Path) -> None: materializer.materialize(repo, base_sha, tmp_path / "out") +def test_materialize_surfaces_cargo_vendor_timeout( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A stalled cargo process fails closed with its stable exception class.""" + repo = tmp_path / "repo" + _init_repo(repo) + _write_single_crate_workspace(repo, generate_lock=False) + base_sha = _commit_all(repo) + + monkeypatch.setattr( + materializer, + "_run_cargo_vendor", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + subprocess.TimeoutExpired(["cargo", "vendor"], 900) + ), + ) + + with pytest.raises( + RuntimeError, + match="could not run trusted cargo vendor for base manifest Cargo.lock: TimeoutExpired", + ): + materializer.materialize(repo, base_sha, tmp_path / "out") + + def test_materialize_surfaces_cargo_vendor_failure_detail( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_noema_document_review_context.py b/tests/test_noema_document_review_context.py index f380cafafb..874bf81b18 100644 --- a/tests/test_noema_document_review_context.py +++ b/tests/test_noema_document_review_context.py @@ -101,6 +101,8 @@ def test_hosted_reader_bundle_is_pinned_and_local(): assert package["dependencies"] == {"@rhwp/core": "0.7.7", "hwp-mcp": "0.3.0"} assert lock["packages"]["node_modules/hwp-mcp"]["version"] == "0.3.0" assert lock["packages"]["node_modules/@rhwp/core"]["version"] == "0.7.7" + assert lock["packages"]["node_modules/fast-uri"]["version"] == "3.1.8" + assert lock["packages"]["node_modules/ip-address"]["version"] == "10.7.2" assert "requirements-noema-document-ci-hashes.txt" in workflow assert "python3 -m pip install --quiet --require-hashes --no-deps" in workflow assert "requirements-noema-document-ci-hashes.txt" in quality_workflow diff --git a/tests/test_noema_preflight_capacity.py b/tests/test_noema_preflight_capacity.py index a6ed03db3c..d6beb753ce 100644 --- a/tests/test_noema_preflight_capacity.py +++ b/tests/test_noema_preflight_capacity.py @@ -218,6 +218,19 @@ def test_directory_report_path_keeps_plain_failure(tmp_path, monkeypatch): assert outputs["transport_capacity_unavailable"] == "false" +def test_symlinked_report_parent_is_rejected(tmp_path): + """A symlinked evidence directory cannot redirect the trusted report read.""" + real_parent = tmp_path / "real-parent" + real_parent.mkdir() + report = _all_429() + (real_parent / "report.json").write_text(json.dumps(report), encoding="utf-8") + symlink_parent = tmp_path / "report-parent" + symlink_parent.symlink_to(real_parent, target_is_directory=True) + + assert capacity.load_preflight_report(real_parent / "report.json") == report + assert capacity.load_preflight_report(symlink_parent / "report.json") is None + + def test_oversized_report_keeps_plain_failure(tmp_path, monkeypatch): """The classifier reads a bounded prefix and rejects anything larger.""" padded = _all_429() diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 5b3ef6c703..5d3fb66605 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1760,11 +1760,12 @@ def test_call_llm_http_400_is_transport_but_not_capacity(monkeypatch, capsys): """A non-transient 400 stays typed transport without authorizing re-dispatch.""" monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") + error_body = io.BytesIO(b"{}") class Opener: def open(self, request): raise noema.urllib.error.HTTPError( - request.full_url, 400, "Bad Request", {}, io.BytesIO(b"{}") + request.full_url, 400, "Bad Request", {}, error_body ) monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) @@ -1774,6 +1775,7 @@ def open(self, request): assert exc_info.value.capacity_unavailable is False assert exc_info.value.http_status == 400 + assert error_body.closed assert "outcome=provider_capacity_unavailable" not in capsys.readouterr().out @@ -1964,7 +1966,7 @@ def test_noema_redirect_handler_rejects_redirects(): handler = noema.NoRedirectHandler() request = noema.urllib.request.Request("https://llm.example.test/chat") - with pytest.raises(noema.urllib.error.HTTPError): + with pytest.raises(noema.urllib.error.HTTPError) as exc_info: handler.redirect_request( request, fp=None, @@ -1973,6 +1975,7 @@ def test_noema_redirect_handler_rejects_redirects(): headers={}, newurl="http://169.254.169.254/latest/meta-data/", ) + exc_info.value.close() def test_call_llm_rejects_control_character_scheme_evasion(monkeypatch): @@ -2879,6 +2882,14 @@ def test_fetch_file_content_at_ref_refuses_malformed_base64(monkeypatch): noema.fetch_file_content_at_ref("owner/repo", "docs/a.md", "deadbeef") +def test_fetch_file_content_at_ref_refuses_malformed_json(monkeypatch): + """A malformed GitHub API envelope fails closed before metadata inspection.""" + monkeypatch.setattr(noema, "run", lambda *args, **kwargs: "{not-json") + + with pytest.raises(RuntimeError, match="GitHub content response was malformed"): + noema.fetch_file_content_at_ref("owner/repo", "docs/a.md", "deadbeef") + + @pytest.mark.parametrize("payload,reason", [ ({"content": "", "encoding": "none", "size": 1048577}, "API omitted"), ({"content": "", "encoding": "base64", "size": 1}, "nonempty file"), diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 8a80d65461..a9124fbac6 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -2513,6 +2513,7 @@ def test_opencode_runs_merge_scheduler_after_review_without_repo_local_dispatch( assert "using %s token" in status_step assert "scripts/ci/opencode_dispatch_status.py" in status_step assert "COVERAGE_EVIDENCE_RESULT" in status_step + assert '--coverage-summary "${COVERAGE_EVIDENCE_SUMMARY:-}"' in status_step assert 'gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}"' in status_step assert 'gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}/reviews"' in status_step assert '[ "${OPENCODE_MODEL_POOL_OUTCOME:-}" != "success" ] &&' not in status_step @@ -3080,6 +3081,8 @@ def test_opencode_model_pool_failure_uses_only_existing_real_model_approval(): assert "no duplicate APPROVE review was posted" in workflow assert "opencode_existing_approval_gate.py" in workflow assert '--head "$HEAD_SHA"' in workflow + assert '--coverage-summary "$COVERAGE_EVIDENCE_SUMMARY"' in workflow + assert workflow.count('--coverage-summary "$COVERAGE_EVIDENCE_SUMMARY"') == 2 assert "--require-opencode-app" in workflow assert ( "same-head real-model OpenCode approval with passed adversarial evidence" diff --git a/tests/test_opencode_existing_approval_gate.py b/tests/test_opencode_existing_approval_gate.py index 9b91ee1f77..797817dab4 100644 --- a/tests/test_opencode_existing_approval_gate.py +++ b/tests/test_opencode_existing_approval_gate.py @@ -68,6 +68,7 @@ def trusted_adversarial_artifacts(tmp_path, monkeypatch): changed_files.chmod(0o600) monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) + monkeypatch.setenv("COVERAGE_EVIDENCE_SUMMARY", "- Result: PASS") monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) monkeypatch.setenv( "OPENCODE_ARTIFACT_MANIFEST_SHA256", @@ -133,6 +134,51 @@ def review(**overrides): return value +@pytest.mark.parametrize( + "summary", + ( + "## Coverage Decision\n\n- Result: NOT MEASURED\n", + "## Coverage Decision\n\n", + "- Result: PASS\n- Result: PASS\n", + "- Result: PASSING\n", + ), +) +def test_coverage_summary_rejects_every_non_unique_pass(summary): + """Reusable approval requires one exact passing coverage decision.""" + assert gate.coverage_summary_rejection_reason(summary) + + +def test_coverage_summary_accepts_one_exact_pass(): + """Diagnostic prose may surround the one authoritative PASS line.""" + assert ( + gate.coverage_summary_rejection_reason( + "## Coverage Decision\n\n- Result: PASS\n\n- Rust: measured\n" + ) + is None + ) + + +@pytest.mark.parametrize( + "summary", + ( + "", + "- Result: NOT MEASURED", + "prefix - Result: PASS", + "- Result: PASS (assumed)", + "- Result: PASS\n- Result: PASS", + "- Result: PASS\n- Result: NOT MEASURED", + ), +) +def test_coverage_decision_rejects_missing_ambiguous_or_incomplete_summary(summary): + """Existing approval reuse requires one exact PASS decision.""" + assert gate.coverage_decision_is_pass(summary) is False + + +def test_coverage_decision_accepts_one_exact_pass_line(): + """One exact PASS line is reusable coverage evidence.""" + assert gate.coverage_decision_is_pass("detail\n- Result: PASS\n") is True + + @pytest.mark.parametrize("payload", [[review()], [[review()]]]) def test_flatten_reviews_and_accept_real_model_approval(payload): reviews = gate.flatten_reviews(payload) @@ -300,10 +346,9 @@ def test_has_reusable_real_model_approval_logs_rejected_candidates(): log = io.StringIO() assert not gate.has_reusable_real_model_approval( [ - review(state="COMMENTED"), + fallback, review(commit_id="b" * 40), review(user={"login": "unknown"}), - fallback, ], HEAD, log=log, @@ -342,6 +387,20 @@ def test_opencode_app_only_mode_accepts_app_approval(): assert "author=opencode-agent[bot]" in log.getvalue() +def test_newer_same_head_changes_requested_revokes_reusable_approval(): + """The latest exact-head OpenCode decision supersedes historical approval.""" + log = io.StringIO() + older_approval = review(id=7) + newer_rejection = review(id=8, state="CHANGES_REQUESTED") + + assert not gate.has_reusable_real_model_approval( + [older_approval, newer_rejection], + HEAD, + log=log, + ) + assert "latest same-head review" in log.getvalue() + + def test_adversarial_validation_rejects_circular_or_unanchored_evidence(): weak = { "status": "passed", @@ -413,30 +472,41 @@ def test_adversarial_validation_rejects_forged_traversal_receipt(): def test_parse_args_and_main(monkeypatch, capsys): - args = gate.parse_args(["--head", HEAD]) + coverage_args = ["--coverage-summary", "- Result: PASS"] + args = gate.parse_args(["--head", HEAD, *coverage_args]) assert args.head == HEAD assert not args.require_opencode_app - strict_args = gate.parse_args(["--head", HEAD, "--require-opencode-app"]) + strict_args = gate.parse_args( + ["--head", HEAD, *coverage_args, "--require-opencode-app"] + ) assert strict_args.require_opencode_app monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps([[review()]]))) - assert gate.main(["--head", HEAD]) == 0 + assert gate.main(["--head", HEAD, *coverage_args]) == 0 + + monkeypatch.setattr(sys, "stdin", io.StringIO("[]")) + assert gate.main( + ["--head", HEAD, "--coverage-summary", "- Result: NOT MEASURED"] + ) == 1 + assert "coverage decision is not PASS" in capsys.readouterr().err monkeypatch.setattr(sys, "stdin", io.StringIO("not-json")) - assert gate.main(["--head", HEAD]) == 2 + assert gate.main(["--head", HEAD, *coverage_args]) == 2 assert "could not parse reviews" in capsys.readouterr().err monkeypatch.setattr(sys, "stdin", io.StringIO("[]")) - assert gate.main(["--head", "short"]) == 2 + assert gate.main(["--head", "short", *coverage_args]) == 2 assert "40-character" in capsys.readouterr().err monkeypatch.setattr(sys, "stdin", io.StringIO("[]")) - assert gate.main(["--head", HEAD]) == 1 + assert gate.main(["--head", HEAD, *coverage_args]) == 1 monkeypatch.setattr( sys, "stdin", io.StringIO(json.dumps([[review(user={"login": "github-actions[bot]"})]])), ) - assert gate.main(["--head", HEAD, "--require-opencode-app"]) == 1 + assert gate.main( + ["--head", HEAD, *coverage_args, "--require-opencode-app"] + ) == 1 diff --git a/tests/test_opencode_queue_priority.py b/tests/test_opencode_queue_priority.py index b1d5aa5c17..19d878a5d4 100644 --- a/tests/test_opencode_queue_priority.py +++ b/tests/test_opencode_queue_priority.py @@ -2,8 +2,15 @@ from __future__ import annotations +import json +import runpy +import subprocess +import sys from datetime import datetime, timezone +import pytest + +import scripts.ci.opencode_queue_priority as queue_priority from scripts.ci.opencode_queue_priority import ( PrState, QueuedRun, @@ -63,3 +70,142 @@ def test_metrics_surface_deferred_backlog_and_priority_position() -> None: assert m["deferred"] == 2 assert m["oldest_deferred_hours"] == 9.0 assert m["priority_positions"] == {"o/a#1": 2} + +def test_fetch_queued_keeps_only_bound_pull_request_titles(monkeypatch: pytest.MonkeyPatch) -> None: + """Malformed display titles cannot enter the cancellation plan.""" + rows = [ + [41, "2026-09-30T10:00:00Z", "ContextualWisdomLab/naruon#1829@abcdef0"], + [42, "2026-09-30T10:01:00Z", "unbound dispatch"], + ] + monkeypatch.setattr(queue_priority, "_gh", lambda *args, **kwargs: "\n".join(map(json.dumps, rows))) + + queued = queue_priority.fetch_queued() + + assert queued == [QueuedRun( + 41, + datetime(2026, 9, 30, 10, 0, tzinfo=timezone.utc), + "ContextualWisdomLab/naruon", + 1829, + "abcdef0", + )] + + +def test_fetch_live_requires_a_trusted_label_actor_and_caches_permission( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A label is priority only when its actor has repository write authority.""" + graphql = { + "data": { + "p0": {"pullRequest": { + "state": "OPEN", + "headRefOid": HEAD, + "labels": {"nodes": [{"name": "review-priority"}]}, + "timelineItems": {"nodes": [ + {"label": {"name": "other"}, "actor": {"login": "maintainer"}}, + {"label": {"name": "review-priority"}, "actor": None}, + {"label": {"name": "review-priority"}, "actor": {"login": "maintainer"}}, + {"label": {"name": "review-priority"}, "actor": {"login": "maintainer"}}, + ]}, + }}, + "p1": {"pullRequest": { + "state": "OPEN", + "headRefOid": NEW, + "labels": {"nodes": [{"name": "review-priority"}]}, + "timelineItems": {"nodes": [ + {"label": {"name": "review-priority"}, "actor": {"login": "outsider"}}, + ]}, + }}, + "p2": {"pullRequest": None}, + "p3": {"pullRequest": { + "state": "OPEN", + "headRefOid": HEAD, + "labels": {"nodes": [{"name": "triage"}]}, + "timelineItems": {"nodes": []}, + }}, + } + } + permission_reads: list[str] = [] + + def fake_gh(*args: str, stdin: str | None = None) -> str: + del stdin + if args[:2] == ("api", "graphql"): + return json.dumps(graphql) + permission_reads.append(args[1]) + if args[1].endswith("/outsider/permission"): + raise subprocess.CalledProcessError(1, args) + return "write\n" + + monkeypatch.setattr(queue_priority, "_gh", fake_gh) + states = queue_priority.fetch_live( + {("o/a", 1), ("o/b", 2), ("o/c", 3), ("o/d", 4)}, + "review-priority", + ) + + assert states[("o/a", 1)] == PrState("OPEN", HEAD, priority=True) + assert states[("o/b", 2)] == PrState("OPEN", NEW, priority=False) + assert ("o/c", 3) not in states + assert states[("o/d", 4)] == PrState("OPEN", HEAD, priority=False) + assert permission_reads == [ + "repos/o/a/collaborators/maintainer/permission", + "repos/o/b/collaborators/outsider/permission", + ] + + +def test_main_records_then_cancels_only_still_queued_targets( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + """Apply mode records the exact plan before cancelling still-queued runs.""" + runs = [run(1, "o/a", 1), run(2, "o/b", 2), run(3, "o/c", 3)] + live = { + ("o/a", 1): PrState("OPEN", HEAD, priority=True), + ("o/b", 2): PrState("OPEN", HEAD, priority=False), + ("o/c", 3): PrState("CLOSED", HEAD, priority=False), + } + monkeypatch.setattr(queue_priority, "fetch_queued", lambda: runs) + monkeypatch.setattr(queue_priority, "fetch_live", lambda keys, label: live) + calls: list[tuple[tuple[str, ...], str | None]] = [] + + def fake_gh(*args: str, stdin: str | None = None) -> str: + calls.append((args, stdin)) + if args[-2:] == ("--jq", ".status"): + return "queued\n" if "/3" in args[1] else "completed\n" + return "" + + monkeypatch.setattr(queue_priority, "_gh", fake_gh) + assert queue_priority.main([ + "--include-current", "--post-issue", "o/control#9", "--apply", + ]) == 0 + + output = capsys.readouterr().out + assert "o/c\t3" in output and "stale" in output + assert "o/b\t2" in output and "deferred" in output + assert calls[0][0][:3] == ("issue", "comment", "9") + assert calls[0][1] is not None and "Cancel list (TSV)" in calls[0][1] + assert any(call[0][:3] == ("api", "-X", "POST") for call in calls) + + +def test_main_refuses_apply_without_an_audit_issue() -> None: + """Cancellation cannot run without first naming its durable audit issue.""" + with pytest.raises(SystemExit, match="2"): + queue_priority.main(["--apply"]) + + +def test_subprocess_boundary_and_script_entrypoint(monkeypatch: pytest.MonkeyPatch) -> None: + """The CLI boundary preserves stdin and the module entrypoint exits cleanly.""" + recorded: dict[str, object] = {} + + def fake_run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + recorded.update(command=command, kwargs=kwargs) + stdout = "[]\n" if command == ["gh", "api", "example"] else "" + return subprocess.CompletedProcess(command, 0, stdout=stdout) + + monkeypatch.setattr(subprocess, "run", fake_run) + assert queue_priority._gh("api", "example", stdin="payload") == "[]\n" + assert recorded["command"] == ["gh", "api", "example"] + assert recorded["kwargs"] == { + "input": "payload", "capture_output": True, "text": True, "check": True, + } + + monkeypatch.setattr(sys, "argv", [str(queue_priority.__file__)]) + with pytest.raises(SystemExit, match="0"): + runpy.run_path(str(queue_priority.__file__), run_name="__main__") diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 2f968654bb..3c7abbff78 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -507,6 +507,7 @@ def test_dispatch_status_requires_live_current_head_approval_and_coverage( decision = dispatch_status.decide_status( model_outcome="success", coverage_result="success", + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": head}}, reviews=[review], @@ -529,6 +530,7 @@ def test_dispatch_status_latest_current_head_decision_is_authoritative( decision = dispatch_status.decide_status( model_outcome="success", coverage_result="success", + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": head}}, reviews=reviews, @@ -546,6 +548,7 @@ def test_dispatch_status_reuses_verified_approval_after_current_pool_exhaustion( decision = dispatch_status.decide_status( model_outcome="exhausted", coverage_result="success", + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": head}}, reviews=[approval_review(head)], @@ -579,6 +582,7 @@ def test_dispatch_status_fails_closed_without_validated_approval( decision = dispatch_status.decide_status( model_outcome=model_outcome, coverage_result=coverage_result, + coverage_summary="- Result: PASS", expected_head=head, pull_request={"head": {"sha": observed_head}}, reviews=[approval_review(head, **review_overrides)], @@ -588,6 +592,55 @@ def test_dispatch_status_fails_closed_without_validated_approval( assert decision["description"] +@pytest.mark.parametrize( + "coverage_summary", + ("- Result: NOT MEASURED", "", "- Result: PASS\n- Result: PASS"), +) +def test_dispatch_status_rejects_nonpassing_coverage_summary( + coverage_summary: str, + trusted_dispatch_status_artifacts: None, +) -> None: + """A successful advisory job cannot replace a unique PASS decision.""" + head = "a" * 40 + decision = dispatch_status.decide_status( + model_outcome="success", + coverage_result="success", + coverage_summary=coverage_summary, + expected_head=head, + pull_request={"head": {"sha": head}}, + reviews=[approval_review(head)], + ) + + assert decision["state"] == "failure" + assert "coverage decision" in decision["description"].lower() + + +@pytest.mark.parametrize( + "coverage_summary", + ( + "", + "- Result: NOT MEASURED", + "- Result: PASS\n- Result: PASS", + "- Result: PASS\n- Result: FAIL", + ), +) +def test_dispatch_status_rejects_non_pass_coverage_decisions( + coverage_summary: str, + trusted_dispatch_status_artifacts: None, +) -> None: + """Job success cannot publish success without one exact PASS decision.""" + head = "a" * 40 + decision = dispatch_status.decide_status( + model_outcome="exhausted", + coverage_result="success", + coverage_summary=coverage_summary, + expected_head=head, + pull_request={"head": {"sha": head}}, + reviews=[approval_review(head)], + ) + assert decision["state"] == "failure" + + def test_dispatch_status_cli_and_evidence_shape_validation( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, @@ -605,6 +658,8 @@ def test_dispatch_status_cli_and_evidence_shape_validation( "success", "--coverage-result", "success", + "--coverage-summary", + "- Result: PASS", "--expected-head", head, "--pull-request-file", diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index aba34f6346..93fbc58c15 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -766,12 +766,16 @@ def test_declared_prefix_for_path_matches_by_path_segment() -> None: def test_github_open_json_maps_not_found_to_artifact_declaration_error(monkeypatch: pytest.MonkeyPatch) -> None: """A 404 from the GitHub API is distinguished from every other transport failure.""" + error_body = BytesIO() monkeypatch.setattr( policy.github_opener, "open", - lambda _request, timeout: (_ for _ in ()).throw(HTTPError("x", 404, "not found", {}, BytesIO())), + lambda _request, timeout: (_ for _ in ()).throw( + HTTPError("x", 404, "not found", {}, error_body) + ), ) with pytest.raises(policy.ArtifactDeclarationNotFoundError): policy._github_open_json("https://api.github.com/repos/a/b", "token") + assert error_body.closed def test_png_structure_validation_fails_closed_on_malformed_chunks() -> None: @@ -1062,6 +1066,7 @@ def test_changed_file_pagination_bound_is_provably_unreachable() -> None: ({"type": "file", "encoding": "none", "size": 1}, "no inline content"), ({"type": "file", "encoding": "none", "size": "not-an-int"}, "malformed size"), ({"type": "file", "encoding": "utf-8", "size": 1, "content": "x"}, "invalid encoding"), + ({"type": "file", "encoding": "base64", "size": 1, "content": 1}, "malformed size"), ({"type": "file", "encoding": "base64", "size": 1, "content": "!"}, "invalid base64"), ({"type": "file", "encoding": "base64", "size": 2, "content": base64.b64encode(b"x").decode()}, "size mismatch"), ({"type": "file", "encoding": "base64", "size": 1, "content": base64.b64encode(b"\xff").decode()}, "not valid UTF-8"), @@ -1247,6 +1252,23 @@ def open_response(request: object, timeout: int) -> FakeResponse: policy._github_open_raw_bytes(url, "token", 3) +@pytest.mark.parametrize("exc", [URLError("dns"), TimeoutError(), HTTPError("x", 500, "bad", {}, BytesIO())]) +def test_github_open_raw_bytes_sanitizes_transport_failures( + monkeypatch: pytest.MonkeyPatch, exc: Exception, +) -> None: + """Raw-blob failures preserve only their stable class, never response text.""" + + def fail(_request: object, timeout: int) -> object: + assert timeout == 30 + raise exc + + monkeypatch.setattr(policy.github_opener, "open", fail) + url = "https://api.github.com/repos/a/b/git/blobs/" + "a" * 40 + with pytest.raises(policy.PolicyError) as raised: + policy._github_open_raw_bytes(url, "token", 4) + assert str(raised.value) == f"GitHub raw blob request failed: {type(exc).__name__}" + + @pytest.mark.parametrize( "url", [ @@ -1268,8 +1290,9 @@ def test_github_open_json_rejects_nonapproved_origins(url: str) -> None: def test_github_opener_never_constructs_redirect_requests() -> None: """The policy opener refuses redirects rather than changing API origins.""" - with pytest.raises(HTTPError): + with pytest.raises(HTTPError) as exc_info: policy.NoRedirectHandler().redirect_request(policy.Request("https://example.com"), None, 302, "Found", {}, "https://evil.example") + exc_info.value.close() def test_annotation_escapes_workflow_command_fields() -> None: diff --git a/tests/test_place_maturin_extension.py b/tests/test_place_maturin_extension.py index a1d6f7757a..2b7460e440 100644 --- a/tests/test_place_maturin_extension.py +++ b/tests/test_place_maturin_extension.py @@ -2,6 +2,8 @@ from __future__ import annotations +import runpy +import sys import zipfile from pathlib import Path @@ -65,6 +67,17 @@ def test_python_source_outside_the_project_is_rejected(tmp_path: Path) -> None: placer.place(wheel, project) +def test_non_string_python_source_is_rejected(tmp_path: Path) -> None: + """A malformed maturin source root cannot be coerced into a filesystem path.""" + + project = _project(tmp_path, "python") + (project / "pyproject.toml").write_text( + "[tool.maturin]\npython-source = 1\n", encoding="utf-8" + ) + with pytest.raises(ValueError, match="must be a string"): + placer.place(tmp_path / "unused.whl", project) + + def test_traversing_wheel_member_is_rejected(tmp_path: Path) -> None: project = _project(tmp_path, "python") wheel = _wheel(tmp_path, {f"../../{_SO}": b"ELF"}) @@ -89,3 +102,34 @@ def test_offline_maturin_build_places_the_extension_for_pytest() -> None: build = workflow.split("build_maturin_extension_if_needed() {", 1)[1].split("\n }", 1)[0] assert 'python3 "$2" "$dist_dir"/*.whl .' in build assert '"${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py"' in build + + +def test_cli_rejects_wrong_arity_and_reports_placement_failure( + tmp_path: Path, capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI distinguishes invocation errors from invalid build artifacts.""" + + assert placer.main([]) == 2 + assert "usage:" in capsys.readouterr().err + + project = _project(tmp_path, "python") + assert placer.main([str(tmp_path / "missing.whl"), str(project)]) == 1 + assert "Could not place" in capsys.readouterr().err + + +def test_script_entrypoint_places_and_reports_extension( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + """The executable script exits successfully after placing a real wheel member.""" + + project = _project(tmp_path, "python") + wheel = _wheel(tmp_path, {f"pkg/{_SO}": b"ELF"}) + script = Path(placer.__file__) + monkeypatch.setattr(sys, "argv", [str(script), str(wheel), str(project)]) + + with pytest.raises(SystemExit) as exited: + runpy.run_path(str(script), run_name="__main__") + + assert exited.value.code == 0 + assert "Placed built extension" in capsys.readouterr().out + assert (project / "python" / "pkg" / _SO).read_bytes() == b"ELF" diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 1bfc13292e..53112ca69e 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "09bbf8181a443f7a5630ec91ca958440c5dcfc68" +REVIEW_DISPATCH_BLOB_SHA = "4aed28460325c40ee479d77e645e6140f548570c" def _workflow_text(path: Path) -> str: diff --git a/tests/test_release_dependency_fanout_plan.py b/tests/test_release_dependency_fanout_plan.py index af996a0428..77283ebce0 100644 --- a/tests/test_release_dependency_fanout_plan.py +++ b/tests/test_release_dependency_fanout_plan.py @@ -96,6 +96,56 @@ def test_fanout_adds_distinct_exact_archive_fixtures(tmp_path: Path) -> None: gate.strix_fanout_plan(capture, report_path, CONTROL, 42, 2, archive_report) +def test_archive_rows_cannot_expand_a_bounded_base_plan_past_the_limit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """A bounded licence plan still fails if runtime rows push the final matrix over its cap.""" + capture, report_path = _allowed(tmp_path) + dependency_count = len(json.loads(report_path.read_text())["dependencies"]) + monkeypatch.setattr(gate, "STRIX_PLAN_LIMIT", dependency_count) + + def archive_row(ecosystem: str, name: str, digest_character: str) -> dict: + source_digest = digest_character * 64 + package_key = f"{ecosystem}/{name}@1" + key = f"{package_key}/sha256/{source_digest}" + fixture = gate.build_fixture( + gate.Dependency(ecosystem, name, "1"), + { + "source_sha256": source_digest, + "archive_members": [], + "install_hook_sources": {}, + "parsed_inputs": [], + "native_libraries": [], + "known_vulnerabilities": [], + }, + ) + fixture["id"] = key + return { + "key": key, + "package_key": package_key, + "name": name, + "version": "1", + "source_sha256": source_digest, + "license": "MIT", + "fixture": fixture, + "fixture_sha256": gate.fixture_digest(fixture), + } + + runtime = archive_row("pypi", "runtime-package", "1") + build = archive_row("pypi", "build-package", "2") + tool = archive_row("github-release", "maturin", "3") + archive_report = tmp_path / "archive-report.json" + archive_report.write_text(json.dumps({ + "schema": "cwl.release-runtime-archive-licenses/3", + "archives": [runtime], + "build_packages": [build], + "build_tools": [tool], + })) + + with pytest.raises(gate.GateError, match="dependency plan exceeds"): + gate.strix_fanout_plan(capture, report_path, CONTROL, 42, 2, archive_report) + + def test_plan_refuses_denied_missing_extra_and_duplicate_scope(tmp_path: Path) -> None: mutators = { "denied": lambda capture, report: report.__setitem__("result", "FAIL"), diff --git a/tests/test_release_dependency_gate.py b/tests/test_release_dependency_gate.py index 62054fff5c..1700d374c2 100644 --- a/tests/test_release_dependency_gate.py +++ b/tests/test_release_dependency_gate.py @@ -11,6 +11,7 @@ import hashlib import io import json +import subprocess import tarfile import zipfile from pathlib import Path @@ -738,6 +739,14 @@ def test_benign_cmdclass_without_lifecycle_override_passes(tmp_path: Path) -> No assert gate.gate(capture).failures == [] +def test_rust_hook_without_process_or_network_namespace_is_benign() -> None: + """Ordinary Rust build code stays allowed while network namespaces are detected.""" + assert gate.detect_install_hooks({"build.rs": "fn main() { println!(\"cargo:rerun\"); }"}) == [] + assert gate.detect_install_hooks({"build.rs": "use std::net; fn main() {}"}) == [ + "build.rs references a process/network namespace" + ] + + # --------------------------------------------------------------------------- # RED: Strix structured evidence # --------------------------------------------------------------------------- @@ -899,6 +908,31 @@ def test_selection_capture_reads_commit_and_rejects_duplicates(tmp_path: Path) - gate._load_selections(capture) +def test_capture_license_selection_cli_publishes_the_exact_commit_blob(tmp_path: Path) -> None: + """The CLI command delegates to the same exact-commit capture boundary.""" + source = tmp_path / "source" + source.mkdir() + path = source / "docs/release-license-selections.json" + path.parent.mkdir() + path.write_text("[]") + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "-qm", "selection"], check=True) + source_sha = subprocess.check_output( + ["git", "-C", str(source), "rev-parse", "HEAD"], text=True, + ).strip() + capture = tmp_path / "capture" + + assert gate.main([ + "capture-license-selections", + "--source", str(source), + "--source-sha", source_sha, + "--capture", str(capture), + ]) == 0 + assert (capture / "license-selections.json").read_text() == "[]" + + def test_selection_loader_refuses_dangling_link_and_nonstring_choice(tmp_path: Path) -> None: path = tmp_path / "license-selections.json" path.symlink_to(tmp_path / "missing") @@ -938,6 +972,58 @@ def metadata_only(command, **kwargs): assert not capture.exists() +def test_selection_capture_refuses_unbound_sources_and_payload_size_races( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """Selection capture accepts only exact regular blobs whose bytes remain bounded.""" + source = tmp_path / "source" + source.mkdir() + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run([ + "git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "--allow-empty", "-qm", "empty", + ], check=True) + sha = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() + capture = tmp_path / "capture" + + with pytest.raises(gate.GateError, match="exact commit SHA"): + gate.capture_license_selections(source, "not-a-sha", capture) + gate.capture_license_selections(source, sha, capture) + assert not capture.exists() + + selection = source / "docs/release-license-selections.json" + selection.parent.mkdir() + selection.symlink_to(source / "missing") + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "-qm", "link"], check=True) + link_sha = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() + with pytest.raises(gate.GateError, match="regular Git blob"): + gate.capture_license_selections(source, link_sha, capture) + + selection.unlink() + selection.write_text("[]") + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "-qm", "regular"], check=True) + regular_sha = subprocess.check_output( + ["git", "-C", str(source), "rev-parse", "HEAD"], text=True, + ).strip() + + original = gate.subprocess.check_output + + def raced_blob(command, **kwargs): + if command[1:3] == ["cat-file", "-s"]: + return "1\n" if kwargs.get("text") else b"1\n" + if command[1:3] == ["cat-file", "blob"]: + return b"x" * (gate._MAX_METADATA_BYTES + 1) + return original(command, **kwargs) + + monkeypatch.setattr(gate.subprocess, "check_output", raced_blob) + with pytest.raises(gate.GateError, match="exceeds bounded size"): + gate.capture_license_selections(source, regular_sha, capture) + + @pytest.mark.parametrize("expression", ["MIT/Apache-2.0", "Apache-2.0/MIT", "Apache-2.0 / MIT"]) def test_cargo_legacy_pair_keeps_choice_and_text_checks(expression: str) -> None: evidence = _cargo_evidence(license_expression=expression) @@ -1034,9 +1120,99 @@ def test_missing_full_text_is_independent_of_dual_license_choice(selection) -> N assert decision.allowed == (selection is not None) -@pytest.mark.parametrize("mutation", [None, "missing_source", "wrong_sha", "foreign_path", +@pytest.mark.parametrize( + ("mutation_name", "expected_message"), + [ + ("nonexact", "exact release commit"), + ("oversized", "blob exceeds bounded size"), + ("not-list", "must be a JSON array"), + ("missing-choice", "lacks one explicit notice selection"), + ("separator", "grant separator differs"), + ("grant", "grant bytes differ"), + ], +) +def test_reviewed_source_notice_refuses_unbound_git_evidence( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mutation_name: str, + expected_message: str, +) -> None: + """Reviewed supplemental grants remain exact-SHA, bounded, and byte separated.""" + subject = "cargo/example@1.0.0" + archive_sha = "a" * 64 + source_sha = "b" * 40 + source = tmp_path / "source" + source.mkdir() + first_grant = b"MIT" + second_grant = b"BSD" + content = first_grant + b"\n\n" + second_grant + if mutation_name == "separator": + content = first_grant + b"xx" + second_grant + elif mutation_name == "grant": + content = b"BAD\n\n" + second_grant + notice_digest = hashlib.sha256(content).hexdigest() + grants = ( + ("LICENSE-MIT", 3, hashlib.sha256(first_grant).hexdigest()), + ("LICENSE-BSD", 3, hashlib.sha256(second_grant).hexdigest()), + ) + monkeypatch.setitem( + gate._REVIEWED_SOURCE_NOTICES, + subject, + (archive_sha, "owner/repo", "c" * 40, notice_digest, {"MIT"}, grants), + ) + upstream = [ + { + "url": f"https://raw.githubusercontent.com/owner/repo/{'c' * 40}/{name}", + "sha256": digest, + } + for name, _, digest in grants + ] + choice = { + "ecosystem": "cargo", + "name": "example", + "version": "1.0.0", + "chosen": "MIT", + "rationale": "Reviewed both immutable grant bytes.", + "archive_sha256": archive_sha, + "upstream_licenses": upstream, + "bundled_notice": { + "path": "python/fast_mlsirm/_licenses/example.txt", + "sha256": notice_digest, + }, + } + choices = [] if mutation_name == "missing-choice" else [choice] + choices_payload = b"{}" if mutation_name == "not-list" else json.dumps(choices).encode() + + def git_blob(command, **kwargs): + operation = command[3] + if operation == "ls-tree": + path = command[-1] + oid = "selection-oid" if path == "docs/release-license-selections.json" else "notice-oid" + return f"100644 blob {oid}\t{path}\n" + if operation == "cat-file" and command[4] == "-s": + size = gate._MAX_METADATA_BYTES + 1 if mutation_name == "oversized" else 1 + return f"{size}\n" if kwargs.get("text") else f"{size}\n".encode() + if operation == "cat-file" and command[4] == "blob": + return choices_payload if command[5] == "selection-oid" else content + raise AssertionError(command) + + monkeypatch.setattr(gate.subprocess, "check_output", git_blob) + selected_sha = "short" if mutation_name == "nonexact" else source_sha + with pytest.raises(gate.GateError, match=expected_message): + gate._source_license_notice( + source, + selected_sha, + subject, + {"ecosystem": "cargo", "source_sha256": archive_sha}, + {"chosen": "MIT", "rationale": "Reviewed both immutable grant bytes."}, + ) + + +@pytest.mark.parametrize("mutation", [None, "missing_source", "wrong_sha", "nonexact_sha", + "foreign_workspace", "workspace_version", "foreign_path", "changed_manifest", "changed_lock", "captured_lock", - "symlink", "identity", "missing_dev"]) + "symlink", "identity", "missing_dev", "matching_dev_lock", + "changed_dev_lock"]) def test_nested_cargo_workspace_requires_immutable_release_source(tmp_path, mutation): import subprocess @@ -1051,8 +1227,14 @@ def test_nested_cargo_workspace_requires_immutable_release_source(tmp_path, muta lock = capture / "cargo/Cargo.lock" lock.write_text(lock.read_text() + '\n[[package]]\nname = "local-core"\nversion = "1.0.0"\n') (wheel / "Cargo.lock").write_bytes(lock.read_bytes()) - if mutation == "missing_dev": + if mutation in {"missing_dev", "matching_dev_lock", "changed_dev_lock"}: (source / "Cargo.lock").write_bytes(lock.read_bytes() + b"# separate development lock\n") + if mutation in {"matching_dev_lock", "changed_dev_lock"}: + dev = capture / "cargo-dev" + dev.mkdir() + dev_lock = ((source / "Cargo.lock").read_bytes() if mutation == "matching_dev_lock" + else b"different development lock\n") + (dev / "Cargo.lock").write_bytes(dev_lock) def git(*args): return subprocess.check_output(["git", "-C", str(source), *args], text=True).strip() git("init", "-q") @@ -1070,6 +1252,20 @@ def git(*args): metadata["resolve"]["nodes"].append({"id": "local-id", "deps": [{"pkg": "greencrate-id"}]}) if mutation == "wrong_sha": sha = "a" * 40 + elif mutation == "nonexact_sha": + sha = "short" + elif mutation == "foreign_workspace": + metadata["workspace_root"] = str(tmp_path / "foreign") + elif mutation == "workspace_version": + core.write_text('[package]\nname = "local-core"\nversion.workspace = true\n') + (wheel / "Cargo.toml").write_text( + '[package]\nname = "fast-mlsirm"\nversion = "0.11.5"\n' + '[workspace]\nmembers = ["../core"]\n[workspace.package]\nversion = "1.0.0"\n' + ) + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Fixture", "-c", + "user.email=fixture@example.invalid", "commit", "-qm", "workspace version"], check=True) + sha = git("rev-parse", "HEAD") elif mutation == "foreign_path": metadata["packages"][-1]["manifest_path"] = str(tmp_path / "foreign/Cargo.toml") elif mutation == "changed_manifest": @@ -1087,14 +1283,22 @@ def git(*args): elif mutation == "identity": metadata["packages"][-1]["name"] = "foreign-core" _write(path, metadata) - if mutation == "missing_dev": + if mutation in {"missing_dev", "matching_dev_lock", "changed_dev_lock"}: release = json.loads((capture / "release.json").read_text()) release["source_sha"] = sha _write(capture / "release.json", release) - with pytest.raises(gate.GateError, match="development Cargo graph is missing"): + if mutation == "matching_dev_lock": + dev_metadata = json.loads((capture / "cargo/metadata.json").read_text()) + dev_metadata["workspace_root"] = str(source) + _write(capture / "cargo-dev/metadata.json", dev_metadata) + assert gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source).passed + return + expected = ("development Cargo graph is missing" if mutation == "missing_dev" + else "development Cargo lock differs from source root") + with pytest.raises(gate.GateError, match=expected): gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source) return - if mutation is not None: + if mutation not in {None, "workspace_version"}: with pytest.raises(gate.GateError, match=gate.CAPTURE_INCOMPLETE): gate._enumerate_cargo(capture, source_root=None if mutation == "missing_source" else source, source_sha=sha) diff --git a/tests/test_release_dependency_gate_capture_and_seal.py b/tests/test_release_dependency_gate_capture_and_seal.py index f3f5753d81..e80aa4c4eb 100644 --- a/tests/test_release_dependency_gate_capture_and_seal.py +++ b/tests/test_release_dependency_gate_capture_and_seal.py @@ -9,10 +9,8 @@ from __future__ import annotations -import builtins import hashlib import json -import runpy from pathlib import Path from typing import Any @@ -37,19 +35,25 @@ def test_gate_import_and_toml_parsing_without_stdlib_tomllib(monkeypatch): - """Exercise the complete module import with the Python 3.10 TOML parser.""" - tomli = pytest.importorskip("tomli") - original = builtins.__import__ + """Select the declared Python 3.10 TOML parser when stdlib lacks one.""" + class BackportParser: + @staticmethod + def loads(source_text): + assert source_text == '[package]\nlicense="MIT"' + return {"package": {"license": "MIT"}} + + tomli = BackportParser() + original = gate.importlib.import_module def import_without_tomllib(name, *args, **kwargs): if name == "tomllib": raise ModuleNotFoundError("No module named 'tomllib'", name="tomllib") - return original(name, *args, **kwargs) + return tomli if name == "tomli" else original(name, *args, **kwargs) - monkeypatch.setattr(builtins, "__import__", import_without_tomllib) - loaded = runpy.run_path(gate.__file__) - assert loaded["tomllib"] is tomli - assert loaded["tomllib"].loads('[package]\nlicense="MIT"')['package']['license'] == "MIT" + monkeypatch.setattr(gate.importlib, "import_module", import_without_tomllib) + parser = gate._import_toml_parser() + assert parser is tomli + assert parser.loads('[package]\nlicense="MIT"')['package']['license'] == "MIT" def test_binder_resolves_next_to_this_script() -> None: diff --git a/tests/test_repository_metadata_live_verification.py b/tests/test_repository_metadata_live_verification.py index 6ae83d8c47..4fecd171e8 100644 --- a/tests/test_repository_metadata_live_verification.py +++ b/tests/test_repository_metadata_live_verification.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import io import importlib.util import json from pathlib import Path @@ -145,10 +146,11 @@ def build_ok(handler): assert len(handlers) == 1 assert isinstance(handlers[0], RECONCILER._NoPagesRedirects) from urllib.error import HTTPError - with pytest.raises(HTTPError): + with pytest.raises(HTTPError) as exc_info: handlers[0].redirect_request( RECONCILER.Request("https://example.com"), None, 302, "redirect", {}, "http://127.0.0.1/" ) + exc_info.value.close() with pytest.raises(RuntimeError, match="not built"): RECONCILER._pages_publication_ready("Repo", {**ready, "status": "building"}) @@ -179,6 +181,28 @@ def build_ok(handler): RECONCILER._pages_publication_ready("Repo", ready) +def test_pages_publication_ready_closes_mapped_http_error(monkeypatch) -> None: + """Pages verification closes the response when it maps an HTTP failure.""" + ready = { + "status": "built", + "html_url": "https://contextualwisdomlab.github.io/Repo/", + } + error_body = io.BytesIO(b"private body") + provider_error = RECONCILER.HTTPError( + ready["html_url"], 503, "private body", {}, error_body + ) + monkeypatch.setattr( + RECONCILER, + "build_opener", + lambda *args: FakeOpener(error=provider_error), + ) + + with pytest.raises(RuntimeError, match="not reachable"): + RECONCILER._pages_publication_ready("Repo", ready) + + assert error_body.closed + + def test_verify_repository_accepts_converged_disabled_and_enabled_pages( monkeypatch, ) -> None: diff --git a/tests/test_repository_metadata_workflow.py b/tests/test_repository_metadata_workflow.py index 7b41a667d9..52251f751b 100644 --- a/tests/test_repository_metadata_workflow.py +++ b/tests/test_repository_metadata_workflow.py @@ -16,3 +16,14 @@ def test_metadata_apply_uses_dedicated_least_privilege_credential() -> None: assert "secrets.PR_REVIEW_MERGE_TOKEN" not in apply_source assert "Require dedicated repository settings credential" in apply_source assert 'test -n "${GH_TOKEN}"' in apply_source + + +def test_metadata_validation_checks_out_published_evidence_ancestry() -> None: + """The full-suite lineage guard must receive every published ancestor object.""" + source = WORKFLOW.read_text(encoding="utf-8") + validate_source = source.split("jobs:\n validate:", 1)[1].split("\n apply:", 1)[0] + checkout_source = validate_source.split("- name: Check out exact revision", 1)[1].split( + "- name: Verify exact revision", 1 + )[0] + + assert "fetch-depth: 0" in checkout_source diff --git a/tests/test_resolve_base_rust_toolchain.py b/tests/test_resolve_base_rust_toolchain.py index db47136c80..26e248a127 100644 --- a/tests/test_resolve_base_rust_toolchain.py +++ b/tests/test_resolve_base_rust_toolchain.py @@ -2,7 +2,9 @@ from __future__ import annotations +import runpy import subprocess +import sys from pathlib import Path import pytest @@ -81,3 +83,37 @@ def test_invalid_base_sha_is_rejected(tmp_path: Path) -> None: def test_central_release_is_itself_exact() -> None: assert resolver.EXACT_RELEASE_RE.fullmatch(resolver.CENTRAL_RUST_TOOLCHAIN) + + +def test_cli_prints_resolved_release_and_rejects_untrusted_sha( + tmp_path: Path, capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI prints a trusted-base pin and reports malformed revision input.""" + + base = _commit(tmp_path, {"rust-toolchain": "1.97.1\n"}) + assert resolver.main(["--repo-root", str(tmp_path), "--base-sha", base]) == 0 + assert capsys.readouterr().out == "1.97.1\n" + + assert resolver.main(["--repo-root", str(tmp_path), "--base-sha", "HEAD"]) == 1 + assert "base SHA must be" in capsys.readouterr().err + + +def test_script_entrypoint_uses_the_trusted_base( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + """The executable script resolves and prints the exact base-commit toolchain.""" + + base = _commit(tmp_path, {"rust-toolchain.toml": '[toolchain]\nchannel = "1.97.1"\n'}) + _commit(tmp_path, {"rust-toolchain.toml": '[toolchain]\nchannel = "1.80.0"\n'}) + script = Path(resolver.__file__) + monkeypatch.setattr( + sys, + "argv", + [str(script), "--repo-root", str(tmp_path), "--base-sha", base], + ) + + with pytest.raises(SystemExit) as exited: + runpy.run_path(str(script), run_name="__main__") + + assert exited.value.code == 0 + assert capsys.readouterr().out == "1.97.1\n" diff --git a/tests/test_review_preflight_concurrency.py b/tests/test_review_preflight_concurrency.py index 641c726a0e..cf161029a0 100644 --- a/tests/test_review_preflight_concurrency.py +++ b/tests/test_review_preflight_concurrency.py @@ -2,6 +2,7 @@ import runpy import threading +from io import BytesIO from pathlib import Path from types import SimpleNamespace from urllib.error import HTTPError @@ -63,12 +64,17 @@ def test_unavailable_pool_fails_closed_within_the_probe_budget(): """Concurrent completion does not enlarge the committed probe budget.""" namespace = runpy.run_path(str(LAUNCHER)) calls = [] + provider_errors = [] class Client: """Return explicit provider rate-limit responses.""" def proxy_send_once(self, agent, endpoint, payload): calls.append(agent.id) - raise HTTPError('https://provider.invalid', 429, 'private body', {}, None) + provider_error = HTTPError( + 'https://provider.invalid', 429, 'private body', {}, None + ) + provider_errors.append(provider_error) + raise provider_error with pytest.raises(namespace['ReviewPreflightError']) as error: namespace['_preflight_review_agents_concurrently'](agents(24), client=Client()) @@ -77,6 +83,57 @@ def proxy_send_once(self, agent, endpoint, payload): assert report['ready_count'] == report['pending_count'] == 0 assert all(row['status'] == 'rejected' and row['http_status'] == 429 for row in report['routes']) assert 'private body' not in str(report) + assert all(provider_error.closed for provider_error in provider_errors) + + +def test_hostile_retry_after_is_rejected_and_http_error_is_closed(): + """Unbounded optional telemetry cannot abort preflight or leak its response.""" + + namespace = runpy.run_path(str(LAUNCHER)) + error_body = BytesIO(b"private body") + provider_error = HTTPError( + "https://provider.invalid", + 429, + "private body", + {"Retry-After": "9" * 5000}, + error_body, + ) + + class Client: + """Return one rate-limit response with hostile optional telemetry.""" + + def proxy_send_once(self, agent, endpoint, payload): + raise provider_error + + with pytest.raises(namespace["ReviewPreflightError"]) as error: + namespace["_preflight_review_agents_concurrently"](agents(1), client=Client()) + + route = error.value.report["routes"][0] + assert route["status"] == "rejected" + assert route["http_status"] == 429 + assert "retry_after_s" not in route + assert "private body" not in str(error.value.report) + assert error_body.closed + + +def test_http_error_is_closed_when_optional_telemetry_raises(): + """Cleanup remains mandatory if later telemetry classification regresses.""" + namespace = runpy.run_path(str(LAUNCHER)) + error_body = BytesIO(b"private body") + provider_error = HTTPError( + "https://provider.invalid", 503, "private body", {}, error_body + ) + + def broken_telemetry(exc): + raise ValueError("telemetry parser failed") + + record_exception = namespace["_record_provider_exception"] + record_exception.__globals__["_safe_retry_after_seconds"] = broken_telemetry + + with pytest.raises(ValueError, match="telemetry parser failed"): + record_exception({}, provider_error) + + assert error_body.closed def test_parallel_escalations_share_one_budget(): diff --git a/tests/test_rust_coverage_fixture_dependencies.py b/tests/test_rust_coverage_fixture_dependencies.py new file mode 100644 index 0000000000..9e6caa5d22 --- /dev/null +++ b/tests/test_rust_coverage_fixture_dependencies.py @@ -0,0 +1,33 @@ +"""Security contracts for the offline Rust coverage dependency fixture.""" + +from pathlib import Path + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 compatibility + import tomli as tomllib + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +FIXTURE_ROOT = REPOSITORY_ROOT / "tests" / "fixtures" / "coverage-cargo" + + +def test_coverage_fixture_uses_patched_pyo3_release() -> None: + """Keep the fixture manifest and lock on the reviewed PyO3 0.29.2 release.""" + manifest = tomllib.loads( + (FIXTURE_ROOT / "Cargo.toml").read_text(encoding="utf-8") + ) + cargo_lock = tomllib.loads( + (FIXTURE_ROOT / "Cargo.lock").read_text(encoding="utf-8") + ) + pyo3_packages = [ + package_entry + for package_entry in cargo_lock["package"] + if package_entry["name"] == "pyo3" + ] + + assert manifest["dependencies"]["pyo3"]["version"] == "=0.29.2" + assert {package_entry["version"] for package_entry in pyo3_packages} == { + "0.29.2" + } + assert len(pyo3_packages) == 1 diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..2231f48ede 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -6,6 +6,7 @@ import socket import subprocess import sys +from io import BytesIO from pathlib import Path import pytest @@ -671,6 +672,7 @@ def test_no_redirect_handler_raises_httperror_without_following(): sandboxed_web_e2e.NoRedirectHandler().redirect_request(request, None, 302, "Found", {}, "http://127.0.0.1") assert exc_info.value.code == 302 + exc_info.value.close() def test_wait_for_url_returns_false_after_timeout(monkeypatch, tmp_path): @@ -695,6 +697,43 @@ def open(self, url, timeout): assert sandboxed_web_e2e.wait_for_url("http://127.0.0.1:8000/health", 1, service) is False +def test_wait_for_url_closes_redirect_response(monkeypatch, tmp_path): + """A rejected readiness redirect releases its file-like HTTP response.""" + + class RunningProcess: + def poll(self): + return None + + ticks = iter([0, 0, 2]) + error_body = BytesIO() + redirect_error = sandboxed_web_e2e.urllib.error.HTTPError( + "http://127.0.0.1:8000/health", 302, "Found", {}, error_body + ) + + class RedirectingOpener: + def open(self, url, timeout): + raise redirect_error + + monkeypatch.setattr(sandboxed_web_e2e.time, "monotonic", lambda: next(ticks)) + monkeypatch.setattr(sandboxed_web_e2e.time, "sleep", lambda seconds: None) + monkeypatch.setattr( + sandboxed_web_e2e.urllib.request, + "build_opener", + lambda *args: RedirectingOpener(), + ) + service = sandboxed_web_e2e.Service( + "web", "serve", RunningProcess(), tmp_path / "web.log" + ) + + assert ( + sandboxed_web_e2e.wait_for_url( + "http://127.0.0.1:8000/health", 1, service + ) + is False + ) + assert error_body.closed + + def test_main_runs_with_stubbed_services(monkeypatch, tmp_path, capsys): """Main records success evidence without requiring real POSIX services.""" repo = tmp_path / "repo" diff --git a/tests/test_spdx_license_policy.py b/tests/test_spdx_license_policy.py index 53bee3ec72..072d93f29d 100644 --- a/tests/test_spdx_license_policy.py +++ b/tests/test_spdx_license_policy.py @@ -131,6 +131,18 @@ def test_dual_license_selection_needs_a_rationale() -> None: assert (decision.allowed, decision.code) == (False, policy.LICENSE_SELECTION_INVALID) +def test_dual_license_selection_must_be_valid_spdx() -> None: + """A rationale cannot make a malformed SPDX selection valid.""" + decision = policy.evaluate_license_expression( + "MIT OR GPL-2.0-only", selection="MIT AND", rationale="commercial choice" + ) + + assert (decision.allowed, decision.code) == ( + False, + policy.LICENSE_SELECTION_INVALID, + ) + + def test_selection_must_name_an_operand_of_the_expression() -> None: """A selection naming a license the dependency never offered is rejected.""" decision = policy.evaluate_license_expression( diff --git a/tests/test_strix_report_scope.py b/tests/test_strix_report_scope.py index 284b34e8d5..955557e214 100644 --- a/tests/test_strix_report_scope.py +++ b/tests/test_strix_report_scope.py @@ -1,32 +1,19 @@ """A completed Strix report must name the PR source it assessed.""" +from __future__ import annotations + import json -import subprocess +import runpy import sys from pathlib import Path -SCRIPT = Path(__file__).resolve().parents[1] / "scripts/ci/strix_report_scope.py" - - -def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_path: Path) -> None: - run = tmp_path / "current-scan" - run.mkdir() - (run / "run.json").write_text( - json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), - encoding="utf-8", - ) - report = run / "penetration_test_report.md" - report.write_text("Python OpenSSH client RCE vulnerability.\n", encoding="utf-8") - command = [sys.executable, str(SCRIPT), str(tmp_path), "python/fast_mlsirm/report.py"] - assert subprocess.run(command, capture_output=True).returncode == 1 +import pytest - report.write_text("Assessed python/fast_mlsirm/report.py; no vulnerabilities found.\n", encoding="utf-8") - assert subprocess.run(command, capture_output=True).returncode == 0 - assert subprocess.run(command[:-1], capture_output=True).returncode == 1 +from scripts.ci import strix_report_scope as report_scope -# Scope section of the 0-finding fast-mlsirm#2246 report (run 36580588738, -# attempt 2): it names the PR-scope directories it audited, not a file. +SCRIPT = Path(__file__).resolve().parents[1] / "scripts/ci/strix_report_scope.py" +CHANGED_PATH = "python/fast_mlsirm/report.py" SCOPED_DIRECTORY_REPORT = """# Methodology **Scope:** @@ -38,38 +25,191 @@ def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_p """ -def _completed_run(tmp_path: Path, report: str) -> None: - run = tmp_path / "current-scan" - run.mkdir() - (run / "run.json").write_text( - json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), - encoding="utf-8", +def _write_report( + output_dir: Path, + *, + metadata: object | None = None, + report_text: str = f"Assessed {CHANGED_PATH}; no vulnerabilities found.\n", +) -> Path: + """Create one ordinary scan report and return its run directory.""" + run_dir = output_dir / "current-scan" + run_dir.mkdir(parents=True, exist_ok=True) + if metadata is None: + metadata = { + "status": "completed", + "scan_results": {"scan_completed": True, "success": True}, + } + (run_dir / "run.json").write_text(json.dumps(metadata), encoding="utf-8") + (run_dir / "penetration_test_report.md").write_text( + report_text, encoding="utf-8" ) - (run / "penetration_test_report.md").write_text(report, encoding="utf-8") + return run_dir -def test_scan_scope_directory_containing_a_changed_file_identifies_the_scope(tmp_path: Path) -> None: - _completed_run(tmp_path, SCOPED_DIRECTORY_REPORT) - changed = ["crates/mlsirm-core/src/gpu_regression.rs", "python/fast_mlsirm/regression.py"] - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 0 +def test_validate_accepts_only_a_completed_scoped_report(tmp_path: Path) -> None: + """A completed report naming a changed source path is accepted.""" + _write_report(tmp_path) + report_scope.validate(tmp_path, [CHANGED_PATH]) -def test_scan_scope_directory_unrelated_to_changed_files_is_rejected(tmp_path: Path) -> None: - _completed_run(tmp_path, SCOPED_DIRECTORY_REPORT) - changed = ["docs/methods.md", "tests/test_regression.py"] - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 +def test_validate_rejects_missing_or_linked_output(tmp_path: Path) -> None: + """The trusted scan root must be a real directory.""" + with pytest.raises(ValueError, match="output directory is missing"): + report_scope.validate(tmp_path / "missing", [CHANGED_PATH]) + real_dir = tmp_path / "real" + real_dir.mkdir() + linked_dir = tmp_path / "linked" + linked_dir.symlink_to(real_dir, target_is_directory=True) + with pytest.raises(ValueError, match="output directory is missing"): + report_scope.validate(linked_dir, [CHANGED_PATH]) -def test_bare_repository_directory_or_scope_root_is_not_enough(tmp_path: Path) -> None: - changed = ["crates/mlsirm-core/src/gpu_regression.rs"] - _completed_run(tmp_path, "Audited crates/mlsirm-core; nothing found.\n") - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 - (tmp_path / "current-scan" / "penetration_test_report.md").write_text( - "Scope: `/workspace/strix-pr-scope.AoFHD6/`; nothing found.\n", encoding="utf-8" - ) - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 - # A repository-root equivalent under the scope root binds nothing either. - (tmp_path / "current-scan" / "penetration_test_report.md").write_text( - "Scope: `/workspace/strix-pr-scope.AoFHD6/.`; nothing found.\n", encoding="utf-8" + +def test_validate_requires_exactly_one_real_run_directory(tmp_path: Path) -> None: + """Files and linked directories never count as the single current run.""" + output_dir = tmp_path / "output" + output_dir.mkdir() + (output_dir / "noise.txt").write_text("ignored", encoding="utf-8") + linked_target = tmp_path / "linked-target" + linked_target.mkdir() + (output_dir / "linked-run").symlink_to( + linked_target, target_is_directory=True ) - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 + with pytest.raises(ValueError, match="exactly one current scan report"): + report_scope.validate(output_dir, [CHANGED_PATH]) + + _write_report(output_dir) + (output_dir / "second-run").mkdir() + with pytest.raises(ValueError, match="exactly one current scan report"): + report_scope.validate(output_dir, [CHANGED_PATH]) + + +@pytest.mark.parametrize("linked_name", ["run.json", "penetration_test_report.md"]) +def test_validate_rejects_missing_or_linked_report_files( + tmp_path: Path, linked_name: str +) -> None: + """Neither report input may be absent or redirected through a symlink.""" + run_dir = _write_report(tmp_path) + target = run_dir / linked_name + target.unlink() + with pytest.raises(ValueError, match="report files are missing or linked"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + outside = tmp_path / f"outside-{linked_name}" + outside.write_text("{}" if linked_name == "run.json" else CHANGED_PATH) + target.symlink_to(outside) + with pytest.raises(ValueError, match="report files are missing or linked"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +@pytest.mark.parametrize( + ("metadata", "expected_error"), + [ + ([], "metadata is not an object"), + ( + {"status": "completed", "scan_results": ["invalid"]}, + "results are not an object", + ), + ( + { + "status": "running", + "scan_results": {"scan_completed": True, "success": True}, + }, + "report is incomplete", + ), + ( + { + "status": "completed", + "scan_results": {"scan_completed": False, "success": True}, + }, + "report is incomplete", + ), + ( + { + "status": "completed", + "scan_results": {"scan_completed": True, "success": False}, + }, + "report is incomplete", + ), + ], +) +def test_validate_rejects_malformed_or_incomplete_metadata( + tmp_path: Path, metadata: object, expected_error: str +) -> None: + """Metadata shape and every completion signal fail closed.""" + _write_report(tmp_path, metadata=metadata) + with pytest.raises(ValueError, match=expected_error): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +def test_validate_rejects_malformed_json_and_unrelated_scope(tmp_path: Path) -> None: + """Unreadable metadata and a report unrelated to the diff never pass.""" + run_dir = _write_report(tmp_path) + (run_dir / "run.json").write_text("{not-json", encoding="utf-8") + with pytest.raises(json.JSONDecodeError): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + _write_report(tmp_path, report_text="Python OpenSSH client RCE vulnerability.\n") + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +def test_scan_scope_directory_containing_a_changed_file_is_accepted( + tmp_path: Path, +) -> None: + """A reviewed scope directory binds changed files beneath that directory.""" + _write_report(tmp_path, report_text=SCOPED_DIRECTORY_REPORT) + changed_paths = [ + "crates/mlsirm-core/src/gpu_regression.rs", + "python/fast_mlsirm/regression.py", + ] + report_scope.validate(tmp_path, changed_paths) + + +def test_scan_scope_directory_unrelated_to_changed_files_is_rejected( + tmp_path: Path, +) -> None: + """A directory scope cannot authorize unrelated changed paths.""" + _write_report(tmp_path, report_text=SCOPED_DIRECTORY_REPORT) + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate(tmp_path, ["docs/methods.md", "tests/test_regression.py"]) + + +@pytest.mark.parametrize( + "report_text", + [ + "Audited crates/mlsirm-core; nothing found.\n", + "Scope: `/workspace/strix-pr-scope.AoFHD6/`; nothing found.\n", + "Scope: `/workspace/strix-pr-scope.AoFHD6/.`; nothing found.\n", + ], +) +def test_bare_repository_directory_or_scope_root_is_rejected( + tmp_path: Path, report_text: str +) -> None: + """Bare directory prose and repository roots bind no changed file.""" + _write_report(tmp_path, report_text=report_text) + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate( + tmp_path, ["crates/mlsirm-core/src/gpu_regression.rs"] + ) + + +def test_cli_reports_validation_error_and_accepts_scoped_success( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """The command boundary maps validation failures to exit 1 and stderr.""" + monkeypatch.setattr(sys, "argv", [str(SCRIPT), str(tmp_path), CHANGED_PATH]) + with pytest.raises(SystemExit) as missing_report_exit: + runpy.run_path(str(SCRIPT), run_name="__main__") + assert missing_report_exit.value.code == 1 + assert "ERROR: Strix report scope: expected exactly one" in capsys.readouterr().err + + _write_report(tmp_path) + monkeypatch.setattr(sys, "argv", [str(SCRIPT), str(tmp_path)]) + with pytest.raises(SystemExit) as empty_scope_exit: + runpy.run_path(str(SCRIPT), run_name="__main__") + assert empty_scope_exit.value.code == 1 + assert "does not identify a changed source file" in capsys.readouterr().err + + monkeypatch.setattr(sys, "argv", [str(SCRIPT), str(tmp_path), CHANGED_PATH]) + runpy.run_path(str(SCRIPT), run_name="__main__") diff --git a/tests/test_strix_runtime_dependencies.py b/tests/test_strix_runtime_dependencies.py index 20444b0abd..c2c6a5eb68 100644 --- a/tests/test_strix_runtime_dependencies.py +++ b/tests/test_strix_runtime_dependencies.py @@ -3,6 +3,31 @@ REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +def _locked_requirement_versions( + requirements_text: str, package_name: str +) -> list[str]: + """Return every exact version row for one normalized package name.""" + package_versions = [] + for requirement_line in requirements_text.splitlines(): + requirement_name, separator, version_and_hash_marker = ( + requirement_line.strip().partition("==") + ) + requirement_name = requirement_name.split("[", 1)[0].strip() + if separator and requirement_name.casefold() == package_name.casefold(): + package_versions.append(version_and_hash_marker.split()[0]) + return package_versions + + +def test_locked_requirement_versions_normalizes_extras() -> None: + """Treat extras as the same distribution when detecting duplicate pins.""" + requirements = "pyjwt==2.14.0\npyjwt[crypto]==2.13.0\n" + + assert _locked_requirement_versions(requirements, "pyjwt") == [ + "2.14.0", + "2.13.0", + ] + + def test_strix_installs_openai_httpx2_runtime() -> None: requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( encoding="utf-8" @@ -27,3 +52,44 @@ def test_strix_anyio_security_pin_is_an_explicit_lock_input() -> None: assert "anyio==4.14.2" in requirements.splitlines() assert "anyio==4.14.2 \\" in requirements_lock.splitlines() + + +def test_strix_pyjwt_security_pin_is_an_explicit_lock_input() -> None: + """Keep the patched PyJWT version reproducible from the source input.""" + requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( + encoding="utf-8" + ) + requirements_lock = ( + REPOSITORY_ROOT / "requirements-strix-ci-hashes.txt" + ).read_text(encoding="utf-8") + + assert _locked_requirement_versions(requirements, "pyjwt") == ["2.15.1"] + assert _locked_requirement_versions(requirements_lock, "pyjwt") == ["2.15.1"] + + +def test_strix_litellm_security_pin_is_an_explicit_lock_input() -> None: + """Keep the patched LiteLLM credential boundary reproducible.""" + requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( + encoding="utf-8" + ) + requirements_lock = ( + REPOSITORY_ROOT / "requirements-strix-ci-hashes.txt" + ).read_text(encoding="utf-8") + + assert _locked_requirement_versions(requirements, "litellm") == ["1.94.3"] + assert _locked_requirement_versions(requirements_lock, "litellm") == ["1.94.3"] + + +def test_python_security_inputs_pin_patched_urllib3() -> None: + """Keep both audited runtimes on the urllib3 security release.""" + requirement_pairs = ( + ("requirements-pip-audit-ci.txt", "requirements-pip-audit-ci-hashes.txt"), + ("requirements-strix-ci.txt", "requirements-strix-ci-hashes.txt"), + ) + + for source_name, lock_name in requirement_pairs: + source_text = (REPOSITORY_ROOT / source_name).read_text(encoding="utf-8") + lock_text = (REPOSITORY_ROOT / lock_name).read_text(encoding="utf-8") + + assert _locked_requirement_versions(source_text, "urllib3") == ["2.8.0"] + assert _locked_requirement_versions(lock_text, "urllib3") == ["2.8.0"] diff --git a/tests/test_strix_unverified_dependency.py b/tests/test_strix_unverified_dependency.py index a4d2ac8c87..7eaf402771 100644 --- a/tests/test_strix_unverified_dependency.py +++ b/tests/test_strix_unverified_dependency.py @@ -8,10 +8,15 @@ from __future__ import annotations +import runpy import subprocess import sys from pathlib import Path +import pytest + +import scripts.ci.strix_unverified_dependency as unverified_dependency + from scripts.ci.strix_unverified_dependency import ( named_packages, unverified_dependency_finding, @@ -98,3 +103,54 @@ def test_cli_exit_status_and_message(tmp_path: Path) -> None: assert "lodash" in result.stderr and "unverified" in result.stderr (repo / "yarn.lock").write_text('lodash@^4.17.20:\n version "4.17.20"\n') assert subprocess.run([sys.executable, str(HELPER), str(report), str(repo)], check=False).returncode == 1 + +def test_manifest_scan_ignores_nonfiles_links_vendor_and_oversized_inputs( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """Only bounded regular dependency manifests influence the verdict.""" + repo = tmp_path / "repo" + repo.mkdir() + (repo / "requirements-extra.in").write_text("present-package==1\n") + (repo / "notes.txt").write_text("ignored-package==1\n") + (repo / "requirements-big.txt").write_text("oversized-package==1\n") + (repo / "linked.txt").symlink_to(repo / "requirements-extra.in") + vendor = repo / "vendor" + vendor.mkdir() + (vendor / "package.json").write_text('{"dependencies":{"vendored-package":"1"}}') + monkeypatch.setattr(unverified_dependency, "MAX_MANIFEST_BYTES", 20) + + manifest_text = unverified_dependency._manifest_text(repo) + + assert "present-package" in manifest_text + assert "ignored-package" not in manifest_text + assert "oversized-package" not in manifest_text + assert "vendored-package" not in manifest_text + + +def test_main_contract_covers_usage_unverified_and_present_dependency( + tmp_path: Path, capsys: pytest.CaptureFixture[str], +) -> None: + """The direct CLI contract returns 2/0/1 for usage/unverified/present.""" + assert unverified_dependency.main(["helper"]) == 2 + assert "Usage:" in capsys.readouterr().err + + repo = _rust_python_repo(tmp_path / "repo") + report = tmp_path / "report.md" + report.write_text(LODASH_REPORT) + assert unverified_dependency.main(["helper", str(report), str(repo)]) == 0 + assert "express, lodash" in capsys.readouterr().err + + (repo / "package.json").write_text('{"dependencies":{"lodash":"4.17.20"}}') + assert unverified_dependency.main(["helper", str(report), str(repo)]) == 1 + + +def test_script_entrypoint_propagates_the_classification_exit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """Executing the helper as a script publishes its classification as exit status.""" + repo = _rust_python_repo(tmp_path / "repo") + report = tmp_path / "report.md" + report.write_text(LODASH_REPORT) + monkeypatch.setattr(sys, "argv", [str(HELPER), str(report), str(repo)]) + with pytest.raises(SystemExit, match="0"): + runpy.run_path(str(HELPER), run_name="__main__") diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py index 50a5ddb5fe..738193a660 100644 --- a/tests/test_trusted_uv_materializer_quality_workflow_contract.py +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -49,6 +49,7 @@ def test_quality_workflow_pins_actions_and_uses_read_only_permissions() -> None: ) == 2 assert workflow.count("persist-credentials: false") == 2 assert workflow.count("ref: ${{ github.event.pull_request.head.sha }}") == 2 + assert workflow.count("fetch-depth: 0") == 1 def test_minimum_python_contract_exercises_the_tomli_fallback() -> None: diff --git a/tests/test_verify_release_scope_evidence_set.py b/tests/test_verify_release_scope_evidence_set.py index 449754b2b3..9f21e266cc 100644 --- a/tests/test_verify_release_scope_evidence_set.py +++ b/tests/test_verify_release_scope_evidence_set.py @@ -583,6 +583,22 @@ def test_build_snapshot_refuses_unlisted_native_file(tmp_path: Path, monkeypatch _build_packages(row, folder) +def test_build_snapshot_refuses_unknown_universal_interpreter_architecture( + tmp_path: Path, +) -> None: + """A universal build receipt must bind one recognized interpreter architecture.""" + scope_root, scope_rows = _prescreen_case(tmp_path) + scope_row = next(row for row in scope_rows if row["leg"].startswith("universal2-apple-darwin-")) + artifact_folder = scope_root / scope_row["artifact_name"] + receipt_path = artifact_folder / f"{scope_row['leg']}.build-first.json" + receipt = json.loads(receipt_path.read_text()) + receipt["build_env"] = "/opt/python/UNKNOWN" + receipt_path.write_text(json.dumps(receipt)) + + with pytest.raises(gate.GateError, match="build interpreter architecture is missing"): + _build_packages(scope_row, artifact_folder) + + def test_runtime_archive_refuses_unknown_native_link(tmp_path: Path, monkeypatch) -> None: """A runtime archive with an unlicensed dynamic target cannot pass prescreen.""" root, rows = _prescreen_case(tmp_path) @@ -978,6 +994,35 @@ def test_prescreen_refuses_complete_rows_without_sdist( prescreen(_scope_with_variants(scope_rows, scope_root), scope_root) +@pytest.mark.parametrize("mutation_name", ["oversized", "digest", "architecture"]) +def test_prescreen_refuses_untrusted_runtime_receipts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation_name: str, +) -> None: + """Runtime receipts are bounded, hash-bound, and structurally validated.""" + scope_root, scope_rows = _prescreen_case(tmp_path) + scope = _scope_with_variants(scope_rows, scope_root) + row = next(item for item in scope_rows if item["leg"] != "sdist") + runtime_name = f"{row['leg']}.runtime.json" + runtime_path = scope_root / row["artifact_name"] / runtime_name + if mutation_name == "oversized": + runtime_path.write_bytes(b"x" * (1024 * 1024 + 1)) + elif mutation_name == "digest": + row["members"][runtime_name] = "0" * 64 + else: + def invalid_architecture(*args, **kwargs): + del args, kwargs + raise DistributionSetError("untrusted architecture") + + monkeypatch.setattr(prescreen_module, "_runtime_target_architecture", invalid_architecture) + + with pytest.raises(gate.GateError, match={ + "oversized": "runtime receipt is oversized", + "digest": "runtime receipt changed after transport", + "architecture": "untrusted architecture", + }[mutation_name]): + prescreen(scope, scope_root) + + def test_prescreen_cli_writes_once_and_refuses_existing_output( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: