Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/agent-review-runtime-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ on:
- ".github/workflows/opencode-review-dispatch.yml"
- "scripts/ci/ensure_rust_llvm19.sh"
- "tests/test_opencode_rust_coverage_toolchain_contract.py"
- "scripts/ci/resolve_base_rust_toolchain.py"
- "tests/test_resolve_base_rust_toolchain.py"
- "scripts/ci/place_maturin_extension.py"
- "tests/test_place_maturin_extension.py"
- "scripts/ci/materialize_base_javascript_packages.py"
- "tests/test_javascript_materializer_docstrings.py"
- "tests/test_pr_review_autofix_nvidia_nim_contract.py"
Expand Down Expand Up @@ -204,6 +208,10 @@ jobs:
.github/workflows/opencode-review-dispatch.yml|\
scripts/ci/ensure_rust_llvm19.sh|\
tests/test_opencode_rust_coverage_toolchain_contract.py|\
scripts/ci/resolve_base_rust_toolchain.py|\
tests/test_resolve_base_rust_toolchain.py|\
scripts/ci/place_maturin_extension.py|\
tests/test_place_maturin_extension.py|\
scripts/ci/materialize_base_javascript_packages.py|\
tests/test_javascript_materializer_docstrings.py|\
docs/doctoring/opencode-rust-coverage-runtime-boundary.md)
Expand Down Expand Up @@ -379,8 +387,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
python -m compileall -q tests/test_opencode_rust_coverage_toolchain_contract.py
python -m pytest -q tests/test_opencode_rust_coverage_toolchain_contract.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

- name: Verify JavaScript materializer documentation contract
if: steps.affected_suites.outputs.opencode == 'true'
Expand Down
68 changes: 58 additions & 10 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,27 @@ jobs:
&& 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
ARG OPENCODE_RUST_TOOLCHAIN=
RUN set -eu; \
[ -n "$OPENCODE_RUST_TOOLCHAIN" ] || exit 0; \
printf '%s\n' "$OPENCODE_RUST_TOOLCHAIN" | grep -Eqx '1\.[0-9]{1,3}\.[0-9]{1,3}'; \
curl --proto '=https' --tlsv1.2 -fsSLo /tmp/rustup-init \
https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init; \
echo 'dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71 /tmp/rustup-init' | sha256sum -c -; \
chmod 0755 /tmp/rustup-init; \
RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/rustup/cargo /tmp/rustup-init -y --no-modify-path \
--profile minimal --default-toolchain "$OPENCODE_RUST_TOOLCHAIN" --component llvm-tools-preview; \
rm -f /tmp/rustup-init; \
toolchain="/usr/local/rustup/toolchains/${OPENCODE_RUST_TOOLCHAIN}-x86_64-unknown-linux-gnu"; \
ln -s "$toolchain/bin/cargo" /usr/local/bin/cargo; \
ln -s "$toolchain/bin/rustc" /usr/local/bin/rustc; \
mkdir -p /usr/local/libexec/opencode-rust; \
ln -s "$toolchain/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-cov" /usr/local/libexec/opencode-rust/llvm-cov; \
ln -s "$toolchain/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata" /usr/local/libexec/opencode-rust/llvm-profdata; \
chmod -R a+rX /usr/local/rustup; \
test "$(/usr/local/bin/rustc --version | cut -d ' ' -f 2)" = "$OPENCODE_RUST_TOOLCHAIN"; \
/usr/local/libexec/opencode-rust/llvm-cov --version >/dev/null; \
/usr/local/libexec/opencode-rust/llvm-profdata --version >/dev/null
COPY base-javascript-packages /tmp/base-javascript-packages
RUN set -eu; \
mkdir -p /opt/corepack /opt/javascript-package-locks /opt/npm-cache /opt/pnpm-store; \
Expand Down Expand Up @@ -956,10 +977,32 @@ jobs:
&& rm -f /usr/local/libexec/install-base-python-locks.py
COPY base-rust-dependencies /opt/base-rust-dependencies
DOCKERFILE
if ! docker build --pull --no-cache --network=default \
--tag "$coverage_tool_image" \
--file "$coverage_build_dir/Dockerfile" \
"$coverage_build_dir"; then
# Debian's rustc lags the MSRV of pinned repositories (fast-mlsirm pins
# 1.97.1; Debian ships 1.85), so a base-pinned repository gets that exact
# release installed at build time. Unpinned repositories keep Debian's.
base_rust_toolchain="$(python3 -I "$GITHUB_WORKSPACE/scripts/ci/resolve_base_rust_toolchain.py" \
--repo-root "$COVERAGE_SOURCE_WORKDIR" --base-sha "$PR_BASE_SHA")"
build_coverage_tool_image() {
docker build --pull --no-cache --network=default \
--build-arg "OPENCODE_RUST_TOOLCHAIN=$1" \
--tag "$coverage_tool_image" \
--file "$coverage_build_dir/Dockerfile" \
"$coverage_build_dir"
}
coverage_llvm_cov=/usr/bin/llvm-cov-19
coverage_llvm_profdata=/usr/bin/llvm-profdata-19
if [ -n "$base_rust_toolchain" ] && build_coverage_tool_image "$base_rust_toolchain"; then
coverage_llvm_cov=/usr/local/libexec/opencode-rust/llvm-cov
coverage_llvm_profdata=/usr/local/libexec/opencode-rust/llvm-profdata
elif [ -n "$base_rust_toolchain" ]; then
# Rollback: a failed toolchain layer must not cost the repository its
# previous Debian-toolchain image.
echo "::warning::Rust ${base_rust_toolchain} layer failed; rebuilding with the Debian toolchain."
if ! build_coverage_tool_image ""; then
echo "::error::Trusted coverage tool image build failed before PR execution."
exit 1
fi
elif ! build_coverage_tool_image ""; then
echo "::error::Trusted coverage tool image build failed before PR execution."
exit 1
fi
Expand Down Expand Up @@ -997,8 +1040,8 @@ jobs:
--env RUNNER_TEMP=/secure-output \
--env GITHUB_OUTPUT=/secure-output/github-output \
--env GITHUB_STEP_SUMMARY=/secure-output/step-summary \
--env LLVM_COV=/usr/bin/llvm-cov-19 \
--env LLVM_PROFDATA=/usr/bin/llvm-profdata-19 \
--env LLVM_COV="$coverage_llvm_cov" \
--env LLVM_PROFDATA="$coverage_llvm_profdata" \
"$coverage_tool_image" \
/bin/bash /trusted-measure-step.sh || sandbox_status=$?

