diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 26e8555967..f1a9eb094f 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -355,6 +355,62 @@ jobs: mkdir -p "$(dirname "$COVERAGE_SOURCE_WORKDIR")" mv "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" git -C "$COVERAGE_SOURCE_WORKDIR" status --short + # Pre-fetch every locked crate graph in the merge tree (root and + # nested workspaces alike) so `cargo llvm-cov` can compile Rust + # coverage after the sandbox loses network access. `cargo fetch` + # downloads content-addressed sources without running build scripts. + # A failed prefetch remains fail-closed: the offline coverage run + # reports the missing dependency for this exact head. + # + # Never let Cargo load PR-controlled configuration while using the + # authenticated runner. The neutral working directory, HOME, and + # CARGO_HOME ensure that only the registry/ and git/ caches are + # copied into the tree-local sandbox home; config and credentials do + # not cross the trust boundary. + prefetch_manifests="$(git -C "$COVERAGE_SOURCE_WORKDIR" ls-files 'Cargo.toml' '*/Cargo.toml')" + sandbox_cargo_home="$COVERAGE_SOURCE_WORKDIR/.opencode-sandbox-home/.cargo" + if [ -e "$COVERAGE_SOURCE_WORKDIR/.opencode-sandbox-home" ] || + [ -L "$COVERAGE_SOURCE_WORKDIR/.opencode-sandbox-home" ]; then + echo "::error::PR tree already contains the reserved .opencode-sandbox-home path; refusing to mix it with trusted Cargo caches." + exit 1 + fi + if [ -n "$prefetch_manifests" ] && command -v cargo >/dev/null 2>&1; then + prefetch_home="$(mktemp -d "${RUNNER_TEMP:-/tmp}/cargo-prefetch.XXXXXX")" + while IFS= read -r prefetch_manifest; do + [ -n "$prefetch_manifest" ] || continue + prefetch_dir="$(dirname "$prefetch_manifest")" + if [ ! -f "$COVERAGE_SOURCE_WORKDIR/$prefetch_dir/Cargo.lock" ]; then + continue + fi + if ! ( + cd "$prefetch_home" \ + && HOME="$prefetch_home" \ + CARGO_HOME="$prefetch_home/.cargo" \ + CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git \ + CARGO_NET_GIT_FETCH_WITH_CLI=false \ + GIT_TERMINAL_PROMPT=0 \ + GIT_CONFIG_NOSYSTEM=1 \ + GIT_CONFIG_GLOBAL=/dev/null \ + env -u GH_TOKEN -u GITHUB_TOKEN \ + -u ACTIONS_ID_TOKEN_REQUEST_TOKEN \ + -u ACTIONS_ID_TOKEN_REQUEST_URL \ + -u ACTIONS_RUNTIME_TOKEN \ + cargo fetch --locked \ + --manifest-path "$COVERAGE_SOURCE_WORKDIR/$prefetch_manifest" + ); then + echo "::warning::Locked cargo prefetch failed for ${prefetch_manifest}; offline Rust coverage will surface the missing dependencies for this head." + fi + done <<<"$prefetch_manifests" + mkdir -p "$sandbox_cargo_home" + for prefetch_cache_dir in registry git; do + if [ -d "$prefetch_home/.cargo/$prefetch_cache_dir" ]; then + cp -a "$prefetch_home/.cargo/$prefetch_cache_dir" "$sandbox_cargo_home/" + fi + done + rm -rf "$prefetch_home" + elif [ -n "$prefetch_manifests" ]; then + echo "::warning::cargo is unavailable on this runner; offline Rust coverage will surface missing dependencies for this head." + fi tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . - name: Upload materialized pull request merge tree @@ -1941,6 +1997,10 @@ jobs: } ensure_rust_toolchain() { + # The sandbox has no network access; resolve Rust dependencies only + # from the content-addressed cache prepared above. + export CARGO_NET_OFFLINE=true + export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git if [ "${LLVM_COV:-}" != "/usr/bin/llvm-cov-19" ] || \ [ "${LLVM_PROFDATA:-}" != "/usr/bin/llvm-profdata-19" ] || \ ! test -x "$LLVM_COV" || ! test -x "$LLVM_PROFDATA"; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b3dba425..4df80cb18f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ - Added `ContextualWisdomLab/governance-risk-compliance` to the `OPENCODE_REPOSITORY_DISPATCH_TARGETS` repository variable directly (the actual source of truth for `ALLOWED_TARGET_REPOSITORIES` in both scheduler workflows) and removed the temporary hardcoded-literal bridge a prior commit had added to `pr-review-merge-scheduler.yml`/`pr-review-fix-scheduler.yml` to work around the variable not yet including it. Hardcoding a specific product repository into these shared scheduler workflows violates this repo's own thin-caller convention (`CLAUDE.md`: "Product hourly callers stay thin. Do not hard-code OriginWeave, aFIPC, naruon, or Keyverse into `pr-review-fix-scheduler.yml`") and broke `test_no_target_repository_is_hard_coded_in_the_shared_scheduler`. Updating the variable achieves the same admission with no code change and no test regression. +### Offline Rust coverage dependency prefetch + +- Repair Rust coverage dependency materialization by prefetching locked Cargo + sources with a neutral runner-owned Cargo home and carrying only the + content-addressed registry/git caches into the existing networkless sandbox. + Both prefetch and runtime force Cargo's git registry protocol so their cache + layouts remain compatible; the reserved sandbox path is rejected even when + Cargo is unavailable, GitHub credentials and PR-controlled Cargo + configuration do not cross the boundary, Git credential prompts are disabled, + and missing caches remain fail-closed. ### Hourly review-repair queue-scan bound - Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up. diff --git a/docs/doctoring/opencode-rust-coverage-runtime-boundary.md b/docs/doctoring/opencode-rust-coverage-runtime-boundary.md index 8adc68d0ea..8c584517c6 100644 --- a/docs/doctoring/opencode-rust-coverage-runtime-boundary.md +++ b/docs/doctoring/opencode-rust-coverage-runtime-boundary.md @@ -23,6 +23,19 @@ host-runner tool, a pull-request-selected path, or a dynamically downloaded LLVM binary. Missing, changed, or non-executable reviewed paths are coverage-evidence failures rather than reasons to measure a different toolchain. +Rust dependency materialization follows the same boundary. The trusted +`coverage-source-tree` job runs `cargo fetch --locked` from a runner-owned, +neutral `HOME`/`CARGO_HOME`, with GitHub credentials removed and Git prompts +disabled, and copies only +Cargo's content-addressed `registry/` and `git/` directories into the +tree-local sandbox home. Prefetch and runtime force Cargo's `git` registry +protocol so runner and container Cargo versions consume the same cache layout. +The PR execution container remains `--network=none` and uses +`CARGO_NET_OFFLINE=true`; a reserved `.opencode-sandbox-home` path in the PR +tree is rejected before the Cargo-availability branch so PR content cannot be +mixed with the trusted cache even when the runner lacks Cargo. Prefetch failure +is retained as a fail-closed offline coverage failure. + NIST SP 800-218 PW.4.1 covers acquiring and maintaining third-party software from expected, trusted sources and reviewing its provenance (Souppaya et al., 2022). PW.4.4 covers verifying the integrity of acquired components. The exact @@ -57,6 +70,8 @@ flowchart LR C --> D["Require both paths executable"] D --> E["Fail closed before cargo llvm-cov"] F["Hashed opencode-review-dispatch.yml"] --> G["Unchanged review-agent key blob"] + H["Trusted cargo fetch: neutral HOME/CARGO_HOME + git protocol"] --> I["registry/ + git/ cache only"] + I --> J["PR coverage: CARGO_NET_OFFLINE=true + --network=none"] ``` Each arrow is fail-closed. A later stage does not repair or broaden an earlier @@ -68,7 +83,11 @@ The reviewed paths are fixed in trusted central workflow source. Pull-request content cannot choose an LLVM package, executable path, download origin, or runtime environment value. The existing coverage sandbox retains `--network=none`, credential/Git isolation, exact-head/base materialization, -and the separately checksum-pinned `cargo-llvm-cov` archive. +and the separately checksum-pinned `cargo-llvm-cov` archive. Cargo dependency +prefetch is performed before PR code execution, without the target repository's +configuration or GitHub credentials, and only content-addressed caches cross +into the sandbox. Both sides force the git registry protocol to avoid a sparse +versus git index-layout mismatch between runner and container Cargo versions. This binding narrows reproducibility risk but does not by itself attest Debian's whole package supply chain or prove a future Rust toolchain is compatible with @@ -99,7 +118,10 @@ compatibility evidence and the same RED→GREEN exact-head verification sequence paths; 3. the helper requires both paths to be executable and exits `1` on mismatch; 4. the helper does not mention unversioned `llvm-cov` / `llvm-profdata`; and -5. every exact path named by the permanent quality workflow's +5. locked Rust dependencies are prefetched before the networkless sandbox, + with neutral Cargo configuration, credential removal, and the same forced + registry protocol on both sides; and +6. every exact path named by the permanent quality workflow's `pull_request.paths` filter resolves to a repository file, including the helper, preventing a dangling documentation trigger from becoming invisible debt. diff --git a/tests/test_opencode_rust_coverage_toolchain_contract.py b/tests/test_opencode_rust_coverage_toolchain_contract.py index cc0c49af6f..80d005e841 100644 --- a/tests/test_opencode_rust_coverage_toolchain_contract.py +++ b/tests/test_opencode_rust_coverage_toolchain_contract.py @@ -101,6 +101,57 @@ def test_isolated_runtime_revalidates_llvm_tools_before_coverage() -> None: assert "exit 1" in helper +def test_rust_coverage_prefetches_locked_crates_before_networkless_sandbox() -> None: + """Materialize only trusted Cargo caches before PR code loses network access.""" + + dispatch = _dispatch_text() + source_tree_start = dispatch.index( + " - name: Materialize pull request merge tree for coverage measurement\n" + ) + source_tree_end = dispatch.index( + " - name: Upload materialized pull request merge tree\n", + source_tree_start, + ) + source_tree = dispatch[source_tree_start:source_tree_end] + + assert "prefetch_manifests=\"$(git -C \"$COVERAGE_SOURCE_WORKDIR\" ls-files 'Cargo.toml' '*/Cargo.toml')\"" in source_tree + assert "cargo fetch --locked" in source_tree + assert "--manifest-path \"$COVERAGE_SOURCE_WORKDIR/$prefetch_manifest\"" in source_tree + assert 'cd "$prefetch_home"' in source_tree + assert 'HOME="$prefetch_home"' in source_tree + assert 'CARGO_HOME="$prefetch_home/.cargo"' in source_tree + assert 'CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git' in source_tree + assert 'GIT_TERMINAL_PROMPT=0' in source_tree + assert "env -u GH_TOKEN -u GITHUB_TOKEN" in source_tree + assert ".opencode-sandbox-home" in source_tree + assert "refusing to mix it with trusted Cargo caches" in source_tree + assert 'for prefetch_cache_dir in registry git; do' in source_tree + assert 'CARGO_NET_OFFLINE=true' in dispatch + assert "docker run --rm --init --network=none" in dispatch + + +def test_reserved_sandbox_path_is_rejected_without_cargo() -> None: + """Reject PR-controlled sandbox state even when the runner lacks Cargo.""" + + dispatch = _dispatch_text() + source_tree_start = dispatch.index( + " - name: Materialize pull request merge tree for coverage measurement\n" + ) + source_tree_end = dispatch.index( + " - name: Upload materialized pull request merge tree\n", + source_tree_start, + ) + source_tree = dispatch[source_tree_start:source_tree_end] + + reserved_path_guard = ( + 'if [ -e "$COVERAGE_SOURCE_WORKDIR/.opencode-sandbox-home" ] ||' + ) + cargo_gate = ( + 'if [ -n "$prefetch_manifests" ] && command -v cargo >/dev/null 2>&1; then' + ) + assert source_tree.index(reserved_path_guard) < source_tree.index(cargo_gate) + + def test_quality_workflow_watches_the_trusted_dispatch_workflow() -> None: """Guard drift in the hashed review-dispatch blob must retrigger this contract.""" diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 2d2304aaf1..75062e8cba 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 = "26e8555967171a5f3974602ac05700c27bddebf1" +REVIEW_DISPATCH_BLOB_SHA = "f1a9eb094fde0715a01d7e24a4e18577e6920a6b" def _workflow_text(path: Path) -> str: