diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 545a2ad7e..3bc1ce6d3 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -726,8 +726,82 @@ jobs: --only-binary=:all: \ -r /tmp/requirements-opencode-review-ci-hashes.txt \ && rm -f /tmp/requirements-opencode-review-ci-hashes.txt - COPY install-base-python-locks.py /usr/local/libexec/install-base-python-locks.py COPY base-python-requirements /tmp/base-python-requirements + RUN set -eu; \ + mkdir -p /opt/base-vcs-dependencies; \ + site_packages="$(python3 -c 'import site; print(site.getsitepackages()[0])')"; \ + path_file="${site_packages}/opencode-base-vcs-dependencies.pth"; \ + : >"$path_file"; \ + dependency_index=0; \ + dependency_list=/tmp/base-vcs-dependencies.tsv; \ + jq -r '.[] | [.import_name, .repository, .commit] | @tsv' \ + /tmp/base-python-requirements/vcs-manifest.json >"$dependency_list"; \ + while IFS="$(printf '\t')" read -r import_name repository commit; do \ + destination="$(printf '/opt/base-vcs-dependencies/dependency-%03d' "$dependency_index")"; \ + git init --quiet "$destination"; \ + git -C "$destination" remote add origin \ + "https://github.com/ContextualWisdomLab/${repository}.git"; \ + git -C "$destination" -c protocol.version=2 fetch \ + --quiet --no-tags --depth=1 origin "$commit"; \ + test "$(git -C "$destination" rev-parse FETCH_HEAD)" = "$commit"; \ + git -C "$destination" checkout --quiet --detach FETCH_HEAD; \ + test "$(git -C "$destination" rev-parse HEAD)" = "$commit"; \ + rm -rf -- "$destination/.git"; \ + import_root=''; \ + python_root=''; \ + candidate_count=0; \ + for candidate in \ + "$destination/src/$import_name" \ + "$destination/src/$import_name.py" \ + "$destination/$import_name" \ + "$destination/$import_name.py"; do \ + if [ -e "$candidate" ] || [ -L "$candidate" ]; then \ + import_root="$candidate"; \ + candidate_count=$((candidate_count + 1)); \ + fi; \ + done; \ + if [ "$candidate_count" -ne 1 ]; then \ + printf 'locked VCS source %s has a missing or ambiguous import root for %s\n' \ + "$repository" "$import_name" >&2; \ + exit 1; \ + fi; \ + if [ -L "$import_root" ] \ + || { [ -d "$import_root" ] \ + && { [ ! -f "$import_root/__init__.py" ] \ + || [ -L "$import_root/__init__.py" ]; }; }; then \ + printf 'locked VCS source %s has a namespace or linked import root for %s\n' \ + "$repository" "$import_name" >&2; \ + exit 1; \ + fi; \ + if find "$destination" -type l -print -quit | grep -q .; then \ + printf 'locked VCS source %s contains a symbolic-link layout\n' \ + "$repository" >&2; \ + exit 1; \ + fi; \ + if find "$destination" -type f \ + \( -name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.dylib' \) \ + -print -quit | grep -q .; then \ + printf 'locked VCS source %s contains a compiled extension\n' \ + "$repository" >&2; \ + exit 1; \ + fi; \ + if find "$destination" -type d \ + \( -name '*.dist-info' -o -name '*.egg-info' \) \ + -print -quit | grep -q .; then \ + printf 'locked VCS source %s contains installed distribution metadata\n' \ + "$repository" >&2; \ + exit 1; \ + fi; \ + case "$import_root" in \ + "$destination/src/"*) python_root="$destination/src" ;; \ + *) python_root="$destination" ;; \ + esac; \ + printf '%s\n' "$python_root" >>"$path_file"; \ + dependency_index=$((dependency_index + 1)); \ + done <"$dependency_list"; \ + rm -f -- "$dependency_list"; \ + chmod -R a+rX /opt/base-vcs-dependencies "$path_file" + COPY install-base-python-locks.py /usr/local/libexec/install-base-python-locks.py RUN python3 -I /usr/local/libexec/install-base-python-locks.py \ --requirements-root /tmp/base-python-requirements \ && rm -rf /tmp/base-python-requirements \ diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 123f5c90a..697038d1c 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -485,14 +485,14 @@ jobs: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }} TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }} TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.base_branch }} - SCHEDULER_ACTIONS_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }} + SCHEDULER_ACTIONS_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.target_repository != github.repository && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }} # Same-repository dispatch credential: when this scheduler runs inside # ContextualWisdomLab/.github (the repository the required workflows are # dispatched on), the runner token can dispatch them without any # cross-repository PAT. The scheduler only uses it when # GITHUB_REPOSITORY equals the dispatch repository. SCHEDULER_DISPATCH_TOKEN: ${{ github.token }} - SCHEDULER_READ_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }} + SCHEDULER_READ_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.target_repository != github.repository && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }} SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.scheduler_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }} SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }} diff --git a/AGENTS.md b/AGENTS.md index 982d24ece..4e906c47c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,11 @@ > **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. -Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md). +Materialize accepts only exact SHA-256 pins, a bounded relative `-r` include +(no `.`/`..`), or an organization-owned HTTPS Git source pinned to a full +commit and exposed without running build hooks; a lone `--require-hashes` +directive is not trust evidence. See +[`docs/doctoring/opencode-exact-vcs-dependency-evidence.md`](docs/doctoring/opencode-exact-vcs-dependency-evidence.md). Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 898a7da77..fe33f5d4f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -142,6 +142,14 @@ CI installs Python tools only with `pip install --require-hashes`. Contract tests pin workflow structure and governance prose so drift fails closed. The trusted `uv` exporter is downloaded from the literal GitHub Releases URL for `uv` 0.12.1; `releases.astral.sh` is not the network sink. +An exact-base `uv.lock` may additionally expose source from an organization-owned +GitHub repository pinned to a full commit: the secret-free image build verifies +the fetched revision and makes its source importable without running package +build or installation hooks. Pull-request execution remains networkless. +Root-level lock files are independent environments unless an explicit include +relationship says otherwise; only one unambiguous two-file supplement pair may +be recovered together, so unrelated toolchains cannot create a synthetic +resolver conflict. ## Related durable documents @@ -157,6 +165,8 @@ trusted `uv` exporter is downloaded from the literal GitHub Releases URL for — current increment's repair-worker decision and APA 7th citations. - [`docs/doctoring/opencode-llm-review-publication.md`](docs/doctoring/opencode-llm-review-publication.md) — LLM probe publication without inventing observed proof. +- [`docs/doctoring/opencode-exact-vcs-dependency-evidence.md`](docs/doctoring/opencode-exact-vcs-dependency-evidence.md) + — import-only exact source dependencies for networkless coverage. - [`docs/doctoring/fast-mlsirm-hourly-review-caller.md`](docs/doctoring/fast-mlsirm-hourly-review-caller.md) — product-specific psychometric repair heartbeat and scientific gates. - [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ba60e64..47c14f765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Fix OpenCode coverage evidence for exact-base, organization-owned Python VCS + dependencies without weakening registry hashes or the networkless PR sandbox, + reject namespace, ambiguous, linked, native-extension, and installed-metadata + layouts, and make exact roots readable by the unprivileged coverage user. + ### Added - Added an hourly organization commercial-readiness coordinator that discovers writable repositories, honors enabled dedicated writer leases and fully paginated live writer runs, refetches exact repository/workflow/run/PR state before dispatch, rotates bounded review-repair and opt-in NVIDIA OpenCode product-development targets, fails nonzero on fleet-wide inspection or dispatch outages, retains three-day JSON receipts, and keeps the existing 15-minute merge scheduler authoritative. @@ -40,6 +45,18 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Used the receiving repository's workflow token for same-repository scheduler + Actions inventory and read calls, while retaining the established mutation + credential chain. An exhausted organization-wide OpenCode App installation + budget can no longer prevent a central `.github` PR from dispatching its + exact-head review; cross-repository targets still require an explicit + credential. +- Kept independently valid root-level Python lock environments separate during + trusted base coverage installation. A directory with more than two candidate + locks no longer collapses unrelated OpenCode, security, and application + environments into one impossible resolver transaction; incomplete hash + closures remain skipped, while each complete hash-pinned closure installs + independently. - Rotated `org-queue-sweep`'s repository walk order by the workflow's own run number before applying the shared organization-wide review-dispatch/branch-update budget, so a fixed early repository in the unsorted `gh api /orgs/{org}/repos` walk order can no longer permanently starve every later repository's ready, all-green, zero-open-thread pull requests of the single per-tick dispatch (`ContextualWisdomLab/.github#1219`). The total per-tick budget is unchanged; only which repository consumes it rotates. - Forward `trigger_reviews=true` explicitly from the trusted OpenCode mention wrapper to the authoritative scheduler while retaining GitHub's ten-key dispatch limit. Source-comment identity remains bound in the verified invocation claim and durable ledger instead of occupying an unused scheduler field, so a successfully routed `@opencode-agent` request now dispatches review work rather than entering queue maintenance with reviews disabled. - Allowed an allowlisted base repository's open fork-head PR to enter the central exact-head OpenCode review path. The scheduler and privileged reviewer still re-read the live PR, bind base/head refs and SHAs, reject malformed repository identities, keep fork source as untrusted data, preserve the existing maintainer-writable update rule, and reserve the final external-head merge for a maintainer. diff --git a/docs/doctoring/fork-head-review-dispatch.md b/docs/doctoring/fork-head-review-dispatch.md index dddef9c20..fe326459f 100644 --- a/docs/doctoring/fork-head-review-dispatch.md +++ b/docs/doctoring/fork-head-review-dispatch.md @@ -40,6 +40,31 @@ repository match, exact-head revalidation before OIDC/model work, canonical head-repository validation, and the absence of the former same-repository guard. +## Same-repository credential isolation + +Targeted scheduler runs `32556458196` and `32556607016` revalidated +`.github#1210` at the exact current head, then stopped before review dispatch +because their general Actions inventory read used an exhausted organization-wide +OpenCode App installation token. The scheduler already carried the receiving +repository's `github.token`, but selected the App for every targeted dispatch, +including the same repository. + +Same-repository `SCHEDULER_ACTIONS_TOKEN` and `SCHEDULER_READ_TOKEN` now use the +job-scoped `github.token`. Cross-repository reads retain the explicit PAT/App +chain, and `GH_TOKEN` retains the existing mutation chain, so this does not +grant the workflow token sibling-repository access or make it authoritative for +branch mutation. This separates the repository-local rate-limit bucket from the +shared App installation bucket and follows GitHub's documented authentication +rate-limit scopes (GitHub, Inc., n.d.-c). The existing +`SCHEDULER_DISPATCH_TOKEN` remains the repository token because GitHub explicitly +permits `repository_dispatch` created with `GITHUB_TOKEN` to start a workflow +(GitHub, Inc., n.d.-d). + +The static regression requires both read and Actions-control expressions to +distinguish a same-repository target from a cross-repository target. The full +Python suite, 100% statement/branch/docstring gates, and the CI-budget Strix +shell gate remain authoritative before publication. + ## APA 7th references GitHub, Inc. (n.d.-a). *REST API endpoints for pull requests*. GitHub Docs. @@ -50,6 +75,13 @@ GitHub, Inc. (n.d.-b). *Secure use reference*. GitHub Docs. Retrieved August 22, 2026, from https://docs.github.com/en/actions/reference/security/secure-use +GitHub, Inc. (n.d.-c). *Rate limits for the REST API*. GitHub Docs. Retrieved +August 22, 2026, from +https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api + +GitHub, Inc. (n.d.-d). *GITHUB_TOKEN*. GitHub Docs. Retrieved August 22, 2026, +from https://docs.github.com/en/actions/concepts/security/github_token + 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 diff --git a/docs/doctoring/opencode-exact-vcs-dependency-evidence.md b/docs/doctoring/opencode-exact-vcs-dependency-evidence.md new file mode 100644 index 000000000..a9b26ae47 --- /dev/null +++ b/docs/doctoring/opencode-exact-vcs-dependency-evidence.md @@ -0,0 +1,115 @@ +# OpenCode exact VCS dependency evidence + +## Decision + +The OpenCode coverage image may expose a Python dependency directly from source +only when the validated base branch's frozen `uv.lock` names an HTTPS GitHub +repository owned by `ContextualWisdomLab` and a full 40-character Git commit. +Registry dependencies remain exact-version, SHA-256-pinned `pip` installs. + +The trusted materializer separates those two dependency classes. The networked, +secret-free image build fetches each approved source revision, verifies that +`FETCH_HEAD` and the checked-out `HEAD` equal the locked commit, removes Git +metadata, verifies a normalized package import root, and records only that +repository root or its `src` directory in a Python path file. It does not run +dependency build or installation code. The pull-request tree still runs later +with no network and no credentials. + +## Root cause + +LineageWeave's protected base lock contains RankWeave at its immutable release +commit because the current RankWeave release is not yet available from PyPI. +`uv export` therefore emitted one exact VCS requirement alongside fully hashed +registry requirements. The former materializer rejected the complete export, +so OpenCode never reached tests and repeatedly requested changes despite the +product's current-head tests passing. + +## Safety boundary + +- Symbolic refs, abbreviated commits, non-HTTPS schemes, credentials, ports, + query strings, fragments, subdirectories, and repositories outside the exact + organization origin fail closed. +- Duplicate references to one repository must resolve to one commit; conflicting + revisions fail before the image build. +- Only metadata read from the validated base SHA can select a dependency. Pull + request source cannot modify the networked image build inputs. +- Source dependencies are import-only. No `pip install`, PEP 517 backend, setup + hook, or dependency lifecycle script runs while the network is available. +- The source repository must be publicly fetchable without credentials, expose + the normalized top-level import package directly or under `src`, and remain a + pure-Python leaf dependency. Private repositories, environment-marked VCS + requirements, namespace/layout aliases, installed-distribution metadata, + entry points, compiled extensions, and registry packages that require the VCS + distribution fail closed instead of expanding the secret-free build boundary. +- The checkout roots and path file are explicitly world-readable so the later + networkless coverage container can run as UID 65532 independently of the + image builder's umask. +- This records exact source inputs for a test image; it does not claim a SLSA + build level or substitute for upstream package publication and attestation. + +These controls follow pip's recommendation to use full VCS commit hashes and +SLSA 1.2's treatment of Git revisions as immutable identifiers, while retaining +the isolated, ephemeral test execution boundary (Python Packaging Authority, +2026c; Supply-chain Levels for Software Artifacts, 2025). They also support the +SSDF practice of preserving dependency provenance and preventing recurrence of +toolchain failures (Souppaya et al., 2022). + +## Verification + +Regression tests cover the real LineageWeave export shape, rejection of unsafe +VCS forms, conflicting commits, deterministic manifests, exact-fetch workflow +commands, and the existing registry hash contract. Before source-only Python +path publication, the image build requires exactly one normalized root, a +regular `__init__.py` for packages, and no symbolic links, compiled extensions, +or installed distribution metadata. Namespace, alias/ambiguous, and native +layouts therefore fail the build. The central Python quality workflow retains +100% statement/branch and docstring coverage. + +## Independent root lock environments + +An exact-base coverage run for `contextual-orchestrator` exposed four +root-level lock candidates: two independently complete application/tool locks +and two security-tool fragments whose transitive `pip` dependency was not +pinned and hashed. The installer correctly identified the incomplete +candidates, but then treated every file sharing the repository root as one +supplement group. That synthetic environment combined mutually exclusive +versions of `rpds-py`, so a valid application lock could not reach the +networkless test phase. + +Directory co-location is not a dependency relationship. The installer now +recovers only an unambiguous directory containing exactly two candidates and at +least one incomplete candidate. Directories containing more root locks require +an explicit requirements-file include graph; absent that evidence, complete +locks install independently and incomplete closures remain skipped. This keeps +pip's all-or-nothing hash rule intact rather than weakening `--require-hashes` +or choosing a dependency version locally. The boundary follows pip's defined +`-r` include mechanism and its requirement that every dependency in hash mode +be pinned and hashed (Python Packaging Authority, 2026a, 2026b). It also +supports SSDF verification of third-party components and secure build +configuration (Souppaya et al., 2022). + +The regression contract uses four root candidates and fails if the installer +combines any of them. A Linux Python 3.14 replay against the exact protected +base materialization preflighted all four, installed the two independently +complete closures, skipped the two incomplete closures with their bounded root +causes, and exited successfully. + +## References + +Python Packaging Authority. (2026a). *Requirements file format*. pip +documentation v26.2.1. +https://pip.pypa.io/en/stable/reference/requirements-file-format/ + +Python Packaging Authority. (2026b). *Secure installs*. pip documentation +v26.2. https://pip.pypa.io/en/stable/topics/secure-installs/ + +Python Packaging Authority. (2026c). *VCS support*. pip documentation v26.2. +https://pip.pypa.io/en/stable/topics/vcs-support/ + +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 + +Supply-chain Levels for Software Artifacts. (2025). *SLSA specification +(Version 1.2)*. https://slsa.dev/spec/v1.2/ diff --git a/scripts/ci/install_base_python_locks.py b/scripts/ci/install_base_python_locks.py index 518fcd689..1b9ab1069 100644 --- a/scripts/ci/install_base_python_locks.py +++ b/scripts/ci/install_base_python_locks.py @@ -235,7 +235,9 @@ def install_materialized_locks( for entry in directory_entries if entry.generated_file not in independently_valid ] - if not invalid_entries or len(directory_entries) < 2: + # ponytail: recover only one unambiguous two-file supplement pair; + # multi-environment directories need an explicit include graph. + if not invalid_entries or len(directory_entries) != 2: continue print( "Preflighting same-directory trusted base Python lock group " diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 314668438..a05212354 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -36,6 +36,13 @@ r"==[^\s;]+(?:\s*;\s*\S(?:.*\S)?)?" ) UV_SHA256_HASH_RE = re.compile(r"--hash=sha256:[0-9a-fA-F]{64}") +UV_EXACT_ORG_VCS_RE = re.compile( + r"(?P[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?" + r"(?:\[[A-Za-z0-9._-]+(?:,[A-Za-z0-9._-]+)*\])?)\s+@\s+" + r"git\+https://github\.com/ContextualWisdomLab/" + r"(?P[A-Za-z0-9_.-]{1,100})\.git@" + r"(?P[0-9a-fA-F]{40})" +) UV_EXPORT_TIMEOUT_SECONDS = 120 TRUSTED_UV_VERSION = "0.12.1" TRUSTED_UV_TARGET_TRIPLE = "x86_64-unknown-linux-gnu" @@ -286,6 +293,42 @@ def _is_fully_hash_pinned_export(content: bytes) -> bool: return bool(lines) and all(_is_fully_hash_pinned_requirement(line) for line in lines) +def _partition_uv_export(content: bytes) -> tuple[bytes, list[dict[str, str]]]: + """Separate registry hash pins from exact organization VCS source pins.""" + registry_requirements: list[str] = [] + vcs_by_repository: dict[str, dict[str, str]] = {} + for line in _requirement_lines(content): + if _is_fully_hash_pinned_requirement(line): + registry_requirements.append(line) + continue + match = UV_EXACT_ORG_VCS_RE.fullmatch(line) + if match is None: + raise ValueError("uv export contains an unsupported dependency line") + dependency = { + "package": match.group("package"), + "import_name": re.sub( + r"[-_.]+", "_", match.group("package").partition("[")[0] + ).lower(), + "repository": match.group("repository"), + "commit": match.group("commit").lower(), + } + repository_key = dependency["repository"].casefold() + previous = vcs_by_repository.get(repository_key) + if previous is not None and previous["commit"] != dependency["commit"]: + raise ValueError("uv export pins one VCS repository to conflicting commits") + vcs_by_repository[repository_key] = dependency + + registry_content = ( + ("\n".join(registry_requirements) + "\n").encode("utf-8") + if registry_requirements + else b"" + ) + return registry_content, sorted( + vcs_by_repository.values(), + key=lambda dependency: dependency["repository"].casefold(), + ) + + def _git(repo_root: pathlib.Path, *args: str) -> bytes: """Run one read-only git command in the materialized repository.""" completed = subprocess.run( @@ -492,7 +535,7 @@ def _reject_unsupported_uv_workspace( def _export_uv_lock( repo_root: pathlib.Path, base_sha: str, lock_path: str -) -> bytes | None: +) -> tuple[bytes, list[dict[str, str]]] | None: """Export one tracked base ``uv.lock`` into a trusted hash-pinned closure. The caller proves that the sibling ``pyproject.toml`` is a regular blob in @@ -534,11 +577,14 @@ def _export_uv_lock( exported = completed.stdout if not _requirement_lines(exported): return None - if not _is_fully_hash_pinned_export(exported): + try: + partitioned = _partition_uv_export(exported) + except ValueError as exc: raise RuntimeError( - f"uv export for tracked base lock {lock_path} was not fully hash-pinned" - ) - return exported + f"uv export for tracked base lock {lock_path} was not fully hash-pinned " + "or exact organization VCS-pinned" + ) from exc + return partitioned def _regular_base_blob_paths(entries: bytes) -> list[tuple[str, pathlib.PurePosixPath]]: @@ -569,8 +615,10 @@ def _regular_base_blob_paths(entries: bytes) -> list[tuple[str, pathlib.PurePosi return regular_blobs -def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: - """Return regular hash-lock blobs from the exact validated base commit.""" +def _base_python_inputs( + repo_root: pathlib.Path, base_sha: str +) -> tuple[list[tuple[str, bytes]], list[dict[str, str]]]: + """Return hash locks and exact VCS sources from one validated base commit.""" if not SHA_RE.fullmatch(base_sha): raise ValueError("base SHA must be exactly 40 hexadecimal characters") @@ -578,6 +626,7 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b regular_blobs = _regular_base_blob_paths(entries) regular_paths = {path for path, _candidate in regular_blobs} locks: list[tuple[str, bytes]] = [] + vcs_by_repository: dict[str, dict[str, str]] = {} for path, candidate in regular_blobs: if _is_candidate_lock_path(candidate): content = _git(repo_root, "show", f"{base_sha}:{path}") @@ -588,8 +637,34 @@ def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, b continue exported = _export_uv_lock(repo_root, base_sha, path) if exported is not None: - locks.append((path, exported)) - return sorted(locks, key=lambda item: item[0]) + registry_content, vcs_dependencies = exported + if registry_content: + locks.append((path, registry_content)) + for dependency in vcs_dependencies: + dependency = {**dependency, "source": path} + repository_key = dependency["repository"].casefold() + previous = vcs_by_repository.get(repository_key) + if ( + previous is not None + and previous["commit"] != dependency["commit"] + ): + raise RuntimeError( + "base uv locks pin one VCS repository " + "to conflicting commits" + ) + vcs_by_repository[repository_key] = dependency + return ( + sorted(locks, key=lambda item: item[0]), + sorted( + vcs_by_repository.values(), + key=lambda dependency: dependency["repository"].casefold(), + ), + ) + + +def base_hash_locks(repo_root: pathlib.Path, base_sha: str) -> list[tuple[str, bytes]]: + """Return regular hash-lock blobs from the exact validated base commit.""" + return _base_python_inputs(repo_root, base_sha)[0] def _included_base_lock_blobs( @@ -661,10 +736,9 @@ def materialize( regular_paths = { path for path, _candidate in _regular_base_blob_paths(entries) } + locks, vcs_manifest = _base_python_inputs(resolved_repo, base_sha) manifest: list[dict[str, str]] = [] - for index, (source_path, content) in enumerate( - base_hash_locks(resolved_repo, base_sha) - ): + for index, (source_path, content) in enumerate(locks): generated_name = f"requirements-{index:03d}.txt" include_directory = f"includes-{index:03d}" included = _included_base_lock_blobs( @@ -692,6 +766,10 @@ def materialize( "".join(f"{entry['file']}\n" for entry in manifest), encoding="utf-8", ) + (output_dir / "vcs-manifest.json").write_text( + json.dumps(vcs_manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) return manifest @@ -719,7 +797,9 @@ def main(argv: list[str] | None = None) -> int: ) else: print( - "No tracked hash-bearing Python requirement candidates exist at the validated base SHA." + "No tracked hash-bearing Python requirement candidates exist at the " + "validated base SHA; any exact VCS source pins are listed in " + "vcs-manifest.json." ) return 0 diff --git a/tests/test_install_base_python_locks.py b/tests/test_install_base_python_locks.py index 4f1feebe6..b6f1782a0 100644 --- a/tests/test_install_base_python_locks.py +++ b/tests/test_install_base_python_locks.py @@ -165,6 +165,61 @@ def fake_runner(command: list[str], **kwargs): assert stderr.getvalue().count("httpx>=0.27") == 2 +def test_does_not_combine_multiple_independent_root_environments(tmp_path) -> None: + """Independent root locks must not become one conflicting recovery closure.""" + for index, source in enumerate( + ( + "requirements-opencode-review-ci.txt", + "requirements-security-ci.txt", + "requirements-security-tools.txt", + "requirements.lock", + ) + ): + write_candidate( + tmp_path, + generated_file=f"requirements-{index:03d}.txt", + source=source, + ) + commands: list[list[str]] = [] + + def fake_runner(command: list[str], **kwargs): + """Make two environments complete and two independently incomplete.""" + commands.append(command) + requirements = [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "-r" + ] + if len(requirements) != 1: + raise AssertionError("independent root environments were combined") + if "--dry-run" in command and requirements[0].endswith( + ("requirements-001.txt", "requirements-002.txt") + ): + return subprocess.CompletedProcess( + command, + 1, + stdout=( + "ERROR: In --require-hashes mode, all requirements must have " + "their versions pinned with ==: pip" + ), + ) + return subprocess.CompletedProcess(command, 0, stdout="") + + stdout = io.StringIO() + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stdout=stdout, + stderr=stderr, + ) + + assert result == 0 + assert len(commands) == 6 + assert "candidates=4 installed=2 skipped=2" in stdout.getvalue() + assert stderr.getvalue().count("pip") == 2 + + @pytest.mark.parametrize( "failure_output", [ diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 6073dbdc1..58ded3740 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -3,6 +3,7 @@ import ast import hashlib import io +import json import runpy import shutil import subprocess @@ -100,6 +101,101 @@ def test_materializes_only_regular_hash_locks_from_exact_base(tmp_path: Path) -> assert "requirements.txt" not in (output / "manifest.json").read_text( encoding="utf-8" ) + assert (output / "vcs-manifest.json").read_text(encoding="utf-8") == "[]\n" + + +def test_materializes_exact_vcs_sources_in_a_separate_manifest( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """VCS source pins never enter a pip --require-hashes input file.""" + repository = tmp_path / "repo" + repository.mkdir() + git(repository, "init") + git(repository, "config", "user.name", "Test") + git(repository, "config", "user.email", "test@example.invalid") + (repository / "tracked.txt").write_text("base\n", encoding="utf-8") + git(repository, "add", ".") + git(repository, "commit", "-m", "base") + base_sha = git(repository, "rev-parse", "HEAD") + hash_lock = b"demo==1 --hash=sha256:" + b"a" * 64 + b"\n" + vcs_sources = [ + { + "package": "rankweave", + "import_name": "rankweave", + "repository": "RankWeave", + "commit": "61c49c50d3b4a24fc9bd7c6d3a7f2f4ba19d7be6", + "source": "uv.lock", + } + ] + monkeypatch.setattr( + materializer, + "_base_python_inputs", + lambda *_args: ([("uv.lock", hash_lock)], vcs_sources), + ) + + output = tmp_path / "output" + materializer.materialize(repository, base_sha, output) + + assert (output / "requirements-000.txt").read_bytes() == hash_lock + assert ( + json.loads((output / "vcs-manifest.json").read_text(encoding="utf-8")) + == vcs_sources + ) + + +def test_base_inputs_preserve_a_vcs_only_export( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A source-only uv closure is useful even without registry requirements.""" + tree = ( + b"100644 blob " + b"a" * 40 + b"\tpyproject.toml\0" + b"100644 blob " + b"b" * 40 + b"\tuv.lock\0" + ) + dependency = { + "package": "rankweave", + "import_name": "rankweave", + "repository": "RankWeave", + "commit": "61c49c50d3b4a24fc9bd7c6d3a7f2f4ba19d7be6", + } + monkeypatch.setattr(materializer, "_git", lambda *_args: tree) + monkeypatch.setattr( + materializer, + "_export_uv_lock", + lambda *_args: (b"", [dependency]), + ) + + locks, vcs_sources = materializer._base_python_inputs(tmp_path, "a" * 40) + + assert locks == [] + assert vcs_sources == [{**dependency, "source": "uv.lock"}] + + +def test_base_inputs_reject_conflicting_vcs_revisions_across_locks( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Separate uv projects cannot select ambiguous revisions of one source.""" + tree = b"".join( + b"100644 blob " + bytes(character, "ascii") * 40 + b"\t" + path + b"\0" + for character, path in ( + ("a", b"first/pyproject.toml"), + ("b", b"first/uv.lock"), + ("c", b"second/pyproject.toml"), + ("d", b"second/uv.lock"), + ) + ) + monkeypatch.setattr(materializer, "_git", lambda *_args: tree) + + def export(_repo: Path, _sha: str, lock_path: str): + commit = "a" * 40 if lock_path.startswith("first/") else "b" * 40 + return b"", [{"package": "demo", "repository": "demo", "commit": commit}] + + monkeypatch.setattr(materializer, "_export_uv_lock", export) + + with pytest.raises(RuntimeError, match="conflicting commits"): + materializer._base_python_inputs(tmp_path, "a" * 40) def test_materializes_hash_pinned_locks_named_beyond_the_legacy_whitelist( @@ -520,7 +616,7 @@ def test_main_reports_when_no_locks_exist( == 0 ) assert ( - "No tracked hash-bearing Python requirement candidates exist" + "any exact VCS source pins are listed in vcs-manifest.json" in capsys.readouterr().out ) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 2cc8044b1..aaea3b0eb 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -679,6 +679,26 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'install -m 0755 "$trusted_base_python_installer"' in measure_step assert "COPY install-base-python-locks.py" in measure_step assert "python3 -I /usr/local/libexec/install-base-python-locks.py" in measure_step + assert '"https://github.com/ContextualWisdomLab/${repository}.git"' in measure_step + assert '--quiet --no-tags --depth=1 origin "$commit"' in measure_step + assert 'rev-parse FETCH_HEAD)" = "$commit"' in measure_step + assert 'rev-parse HEAD)" = "$commit"' in measure_step + assert "opencode-base-vcs-dependencies.pth" in measure_step + assert 'vcs-manifest.json >"$dependency_list"' in measure_step + assert 'done <"$dependency_list"' in measure_step + assert 'candidate_count=$((candidate_count + 1))' in measure_step + assert '[ "$candidate_count" -ne 1 ]' in measure_step + assert "has a missing or ambiguous import root" in measure_step + assert '[ ! -f "$import_root/__init__.py" ]' in measure_step + assert "has a namespace or linked import root" in measure_step + assert 'find "$destination" -type l -print -quit' in measure_step + assert "contains a symbolic-link layout" in measure_step + assert "-name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.dylib'" in measure_step + assert "contains a compiled extension" in measure_step + assert "-name '*.dist-info' -o -name '*.egg-info'" in measure_step + assert "contains installed distribution metadata" in measure_step + assert 'printf \'%s\\n\' "$python_root" >>"$path_file"' in measure_step + assert 'chmod -R a+rX /opt/base-vcs-dependencies "$path_file"' in measure_step assert "docker build --pull --no-cache --network=default" in measure_step assert '"$coverage_build_dir"' in measure_step assert measure_step.index("docker build --pull --no-cache") < measure_step.index( @@ -1965,14 +1985,16 @@ def test_merge_scheduler_uses_escalating_mutation_credentials(): assert "secrets.PR_REVIEW_MERGE_TOKEN" in workflow assert "secrets.OPENCODE_APPROVE_TOKEN" in workflow assert "steps.scheduler_app_token.outputs.token" in workflow - assert ( - "SCHEDULER_READ_TOKEN: ${{ github.event_name == 'repository_dispatch' " - "&& github.event.client_payload.target_repository != '' && " - "(secrets.PR_REVIEW_MERGE_TOKEN || " - "secrets.OPENCODE_APPROVE_TOKEN || " - "steps.scheduler_app_token.outputs.token) || github.token }}" - in workflow - ) + for token_name in ("SCHEDULER_ACTIONS_TOKEN", "SCHEDULER_READ_TOKEN"): + assert ( + f"{token_name}: ${{{{ github.event_name == 'repository_dispatch' " + "&& github.event.client_payload.target_repository != '' && " + "github.event.client_payload.target_repository != github.repository && " + "(secrets.PR_REVIEW_MERGE_TOKEN || " + "secrets.OPENCODE_APPROVE_TOKEN || " + "steps.scheduler_app_token.outputs.token) || github.token }}" + in workflow + ) assert "SCHEDULER_MUTATION_TOKEN_SOURCE" in workflow assert 'default: "1"' in workflow assert 'review_dispatch_limit="-1"' in workflow diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 222a92da3..d2d87b9e3 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -1,16 +1,15 @@ """Contract tests for the scheduled OpenCode review-autofix trust boundary.""" import hashlib -from pathlib import Path import re import subprocess +from pathlib import Path import pytest from scripts.ci import pr_review_autofix_context as context from scripts.ci import pr_review_conflict_scope as scope - AUTOFIX_WORKFLOW = Path(".github/workflows/pr-review-autofix.yml") FIX_SCHEDULER_WORKFLOW = Path(".github/workflows/pr-review-fix-scheduler.yml") HOURLY_CALLER_WORKFLOW = Path( @@ -20,7 +19,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 = "545a2ad7e667220de263805d2c560752a4448347" +REVIEW_DISPATCH_BLOB_SHA = "3bc1ce6d385bce569e7a7ba037f149a8f18039d4" def _workflow_text(path: Path) -> str: diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index caca1a4b0..481a5ee14 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -128,6 +128,7 @@ def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> Non assert ( "github.event_name == 'repository_dispatch' && " "github.event.client_payload.target_repository != '' && " + "github.event.client_payload.target_repository != github.repository && " "(secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || " "steps.scheduler_app_token.outputs.token) || github.token" ) in inspect diff --git a/tests/test_uv_export_isolation_contract.py b/tests/test_uv_export_isolation_contract.py index f24a6ac8e..76b72fdc7 100644 --- a/tests/test_uv_export_isolation_contract.py +++ b/tests/test_uv_export_isolation_contract.py @@ -97,6 +97,58 @@ def test_uv_export_accepts_exact_package_pins_with_markers_and_multiple_hashes() assert materializer._is_fully_hash_pinned_export(content) is True +def test_uv_export_partitions_hashes_and_exact_organization_vcs_sources() -> None: + """An immutable organization source pin is separated from pip hash locks.""" + content = ( + b"demo==1.2.3 --hash=sha256:" + b"a" * 64 + b"\n" + b"rank.weave-extra[GPU] @ git+https://github.com/ContextualWisdomLab/RankWeave.git@" + b"61c49c50d3b4a24fc9bd7c6d3a7f2f4ba19d7be6\n" + ) + + registry, vcs_sources = materializer._partition_uv_export(content) + + assert registry == b"demo==1.2.3 --hash=sha256:" + b"a" * 64 + b"\n" + assert vcs_sources == [ + { + "package": "rank.weave-extra[GPU]", + "import_name": "rank_weave_extra", + "repository": "RankWeave", + "commit": "61c49c50d3b4a24fc9bd7c6d3a7f2f4ba19d7be6", + } + ] + + +@pytest.mark.parametrize( + "requirement", + [ + "demo @ git+http://github.com/ContextualWisdomLab/demo.git@" + "a" * 40, + "demo @ git+https://github.com/other/demo.git@" + "a" * 40, + "demo @ git+https://github.com/ContextualWisdomLab/demo.git@main", + "demo @ git+https://github.com/ContextualWisdomLab/demo.git@" + + "a" * 40 + + "#subdirectory=python", + ], +) +def test_uv_export_rejects_unbounded_vcs_sources(requirement: str) -> None: + """Only the exact organization HTTPS origin and a full commit are accepted.""" + with pytest.raises(ValueError, match="unsupported dependency"): + materializer._partition_uv_export(f"{requirement}\n".encode()) + + +def test_uv_export_rejects_conflicting_commits_for_one_repository() -> None: + """One import path cannot ambiguously combine two repository revisions.""" + with pytest.raises(ValueError, match="conflicting commits"): + materializer._partition_uv_export( + ( + "first @ git+https://github.com/ContextualWisdomLab/demo.git@" + + "a" * 40 + + "\nsecond @ git+https://github.com/ContextualWisdomLab/Demo.git@" + + "b" * 40 + + "\n" + ).encode() + ) + + def test_tracked_pyproject_read_failure_is_not_misclassified_as_orphan( tmp_path: Path, monkeypatch: pytest.MonkeyPatch,