Expand Down Expand Up @@ -1355,7 +1398,8 @@ jobs:
dist_dir="$(mktemp -d)"
python3 -m maturin build --offline --release -o "$dist_dir"
python3 -m pip install --user --no-index --no-deps --force-reinstall "$dist_dir"/*.whl
rm -rf "$dist_dir"' bash "$project_dir"
python3 "$2" "$dist_dir"/*.whl .
rm -rf "$dist_dir"' bash "$project_dir" "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py"
}

run_python_test_coverage() {
Expand Down Expand Up @@ -2077,13 +2121,17 @@ jobs:
}

ensure_rust_toolchain() {
if [ "${LLVM_COV:-}" != "/usr/bin/llvm-cov-19" ] || \
[ "${LLVM_PROFDATA:-}" != "/usr/bin/llvm-profdata-19" ] || \
case "${LLVM_COV:-}:${LLVM_PROFDATA:-}" in
/usr/bin/llvm-cov-19:/usr/bin/llvm-profdata-19) llvm_pair_reviewed=1 ;;
/usr/local/libexec/opencode-rust/llvm-cov:/usr/local/libexec/opencode-rust/llvm-profdata) llvm_pair_reviewed=1 ;;
*) llvm_pair_reviewed=0 ;;
esac
if [ "$llvm_pair_reviewed" != 1 ] || \
! test -x "$LLVM_COV" || ! test -x "$LLVM_PROFDATA"; then
append "### Rust coverage toolchain"
append ""
append "- Result: FAIL"
append "- Reason: the networkless coverage runtime did not preserve the reviewed LLVM 19 tool paths."
append "- Reason: the networkless coverage runtime did not preserve the reviewed LLVM tool paths (Debian LLVM 19 or the base-pinned Rust toolchain)."
append "- Fix: rebuild the trusted coverage image and preserve the exact LLVM bindings at the Docker boundary."
append ""
failures=$((failures + 1))
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.d/20260930-coverage-base-pinned-rust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Coverage sandbox installs the base-pinned Rust release

- The coverage image shipped Debian's rustc 1.85, but fast-mlsirm pins 1.97.1 and its
dependencies need at least 1.90. `maturin build --offline` failed, `_core` never imported, and
every fast-mlsirm coverage run fell to about 68% against its 100% gate, so no fast-mlsirm pull
request could reach an APPROVED review. When the base commit pins an exact `1.x.y` release in
`rust-toolchain(.toml)`, the image now installs it from a SHA-256-verified `rustup-init`
(minimal profile plus `llvm-tools-preview`) and binds Rust coverage to that release's LLVM tools.
Repositories without a pin keep the Debian toolchain, and a failed toolchain layer rebuilds the
previous image. See `docs/doctoring/opencode-rust-coverage-runtime-boundary.md`.
- The offline maturin build now also copies the wheel's compiled extension into the project's
`tool.maturin.python-source` package (`scripts/ci/place_maturin_extension.py`), as
`maturin develop` would. fast-mlsirm's pytest `pythonpath = ["python"]` imports the source tree
first, so a `_core` present only in site-packages stayed shadowed even after a successful build.
30 changes: 28 additions & 2 deletions docs/doctoring/opencode-rust-coverage-runtime-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,35 @@ declarations and the isolated container's `docker run --env` arguments. If that
workflow changes, its `REVIEW_DISPATCH_BLOB_SHA` pin must change with it; this
does not rewrite the review-agent key system.

### Base-pinned Rust releases (2026-09-30)

Debian's `rustc` (1.85) is older than the MSRV of repositories that pin a newer
release; fast-mlsirm pins 1.97.1 and its dependency graph needs at least 1.90,
so `maturin build --offline` could never build `_core` and coverage evidence
failed for every fast-mlsirm pull request. When the live-validated **base**
commit carries `rust-toolchain.toml` (or `rust-toolchain`),
`scripts/ci/resolve_base_rust_toolchain.py` selects its exact `1.x.y` channel;
`stable`, `nightly-*`, custom `path` toolchains and unparsable files select the
central pin instead. The pull-request head's pin is never read. The image build
(network allowed, before any PR content is mounted) then downloads
`rustup-init` 1.29.1 from `static.rust-lang.org`, verifies its SHA-256, and
installs only that release with `--profile minimal --component
llvm-tools-preview`; rustup checks each component against the hashes in the
channel manifest. `cargo` and `rustc` are symlinked straight to the toolchain
binaries, so no rustup proxy runs inside the `--network=none` sandbox, and the
second reviewed LLVM pair becomes:

- `LLVM_COV=/usr/local/libexec/opencode-rust/llvm-cov`
- `LLVM_PROFDATA=/usr/local/libexec/opencode-rust/llvm-profdata`

Those tools ship with the same rustc release, so they match its profile format.
Repositories without a base pin keep the Debian image unchanged, and if the
pinned layer fails to build, the job rebuilds the Debian image rather than
losing coverage for that repository.

The runtime MUST NOT fall back to unversioned `llvm-cov` or `llvm-profdata`, a
host-runner tool, a pull-request-selected path, or a dynamically downloaded LLVM
binary. Missing, changed, or non-executable reviewed paths are coverage-evidence
host-runner tool, a pull-request-selected path, or an LLVM binary downloaded at
PR runtime. Missing, changed, or non-executable reviewed paths are coverage-evidence
failures rather than reasons to measure a different toolchain.

NIST SP 800-218 PW.4.1 covers acquiring and maintaining third-party software
Expand Down
65 changes: 65 additions & 0 deletions scripts/ci/place_maturin_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Copy a built wheel's extension modules into the project's Python source tree.

Projects such as fast-mlsirm set pytest ``pythonpath = ["python"]``, so the test
suite imports ``python/<package>`` rather than the installed wheel, and an
extension that exists only in site-packages is shadowed (``cannot import name
'_core'``). This mirrors what ``maturin develop`` does: only compiled extension
members are copied, only into package directories the source tree already has.
"""

from __future__ import annotations

import pathlib
import sys
import tomllib
import zipfile

EXTENSION_SUFFIXES = (".so", ".pyd")


def _inside(path: pathlib.Path, root: pathlib.Path) -> bool:
return path == root or root in path.parents


def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]:
project = project_dir.resolve()
pyproject = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8"))
python_source = pyproject.get("tool", {}).get("maturin", {}).get("python-source", ".")
if not isinstance(python_source, str):
raise ValueError("tool.maturin.python-source must be a string")
source_root = (project / python_source).resolve()
if not _inside(source_root, project):
raise ValueError("tool.maturin.python-source escapes the project directory")

placed = []
with zipfile.ZipFile(wheel) as archive:
for member in archive.infolist():
if member.is_dir() or not member.filename.endswith(EXTENSION_SUFFIXES):
continue
target = (source_root / member.filename).resolve()
if not _inside(target, source_root):
raise ValueError(f"wheel member escapes the source tree: {member.filename}")
if not target.parent.is_dir():
continue
target.write_bytes(archive.read(member))
placed.append(target)
return placed


def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: place_maturin_extension.py WHEEL PROJECT_DIR", file=sys.stderr)
return 2
try:
placed = place(pathlib.Path(argv[0]), pathlib.Path(argv[1]))
except (OSError, ValueError, zipfile.BadZipFile, tomllib.TOMLDecodeError) as exc:
print(f"Could not place the built extension in the source tree: {exc}", file=sys.stderr)
return 1
for path in placed:
print(f"Placed built extension at {path}")
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
82 changes: 82 additions & 0 deletions scripts/ci/resolve_base_rust_toolchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Print the exact Rust release the trusted coverage image must install.

The pin is read only from the live-validated BASE commit, never from the pull
request head. An empty result keeps the image's Debian toolchain unchanged; a
base pin that is not an exact stable release (``stable``, ``nightly-*``, a
custom ``path``, or unparsable TOML) falls back to ``CENTRAL_RUST_TOOLCHAIN``.
"""

from __future__ import annotations

import argparse
import pathlib
import re
import subprocess
import sys
import tomllib

SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$")
EXACT_RELEASE_RE = re.compile(r"1\.[0-9]{1,3}\.[0-9]{1,3}")
CENTRAL_RUST_TOOLCHAIN = "1.97.1"
PIN_FILES = ("rust-toolchain.toml", "rust-toolchain")


def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None:
completed = subprocess.run(
["git", "-C", str(repo_root), "show", f"{base_sha}:{path}"],
check=False,
capture_output=True,
)
if completed.returncode != 0:
return None
return completed.stdout.decode("utf-8", errors="replace")


def _channel(content: str) -> str | None:
try:
toolchain = tomllib.loads(content).get("toolchain")
except tomllib.TOMLDecodeError:
# The legacy `rust-toolchain` file may be a bare channel line.
lines = content.split()
return lines[0] if len(lines) == 1 else None
if not isinstance(toolchain, dict) or "path" in toolchain:
return None
channel = toolchain.get("channel")
return channel if isinstance(channel, str) else None


def resolve(repo_root: pathlib.Path, base_sha: str) -> str:
if not SHA_RE.fullmatch(base_sha):
raise ValueError("base SHA must be a full 40-character commit id")
for path in PIN_FILES:
content = _base_blob(repo_root, base_sha, path)
if content is None:
continue
channel = _channel(content)
if channel and EXACT_RELEASE_RE.fullmatch(channel):
return channel
print(
f"::warning::{path} at the base SHA does not pin an exact stable Rust release; "
f"using the central {CENTRAL_RUST_TOOLCHAIN} toolchain.",
file=sys.stderr,
)
return CENTRAL_RUST_TOOLCHAIN
return ""


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", required=True, type=pathlib.Path)
parser.add_argument("--base-sha", required=True)
args = parser.parse_args(argv)
try:
print(resolve(args.repo_root, args.base_sha))
except ValueError as exc:
print(f"::error::{exc}", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading