diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index dd2d2dd5..b0ed081e 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -541,6 +541,75 @@ jobs: mkdir -p "$sandbox_result_dir" chmod 0700 "$sandbox_result_dir" + # Build the coverage tool image before the pull-request tree is + # mounted anywhere. The networked build context contains only this + # trusted Dockerfile plus the reviewed, hash-pinned CI requirements + # from the default-branch checkout; it never contains PR-controlled + # source, dependency manifests, credentials, or runner command files. + coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + coverage_build_dir="${RUNNER_TEMP}/opencode-coverage-tool-build" + trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt" + if [ ! -f "$trusted_ci_requirements" ] || [ -L "$trusted_ci_requirements" ]; then + echo "::error::Trusted coverage requirements must be a regular non-symlink file." + exit 1 + fi + sudo rm -rf "$coverage_build_dir" + mkdir -p "$coverage_build_dir" + chmod 0700 "$coverage_build_dir" + install -m 0644 "$trusted_ci_requirements" \ + "$coverage_build_dir/requirements-opencode-review-ci-hashes.txt" + cat >"$coverage_build_dir/Dockerfile" <<'DOCKERFILE' + FROM docker.io/library/ubuntu@sha256:52df9b1ee71626e0088f7d400d5c6b5f7bb916f8f0c82b474289a4ece6cf3faf + ENV DEBIAN_FRONTEND=noninteractive + RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + ca-certificates \ + cargo \ + curl \ + git \ + jq \ + libcurl4-openssl-dev \ + libssl-dev \ + libxml2-dev \ + mesa-vulkan-drivers \ + libvulkan1 \ + nodejs \ + npm \ + pkg-config \ + python3 \ + python3-pip \ + python3-venv \ + r-base \ + r-cran-covr \ + r-cran-testthat \ + rustc \ + util-linux \ + vulkan-tools \ + && rm -rf /var/lib/apt/lists/* + RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/cargo-llvm-cov.tar.gz \ + https://github.com/taiki-e/cargo-llvm-cov/releases/download/v0.8.7/cargo-llvm-cov-x86_64-unknown-linux-musl.tar.gz \ + && echo '967b5cc996c29d8baa52bbb4595ef1f53af35255af8e2036ddbc6468d7b523c7 /tmp/cargo-llvm-cov.tar.gz' | sha256sum -c - \ + && tar -xzf /tmp/cargo-llvm-cov.tar.gz -C /usr/local/bin cargo-llvm-cov \ + && chmod 0755 /usr/local/bin/cargo-llvm-cov \ + && rm -f /tmp/cargo-llvm-cov.tar.gz + COPY requirements-opencode-review-ci-hashes.txt /tmp/requirements-opencode-review-ci-hashes.txt + RUN python3 -m pip install \ + --break-system-packages \ + --disable-pip-version-check \ + --require-hashes \ + --only-binary=:all: \ + -r /tmp/requirements-opencode-review-ci-hashes.txt \ + && rm -f /tmp/requirements-opencode-review-ci-hashes.txt + DOCKERFILE + if ! docker build --pull --no-cache --network=default \ + --tag "$coverage_tool_image" \ + --file "$coverage_build_dir/Dockerfile" \ + "$coverage_build_dir"; then + echo "::error::Trusted coverage tool image build failed before PR execution." + exit 1 + fi + sudo rm -rf "$coverage_build_dir" + sandbox_status=0 docker run --rm --init --network=none \ --name "opencode-coverage-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ @@ -573,7 +642,7 @@ jobs: --env RUNNER_TEMP=/secure-output \ --env GITHUB_OUTPUT=/secure-output/github-output \ --env GITHUB_STEP_SUMMARY=/secure-output/step-summary \ - docker.io/library/ubuntu@sha256:52df9b1ee71626e0088f7d400d5c6b5f7bb916f8f0c82b474289a4ece6cf3faf \ + "$coverage_tool_image" \ /bin/bash /trusted-measure-step.sh || sandbox_status=$? sandbox_output="${sandbox_result_dir}/github-output" @@ -596,35 +665,6 @@ jobs: exit 0 fi - export DEBIAN_FRONTEND=noninteractive - apt-get update - apt-get install --no-install-recommends -y \ - ca-certificates \ - cargo \ - curl \ - git \ - jq \ - libcurl4-openssl-dev \ - libssl-dev \ - libxml2-dev \ - nodejs \ - npm \ - python3 \ - python3-pip \ - python3-venv \ - r-base \ - r-cran-covr \ - r-cran-testthat \ - rustc \ - util-linux - rm -rf /var/lib/apt/lists/* - python3 -m pip install \ - --break-system-packages \ - --disable-pip-version-check \ - --require-hashes \ - --only-binary=:all: \ - -r /trusted/requirements-opencode-review-ci-hashes.txt - # Use a fixed nobody-like identity that cannot traverse the host-owned # /out bind mount. This prevents even a daemonized test process from # racing trusted result publication. @@ -1291,8 +1331,7 @@ jobs: if command -v Rscript >/dev/null 2>&1 && dpkg -s libcurl4-openssl-dev libssl-dev libxml2-dev >/dev/null 2>&1; then return 0 fi - run_and_capture "R runtime and signed distribution coverage packages" \ - bash -c 'sudo apt-get update && sudo apt-get install -y r-base r-cran-covr r-cran-testthat libcurl4-openssl-dev libssl-dev libxml2-dev' + return 1 } run_r_test_coverage() { @@ -1349,10 +1388,6 @@ jobs: # coverage command still runs and reports any uncovered GPU lines # exactly as before, so Rust repositories without GPU code are # unaffected and no gate is weakened. - if ! ls /usr/share/vulkan/icd.d/lvp_icd*.json >/dev/null 2>&1; then - run_and_capture "Software Vulkan adapter (Mesa lavapipe) for GPGPU coverage" \ - bash -c 'sudo apt-get update && sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers libvulkan1 vulkan-tools || true' - fi if ls /usr/share/vulkan/icd.d/lvp_icd*.json >/dev/null 2>&1; then lvp_icd="$(ls /usr/share/vulkan/icd.d/lvp_icd*.json | head -n1)" export VK_ICD_FILENAMES="$lvp_icd" @@ -1384,10 +1419,6 @@ jobs: if ! find . -name tauri.conf.json -not -path '*/node_modules/*' -not -path '*/target/*' -print -quit 2>/dev/null | grep -q .; then return 0 fi - if ! pkg-config --exists webkit2gtk-4.1 2>/dev/null; then - run_and_capture "Tauri Linux system libraries (WebKitGTK/GTK3) for desktop coverage" \ - bash -c 'sudo apt-get update && sudo apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev || true' - fi if pkg-config --exists webkit2gtk-4.1 2>/dev/null; then append "### Tauri desktop coverage dependencies" append "" @@ -1414,8 +1445,15 @@ jobs: failures=$((failures + 1)) return 1 fi - if [ ! -x /work/.opencode-sandbox-home/.cargo/bin/cargo-llvm-cov ]; then - run_and_capture "Rust coverage tooling (cargo-llvm-cov 0.8.7)" cargo install cargo-llvm-cov --version 0.8.7 --locked + if ! command -v cargo-llvm-cov >/dev/null 2>&1; then + append "### Rust coverage toolchain" + append "" + append "- Result: FAIL" + append "- Reason: the trusted offline coverage image is missing cargo-llvm-cov 0.8.7." + append "- Fix: rebuild the trusted coverage image before rerunning current-head evidence." + append "" + failures=$((failures + 1)) + return 1 fi ensure_rust_gpu_adapter ensure_rust_desktop_deps diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 64f79a5a..b8123a3a 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -523,7 +523,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'R_LIBS_USER="/work/.opencode-r-library"' "opencode R coverage isolates the package library inside the untrusted worktree" assert_file_not_contains "$workflow_file" 'install.packages(' "opencode R coverage never installs PR-selected mutable packages" assert_file_contains "$workflow_file" "libcurl4-openssl-dev libssl-dev libxml2-dev" "opencode R coverage installs system headers required by covr dependencies" - assert_file_contains "$workflow_file" "r-cran-covr r-cran-testthat" "opencode R coverage uses signed distribution packages instead of mutable CRAN resolution" + assert_file_contains "$workflow_file" "r-cran-covr" "opencode R coverage uses the signed distribution covr package instead of mutable CRAN resolution" + assert_file_contains "$workflow_file" "r-cran-testthat" "opencode R coverage uses the signed distribution testthat package instead of mutable CRAN resolution" assert_file_contains "$workflow_file" "R package testthat suite" "opencode R package coverage requires package testthat evidence" assert_file_contains "$workflow_file" "testthat unavailable in coverage runner; deferring to required peer R CMD check evidence." "opencode R package tests defer only when testthat cannot be installed in the coverage runner" assert_file_contains "$workflow_file" "covr package_coverage unavailable after package tests; treating missing-line report as advisory." "opencode R package coverage does not block on covr installation reproduction after tests pass" @@ -862,7 +863,9 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" "Coverage merge tree could not be materialized" "coverage evidence logs an actionable merge-tree failure reason" assert_file_contains "$workflow_file" "--require-hashes" "coverage tooling installs from a hash-pinned lock" assert_file_contains "$workflow_file" "--only-binary=:all:" "coverage tooling installs only binary packages from the pinned lock" - assert_file_contains "$workflow_file" "-r /trusted/requirements-opencode-review-ci-hashes.txt" "coverage sandbox installs the trusted hash lock rather than PR-controlled requirements" + assert_file_contains "$workflow_file" 'trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt"' "coverage tooling sources its hash lock from the trusted default-branch checkout" + assert_file_contains "$workflow_file" '"$coverage_build_dir/requirements-opencode-review-ci-hashes.txt"' "coverage tooling copies the trusted hash lock into the isolated build context" + assert_file_contains "$workflow_file" "-r /tmp/requirements-opencode-review-ci-hashes.txt" "coverage image installs the trusted hash lock rather than PR-controlled requirements" assert_file_contains "$workflow_file" 'GITHUB_ENV=/dev/null' "PR-controlled coverage commands cannot write runner environment command files" assert_file_contains "$workflow_file" 'GITHUB_PATH=/dev/null' "PR-controlled coverage commands cannot extend later-step PATH" assert_file_contains "$workflow_file" 'GITHUB_OUTPUT=/dev/null' "PR-controlled coverage commands cannot forge trusted step outputs" @@ -896,7 +899,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$GATE_SCRIPT" 'STRIX_EXECUTABLE_PATH must be outside the untrusted scan target' "Strix executable cannot come from the scan target" assert_file_not_contains "$GATE_SCRIPT" 'shutil.which("strix")' "Strix gate never resolves its credential-bearing executable through inherited PATH" assert_file_not_contains "$workflow_file" "https://sh.rustup.rs" "coverage refuses a mutable Rust network installer" - assert_file_contains "$workflow_file" "cargo install cargo-llvm-cov --version 0.8.7 --locked" "coverage pins cargo-llvm-cov" + assert_file_contains "$workflow_file" "cargo-llvm-cov-x86_64-unknown-linux-musl.tar.gz" "coverage pins the official cargo-llvm-cov 0.8.7 Linux asset" + assert_file_contains "$workflow_file" "967b5cc996c29d8baa52bbb4595ef1f53af35255af8e2036ddbc6468d7b523c7" "coverage verifies the official cargo-llvm-cov 0.8.7 asset digest" assert_file_contains "$workflow_file" "Run merge scheduler after approval" "opencode approval runs the merge scheduler after current-head review publication" assert_file_contains "$workflow_file" "python3 scripts/ci/pr_review_merge_scheduler.py" "opencode approval directly executes the trusted central merge scheduler when required workflows are not repo-local dispatch targets" assert_file_contains "$workflow_file" "--require-opencode-app" "opencode approval reuse and post-publication follow-up reject GitHub Actions-authored review evidence" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index b7b96b7c..a533580f 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -330,6 +330,14 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "docker.io/library/ubuntu@sha256:" in measure_step assert "apt-get install --no-install-recommends -y" in measure_step assert "--require-hashes" in measure_step + assert 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' in measure_step + assert "The networked build context contains only this" in measure_step + assert 'install -m 0644 "$trusted_ci_requirements"' in measure_step + assert "docker build --pull --no-cache --network=default" in measure_step + assert '"$coverage_build_dir"' in measure_step + assert measure_step.index("docker build --pull --no-cache") < measure_step.index( + "docker run --rm --init --network=none" + ) assert "--cap-drop ALL" in measure_step # Docker already creates a private PID namespace by default. Passing the # unsupported literal `private` makes hosted-runner Docker exit 125 before @@ -367,6 +375,12 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): ) assert "CARGO_HOME=/work/.opencode-sandbox-home/.cargo" in measure_step assert "docker run --rm --init --network=none" in measure_step + sandbox_runtime = measure_step.split( + " export OPENCODE_SANDBOX_UID=65532", 1 + )[1] + assert "apt-get" not in sandbox_runtime + assert "cargo install" not in sandbox_runtime + assert "command -v cargo-llvm-cov" in sandbox_runtime assert ( 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"' in measure_step @@ -1647,7 +1661,12 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert "yarn install --immutable --mode=skip-builds" in coverage_job assert 'corepack prepare "${runner}@latest"' not in coverage_job assert "https://sh.rustup.rs" not in coverage_job - assert "cargo install cargo-llvm-cov --version 0.8.7 --locked" in coverage_job + assert "cargo-llvm-cov-x86_64-unknown-linux-musl.tar.gz" in coverage_job + assert ( + "967b5cc996c29d8baa52bbb4595ef1f53af35255af8e2036ddbc6468d7b523c7" + in coverage_job + ) + assert "sha256sum -c -" in coverage_job assert "install.packages(" not in coverage_job target_condition = target_job.split(" runs-on:", 1)[0]