diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 83f6830d5..de1c4800d 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -652,11 +652,16 @@ jobs: r-base \ r-cran-covr \ r-cran-testthat \ + llvm-19 \ rustc \ util-linux \ vulkan-tools \ xz-utils \ && rm -rf /var/lib/apt/lists/* + ENV LLVM_COV=/usr/bin/llvm-cov-19 + ENV LLVM_PROFDATA=/usr/bin/llvm-profdata-19 + RUN test -x "$LLVM_COV" + RUN test -x "$LLVM_PROFDATA" RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/node-linux-x64.tar.xz \ https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.xz \ && echo '55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742 /tmp/node-linux-x64.tar.xz' | sha256sum -c - \ @@ -769,6 +774,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 \ "$coverage_tool_image" \ /bin/bash /trusted-measure-step.sh || sandbox_status=$? @@ -1709,6 +1716,18 @@ jobs: } ensure_rust_toolchain() { + if [ "${LLVM_COV:-}" != "/usr/bin/llvm-cov-19" ] || \ + [ "${LLVM_PROFDATA:-}" != "/usr/bin/llvm-profdata-19" ] || \ + ! 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 "- Fix: rebuild the trusted coverage image and preserve the exact LLVM bindings at the Docker boundary." + append "" + failures=$((failures + 1)) + return 1 + fi if ! command -v cargo >/dev/null 2>&1; then append "### Rust coverage toolchain" append "" diff --git a/.github/workflows/opencode-rust-coverage-toolchain-quality-ci.yml b/.github/workflows/opencode-rust-coverage-toolchain-quality-ci.yml new file mode 100644 index 000000000..5e3d6c425 --- /dev/null +++ b/.github/workflows/opencode-rust-coverage-toolchain-quality-ci.yml @@ -0,0 +1,58 @@ +name: OpenCode Rust Coverage Toolchain Quality CI + +on: + pull_request: + paths: + - ".github/workflows/opencode-review-dispatch.yml" + - ".github/workflows/opencode-rust-coverage-toolchain-quality-ci.yml" + - "scripts/ci/ensure_rust_llvm19.sh" + - "tests/test_opencode_rust_coverage_toolchain_contract.py" + - "tests/test_pr_review_autofix_nvidia_nim_contract.py" + - "docs/doctoring/opencode-rust-coverage-runtime-boundary.md" + - "CHANGELOG.md" + +permissions: + contents: read + +concurrency: + group: opencode-rust-coverage-toolchain-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: quality + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact pull request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: requirements-opencode-review-ci-hashes.txt + + - name: Install exact hash-locked test tooling + run: >- + python -m pip install --disable-pip-version-check --require-hashes + -r requirements-opencode-review-ci-hashes.txt + + - name: Run permanent LLVM runtime-boundary contract + 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 + git diff --check "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" diff --git a/.github/workflows/repair-pr827-coderabbit-comments.yml b/.github/workflows/repair-pr827-coderabbit-comments.yml new file mode 100644 index 000000000..7221c4565 --- /dev/null +++ b/.github/workflows/repair-pr827-coderabbit-comments.yml @@ -0,0 +1,105 @@ +name: Repair PR 827 CodeRabbit comments + +on: + pull_request: + types: [synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: repair-pr827-coderabbit-comments + cancel-in-progress: true + +jobs: + repair: + if: >- + github.event.pull_request.number == 827 && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'fix/opencode-rust-coverage-runtime-boundary-main' && + github.event.pull_request.head.user.login != 'github-actions[bot]' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: write + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact PR branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: fix/opencode-rust-coverage-runtime-boundary-main + fetch-depth: 0 + persist-credentials: true + + - name: Set up Python 3.14 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + cache: pip + cache-dependency-path: requirements-opencode-review-ci-hashes.txt + + - name: Install exact hash-locked test tooling + run: >- + python -m pip install --disable-pip-version-check --require-hashes + -r requirements-opencode-review-ci-hashes.txt + + - name: Apply bounded non-workflow repairs + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + repair = Path('scripts/ci/repair_pr827_coderabbit_comments.py') + repair_text = repair.read_text(encoding='utf-8') + old = ' destination = output_dir / include_directory / Path(*relative_target.parts)\n' + new = ' destination = output_dir / include_directory / pathlib.Path(*relative_target.parts)\n' + if repair_text.count(old) != 1: + raise SystemExit('expected one unqualified generated Path reference') + repair.write_text(repair_text.replace(old, new, 1), encoding='utf-8') + PY + python scripts/ci/repair_pr827_coderabbit_comments.py + # The ordinary Actions token cannot update workflow files. The license + # basis is already recorded in the doctoring document, so retain the + # reviewed workflow source and publish the non-workflow repair only. + git checkout -- .github/workflows/opencode-review-dispatch.yml + rm -f scripts/ci/repair_pr827_coderabbit_comments.py + + - name: Verify materialization, coverage, docs, and syntax + run: | + set -euo pipefail + python -m pytest -q \ + tests/test_materialize_base_python_requirements.py \ + tests/test_opencode_rust_coverage_toolchain_contract.py + python -m coverage erase + python -m coverage run -m pytest tests + python -m coverage report --show-missing --fail-under=100 + python -m compileall -q scripts tests + git diff --check + + - name: Commit verified non-workflow repair + run: | + set -euo pipefail + # Restore the temporary repair driver so this commit contains only + # the reviewed product/test/doctoring changes. It is removed through + # the connector immediately after the verified push. + git checkout -- scripts/ci/repair_pr827_coderabbit_comments.py + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + scripts/ci/materialize_base_python_requirements.py \ + tests/test_materialize_base_python_requirements.py \ + CHANGELOG.md \ + docs/doctoring/opencode-rust-coverage-runtime-boundary.md + git diff --cached --check + if git diff --cached --quiet; then + echo 'No non-workflow repair changes remain; the rerun is complete.' + exit 0 + fi + git commit -m 'fix(coverage): preserve bounded requirement includes' + git push origin HEAD:fix/opencode-rust-coverage-runtime-boundary-main diff --git a/docs/doctoring/opencode-rust-coverage-runtime-boundary.md b/docs/doctoring/opencode-rust-coverage-runtime-boundary.md new file mode 100644 index 000000000..8adc68d0e --- /dev/null +++ b/docs/doctoring/opencode-rust-coverage-runtime-boundary.md @@ -0,0 +1,132 @@ +# OpenCode Rust coverage LLVM runtime boundary + +## Decision + +The trusted OpenCode coverage sandbox binds Rust coverage to the reviewed LLVM +19 executables shipped by Debian's `llvm-19` package: + +- `LLVM_COV=/usr/bin/llvm-cov-19` +- `LLVM_PROFDATA=/usr/bin/llvm-profdata-19` + +These are compatibility and trust-boundary constants, not caller-selectable +configuration. The reviewed helper `scripts/ci/ensure_rust_llvm19.sh` validates +both exact paths and fails closed unless the live `LLVM_COV` / `LLVM_PROFDATA` +values match and are executable before Rust coverage evidence is admitted. The +actual environment binding is owned by +`.github/workflows/opencode-review-dispatch.yml`, through its Dockerfile `ENV` +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. + +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 +failures rather than reasons to measure a different toolchain. + +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 +`/usr/bin/llvm-cov-19` and `/usr/bin/llvm-profdata-19` bindings are +producer-selection controls: they select reviewed paths and `test -x` verifies +executability. They do not hash or signature-verify the Debian package or binary; +package/image hashes, signatures, repository metadata, and attestations remain +separate PW.4.4 integrity controls and must not be inferred from path equality. + +## Why the boundary exists + +`cargo-llvm-cov` is a wrapper around Rust's LLVM source-based coverage and +explicitly supports `LLVM_COV` and `LLVM_PROFDATA` as path overrides. Its +current project documentation states that the LLVM tools must be compatible +with the LLVM version used by `rustc`. Allowing ambient `PATH` discovery would +therefore make a runner-image change capable of silently changing the coverage +producer. + +Debian publishes `llvm-19` from the `llvm-toolchain-19` source package; its +official copyright record states `Apache-2.0 WITH LLVM-exception`. Debian package +file inventories expose versioned LLVM 19 tool entry points including +`llvm-cov-19`. Pinning those reviewed executable names inside the image converts +ambient path selection into an explicit, testable producer contract; the Debian +copyright record supplies the package license basis, not executable integrity. + +## Trust-boundary sequence + +```mermaid +flowchart LR + A["Reviewed helper scripts/ci/ensure_rust_llvm19.sh"] --> B["Default LLVM_COV_PATH / LLVM_PROFDATA_PATH"] + B --> C["Require live LLVM_COV and LLVM_PROFDATA equality"] + 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"] +``` + +Each arrow is fail-closed. A later stage does not repair or broaden an earlier +stage's failed trust decision. + +## Security and supply-chain implications + +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. + +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 +LLVM 19. A future rustc or base-image upgrade must revalidate compatibility and +update this contract, its tests, and CHANGELOG in one reviewed change rather +than silently selecting a different binary. + +## Failure and recovery + +If the image cannot install `llvm-19`, either reviewed executable is missing or +non-executable, the runtime value differs from the literal reviewed path, or the +isolated runtime does not receive the values, Rust coverage fails closed before +`cargo llvm-cov` runs. The operator should identify whether the failure comes +from Debian package availability, the pinned image/base generation, a central +workflow regression, or an intentional Rust/LLVM compatibility change. + +Do not work around the failure by removing the exact-value check, using an +unversioned executable, adding network access to the PR runtime, or accepting a +host-provided path. A deliberate toolchain migration requires fresh authoritative +compatibility evidence and the same RED→GREEN exact-head verification sequence. + +## Verification contract + +`tests/test_opencode_rust_coverage_toolchain_contract.py` proves that: + +1. the helper defaults both reviewed LLVM 19 executable paths; +2. the helper requires live `LLVM_COV` / `LLVM_PROFDATA` equality with those + 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 + `pull_request.paths` filter resolves to a repository file, including the + helper, preventing a dangling documentation trigger from becoming + invisible debt. + +The permanent quality workflow runs on Python 3.14, checks out the exact PR head, +executes the focused contract, compiles the test, and applies `git diff --check`. +Repository security and supply-chain workflows remain separate authorities. + +## References + +Debian Project. (2026). *Package: llvm-19 (1:19.1.7-3~deb12u1), bookworm*. +Debian Packages. Retrieved August 10, 2026, from +https://packages.debian.org/bookworm/llvm-19 + +Debian Project. (2026). *File list of package llvm-19*. Debian Packages. +Retrieved August 10, 2026, from +https://packages.debian.org/bookworm/amd64/llvm-19/filelist + +Debian Project. (2026). *Copyright file for llvm-toolchain-19 19.1.7-20*. +Debian FTP Masters. Retrieved August 15, 2026, from +https://metadata.ftp-master.debian.org/changelogs/main/l/llvm-toolchain-19/llvm-toolchain-19_19.1.7-20_copyright + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development +Framework (SSDF) version 1.1: Recommendations for mitigating the risk of +software vulnerabilities* (NIST Special Publication 800-218). National +Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 + +Taiki Endo. (2026). *cargo-llvm-cov: Cargo subcommand to use LLVM source-based +code coverage*. GitHub. Retrieved August 10, 2026, from +https://github.com/taiki-e/cargo-llvm-cov diff --git a/docs/doctoring/trusted-uv-flat-include-isolation.md b/docs/doctoring/trusted-uv-flat-include-isolation.md index 1f5178aae..aa827d85f 100644 --- a/docs/doctoring/trusted-uv-flat-include-isolation.md +++ b/docs/doctoring/trusted-uv-flat-include-isolation.md @@ -1,44 +1,42 @@ -# Trusted uv flat-include isolation +# Trusted uv bounded-include materialization ## Status -Accepted on 2026-08-18 for generated base Python lock publication. +Accepted on 2026-08-20 for generated base Python lock publication. ## Buyer-facing failure -The central coverage lane renames every selected source lock to a generated flat -name such as `requirements-000.txt`. A source requirements file containing a -relative `-r` or `--requirement` directive is valid pip syntax, but pip resolves -the referenced path relative to the generated output location. Publishing only -the referrer can therefore fail a downstream repository before its own tests, -branch coverage, or docstring evidence executes. +The central coverage lane renames selected source locks to generated names such +as `requirements-000.txt`. A source requirements file containing a relative +`-r` or `--requirement` directive must keep that source-directory relationship; +publishing only the referrer can otherwise fail a downstream repository before +its own tests, branch coverage, or docstring evidence executes. ## Root cause and decision -The previous implementation conflated two authority boundaries: +The materializer separates two authority boundaries: - `_is_hash_pinned` answers whether a source file uses bounded requirements syntax, including a normalized relative include; and -- `base_hash_locks` decides whether one source blob can be copied independently - under a generated flat name. - -A bounded relative include may pass the first question while failing the second. -The materializer now keeps bounded-include syntax diagnostics unchanged but uses -`_is_flat_materializable_lock` for publication. That predicate admits only a -non-empty, standalone closure whose logical requirement lines are exact `==` -pins carrying complete SHA-256 hashes. `base_hash_locks` also uses the existing -path-aware candidate predicate, so independently complete direct `.txt` children -such as `requirements/ci.txt` and `service/requirements/package.txt` remain -eligible. +- `base_hash_locks` inventories those exact-base referrers and complete included + blobs for graph-aware publication. + +A bounded include is resolved relative to its exact base-tree parent, required to +be a regular blob containing only complete SHA-256 package pins, and written +under a preserved generated include directory. The root lock is rewritten to +that generated relative path. Missing targets, nested includes, unsafe path +components, and non-pinned leaves fail closed. Path-aware candidate discovery +continues to include direct `.txt` children such as `requirements/ci.txt` and +`service/requirements/package.txt`. ## Security and ownership boundary No URL, proxy, redirect, package index, caller-controlled header, output path, review authority, credential, or repository write scope is expanded. The fixed -GitHub Releases uv download and redirect boundary is unchanged. Relative include -publication remains fail-closed until a separately reviewed implementation can -reconstruct the complete immutable include graph, preserve source-directory -identity, rewrite every edge, and prove the resulting closure. +GitHub Releases uv download and redirect boundary is unchanged. Include edges +are read only from the exact validated base revision, remain relative only inside +the generated include subtree, and are never followed beyond one direct +hash-pinned leaf. This is a central `.github` materialization correction. Product repositories, including BandScope, retain ownership of their own requirements, tests, and @@ -49,26 +47,26 @@ to work around a generated-path defect. The regression suite proves all of the following: -1. both `-r` and `--requirement` referrers are excluded from flat publication; -2. an independently complete referenced lock remains eligible; +1. both `-r` and `--requirement` referrers are rewritten to preserved generated + include paths; +2. missing, unsafe, nested, or non-pinned included locks fail closed; 3. complete direct `.txt` children of a directory named `requirements` are discovered; and -4. empty, directive-only, standalone exact-pin, and include-only inputs exercise - both branches of the publication predicate. +4. empty, directive-only, standalone exact-pin, and bounded-include inputs + exercise both branches of the publication predicate. Merge requires the focused trusted-uv suite, complete central tests, production statement and branch coverage at 100%, complete production docstrings, Python 3.10 and current-stable compilation, exact-head security checks, and ordinary protected-branch review. A downstream repository using nested requirements -should publish one standalone hash-locked closure or wait for a graph-aware -materializer; operators must not manually copy or rename an unresolved include. +should keep each included leaf as an exact SHA-256-pinned regular base blob; +operators must not manually copy or rename an unresolved include. ## Rollback -Do not restore relative include publication. A rollback would reintroduce a -source-relative edge into a namespace that no longer preserves source location. -Restore only after a graph-aware implementation has equivalent RED fixtures, -immutable edge rewriting, closure verification, and the same security gates. +Do not weaken the exact-base path, leaf-pin, or direct-include checks. Any future +rollback must retain equivalent missing-target, unsafe-path, nested-include, and +immutable edge-rewriting fixtures with the same security gates. ## APA 7th references diff --git a/scripts/ci/ensure_rust_llvm19.sh b/scripts/ci/ensure_rust_llvm19.sh new file mode 100755 index 000000000..fb85d4aa9 --- /dev/null +++ b/scripts/ci/ensure_rust_llvm19.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Fail closed unless the reviewed LLVM 19 coverage tools are bound. +set -euo pipefail + +LLVM_COV_PATH="/usr/bin/llvm-cov-19" +LLVM_PROFDATA_PATH="/usr/bin/llvm-profdata-19" + +if [ "${LLVM_COV:-}" != "$LLVM_COV_PATH" ] || + [ "${LLVM_PROFDATA:-}" != "$LLVM_PROFDATA_PATH" ] || + ! test -x "${LLVM_COV:-}" || + ! test -x "${LLVM_PROFDATA:-}"; then + printf 'Rust coverage runtime did not preserve reviewed LLVM 19 tool paths (%s, %s).\n' \ + "$LLVM_COV_PATH" "$LLVM_PROFDATA_PATH" >&2 + exit 1 +fi diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 41b60afd8..314668438 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -166,22 +166,19 @@ def _is_candidate_lock_path(path: pathlib.PurePosixPath) -> bool: ) -def _is_bounded_requirement_include(line: str) -> bool: - """Return whether one requirements include names a bounded relative file. - - Includes are accepted only as a two-token ``-r``/``--requirement`` form - whose target is itself a candidate lock path written as a normalized - relative POSIX path. Absolute paths, ``.`` or ``..`` components, double - slashes, URLs, option-like targets, shell/Windows path separators, - fragments, queries, extra inline options or hashes, and includes of - non-lock files are rejected before a base-owned file can enter the - trusted build context. - The downstream installer still proves that the candidate is an independently - complete hash closure; this predicate grants syntax eligibility only. +def _bounded_requirement_include_target( + line: str, +) -> pathlib.PurePosixPath | None: + """Return the safe relative target of one bounded requirements include. + + The target may use any normalized relative ``.txt`` name, including names + such as ``other-hashes.txt``. Eligibility does not confer trust: the exact + base-tree target must later be a regular blob containing only exact + SHA-256-pinned package requirements. """ fields = line.split() if len(fields) != 2 or fields[0] not in {"-r", "--requirement"}: - return False + return None target = fields[1] if ( target.startswith(("-", "~")) @@ -190,16 +187,23 @@ def _is_bounded_requirement_include(line: str) -> bool: or "?" in target or "#" in target ): - return False + return None include_path = pathlib.PurePosixPath(target) - return ( - bool(include_path.parts) - and target == include_path.as_posix() - and not include_path.is_absolute() - and "." not in include_path.parts - and ".." not in include_path.parts - and _is_candidate_lock_path(include_path) - ) + if ( + not include_path.parts + or target != include_path.as_posix() + or include_path.is_absolute() + or "." in include_path.parts + or ".." in include_path.parts + or include_path.suffix != ".txt" + ): + return None + return include_path + + +def _is_bounded_requirement_include(line: str) -> bool: + """Return whether one include has a safe relative ``.txt`` target.""" + return _bounded_requirement_include_target(line) is not None def _requirement_lines(content: bytes) -> list[str]: @@ -577,7 +581,7 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b for path, candidate in regular_blobs: if _is_candidate_lock_path(candidate): content = _git(repo_root, "show", f"{base_sha}:{path}") - if _is_flat_materializable_lock(content): + if _is_hash_pinned(content): locks.append((path, content)) elif candidate.name == "uv.lock": if _uv_pyproject_path(path) not in regular_paths: @@ -588,23 +592,96 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b return sorted(locks, key=lambda item: item[0]) +def _included_base_lock_blobs( + repo_root: pathlib.Path, + base_sha: str, + source_path: str, + content: bytes, + regular_paths: set[str], +) -> list[tuple[pathlib.PurePosixPath, bytes]]: + """Load direct bounded includes from the exact base as complete closures.""" + source_parent = pathlib.PurePosixPath(source_path).parent + included: dict[pathlib.PurePosixPath, bytes] = {} + for line in _requirement_lines(content): + target = _bounded_requirement_include_target(line) + if target is None: + continue + resolved = source_parent / target + resolved_path = resolved.as_posix() + if resolved_path not in regular_paths: + raise RuntimeError( + f"bounded include {target} from {source_path} is not a regular base blob" + ) + included_content = _git(repo_root, "show", f"{base_sha}:{resolved_path}") + if not _is_flat_materializable_lock(included_content): + raise RuntimeError( + f"bounded include {resolved_path} must contain only exact SHA-256 pins" + ) + included[target] = included_content + return sorted(included.items(), key=lambda item: item[0].as_posix()) + + +def _rewrite_materialized_includes( + content: bytes, include_directory: str, source_path: str = "" +) -> bytes: + """Rewrite root include targets to their preserved generated subtree.""" + try: + text = content.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + raise RuntimeError(f"base lock {source_path} is not valid UTF-8") from exc + rewritten: list[str] = [] + for raw_line in text.splitlines(keepends=True): + body = raw_line.rstrip("\r\n") + ending = raw_line[len(body) :] + stripped = body.strip() + target = _bounded_requirement_include_target(stripped) + if target is None: + rewritten.append(raw_line) + continue + indentation = body[: len(body) - len(body.lstrip())] + option = stripped.split()[0] + rewritten.append( + f"{indentation}{option} {include_directory}/{target.as_posix()}{ending}" + ) + return "".join(rewritten).encode("utf-8") + + def materialize( repo_root: pathlib.Path, base_sha: str, output_dir: pathlib.Path, ) -> list[dict[str, str]]: - """Write base lock blobs under generated names safe for a Docker build context.""" + """Write base locks and resolvable bounded includes into a safe context.""" if output_dir.exists() and output_dir.is_symlink(): raise ValueError("output directory must not be a symlink") output_dir.mkdir(parents=True, exist_ok=True) + resolved_repo = repo_root.resolve() + entries = _git(resolved_repo, "ls-tree", "-r", "-z", "--full-tree", base_sha) + regular_paths = { + path for path, _candidate in _regular_base_blob_paths(entries) + } manifest: list[dict[str, str]] = [] for index, (source_path, content) in enumerate( - base_hash_locks(repo_root.resolve(), base_sha) + base_hash_locks(resolved_repo, base_sha) ): generated_name = f"requirements-{index:03d}.txt" + include_directory = f"includes-{index:03d}" + included = _included_base_lock_blobs( + resolved_repo, + base_sha, + source_path, + content, + regular_paths, + ) + for relative_target, included_content in included: + destination = output_dir / include_directory / pathlib.Path(*relative_target.parts) + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(included_content) destination = output_dir / generated_name - destination.write_bytes(content) + destination.write_bytes( + _rewrite_materialized_includes(content, include_directory, source_path) + ) manifest.append({"file": generated_name, "source": source_path}) (output_dir / "manifest.json").write_text( diff --git a/scripts/ci/repair_pr827_coderabbit_comments.py b/scripts/ci/repair_pr827_coderabbit_comments.py new file mode 100644 index 000000000..8b1df14cb --- /dev/null +++ b/scripts/ci/repair_pr827_coderabbit_comments.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python3 +"""Apply the verified CodeRabbit repairs for pull request 827.""" + +from __future__ import annotations + +from pathlib import Path + + +def replace_once( + path: str, old: str, new: str, *, allow_repeated: bool = False +) -> None: + """Replace one exact fragment, optionally tolerating repeated history markers.""" + file_path = Path(path) + text = file_path.read_text(encoding="utf-8") + count = text.count(old) + if count == 0 and ( + text.count(new) == 1 or (allow_repeated and text.count(new) > 0) + ): + return + if count == 0 or (count != 1 and not allow_repeated): + raise SystemExit(f"{path}: expected one replacement marker, found {count}") + file_path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def insert_before(path: str, anchor: str, addition: str) -> None: + """Insert an addition before one unique anchor, at most once.""" + file_path = Path(path) + text = file_path.read_text(encoding="utf-8") + if addition in text: + return + count = text.count(anchor) + if count != 1: + raise SystemExit(f"{path}: expected one insertion anchor, found {count}") + file_path.write_text(text.replace(anchor, addition + anchor, 1), encoding="utf-8") + + +def insert_after(path: str, anchor: str, addition: str) -> None: + """Insert an addition after one unique anchor, at most once.""" + file_path = Path(path) + text = file_path.read_text(encoding="utf-8") + if addition in text: + return + count = text.count(anchor) + if count != 1: + raise SystemExit(f"{path}: expected one insertion anchor, found {count}") + file_path.write_text(text.replace(anchor, anchor + addition, 1), encoding="utf-8") + + +def replace_between(path: str, start: str, end: str, replacement: str) -> None: + """Replace a uniquely delimited source section idempotently.""" + file_path = Path(path) + text = file_path.read_text(encoding="utf-8") + start_index = text.find(start) + if start_index < 0 and text.count(replacement) == 1: + return + if start_index < 0 or text.find(start, start_index + 1) >= 0: + raise SystemExit(f"{path}: start marker missing or ambiguous") + end_index = text.find(end, start_index) + if end_index < 0: + raise SystemExit(f"{path}: end marker missing") + file_path.write_text( + text[:start_index] + replacement + text[end_index:], encoding="utf-8" + ) + + +SCRIPT = "scripts/ci/materialize_base_python_requirements.py" +TEST = "tests/test_materialize_base_python_requirements.py" +DOC = "docs/doctoring/opencode-rust-coverage-runtime-boundary.md" +WORKFLOW = ".github/workflows/opencode-review-dispatch.yml" + +replace_between( + SCRIPT, + "def _is_bounded_requirement_include(line: str) -> bool:\n", + "def _requirement_lines(content: bytes) -> list[str]:\n", + '''def _bounded_requirement_include_target( + line: str, +) -> pathlib.PurePosixPath | None: + """Return the safe relative target of one bounded requirements include. + + The target may use any normalized relative ``.txt`` name, including names + such as ``other-hashes.txt``. Eligibility does not confer trust: the exact + base-tree target must later be a regular blob containing only exact + SHA-256-pinned package requirements. + """ + fields = line.split() + if len(fields) != 2 or fields[0] not in {"-r", "--requirement"}: + return None + target = fields[1] + if ( + target.startswith(("-", "~")) + or "\\\\" in target + or ":" in target + or "?" in target + or "#" in target + ): + return None + include_path = pathlib.PurePosixPath(target) + if ( + not include_path.parts + or target != include_path.as_posix() + or include_path.is_absolute() + or "." in include_path.parts + or ".." in include_path.parts + or include_path.suffix != ".txt" + ): + return None + return include_path + + +def _is_bounded_requirement_include(line: str) -> bool: + """Return whether one include has a safe relative ``.txt`` target.""" + return _bounded_requirement_include_target(line) is not None + + +''', +) + +replace_once( + SCRIPT, + " if _is_candidate_lock_name(candidate.name):\n", + " if _is_candidate_lock_path(candidate):\n", +) + +helpers = '''def _included_base_lock_blobs( + repo_root: pathlib.Path, + base_sha: str, + source_path: str, + content: bytes, + regular_paths: set[str], +) -> list[tuple[pathlib.PurePosixPath, bytes]]: + """Load direct bounded includes from the exact base as complete closures.""" + source_parent = pathlib.PurePosixPath(source_path).parent + included: dict[pathlib.PurePosixPath, bytes] = {} + for line in _requirement_lines(content): + target = _bounded_requirement_include_target(line) + if target is None: + continue + resolved = source_parent / target + resolved_path = resolved.as_posix() + if resolved_path not in regular_paths: + raise RuntimeError( + f"bounded include {target} from {source_path} is not a regular base blob" + ) + included_content = _git(repo_root, "show", f"{base_sha}:{resolved_path}") + if not _is_flat_materializable_lock(included_content): + raise RuntimeError( + f"bounded include {resolved_path} must contain only exact SHA-256 pins" + ) + included[target] = included_content + return sorted(included.items(), key=lambda item: item[0].as_posix()) + + +def _rewrite_materialized_includes( + content: bytes, include_directory: str, source_path: str = "" +) -> bytes: + """Rewrite root include targets to their preserved generated subtree.""" + try: + text = content.decode("utf-8", errors="strict") + except UnicodeDecodeError as exc: + raise RuntimeError(f"base lock {source_path} is not valid UTF-8") from exc + rewritten: list[str] = [] + for raw_line in text.splitlines(keepends=True): + body = raw_line.rstrip("\\r\\n") + ending = raw_line[len(body) :] + stripped = body.strip() + target = _bounded_requirement_include_target(stripped) + if target is None: + rewritten.append(raw_line) + continue + indentation = body[: len(body) - len(body.lstrip())] + option = stripped.split()[0] + rewritten.append( + f"{indentation}{option} {include_directory}/{target.as_posix()}{ending}" + ) + return "".join(rewritten).encode("utf-8") + + +''' +insert_before(SCRIPT, "def materialize(\n", helpers) + +replace_between( + SCRIPT, + "def materialize(\n", + "def main(argv: list[str] | None = None) -> int:\n", + '''def materialize( + repo_root: pathlib.Path, + base_sha: str, + output_dir: pathlib.Path, +) -> list[dict[str, str]]: + """Write base locks and resolvable bounded includes into a safe context.""" + if output_dir.exists() and output_dir.is_symlink(): + raise ValueError("output directory must not be a symlink") + output_dir.mkdir(parents=True, exist_ok=True) + + resolved_repo = repo_root.resolve() + entries = _git(resolved_repo, "ls-tree", "-r", "-z", "--full-tree", base_sha) + regular_paths = { + path for path, _candidate in _regular_base_blob_paths(entries) + } + manifest: list[dict[str, str]] = [] + for index, (source_path, content) in enumerate( + base_hash_locks(resolved_repo, base_sha) + ): + generated_name = f"requirements-{index:03d}.txt" + include_directory = f"includes-{index:03d}" + included = _included_base_lock_blobs( + resolved_repo, + base_sha, + source_path, + content, + regular_paths, + ) + for relative_target, included_content in included: + destination = output_dir / include_directory / Path(*relative_target.parts) + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(included_content) + destination = output_dir / generated_name + destination.write_bytes( + _rewrite_materialized_includes(content, include_directory, source_path) + ) + manifest.append({"file": generated_name, "source": source_path}) + + (output_dir / "manifest.json").write_text( + json.dumps(manifest, indent=2, sort_keys=True) + "\\n", + encoding="utf-8", + ) + (output_dir / "manifest.txt").write_text( + "".join(f"{entry['file']}\\n" for entry in manifest), + encoding="utf-8", + ) + return manifest + + +''', +) + +insert_before(TEST, "from pathlib import Path\n", "import zipfile\n") +replace_once( + TEST, + ' assert not materializer._is_hash_pinned(b"-r other-hashes.txt\\n")\n', + ' assert materializer._is_hash_pinned(b"-r other-hashes.txt\\n")\n', +) +insert_after( + TEST, + ' assert not materializer._is_candidate_lock_name("pyproject.toml")\n', + ' assert materializer._is_candidate_lock_path(\n' + ' materializer.pathlib.PurePosixPath("requirements/ci.txt")\n' + ' )\n' + ' assert materializer._is_candidate_lock_path(\n' + ' materializer.pathlib.PurePosixPath("service/requirements/package.txt")\n' + ' )\n' + ' assert not materializer._is_candidate_lock_path(\n' + ' materializer.pathlib.PurePosixPath("service/config/ci.txt")\n' + ' )\n', +) +insert_after( + TEST, + ' (repo / "requirements-test.txt").write_text(\n' + ' "hypothesis==6 --hash=sha256:" + ("b" * 64) + "\\n",\n' + ' encoding="utf-8",\n' + ' )\n', + ' requirements_dir = repo / "requirements"\n' + ' requirements_dir.mkdir()\n' + ' (requirements_dir / "ci.txt").write_text(\n' + ' "pytest==9 --hash=sha256:" + ("c" * 64) + "\\n",\n' + ' encoding="utf-8",\n' + ' )\n', +) +replace_once( + TEST, + ' "requirements-test.txt",\n' + ' "services/account_unification/requirements-dev.txt",\n', + ' "requirements-test.txt",\n' + ' "requirements/ci.txt",\n' + ' "services/account_unification/requirements-dev.txt",\n', +) +insert_before( + TEST, + ' between_file.write_text("START old", encoding="utf-8")\n', + ''' before_file = tmp_path / "before.txt" + before_file.write_text("ANCHOR", encoding="utf-8") + insert_before = namespace["insert_before"] + insert_before(str(before_file), "ANCHOR", "PREFIX ") # type: ignore[operator] + assert before_file.read_text(encoding="utf-8") == "PREFIX ANCHOR" + insert_before(str(before_file), "ANCHOR", "PREFIX ") # type: ignore[operator] + with pytest.raises(SystemExit, match="expected one insertion anchor"): + insert_before(str(before_file), "MISSING", "OTHER ") # type: ignore[operator] + after_file = tmp_path / "after.txt" + after_file.write_text("ANCHOR", encoding="utf-8") + insert_after = namespace["insert_after"] + insert_after(str(after_file), "ANCHOR", " SUFFIX") # type: ignore[operator] + assert after_file.read_text(encoding="utf-8") == "ANCHOR SUFFIX" + insert_after(str(after_file), "ANCHOR", " SUFFIX") # type: ignore[operator] + with pytest.raises(SystemExit, match="expected one insertion anchor"): + insert_after(str(after_file), "MISSING", " OTHER") # type: ignore[operator] +''', +) + +integration_test = '''def test_materialized_bounded_include_is_resolvable_by_pip(tmp_path: Path) -> None: + """A safe base-owned include survives flattening and pip hash preflight.""" + repo = tmp_path / "repo" + repo.mkdir() + git(repo, "init") + git(repo, "config", "user.name", "Test") + git(repo, "config", "user.email", "test@example.invalid") + + wheel_dir = tmp_path / "wheels" + wheel_dir.mkdir() + wheel = wheel_dir / "demo-1-py3-none-any.whl" + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr("demo/__init__.py", "__version__ = '1'\\n") + archive.writestr( + "demo-1.dist-info/METADATA", + "Metadata-Version: 2.1\\nName: demo\\nVersion: 1\\n", + ) + archive.writestr( + "demo-1.dist-info/WHEEL", + "Wheel-Version: 1.0\\nGenerator: TEPP-test\\n" + "Root-Is-Purelib: true\\nTag: py3-none-any\\n", + ) + archive.writestr("demo-1.dist-info/RECORD", "") + digest = hashlib.sha256(wheel.read_bytes()).hexdigest() + + (repo / "requirements.txt").write_text( + "-r other-hashes.txt\\n", encoding="utf-8" + ) + (repo / "other-hashes.txt").write_text( + f"--require-hashes\\ndemo==1 --hash=sha256:{digest}\\n", encoding="utf-8" + ) + git(repo, "add", ".") + git(repo, "commit", "-m", "base") + base_sha = git(repo, "rev-parse", "HEAD") + + output = tmp_path / "output" + manifest = materializer.materialize(repo, base_sha, output) + assert manifest == [{"file": "requirements-000.txt", "source": "requirements.txt"}] + assert (output / "requirements-000.txt").read_text(encoding="utf-8") == ( + "-r includes-000/other-hashes.txt\\n" + ) + assert (output / "includes-000" / "other-hashes.txt").is_file() + + completed = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--dry-run", + "--ignore-installed", + "--disable-pip-version-check", + "--no-index", + "--find-links", + str(wheel_dir), + "--require-hashes", + "-r", + str(output / "requirements-000.txt"), + ], + check=False, + capture_output=True, + text=True, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr + + +def test_materialization_rejects_missing_or_nested_include(tmp_path: Path) -> None: + """Includes must resolve to direct complete hash closures in the exact base.""" + repo = tmp_path / "repo" + repo.mkdir() + git(repo, "init") + git(repo, "config", "user.name", "Test") + git(repo, "config", "user.email", "test@example.invalid") + (repo / "requirements.txt").write_text("-r child.txt\\n", encoding="utf-8") + git(repo, "add", ".") + git(repo, "commit", "-m", "missing") + missing_sha = git(repo, "rev-parse", "HEAD") + with pytest.raises(RuntimeError, match="not a regular base blob"): + materializer.materialize(repo, missing_sha, tmp_path / "missing-output") + + (repo / "child.txt").write_text("-r grandchild.txt\\n", encoding="utf-8") + (repo / "grandchild.txt").write_text( + "demo==1 --hash=sha256:" + ("d" * 64) + "\\n", encoding="utf-8" + ) + git(repo, "add", ".") + git(repo, "commit", "-m", "nested") + nested_sha = git(repo, "rev-parse", "HEAD") + with pytest.raises(RuntimeError, match="must contain only exact SHA-256 pins"): + materializer.materialize(repo, nested_sha, tmp_path / "nested-output") + + with pytest.raises(RuntimeError, match="base lock requirements.txt is not valid UTF-8"): + materializer._rewrite_materialized_includes( + b"\\xff", "includes-000", "requirements.txt" + ) + + +''' +insert_before(TEST, "def test_rejects_invalid_base_sha", integration_test) +replace_once( + TEST, + ' assert not materializer._is_bounded_requirement_include("-r /abs/requirements.txt")\n', + ' assert not materializer._is_bounded_requirement_include("-r /abs/requirements.txt")\n' + ' assert not materializer._is_bounded_requirement_include("-r pyproject.toml")\n', +) +replace_once( + "tests/test_opencode_rust_coverage_toolchain_contract.py", + """ assert f'"${{LLVM_COV:-}}" != "$LLVM_COV_PATH"' in helper + assert f'"${{LLVM_PROFDATA:-}}" != "$LLVM_PROFDATA_PATH"' in helper +""", + """ assert '"${LLVM_COV:-}" != "$LLVM_COV_PATH"' in helper + assert '"${LLVM_PROFDATA:-}" != "$LLVM_PROFDATA_PATH"' in helper +""", +) +insert_after( + "tests/test_opencode_rust_coverage_toolchain_contract.py", + " for relative_path in watched_paths:\n" + " assert (_REPOSITORY_ROOT / relative_path).is_file(), relative_path\n", + """ doctoring = ( + _REPOSITORY_ROOT + / "docs/doctoring/opencode-rust-coverage-runtime-boundary.md" + ).read_text(encoding="utf-8") + assert "/usr/bin/llvm-cov-19" in doctoring + assert "/usr/bin/llvm-profdata-19" in doctoring + assert "unversioned `llvm-cov`" in doctoring + assert "fails closed" in doctoring +""", +) + +replace_once( + "CHANGELOG.md", + "- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context.\n", + "- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. Includes such as `-r other-hashes.txt` remain allowed when the exact base-tree target is a regular, complete SHA-256-pinned closure; a lone `--require-hashes` directive, dotted `./lock.txt`, traversal, absolute, URL, and option-like targets fail closed.\n", + allow_repeated=True, +) + +replace_once( + DOC, + "These are compatibility and trust-boundary constants, not caller-selectable\nconfiguration. The reviewed helper `scripts/ci/ensure_rust_llvm19.sh` binds both\nexact paths and fails closed unless the live `LLVM_COV` / `LLVM_PROFDATA`\nvalues match and are executable before Rust coverage evidence is admitted. The\nindependent OpenCode review-dispatch workflow stays byte-for-byte so the\nreview-agent key system is not rewritten to carry this runtime check.\n", + "These are compatibility and trust-boundary constants, not caller-selectable\nconfiguration. The reviewed helper `scripts/ci/ensure_rust_llvm19.sh` validates\nboth exact paths and fails closed unless the live `LLVM_COV` / `LLVM_PROFDATA`\nvalues match and are executable before Rust coverage evidence is admitted. The\nactual environment binding is owned by\n`.github/workflows/opencode-review-dispatch.yml`, through its Dockerfile `ENV`\ndeclarations and the isolated container's `docker run --env` arguments. If that\nworkflow changes, its `REVIEW_DISPATCH_BLOB_SHA` pin must change with it; this\ndoes not rewrite the review-agent key system.\n", +) +replace_once( + DOC, + "NIST SP 800-218 PW.4.1 requires third-party software to come from expected,\ntrusted sources with integrity verification (Souppaya et al., 2022). Binding\ncoverage to the reviewed `/usr/bin/llvm-cov-19` and\n`/usr/bin/llvm-profdata-19` executables is that verification; an ambient\n`PATH` lookup would treat a runner-image change as a new producer.\n", + "NIST SP 800-218 PW.4.1 covers acquiring and maintaining third-party software\nfrom expected, trusted sources and reviewing its provenance (Souppaya et al.,\n2022). PW.4.4 covers verifying the integrity of acquired components. The exact\n`/usr/bin/llvm-cov-19` and `/usr/bin/llvm-profdata-19` bindings are\nproducer-selection controls: they select reviewed paths and `test -x` verifies\nexecutability. They do not hash or signature-verify the Debian package or binary;\npackage/image hashes, signatures, repository metadata, and attestations remain\nseparate PW.4.4 integrity controls and must not be inferred from path equality.\n", +) +replace_once( + DOC, + "Debian bookworm currently publishes the versioned `llvm-19` package from\n`llvm-toolchain-19`; Debian package file inventories expose versioned LLVM 19\ntool entry points including `llvm-cov-19`. Pinning the reviewed executable names\ninside the image converts that mutable ambient dependency into an explicit\ncontract that can be checked before source execution.\n", + "Debian publishes `llvm-19` from the `llvm-toolchain-19` source package; its\nofficial copyright record states `Apache-2.0 WITH LLVM-exception`. Debian package\nfile inventories expose versioned LLVM 19 tool entry points including\n`llvm-cov-19`. Pinning those reviewed executable names inside the image converts\nambient path selection into an explicit, testable producer contract; the Debian\ncopyright record supplies the package license basis, not executable integrity.\n", +) +replace_once( + DOC, + "Debian Project. (2026). *File list of package llvm-19*. Debian Packages.\nRetrieved August 10, 2026, from\nhttps://packages.debian.org/bookworm/amd64/llvm-19/filelist\n\n", + "Debian Project. (2026). *File list of package llvm-19*. Debian Packages.\nRetrieved August 10, 2026, from\nhttps://packages.debian.org/bookworm/amd64/llvm-19/filelist\n\nDebian Project. (2026). *Copyright file for llvm-toolchain-19 19.1.7-20*.\nDebian FTP Masters. Retrieved August 15, 2026, from\nhttps://metadata.ftp-master.debian.org/changelogs/main/l/llvm-toolchain-19/llvm-toolchain-19_19.1.7-20_copyright\n\n", +) + +replace_once( + WORKFLOW, + " RUN apt-get update \\\n && apt-get install --no-install-recommends -y \\\n", + " # llvm-19 / llvm-toolchain-19: Apache-2.0 WITH LLVM-exception. See docs/doctoring/opencode-rust-coverage-runtime-boundary.md.\n" + " RUN apt-get update \\\n && apt-get install --no-install-recommends -y \\\n", +) diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 5bc56ed8f..6073dbdc1 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -1,11 +1,14 @@ from __future__ import annotations +import ast import hashlib import io import runpy +import shutil import subprocess import sys import tarfile +import zipfile from pathlib import Path import pytest @@ -125,6 +128,12 @@ def test_materializes_hash_pinned_locks_named_beyond_the_legacy_whitelist( "hypothesis==6 --hash=sha256:" + ("b" * 64) + "\n", encoding="utf-8", ) + requirements_dir = repo / "requirements" + requirements_dir.mkdir() + (requirements_dir / "ci.txt").write_text( + "pytest==9 --hash=sha256:" + ("c" * 64) + "\n", + encoding="utf-8", + ) (repo / "uv.lock").write_text( "version = 1\n[[package]]\nname = 'x'\n", encoding="utf-8" ) @@ -138,6 +147,7 @@ def test_materializes_hash_pinned_locks_named_beyond_the_legacy_whitelist( assert [entry["source"] for entry in manifest] == [ "requirements-test.txt", + "requirements/ci.txt", "services/account_unification/requirements-dev.txt", ] @@ -152,6 +162,15 @@ def test_lock_name_candidates_are_pip_requirements_files() -> None: ) assert not materializer._is_candidate_lock_name("uv.lock") assert not materializer._is_candidate_lock_name("pyproject.toml") + assert materializer._is_candidate_lock_path( + materializer.pathlib.PurePosixPath("requirements/ci.txt") + ) + assert materializer._is_candidate_lock_path( + materializer.pathlib.PurePosixPath("service/requirements/package.txt") + ) + assert not materializer._is_candidate_lock_path( + materializer.pathlib.PurePosixPath("service/config/ci.txt") + ) def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> None: @@ -160,7 +179,7 @@ def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> assert not materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") assert materializer._is_hash_pinned(b"demo==1 --hash=sha256:" + b"a" * 64 + b"\n") assert materializer._is_hash_pinned(b"-r requirements-other.txt\n") - assert not materializer._is_hash_pinned(b"-r other-hashes.txt\n") + assert materializer._is_hash_pinned(b"-r other-hashes.txt\n") assert not materializer._is_hash_pinned(b"-r ./requirements-other.txt\n") assert not materializer._is_hash_pinned(b"-r ../escape.txt\n") assert materializer._is_bounded_requirement_include( @@ -175,6 +194,7 @@ def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> assert not materializer._is_bounded_requirement_include(r"-r foo\\bar.txt") assert not materializer._is_bounded_requirement_include("-r") assert not materializer._is_bounded_requirement_include("-r /abs/requirements.txt") + assert not materializer._is_bounded_requirement_include("-r pyproject.toml") assert not materializer._is_hash_pinned(b"untrusted==1\n") # uv export / pip-compile multi-line continuation format (spec, then --hash= lines). assert materializer._is_hash_pinned( @@ -186,6 +206,210 @@ def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> ) +def test_materialized_bounded_include_is_resolvable_by_pip(tmp_path: Path) -> None: + """A safe base-owned include survives flattening and pip hash preflight.""" + repo = tmp_path / "repo" + repo.mkdir() + git(repo, "init") + git(repo, "config", "user.name", "Test") + git(repo, "config", "user.email", "test@example.invalid") + + wheel_dir = tmp_path / "wheels" + wheel_dir.mkdir() + wheel = wheel_dir / "demo-1-py3-none-any.whl" + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr("demo/__init__.py", "__version__ = '1'\n") + archive.writestr( + "demo-1.dist-info/METADATA", + "Metadata-Version: 2.1\nName: demo\nVersion: 1\n", + ) + archive.writestr( + "demo-1.dist-info/WHEEL", + "Wheel-Version: 1.0\nGenerator: TEPP-test\n" + "Root-Is-Purelib: true\nTag: py3-none-any\n", + ) + archive.writestr("demo-1.dist-info/RECORD", "") + digest = hashlib.sha256(wheel.read_bytes()).hexdigest() + + (repo / "requirements.txt").write_text( + "-r other-hashes.txt\n", encoding="utf-8" + ) + (repo / "other-hashes.txt").write_text( + f"--require-hashes\ndemo==1 --hash=sha256:{digest}\n", encoding="utf-8" + ) + git(repo, "add", ".") + git(repo, "commit", "-m", "base") + base_sha = git(repo, "rev-parse", "HEAD") + + output = tmp_path / "output" + manifest = materializer.materialize(repo, base_sha, output) + assert manifest == [{"file": "requirements-000.txt", "source": "requirements.txt"}] + assert (output / "requirements-000.txt").read_text(encoding="utf-8") == ( + "-r includes-000/other-hashes.txt\n" + ) + assert (output / "includes-000" / "other-hashes.txt").is_file() + + completed = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--dry-run", + "--ignore-installed", + "--disable-pip-version-check", + "--no-index", + "--find-links", + str(wheel_dir), + "--require-hashes", + "-r", + str(output / "requirements-000.txt"), + ], + check=False, + capture_output=True, + text=True, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr + + +def test_materialization_rejects_missing_or_nested_include(tmp_path: Path) -> None: + """Includes must resolve to direct complete hash closures in the exact base.""" + repo = tmp_path / "repo" + repo.mkdir() + git(repo, "init") + git(repo, "config", "user.name", "Test") + git(repo, "config", "user.email", "test@example.invalid") + (repo / "requirements.txt").write_text("-r child.txt\n", encoding="utf-8") + git(repo, "add", ".") + git(repo, "commit", "-m", "missing") + missing_sha = git(repo, "rev-parse", "HEAD") + with pytest.raises(RuntimeError, match="not a regular base blob"): + materializer.materialize(repo, missing_sha, tmp_path / "missing-output") + + (repo / "child.txt").write_text("-r grandchild.txt\n", encoding="utf-8") + (repo / "grandchild.txt").write_text( + "demo==1 --hash=sha256:" + ("d" * 64) + "\n", encoding="utf-8" + ) + git(repo, "add", ".") + git(repo, "commit", "-m", "nested") + nested_sha = git(repo, "rev-parse", "HEAD") + with pytest.raises(RuntimeError, match="must contain only exact SHA-256 pins"): + materializer.materialize(repo, nested_sha, tmp_path / "nested-output") + + with pytest.raises(RuntimeError, match="base lock requirements.txt is not valid UTF-8"): + materializer._rewrite_materialized_includes( + b"\xff", "includes-000", "requirements.txt" + ) + + +def test_bounded_repair_driver_runs_against_a_staged_fixture( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The one-shot repair driver applies every guarded edit in isolation.""" + repository_root = Path(__file__).parents[1] + relative_files = ( + "scripts/ci/repair_pr827_coderabbit_comments.py", + "scripts/ci/materialize_base_python_requirements.py", + "tests/test_materialize_base_python_requirements.py", + "tests/test_opencode_rust_coverage_toolchain_contract.py", + "docs/doctoring/opencode-rust-coverage-runtime-boundary.md", + ".github/workflows/opencode-review-dispatch.yml", + "CHANGELOG.md", + ) + for relative_file in relative_files: + source = repository_root / relative_file + destination = tmp_path / relative_file + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + + changelog = tmp_path / "CHANGELOG.md" + changelog.write_text( + changelog.read_text(encoding="utf-8").replace( + "## [Unreleased]\n", + "## [Unreleased]\n\n" + "- Materialized base Python locks only when every package line is an exact " + "SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone " + "`--require-hashes` directive, a dotted include such as `./lock.txt`, or " + "`-r other-hashes.txt` no longer enters the trusted build context.\n", + 1, + ), + encoding="utf-8", + ) + + monkeypatch.chdir(tmp_path) + runpy.run_path( + str(repository_root / "scripts/ci/repair_pr827_coderabbit_comments.py"), + run_name="__main__", + ) + + materializer_source = ( + tmp_path / "scripts/ci/materialize_base_python_requirements.py" + ).read_text(encoding="utf-8") + assert "def _bounded_requirement_include_target(" in materializer_source + assert "def _included_base_lock_blobs(" in materializer_source + assert "includes-000/" in ( + tmp_path / "tests/test_materialize_base_python_requirements.py" + ).read_text(encoding="utf-8") + assert "Apache-2.0 WITH LLVM-exception" in ( + tmp_path / "docs/doctoring/opencode-rust-coverage-runtime-boundary.md" + ).read_text(encoding="utf-8") + + script_path = repository_root / "scripts/ci/repair_pr827_coderabbit_comments.py" + tree = ast.parse(script_path.read_text(encoding="utf-8"), filename=str(script_path)) + definitions = [node for node in tree.body if isinstance(node, ast.FunctionDef)] + namespace: dict[str, object] = {"Path": Path} + exec( + compile( + ast.fix_missing_locations(ast.Module(body=definitions, type_ignores=[])), + str(script_path), + "exec", + ), + namespace, + ) + replace_once = namespace["replace_once"] + replace_between = namespace["replace_between"] + once_file = tmp_path / "once.txt" + once_file.write_text("old", encoding="utf-8") + replace_once(str(once_file), "old", "new") # type: ignore[operator] + assert once_file.read_text(encoding="utf-8") == "new" + with pytest.raises(SystemExit, match="expected one replacement marker"): + replace_once(str(once_file), "missing", "other") # type: ignore[operator] + repeated_file = tmp_path / "repeated.txt" + repeated_file.write_text("oldold", encoding="utf-8") + replace_once( # type: ignore[operator] + str(repeated_file), "old", "new", allow_repeated=True + ) + assert repeated_file.read_text(encoding="utf-8") == "newold" + + between_file = tmp_path / "between.txt" + between_file.write_text("START old END", encoding="utf-8") + replace_between(str(between_file), "START", "END", "START new ") # type: ignore[operator] + assert between_file.read_text(encoding="utf-8") == "START new END" + replace_between(str(between_file), "MISSING", "END", "START new ") # type: ignore[operator] + before_file = tmp_path / "before.txt" + before_file.write_text("ANCHOR", encoding="utf-8") + insert_before = namespace["insert_before"] + insert_before(str(before_file), "ANCHOR", "PREFIX ") # type: ignore[operator] + assert before_file.read_text(encoding="utf-8") == "PREFIX ANCHOR" + insert_before(str(before_file), "ANCHOR", "PREFIX ") # type: ignore[operator] + with pytest.raises(SystemExit, match="expected one insertion anchor"): + insert_before(str(before_file), "MISSING", "OTHER ") # type: ignore[operator] + after_file = tmp_path / "after.txt" + after_file.write_text("ANCHOR", encoding="utf-8") + insert_after = namespace["insert_after"] + insert_after(str(after_file), "ANCHOR", " SUFFIX") # type: ignore[operator] + assert after_file.read_text(encoding="utf-8") == "ANCHOR SUFFIX" + insert_after(str(after_file), "ANCHOR", " SUFFIX") # type: ignore[operator] + with pytest.raises(SystemExit, match="expected one insertion anchor"): + insert_after(str(after_file), "MISSING", " OTHER") # type: ignore[operator] + between_file.write_text("START old START END", encoding="utf-8") + with pytest.raises(SystemExit, match="start marker missing or ambiguous"): + replace_between(str(between_file), "START", "END", "replacement") # type: ignore[operator] + between_file.write_text("START old", encoding="utf-8") + with pytest.raises(SystemExit, match="end marker missing"): + replace_between(str(between_file), "START", "END", "replacement") # type: ignore[operator] + + def test_rejects_invalid_base_sha(tmp_path: Path) -> None: """Git options and symbolic refs cannot cross the exact-SHA boundary.""" with pytest.raises(ValueError, match="40 hexadecimal"): diff --git a/tests/test_opencode_rust_coverage_toolchain_contract.py b/tests/test_opencode_rust_coverage_toolchain_contract.py new file mode 100644 index 000000000..b1fd4a124 --- /dev/null +++ b/tests/test_opencode_rust_coverage_toolchain_contract.py @@ -0,0 +1,210 @@ +"""Permanent contract for the trusted Rust LLVM coverage toolchain.""" + +from __future__ import annotations + +import os +import re +import stat +import subprocess +from pathlib import Path + +import pytest + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_HELPER_PATH = _REPOSITORY_ROOT / "scripts/ci/ensure_rust_llvm19.sh" +_DISPATCH_WORKFLOW_PATH = ( + _REPOSITORY_ROOT / ".github/workflows/opencode-review-dispatch.yml" +) +_QUALITY_WORKFLOW_PATH = ( + _REPOSITORY_ROOT / ".github/workflows/opencode-rust-coverage-toolchain-quality-ci.yml" +) +_NIM_CONTRACT_PATH = ( + _REPOSITORY_ROOT / "tests/test_pr_review_autofix_nvidia_nim_contract.py" +) +_LLVM_COV_PATH = "/usr/bin/llvm-cov-19" +_LLVM_PROFDATA_PATH = "/usr/bin/llvm-profdata-19" +_BLOB_SHA_PATTERN = re.compile( + r'^REVIEW_DISPATCH_BLOB_SHA = "([0-9a-f]{40})"$', + re.MULTILINE, +) + + +def _helper_text() -> str: + """Return the reviewed LLVM 19 runtime-boundary helper.""" + + return _HELPER_PATH.read_text(encoding="utf-8") + + +def _dispatch_text() -> str: + """Return the trusted coverage-image and sandbox workflow.""" + + return _DISPATCH_WORKFLOW_PATH.read_text(encoding="utf-8") + + +def _run_helper( + env: dict[str, str], +) -> subprocess.CompletedProcess[str]: + """Execute the helper with a caller-supplied environment.""" + + return subprocess.run( + ["bash", str(_HELPER_PATH)], + check=False, + capture_output=True, + text=True, + env=env, + ) + + +def test_trusted_coverage_image_provisions_verified_llvm_19_tools() -> None: + """Require Debian llvm-19 and build-time executability in trusted source.""" + + dispatch = _dispatch_text() + assert " llvm-19 \\\n" in dispatch + assert f"ENV LLVM_COV={_LLVM_COV_PATH}\n" in dispatch + assert f"ENV LLVM_PROFDATA={_LLVM_PROFDATA_PATH}\n" in dispatch + assert 'RUN test -x "$LLVM_COV"\n' in dispatch + assert 'RUN test -x "$LLVM_PROFDATA"\n' in dispatch + + +def test_isolated_runtime_receives_reviewed_llvm_constants() -> None: + """Require exact LLVM 19 path constants at the Docker sandbox boundary.""" + + dispatch = _dispatch_text() + helper = _helper_text() + assert f" --env LLVM_COV={_LLVM_COV_PATH} \\\n" in dispatch + assert f" --env LLVM_PROFDATA={_LLVM_PROFDATA_PATH} \\\n" in dispatch + assert _LLVM_COV_PATH in helper + assert _LLVM_PROFDATA_PATH in helper + assert "unversioned" not in helper + assert "${LLVM_COV_PATH:-" not in helper + assert "${LLVM_PROFDATA_PATH:-" not in helper + + +def test_isolated_runtime_revalidates_llvm_tools_before_coverage() -> None: + """Require the trusted toolchain guard to fail closed before cargo llvm-cov.""" + + dispatch = _dispatch_text() + helper = _helper_text() + assert f'[ "${{LLVM_COV:-}}" != "{_LLVM_COV_PATH}" ]' in dispatch + assert f'[ "${{LLVM_PROFDATA:-}}" != "{_LLVM_PROFDATA_PATH}" ]' in dispatch + assert 'test -x "$LLVM_COV"' in dispatch + assert 'test -x "$LLVM_PROFDATA"' in dispatch + assert "networkless coverage runtime did not preserve the reviewed LLVM 19" in dispatch + assert f'LLVM_COV_PATH="{_LLVM_COV_PATH}"' in helper + assert f'LLVM_PROFDATA_PATH="{_LLVM_PROFDATA_PATH}"' in helper + assert '"${LLVM_COV:-}" != "$LLVM_COV_PATH"' in helper + assert '"${LLVM_PROFDATA:-}" != "$LLVM_PROFDATA_PATH"' in helper + assert "exit 1" in helper + + +def test_quality_workflow_watches_the_trusted_dispatch_workflow() -> None: + """Guard drift in the hashed review-dispatch blob must retrigger this contract.""" + + quality_workflow = _QUALITY_WORKFLOW_PATH.read_text(encoding="utf-8") + assert ( + ' - ".github/workflows/opencode-review-dispatch.yml"\n' in quality_workflow + ) + assert ( + ' - "tests/test_pr_review_autofix_nvidia_nim_contract.py"\n' + in quality_workflow + ) + + +def test_review_dispatch_blob_sha_stays_paired_with_trusted_workflow() -> None: + """A dispatch.yml rewrite must update the independent review-dispatch blob pin.""" + + nim_contract = _NIM_CONTRACT_PATH.read_text(encoding="utf-8") + match = _BLOB_SHA_PATTERN.search(nim_contract) + assert match is not None + hashed = subprocess.run( + ["git", "hash-object", str(_DISPATCH_WORKFLOW_PATH)], + check=True, + capture_output=True, + text=True, + ) + assert hashed.stdout.strip() == match.group(1) + + +def test_quality_workflow_watched_paths_resolve_to_repository_files() -> None: + """Every watched path and its documented runtime contract must exist.""" + + quality_workflow = _QUALITY_WORKFLOW_PATH.read_text(encoding="utf-8") + watched_section = quality_workflow.split(" paths:\n", 1)[1].split( + "\n\npermissions:\n", 1 + )[0] + watched_paths = [ + line.strip()[2:].strip('"') + for line in watched_section.splitlines() + if line.strip().startswith("- ") + ] + + assert watched_paths + assert ".github/workflows/opencode-review-dispatch.yml" in watched_paths + assert "tests/test_pr_review_autofix_nvidia_nim_contract.py" in watched_paths + for relative_path in watched_paths: + assert (_REPOSITORY_ROOT / relative_path).is_file(), relative_path + doctoring = ( + _REPOSITORY_ROOT + / "docs/doctoring/opencode-rust-coverage-runtime-boundary.md" + ).read_text(encoding="utf-8") + assert "/usr/bin/llvm-cov-19" in doctoring + assert "/usr/bin/llvm-profdata-19" in doctoring + assert "unversioned `llvm-cov`" in doctoring + assert "fails closed" in doctoring + + +def test_helper_fails_closed_when_reviewed_paths_are_unbound() -> None: + """A coverage runtime without the reviewed LLVM env cannot produce evidence.""" + + result = _run_helper({"PATH": os.environ.get("PATH", "/usr/bin")}) + + assert result.returncode == 1 + assert _LLVM_COV_PATH in result.stderr + assert _LLVM_PROFDATA_PATH in result.stderr + + +def test_helper_fails_closed_when_caller_overrides_the_reviewed_paths( + tmp_path: Path, +) -> None: + """Caller-selected LLVM_COV_PATH values cannot retarget the reviewed tools.""" + + decoy = tmp_path / "llvm-cov-decoy" + decoy.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + decoy.chmod(decoy.stat().st_mode | stat.S_IXUSR) + result = _run_helper( + { + "PATH": os.environ.get("PATH", "/usr/bin"), + "LLVM_COV": str(decoy), + "LLVM_PROFDATA": str(decoy), + "LLVM_COV_PATH": str(decoy), + "LLVM_PROFDATA_PATH": str(decoy), + } + ) + + assert result.returncode == 1 + assert _LLVM_COV_PATH in result.stderr + + +@pytest.mark.skipif( + not Path(_LLVM_COV_PATH).is_file() or not Path(_LLVM_PROFDATA_PATH).is_file(), + reason="reviewed LLVM 19 tools are not installed on this host", +) +def test_helper_admits_the_reviewed_llvm_19_tools_when_present() -> None: + """The helper accepts only the reviewed executable paths when they exist.""" + + if not os.access(_LLVM_COV_PATH, os.X_OK) or not os.access( + _LLVM_PROFDATA_PATH, os.X_OK + ): + pytest.skip("reviewed LLVM 19 tools are not executable on this host") + + result = _run_helper( + { + "PATH": os.environ.get("PATH", "/usr/bin"), + "LLVM_COV": _LLVM_COV_PATH, + "LLVM_PROFDATA": _LLVM_PROFDATA_PATH, + } + ) + + assert result.returncode == 0 + assert result.stderr == "" diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 1bbd98750..335bfa13d 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -20,7 +20,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 = "83f6830d5c21a324b4dbcd4e5c21a07968994b81" +REVIEW_DISPATCH_BLOB_SHA = "de1c4800d362bda4a90c31a0c8e39687a782afb4" def _workflow_text(path: Path) -> str: diff --git a/tests/test_uv_flat_lock_publication_boundary.py b/tests/test_uv_flat_lock_publication_boundary.py index 6ef1ac2f0..8345c5745 100644 --- a/tests/test_uv_flat_lock_publication_boundary.py +++ b/tests/test_uv_flat_lock_publication_boundary.py @@ -34,12 +34,12 @@ def test_flat_materializable_lock_requires_a_standalone_exact_closure( @pytest.mark.parametrize("directive", ["-r", "--requirement"]) -def test_flat_publication_excludes_relative_include_referrers( +def test_materialization_preserves_relative_include_referrers( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, directive: str, ) -> None: - """A generated flat name cannot preserve a source-relative include edge.""" + """A bounded source-relative include is retained for graph-aware materialization.""" tree = ( b"100644 blob " + (b"0" * 40) @@ -62,7 +62,8 @@ def fake_git(_repo_root: Path, *args: str) -> bytes: monkeypatch.setattr(materializer, "_git", fake_git) assert materializer.base_hash_locks(tmp_path, "a" * 40) == [ - ("requirements-other.txt", target_lock) + ("requirements-other.txt", target_lock), + ("requirements.txt", f"{directive} requirements-other.txt\n".encode()), ] @@ -70,7 +71,7 @@ def test_flat_publication_discovers_standalone_requirements_directory_locks( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Path-aware discovery keeps complete direct requirements-directory locks.""" + """Path-aware discovery keeps standalone and bounded include locks.""" tree = ( b"100644 blob " + (b"0" * 40) @@ -101,6 +102,7 @@ def fake_git(_repo_root: Path, *args: str) -> bytes: monkeypatch.setattr(materializer, "_git", fake_git) assert materializer.base_hash_locks(tmp_path, "a" * 40) == [ + ("requirements.txt", b"-r requirements/ci.txt\n"), ("requirements/ci.txt", ci_lock), ("service/requirements/package.txt", service_lock), ]