Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/trusted-uv-materializer-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- "scripts/ci/materialize_base_python_requirements.py"
- "tests/conftest.py"
- "tests/test_materialize*.py"
- "tests/test_requirements_directory_lock_materialization.py"
- "tests/test_trusted_uv*.py"
- "tests/test_uv*.py"
- "tests/test_repository_branch_coverage_*.py"
Expand All @@ -20,6 +21,7 @@ on:
- "scripts/ci/materialize_base_python_requirements.py"
- "tests/conftest.py"
- "tests/test_materialize*.py"
- "tests/test_requirements_directory_lock_materialization.py"
- "tests/test_trusted_uv*.py"
- "tests/test_uv*.py"
- "tests/test_repository_branch_coverage_*.py"
Expand Down Expand Up @@ -126,6 +128,7 @@ jobs:
python -m coverage run -m pytest \
tests/test_materialize_base_python_requirements.py \
tests/test_materialize_uv_export_hash_contract.py \
tests/test_requirements_directory_lock_materialization.py \
tests/test_trusted_uv_download_contract.py \
tests/test_trusted_uv_portability_and_streaming.py \
tests/test_uv_export_isolation_contract.py \
Expand All @@ -152,6 +155,7 @@ jobs:
scripts/ci/materialize_base_python_requirements.py \
tests/test_materialize_base_python_requirements.py \
tests/test_materialize_uv_export_hash_contract.py \
tests/test_requirements_directory_lock_materialization.py \
tests/test_trusted_uv_download_contract.py \
tests/test_trusted_uv_portability_and_streaming.py \
tests/test_uv_export_isolation_contract.py \
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Materialize complete hash-pinned `requirements/ci.txt` and other direct `requirements/*.txt` base-owned closures so isolated OpenCode coverage imports repository runtime dependencies without trusting pull-request metadata or broadening network access.
- Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision.
- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities.
49 changes: 49 additions & 0 deletions docs/doctoring/trusted-requirements-directory-lock-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Trusted requirements-directory lock discovery

## Decision

The central OpenCode coverage image materializes dependency closures only from
regular files in the authenticated pull-request base commit. In addition to the
conventional `requirements*.txt` and `requirements.lock` names, it recognizes a
`.txt` file that is a **direct child** of a directory named `requirements`, such
as `requirements/ci.txt` or `services/scoring_service/requirements/package.txt`.

The path rule grants candidate status only. The existing content boundary still
requires nonempty hash-pinned logical requirements, records the exact trusted
source path in the manifest, and preflights each candidate as an independently
installable `pip --require-hashes` closure. Unpinned notes, input files, nested
descendants, symbolic links, pull-request-only files, and malformed Git tree
entries remain excluded.

## Operational reason

Concrete environment locks are frequently organized below a `requirements`
directory and use role names such as `ci.txt` or `package.txt`. Ignoring those
safe base-owned locks leaves isolated coverage without runtime dependencies even
when the repository maintains a complete generated closure. The resulting import
failure measures the coverage image rather than the changed production code.

## Verification

- A failing contract first proved that `requirements/ci.txt` was undiscoverable.
- Direct `requirements/*.txt` and nested-service equivalents are accepted.
- A deeper `requirements/nested/ci.txt` path and unrelated `docs/ci.txt` remain
ineligible.
- Only the hash-pinned candidate is emitted from a realistic temporary Git base;
unpinned `.in` and human-readable `.txt` files remain absent.
- The focused materializer suite, complete central suite, statement and branch
coverage, docstring gate, compilation, and exact-head security workflows are
required before merge.

## References

Python Packaging Authority. (2026). *Install requires vs requirements files*.
Python Packaging User Guide.
https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/

Python Packaging Authority. (2026). *Repeatable installs*. pip documentation.
https://pip.pypa.io/en/stable/topics/repeatable-installs/

Python Packaging Authority. (2026). *Requirements file format*. pip
documentation.
https://pip.pypa.io/en/stable/reference/requirements-file-format/
17 changes: 16 additions & 1 deletion scripts/ci/materialize_base_python_requirements.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ def _is_candidate_lock_name(name: str) -> bool:
)


def _is_candidate_lock_path(path: pathlib.PurePosixPath) -> bool:
"""Return whether one safe tracked path can name a pip requirements lock.

In addition to conventional ``requirements*.txt`` names, repositories often
keep concrete environment closures as direct children such as
``requirements/ci.txt`` or ``service/requirements/package.txt``. Only direct
``.txt`` children of a directory named ``requirements`` gain this path-based
eligibility; content must still pass the independent complete hash-pin
validation before it reaches the trusted image build context.
"""
return _is_candidate_lock_name(path.name) or (
path.suffix == ".txt" and path.parent.name == "requirements"
)


def _requirement_lines(content: bytes) -> list[str]:
"""Return logical requirement lines, joining backslash line-continuations.

Expand Down Expand Up @@ -459,7 +474,7 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b
regular_paths = {path for path, _candidate in regular_blobs}
locks: list[tuple[str, bytes]] = []
for path, candidate in regular_blobs:
if _is_candidate_lock_name(candidate.name):
if _is_candidate_lock_path(candidate):
content = _git(repo_root, "show", f"{base_sha}:{path}")
if _is_hash_pinned(content):
locks.append((path, content))
Expand Down
64 changes: 64 additions & 0 deletions tests/test_requirements_directory_lock_materialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Regression contracts for trusted locks kept in a requirements directory."""

from __future__ import annotations

import subprocess
from pathlib import Path, PurePosixPath

from scripts.ci import materialize_base_python_requirements as materializer


def _git(repo: Path, *args: str) -> str:
"""Run one deterministic Git command in a temporary fixture repository."""
return subprocess.run(
["git", "-C", str(repo), *args],
check=True,
capture_output=True,
text=True,
).stdout.strip()


def test_requirements_directory_txt_is_a_candidate_lock_path() -> None:
"""A direct ``requirements/*.txt`` lock is discoverable by its safe path."""
assert materializer._is_candidate_lock_path(PurePosixPath("requirements/ci.txt"))
assert materializer._is_candidate_lock_path(
PurePosixPath("services/scoring_service/requirements/package.txt")
)
assert not materializer._is_candidate_lock_path(
PurePosixPath("requirements/nested/ci.txt")
)
assert not materializer._is_candidate_lock_path(PurePosixPath("docs/ci.txt"))


def test_materializes_hash_pinned_requirements_directory_lock(
tmp_path: Path,
) -> None:
"""The exact base ``requirements/ci.txt`` closure reaches offline coverage."""
repo = tmp_path / "repo"
requirements_dir = repo / "requirements"
requirements_dir.mkdir(parents=True)
_git(repo, "init")
_git(repo, "config", "user.name", "Test")
_git(repo, "config", "user.email", "test@example.invalid")

(requirements_dir / "ci.txt").write_text(
"numpy==2.5.1 --hash=sha256:" + ("a" * 64) + "\n",
encoding="utf-8",
)
(requirements_dir / "ci.in").write_text("numpy>=2\n", encoding="utf-8")
(requirements_dir / "notes.txt").write_text(
"human-readable notes only\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/ci.txt"}
]
assert (output / "requirements-000.txt").read_text(encoding="utf-8").startswith(
"numpy==2.5.1"
)
Loading