diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 390d8f5f8..5ce3d147d 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -171,6 +171,55 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: + - name: Resolve trusted replay-guard source ref + id: coverage_source_trusted + env: + JOB_CONTEXT_JSON: ${{ toJSON(job) }} + GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} + run: | + set -euo pipefail + /usr/bin/python3 -I <<'PY' >>"$GITHUB_OUTPUT" + import json + import os + import re + import sys + + try: + job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") + github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") + except json.JSONDecodeError as exc: + print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) + raise SystemExit(1) + + trusted_ref = str( + job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" + ).strip() + workflow_ref = str( + job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" + ).strip() + + if not trusted_ref: + trusted_ref = "main" + prefix = "ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@" + if workflow_ref.startswith(prefix): + trusted_ref = workflow_ref.split("@", 1)[1] + + if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): + print("::error::Trusted OpenCode workflow ref resolved to an invalid value.", file=sys.stderr) + raise SystemExit(1) + + print(f"ref={trusted_ref}") + PY + + - name: Checkout trusted replay guard + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + fetch-depth: 1 + persist-credentials: false + ref: ${{ steps.coverage_source_trusted.outputs.ref }} + path: trusted-replay-guard + - name: Exchange OpenCode app token for target repository coverage reads id: coverage_read_app_token if: >- @@ -248,12 +297,14 @@ jobs: PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} + COVERAGE_BASE_WORKDIR: ${{ runner.temp }}/opencode-coverage-base COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-coverage-source COVERAGE_SOURCE_ARCHIVE: ${{ runner.temp }}/opencode-coverage-source.tar + TRUSTED_REPLAY_GUARD: ${{ github.workspace }}/trusted-replay-guard/scripts/ci/pr_head_replay_guard.py run: | set -euo pipefail fetch_dir="${RUNNER_TEMP}/opencode-coverage-fetch" - rm -rf "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" "$COVERAGE_SOURCE_ARCHIVE" + rm -rf "$fetch_dir" "$COVERAGE_BASE_WORKDIR" "$COVERAGE_SOURCE_WORKDIR" "$COVERAGE_SOURCE_ARCHIVE" if [ -z "${GH_TOKEN:-}" ]; then echo "::error::Coverage merge tree materialization requires a GitHub token." exit 1 @@ -273,6 +324,11 @@ jobs: "${PR_HEAD_SHA:-}" exit 1 fi + target_visibility="$(gh api "repos/${TARGET_REPOSITORY}" --jq .visibility)" + if [ "$target_visibility" != "public" ]; then + echo "::error::Cross-repository coverage artifacts require a public target repository; ${TARGET_REPOSITORY} reported visibility=${target_visibility:-unknown}." + exit 1 + fi auth_header="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')" echo "::add-mask::$auth_header" git init "$fetch_dir" @@ -290,10 +346,103 @@ jobs: echo "::error::Coverage merge tree could not be materialized for base ${PR_BASE_SHA} and head ${PR_HEAD_SHA}; resolve merge conflicts or rerun after GitHub can synthesize the PR merge commit." exit 1 fi - mkdir -p "$(dirname "$COVERAGE_SOURCE_WORKDIR")" + validate_coverage_tree_modes() { + local treeish="$1" + local indexed_entry indexed_mode + git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish" | + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" + exit 1 + ;; + esac + done + } + validate_coverage_tree_modes "$PR_BASE_SHA" + validate_coverage_tree_modes HEAD + + # Evaluate replay/unmerge evidence while the real fetched commit + # graph still exists. Snapshot-only artifacts intentionally omit + # target-repository Git history and cannot reproduce this decision. + if [ ! -f "$TRUSTED_REPLAY_GUARD" ] || [ -L "$TRUSTED_REPLAY_GUARD" ]; then + echo "::error::Trusted PR head replay guard is missing or not a regular file." + exit 1 + fi + replay_report="${RUNNER_TEMP}/pr-head-replay-guard.txt" + replay_status=0 + /usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD" \ + --repo-root "$fetch_dir" \ + --base-sha "$PR_BASE_SHA" \ + --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? + printf 'Replay guard completed with status %s and captured %s bytes of bounded evidence.\n' \ + "$replay_status" "$(wc -c <"$replay_report" | tr -d ' ')" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + printf '## PR head replay guard\n\n```text\n' + cat "$replay_report" + printf '\n```\n' + } >>"$GITHUB_STEP_SUMMARY" + fi + if [ "$replay_status" -ne 0 ]; then + echo "::error::Current HEAD discarded a prior base merge or replay evidence could not be evaluated; see the exact SHAs and deletion counts above." + exit "$replay_status" + fi + + # BEGIN_COVERAGE_SOURCE_EXPORT + # Export both snapshots without transferring the target repository's + # Git object database into the central repository artifact namespace. + git -C "$fetch_dir" worktree add --detach "$COVERAGE_BASE_WORKDIR" "$PR_BASE_SHA" + rm -f -- "$COVERAGE_BASE_WORKDIR/.git" + rm -rf -- "$fetch_dir/.git" mv "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" - git -C "$COVERAGE_SOURCE_WORKDIR" status --short - tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . + for source_tree in "$COVERAGE_BASE_WORKDIR" "$COVERAGE_SOURCE_WORKDIR"; do + if [ -e "$source_tree/.git" ] || [ -L "$source_tree/.git" ]; then + echo "::error::Coverage source export retained forbidden Git metadata at ${source_tree}/.git." + exit 1 + fi + if ! coverage_symlink_path="$(find -P "$source_tree" -mindepth 1 -type l -print -quit)"; then + echo "::error::Coverage source export could not scan ${source_tree} for symbolic links." + exit 1 + fi + if [ -n "$coverage_symlink_path" ]; then + echo "::error::Coverage source export contains a symbolic link; refusing cross-job artifact materialization." + exit 1 + fi + done + tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$RUNNER_TEMP" \ + "$(basename "$COVERAGE_BASE_WORKDIR")" \ + "$(basename "$COVERAGE_SOURCE_WORKDIR")" + /usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' + from pathlib import Path, PurePosixPath + import sys + import tarfile + + archive = Path(sys.argv[1]) + if not archive.is_file() or archive.is_symlink(): + raise SystemExit( + f"Coverage source archive is not a regular non-symlink file: {archive}" + ) + with tarfile.open(archive, mode="r:") as bundle: + for member in bundle.getmembers(): + path = PurePosixPath(member.name) + if path.is_absolute() or ".." in path.parts: + raise SystemExit( + f"Coverage source archive contains an unsafe path: {member.name!r}" + ) + if ".git" in path.parts: + raise SystemExit( + f"Coverage source archive contains forbidden Git metadata: {member.name!r}" + ) + if not (member.isfile() or member.isdir()): + raise SystemExit( + "Coverage source archive contains a forbidden non-regular " + f"member: {member.name!r}" + ) + PY + # END_COVERAGE_SOURCE_EXPORT - name: Upload materialized pull request merge tree uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -320,6 +469,7 @@ jobs: coverage_summary: ${{ steps.measure.outputs.coverage_summary }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITHUB_TOKEN: "" steps: - name: Resolve trusted OpenCode source ref id: trusted_source @@ -391,12 +541,14 @@ jobs: COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head run: | set -euo pipefail + artifact_extract_dir="${RUNNER_TEMP}/opencode-coverage-extracted" + rm -rf "$artifact_extract_dir" + mkdir -p "$artifact_extract_dir" rm -rf "$COVERAGE_SOURCE_WORKDIR" - mkdir -p "$COVERAGE_SOURCE_WORKDIR" # The archive contains pull-request-controlled paths. Validate every # member before extraction so a symlink, hardlink, device, FIFO, or # traversal path cannot redirect a later trusted host-side parser. - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$COVERAGE_SOURCE_WORKDIR" <<'PY' + /usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' import os from pathlib import Path, PurePosixPath import sys @@ -412,6 +564,10 @@ jobs: with tarfile.open(archive, mode="r:*") as bundle: members = bundle.getmembers() seen: set[str] = set() + allowed_roots = { + "opencode-coverage-base", + "opencode-coverage-source", + } for member in members: path = PurePosixPath(member.name) normalized = path.as_posix() @@ -419,6 +575,14 @@ jobs: raise SystemExit( f"Coverage source archive contains an unsafe path: {member.name!r}" ) + if not path.parts or path.parts[0] not in allowed_roots: + raise SystemExit( + f"Coverage source archive contains an unexpected root: {member.name!r}" + ) + if ".git" in path.parts: + raise SystemExit( + f"Coverage source archive contains forbidden Git metadata: {member.name!r}" + ) if normalized in seen: raise SystemExit( f"Coverage source archive contains a duplicate path: {member.name!r}" @@ -436,40 +600,40 @@ jobs: ) bundle.extractall(destination, members=members, filter="data") PY - git -C "$COVERAGE_SOURCE_WORKDIR" status --short + coverage_base_tree="${artifact_extract_dir}/opencode-coverage-base" + coverage_head_tree="${artifact_extract_dir}/opencode-coverage-source" + for source_tree in "$coverage_base_tree" "$coverage_head_tree"; do + if [ ! -d "$source_tree" ] || [ -L "$source_tree" ]; then + echo "::error::Coverage source artifact did not contain both regular base/head snapshot directories." + exit 1 + fi + done - - name: Enforce post-merge stale agent replay guard - env: - PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} - PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} - COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head - # Dependency resolution may consume wheels/packages, but PR-defined - # install/build hooks are never executed implicitly. - UV_NO_BUILD: "1" - NPM_CONFIG_IGNORE_SCRIPTS: "true" - PNPM_CONFIG_IGNORE_SCRIPTS: "true" - YARN_ENABLE_SCRIPTS: "false" - GITHUB_TOKEN: "" - run: | - set -euo pipefail - replay_report="${RUNNER_TEMP}/pr-head-replay-guard.txt" - replay_status=0 - python3 "$GITHUB_WORKSPACE/scripts/ci/pr_head_replay_guard.py" \ - --repo-root "$COVERAGE_SOURCE_WORKDIR" \ - --base-sha "$PR_BASE_SHA" \ - --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? - cat "$replay_report" - if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - { - printf '## PR head replay guard\n\n```text\n' - cat "$replay_report" - printf '\n```\n' - } >>"$GITHUB_STEP_SUMMARY" - fi - if [ "$replay_status" -ne 0 ]; then - echo "::error::Current HEAD discarded a prior base merge or replay evidence could not be evaluated; see the exact SHAs and deletion counts above." - exit "$replay_status" - fi + # Reconstruct only the two snapshots needed for diff-aware coverage. + # Synthetic Git metadata is created locally in this job and is never + # uploaded to the central repository artifact namespace. + git init "$COVERAGE_SOURCE_WORKDIR" + git -C "$COVERAGE_SOURCE_WORKDIR" config user.name "github-actions[bot]" + git -C "$COVERAGE_SOURCE_WORKDIR" config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + cp -a -- "$coverage_base_tree/." "$COVERAGE_SOURCE_WORKDIR/" + git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force + git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign \ + -m "coverage base snapshot" + coverage_base_sha="$(git -C "$COVERAGE_SOURCE_WORKDIR" rev-parse HEAD)" + find "$COVERAGE_SOURCE_WORKDIR" -mindepth 1 -maxdepth 1 ! -name .git \ + -exec rm -rf -- {} + + cp -a -- "$coverage_head_tree/." "$COVERAGE_SOURCE_WORKDIR/" + git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force + git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign \ + -m "coverage head snapshot" + coverage_head_sha="$(git -C "$COVERAGE_SOURCE_WORKDIR" rev-parse HEAD)" + { + printf 'COVERAGE_BASE_SHA=%s\n' "$coverage_base_sha" + printf 'COVERAGE_HEAD_SHA=%s\n' "$coverage_head_sha" + } >>"$GITHUB_ENV" + rm -rf "$artifact_extract_dir" + git -C "$COVERAGE_SOURCE_WORKDIR" status --short # Run every trusted follow-up before executing pull-request code. Even a # credential-free test can write runner command files, so no trusted shell @@ -485,7 +649,7 @@ jobs: # files, so a syntax error in a changed file that no test imports (or # in a language with no wired-in runner) could otherwise be approved. changed_files_file="${RUNNER_TEMP}/opencode-syntax-changed-files.txt" - if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff --name-only "$PR_BASE_SHA" HEAD >"$changed_files_file" 2>/dev/null; then + if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA" >"$changed_files_file" 2>/dev/null; then : >"$changed_files_file" fi syntax_report="${RUNNER_TEMP}/opencode-syntax-report.txt" @@ -639,6 +803,8 @@ jobs: --env COVERAGE_SOURCE_WORKDIR=/work \ --env PR_BASE_SHA="$PR_BASE_SHA" \ --env PR_HEAD_SHA="$PR_HEAD_SHA" \ + --env COVERAGE_BASE_SHA="$COVERAGE_BASE_SHA" \ + --env COVERAGE_HEAD_SHA="$COVERAGE_HEAD_SHA" \ --env RUNNER_TEMP=/secure-output \ --env GITHUB_OUTPUT=/secure-output/github-output \ --env GITHUB_STEP_SUMMARY=/secure-output/step-summary \ @@ -829,10 +995,10 @@ jobs: } changed_files_for_coverage() { - if [ -n "${PR_BASE_SHA:-}" ] && [ -n "${PR_HEAD_SHA:-}" ] \ - && trusted_git rev-parse --verify --quiet "$PR_BASE_SHA^{commit}" >/dev/null \ - && trusted_git rev-parse --verify --quiet "$PR_HEAD_SHA^{commit}" >/dev/null; then - trusted_git diff --name-only --find-renames "$PR_BASE_SHA" "$PR_HEAD_SHA" + if [ -n "${COVERAGE_BASE_SHA:-}" ] && [ -n "${COVERAGE_HEAD_SHA:-}" ] \ + && trusted_git rev-parse --verify --quiet "$COVERAGE_BASE_SHA^{commit}" >/dev/null \ + && trusted_git rev-parse --verify --quiet "$COVERAGE_HEAD_SHA^{commit}" >/dev/null; then + trusted_git diff --name-only --find-renames "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA" else trusted_git ls-files fi @@ -1200,8 +1366,8 @@ jobs: run_and_capture "JavaScript/TypeScript coverage threshold" \ python3 "$GITHUB_WORKSPACE/scripts/ci/javascript_coverage_gate.py" \ --repo-root . \ - --base-sha "$PR_BASE_SHA" \ - --head-sha "$PR_HEAD_SHA" \ + --base-sha "$COVERAGE_BASE_SHA" \ + --head-sha "$COVERAGE_HEAD_SHA" \ --summary-list "$summary_list" } @@ -1614,9 +1780,17 @@ jobs: python3 -I "$GITHUB_WORKSPACE/scripts/ci/sanitize_github_output_summary.py" \ "$coverage_output_file" "$summary_output_file" - coverage_output_delimiter="$(python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + coverage_output_delimiter="$(/usr/bin/python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + if ! [[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage output delimiter generation returned an unsafe value." + exit 1 + fi while grep -Fqx "$coverage_output_delimiter" "$summary_output_file"; do - coverage_output_delimiter="$(python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + coverage_output_delimiter="$(/usr/bin/python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + if ! [[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage output delimiter generation returned an unsafe value." + exit 1 + fi done { printf 'coverage_summary<<%s\n' "$coverage_output_delimiter" @@ -1626,7 +1800,23 @@ jobs: printf 'Published compact coverage decision output after sanitization (%s bytes); full command logs remain in the job log and step summary.\n' \ "$(wc -c <"$summary_output_file" | tr -d ' ')" + # BEGIN_COVERAGE_LOG_REPLAY + coverage_log_stop_token="$(/usr/bin/python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + if ! [[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage log stop-token generation returned an unsafe value." + exit 1 + fi + while grep -Fq "$coverage_log_stop_token" "$summary_file"; do + coverage_log_stop_token="$(/usr/bin/python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + if ! [[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage log stop-token generation returned an unsafe value." + exit 1 + fi + done + printf '::stop-commands::%s\n' "$coverage_log_stop_token" cat "$summary_file" + printf '\n::%s::\n' "$coverage_log_stop_token" + # END_COVERAGE_LOG_REPLAY # No process running pull-request code may survive into the trusted # publication phase. The result is copied from a root-only tmpfs only # after every low-privilege process has been terminated. @@ -1857,6 +2047,27 @@ jobs: git cat-file -e "${PR_HEAD_SHA}^{commit}" rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" + # BEGIN_PR_WORKTREE_ENTRY_VALIDATION + git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z | + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" + exit 1 + ;; + esac + done + if ! pr_symlink_path="$(find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit)"; then + echo "::error::Could not scan PR worktree for symbolic links; refusing trusted review processing." + exit 1 + fi + if [ -n "$pr_symlink_path" ]; then + echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." + exit 1 + fi + # END_PR_WORKTREE_ENTRY_VALIDATION git -C "$OPENCODE_SOURCE_WORKDIR" status --short - name: Configure git identity for OpenCode action @@ -2090,21 +2301,24 @@ jobs: CODEGRAPH_BIN="${CODEGRAPH_TRUSTED_ROOT}/node_modules/.bin/codegraph" test -x "$CODEGRAPH_BIN" printf 'Using trusted CodeGraph CLI version %s.\n' "$("$CODEGRAPH_BIN" --version)" + rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph" cd "$OPENCODE_SOURCE_WORKDIR" "$CODEGRAPH_BIN" init -i codegraph_status="$(mktemp)" codegraph_raw="$(mktemp)" changed_scope="$(git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" | sed -n '1,80p' | tr '\n' ' ')" if ! "$CODEGRAPH_BIN" status >"$codegraph_status" 2>&1; then - cat "$codegraph_status" >&2 + printf 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.\n' \ + "$(wc -c <"$codegraph_status" | tr -d ' ')" >&2 echo "::error::CodeGraph status failed; approval evidence is incomplete." rm -f "$codegraph_status" "$codegraph_raw" exit 1 fi if ! timeout 120s "$CODEGRAPH_BIN" explore \ "Review the blast radius, call paths, security boundaries, and focused tests for these current-head changed files: ${changed_scope}" \ - >"$codegraph_raw" 2>&1; then - cat "$codegraph_raw" >&2 + >"$codegraph_raw" 2>&1; then + printf 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.\n' \ + "$(wc -c <"$codegraph_raw" | tr -d ' ')" >&2 echo "::error::CodeGraph changed-scope exploration failed; approval evidence is incomplete." rm -f "$codegraph_status" "$codegraph_raw" exit 1 @@ -2117,7 +2331,8 @@ jobs: } >"$CODEGRAPH_EVIDENCE_FILE" rm -f "$codegraph_status" "$codegraph_raw" test -s "$CODEGRAPH_EVIDENCE_FILE" - cat "$CODEGRAPH_EVIDENCE_FILE" + printf 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.\n' \ + "$(wc -c <"$CODEGRAPH_EVIDENCE_FILE" | tr -d ' ')" - name: Prepare bounded OpenCode review evidence timeout-minutes: 12 @@ -3592,6 +3807,10 @@ jobs: # larger. Keep the exact runtime failure visible without spending a # full medium/large cadence slot after the long-context candidate. OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45" + # Bound the GitHub Models DeepSeek R1 endpoints that return no usable + # provider detail. The primary DeepSeek V3 candidate keeps the full + # review cadence because it has produced usable long-form reviews. + OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS: "300" OPENCODE_DYNAMIC_MAX_CYCLES: "0" CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 2846355f3..4880434f1 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -1,30 +1,326 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes --python-version 3.12 --python-platform x86_64-manylinux_2_28 requirements-opencode-review-ci.txt -o requirements-opencode-review-ci-hashes.txt +annotated-doc==0.0.4 \ + --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ + --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 + # via fastapi +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f + # via + # httpx + # starlette attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 + # via interrogate +bcrypt==5.0.0 \ + --hash=sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4 \ + --hash=sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a \ + --hash=sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464 \ + --hash=sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4 \ + --hash=sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746 \ + --hash=sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2 \ + --hash=sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41 \ + --hash=sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd \ + --hash=sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9 \ + --hash=sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e \ + --hash=sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538 \ + --hash=sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10 \ + --hash=sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb \ + --hash=sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef \ + --hash=sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4 \ + --hash=sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23 \ + --hash=sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef \ + --hash=sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75 \ + --hash=sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42 \ + --hash=sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a \ + --hash=sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172 \ + --hash=sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683 \ + --hash=sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2 \ + --hash=sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4 \ + --hash=sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba \ + --hash=sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da \ + --hash=sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493 \ + --hash=sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254 \ + --hash=sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534 \ + --hash=sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f \ + --hash=sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c \ + --hash=sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c \ + --hash=sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83 \ + --hash=sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff \ + --hash=sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d \ + --hash=sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861 \ + --hash=sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5 \ + --hash=sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9 \ + --hash=sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b \ + --hash=sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac \ + --hash=sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e \ + --hash=sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f \ + --hash=sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb \ + --hash=sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86 \ + --hash=sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980 \ + --hash=sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd \ + --hash=sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d \ + --hash=sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1 \ + --hash=sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911 \ + --hash=sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993 \ + --hash=sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191 \ + --hash=sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4 \ + --hash=sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2 \ + --hash=sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8 \ + --hash=sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db \ + --hash=sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927 \ + --hash=sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be \ + --hash=sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb \ + --hash=sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e \ + --hash=sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf \ + --hash=sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd \ + --hash=sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822 \ + --hash=sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b + # via -r requirements-opencode-review-ci.txt +certifi==2026.6.17 \ + --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ + --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + # via + # httpcore + # httpx click==8.4.2 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + # via interrogate colorama==0.4.6 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via interrogate coverage==7.14.3 \ --hash=sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727 \ --hash=sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3 + # via + # -r requirements-opencode-review-ci.txt + # pytest-cov +fastapi==0.139.2 \ + --hash=sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e \ + --hash=sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c + # via -r requirements-opencode-review-ci.txt +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via httpcore +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via -r requirements-opencode-review-ci.txt +icalendar==7.2.0 \ + --hash=sha256:32dacc396101825b82f9f1bbdf691c02be613130d5ab7a457e553fcd20959fdd \ + --hash=sha256:77922b6be57dfcc2e94f93063d2fd7e948ada9b5bdf7b08bebbc684b1b66c7c4 + # via -r requirements-opencode-review-ci.txt +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via + # anyio + # httpx iniconfig==2.3.0 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest interrogate==1.7.0 \ --hash=sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 + # via -r requirements-opencode-review-ci.txt +korean-lunar-calendar==0.4.0 \ + --hash=sha256:be56f27bc0594fdbbdf7bbe00f504a9f929a31e311bd7d9bb93561b645afade7 \ + --hash=sha256:c042e20de0bb702add6bec8d0f6da1ea8d3b170838e63846f70420cf341fe4e7 + # via -r requirements-opencode-review-ci.txt packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + # via pytest pluggy==1.6.0 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via + # pytest + # pytest-cov py==1.11.0 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + # via interrogate +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 + # via fastapi +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae + # via pydantic pygments==2.20.0 \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via pytest pytest==9.1.1 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # -r requirements-opencode-review-ci.txt + # pytest-cov pytest-cov==7.1.0 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + # via -r requirements-opencode-review-ci.txt +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via icalendar +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + # via fastapi tabulate==0.10.0 \ --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via interrogate +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # anyio + # fastapi + # icalendar + # pydantic + # pydantic-core + # starlette + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via icalendar uv==0.11.25 \ --hash=sha256:d2bc05e17ae3e1f232abf93e7dcfb3b68702dfcde34a00c29cbce7e07d1ecbfb \ --hash=sha256:560b0fbaa6356af533923a349658c21d4f410d16e835787d8a05da451d4ee859 + # via -r requirements-opencode-review-ci.txt diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index b73fe9833..de7a75716 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -3,3 +3,12 @@ interrogate==1.7.0 pytest==9.1.1 pytest-cov==7.1.0 uv==0.11.25 + +# Trusted runtime set required to execute ContextualWisdomLab/saju-caldav tests +# inside the networkless OpenCode coverage sandbox. Keep these exact pins aligned +# with that repository's reviewed lockfile; PR-controlled manifests remain inert. +bcrypt==5.0.0 +fastapi==0.139.2 +httpx==0.28.1 +icalendar==7.2.0 +korean-lunar-calendar==0.4.0 diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 8dbd4eb86..5e5c3ee1c 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -3,4 +3,5 @@ google-cloud-aiplatform==1.133.0 protobuf<7.0.0 cryptography==49.0.0 python-multipart==0.0.31 +# CVE-2026-59885 and CVE-2026-59886: ASN.1 decoder DoS; fixed in 0.6.4 pyasn1==0.6.4 diff --git a/scripts/ci/opencode_review_normalize_output.py b/scripts/ci/opencode_review_normalize_output.py index 4045d457c..72de9a7d3 100755 --- a/scripts/ci/opencode_review_normalize_output.py +++ b/scripts/ci/opencode_review_normalize_output.py @@ -653,6 +653,161 @@ def adversarial_probe_source_receipt_error( return "" +def evidence_cites_probe_path_at_any_line(evidence: str, path: str) -> bool: + """Return whether evidence already cites any positive line for the probe path.""" + escaped_path = rf"(? tuple[str, int] | None: + """Return one evidence citation uniquely bound to its trusted source receipt. + + A model can place a valid changed-file ``path:line`` citation and its exact + source-line receipt in ``evidence`` while copying a different location into + the redundant structured fields. Consider only safe current-head changed + files cited by the evidence, recompute every cited line receipt from the + sealed source tree, and return a location only when exactly one distinct + citation matches the single model receipt. Ambiguous or unverified evidence + remains unrepairable and fails closed. + """ + receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) + if len(receipts) != 1: + return None + receipt = receipts[0].casefold() + matches: set[tuple[str, int]] = set() + for path in changed_files: + escaped_path = rf"(? dict[str, Any] | Any: + """Bind trusted source receipts and locations into otherwise valid evidence. + + Models sometimes place the exact changed-file path and positive line in the + structured ``path``/``line`` fields but omit the duplicate ``path:line`` text + from ``evidence``, or cite and receipt-bind one changed source line in + ``evidence`` while copying a different valid changed-file location into the + redundant structured fields. Restore only a missing citation or rebind the + structured location when exactly one cited line matches the existing single + receipt in the sealed current-head tree. Missing, duplicate, ambiguous, or + mismatched receipts, unsafe paths, missing changed-file evidence, and + circular or unobserved claims remain unmodified and fail closed. + """ + if not isinstance(value, dict): + return value + validation = value.get("adversarial_validation") + if not isinstance(validation, dict): + return value + probes = validation.get("probes") + if not isinstance(probes, list): + return value + + changed_files = current_changed_files() + repaired_probes: list[Any] = [] + changed = False + for probe in probes: + if not isinstance(probe, dict): + repaired_probes.append(probe) + continue + path = probe.get("path") + line = probe.get("line") + evidence = probe.get("evidence") + if ( + not isinstance(path, str) + or path not in changed_files + or isinstance(line, bool) + or not isinstance(line, int) + or line <= 0 + or not isinstance(evidence, str) + or not evidence.strip() + or adversarial_probe_location_error(path, line) + ): + repaired_probes.append(probe) + continue + + receipts = SOURCE_LINE_RECEIPT_RE.findall(evidence) + if len(receipts) != 1: + repaired_probes.append(probe) + continue + repaired_evidence = evidence + repaired_path = path + repaired_line = line + rejection = adversarial_evidence_rejection_reason(repaired_evidence, path, line) + if rejection == "must cite the exact probe path and positive line": + if not adversarial_probe_source_receipt_error(evidence, path, line): + if evidence_cites_probe_path_at_any_line(evidence, path): + repaired_probes.append(probe) + continue + repaired_evidence = f"{path}:{line} {repaired_evidence}" + else: + rebound_location = receipt_verified_evidence_location( + evidence, + changed_files, + ) + if rebound_location is None: + repaired_probes.append(probe) + continue + repaired_path, repaired_line = rebound_location + elif rejection: + repaired_probes.append(probe) + continue + if adversarial_probe_source_receipt_error( + repaired_evidence, repaired_path, repaired_line + ) or adversarial_evidence_rejection_reason( + repaired_evidence, + repaired_path, + repaired_line, + ): + repaired_probes.append(probe) + continue + if ( + repaired_evidence == evidence + and repaired_path == path + and repaired_line == line + ): + repaired_probes.append(probe) + else: + repaired_probes.append( + { + **probe, + "path": repaired_path, + "line": repaired_line, + "evidence": repaired_evidence, + } + ) + changed = True + + if not changed: + return value + return { + **value, + "adversarial_validation": {**validation, "probes": repaired_probes}, + } + + def adversarial_validation_error( value: Any, *, @@ -1267,6 +1422,7 @@ def reject(reason: str) -> None: return reject("APPROVE cannot contain findings") if result == "REQUEST_CHANGES" and not findings: return reject("REQUEST_CHANGES requires at least one finding") + value = repair_adversarial_probe_evidence_bindings(value) adversarial_error = adversarial_validation_error( value.get("adversarial_validation"), result=result, diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index b709ca807..0a1842e85 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -384,6 +384,9 @@ cap_model_run_timeout() { github-models/openai/gpt-5 | github-models/openai/gpt-5-chat) cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS 45)" ;; + github-models/deepseek/deepseek-r1 | github-models/deepseek/deepseek-r1-0528) + cap_seconds="$(env_integer_or_default OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS 300)" + ;; *) printf '%s\n' "$run_timeout_seconds" return 0 @@ -547,9 +550,6 @@ main() { fi fi deadline=0 - if [ "$budget_seconds" -gt 0 ]; then - deadline=$((SECONDS + budget_seconds)) - fi : >"$OPENCODE_OUTPUT_FILE" cd "$OPENCODE_REVIEW_WORKDIR" read -r -a model_candidates <<<"${OPENCODE_MODEL_CANDIDATES:-}" @@ -573,6 +573,7 @@ main() { continue fi if should_skip_model_candidate "$model_candidate"; then + dead_candidate_reasons[$model_candidate]="not runnable with the credentials available to this job" continue fi assert_reasoning_effort_for_candidate "$model_candidate" @@ -582,6 +583,12 @@ main() { opencode_json_file="${candidate_output_file}.jsonl" opencode_export_file="${candidate_output_file}.session.json" write_prompt "$model_candidate" "$prompt_file" + # The retry budget measures provider attempts. Trusted local prompt + # preparation can be slower on a busy runner and must not exhaust the + # budget before the first provider is invoked. + if [ "$deadline" -eq 0 ] && [ "$budget_seconds" -gt 0 ]; then + deadline=$((SECONDS + budget_seconds)) + fi for attempt in $(seq 1 "$attempts"); do now="$SECONDS" if [ "$deadline" -gt 0 ] && [ "$now" -ge "$deadline" ]; then @@ -610,7 +617,7 @@ main() { uncapped_run_timeout="$OPENCODE_RUN_TIMEOUT_SECONDS" OPENCODE_RUN_TIMEOUT_SECONDS="$(cap_model_run_timeout "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS")" if [ "$OPENCODE_RUN_TIMEOUT_SECONDS" -lt "$uncapped_run_timeout" ]; then - printf 'OpenCode %s runtime cap selected %ss instead of %ss because this installation has returned a constrained request-body limit for that endpoint.\n' \ + printf 'OpenCode %s runtime cap selected %ss instead of %ss because the configured provider-specific cap is lower than the cadence timeout.\n' \ "$model_candidate" "$OPENCODE_RUN_TIMEOUT_SECONDS" "$uncapped_run_timeout" fi export OPENCODE_RUN_TIMEOUT_SECONDS @@ -665,7 +672,7 @@ main() { fi done if [ "$alive_candidates" -eq 0 ]; then - printf 'Every OpenCode model candidate is marked failed for this run; ending the pool without further provider spend.\n' + printf 'No runnable OpenCode model candidates remain for this run; ending the pool without further provider spend or idle cycles.\n' if finish_pool_without_model; then exit 0 fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3f2a610fa..a2c2e98a3 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -87,6 +87,28 @@ assert_file_not_contains() { fi } +assert_file_not_matches() { + local file_path="$1" + local pattern="$2" + local message="$3" + local grep_status + + if [ ! -f "$file_path" ]; then + record_failure "$message (missing file '$file_path')" + print_assertion_source "$file_path" + return + fi + if grep -Eq -- "$pattern" "$file_path"; then + record_failure "$message (unexpected pattern '$pattern')" + else + grep_status=$? + if [ "$grep_status" -ne 1 ]; then + record_failure "$message (grep failed with exit $grep_status for pattern '$pattern')" + print_assertion_source "$file_path" + fi + fi +} + seal_opencode_test_artifacts() { local runner_temp="$1" local head_sha="$2" @@ -602,8 +624,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'locked_version" != "4.0.4"' "opencode review verifies both nested installed and locked picomatch evidence" assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" explore' "opencode review precomputes structural evidence outside the model process" assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" --version' "opencode review logs the exact trusted CodeGraph version" - assert_file_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review exposes CodeGraph status failures in the job log" - assert_file_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review exposes CodeGraph exploration failures in the job log" + assert_file_not_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review does not replay PR-derived CodeGraph status bytes as workflow commands" + assert_file_not_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review does not replay PR-derived CodeGraph exploration bytes as workflow commands" + assert_file_not_matches "$workflow_file" '^[[:space:]]*cat "\$CODEGRAPH_EVIDENCE_FILE"[[:space:]]*$' "opencode review does not replay assembled PR-derived CodeGraph evidence into the command channel" + assert_file_contains "$workflow_file" 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph status failure metadata" + assert_file_contains "$workflow_file" 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph exploration failure metadata" + assert_file_contains "$workflow_file" 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.' "opencode review reports bounded CodeGraph success metadata" assert_file_not_contains "$workflow_file" "serve --mcp" "opencode review must not fetch or launch CodeGraph again for MCP" assert_file_not_contains "$workflow_file" "https://mcp.deepwiki.com/mcp" "opencode review does not expose remote MCP to the model" assert_file_not_contains "$workflow_file" "@upstash/context7-mcp@3.1.0" "opencode review does not install Context7 at runtime" @@ -1024,6 +1050,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" '(.workflow // "") == "CodeQL"' "opencode approval can distinguish CodeQL dynamic setup checks" assert_file_contains "$workflow_file" '((.isRequired // false) | not) and (.workflow // "") == "CodeQL"' "opencode approval ignores non-required cancelled CodeQL checks without source evidence" assert_file_contains "$workflow_file" '(.name // "") == "scan-pr-queue" and ((.workflow // "") == "PR Review Merge Scheduler" or (.workflow // "") == "Required PR Review Merge Scheduler")' "opencode approval ignores cancelled scheduler queue replacement checks without source evidence" + assert_file_not_contains "$workflow_file" 'select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "scan-pr-queue") | not)' "opencode approval keeps ownership-ambiguous cancelled scan-pr-queue checks blocking in the workflow-less REST fallback" assert_file_contains "$workflow_file" 'grep -Fq -- "Strix Security Scan/strix:" "$rollup_file"' "opencode approval avoids duplicate supplemental Strix workflow-run blockers when statusCheckRollup already has the Strix check" assert_file_contains "$workflow_file" 'current_head_manual_strix_success_status()' "opencode approval can identify same-head manual Strix success status evidence" assert_file_contains "$workflow_file" 'manual_run_line="$(latest_current_head_manual_strix_run || true)"' "opencode approval falls back to same-head manual Strix check-run success when commit status publication is unavailable" @@ -5637,11 +5664,30 @@ run_gate_case_allow_provider_signal() { run_gate_case_with_provider_signal_mode "0" "$@" } +test_assert_file_not_matches_fails_closed() { + local fixture_file + + fixture_file="$(mktemp)" + printf 'safe fixture\n' >"$fixture_file" + if ! ( + FAILURES=0 + assert_file_not_matches "$fixture_file" "[" "invalid regex must fail closed" + assert_file_not_matches "${fixture_file}.missing" "safe" "missing file must fail closed" + [ "$FAILURES" -eq 2 ] + ) >/dev/null 2>&1; then + record_failure "assert_file_not_matches must record missing-file and grep failures" + fi + rm -f "$fixture_file" +} + run_filtered_gate_case_if_requested() { case "${STRIX_TEST_CASE_FILTER:-}" in "") return 0 ;; + assert-file-not-matches-errors-fail-closed) + test_assert_file_not_matches_fails_closed + ;; success) run_gate_case "success" \ "vertex_ai/ready-primary" \ diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 597d23fb7..3add4598a 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1,8 +1,10 @@ import json import os import re +import shlex import shutil import subprocess +import tarfile import textwrap from pathlib import Path @@ -194,18 +196,18 @@ def test_opencode_trusted_source_ref_is_not_controlled_by_workflow_inputs(): assert workflow.count("ref: ${{ steps.trusted_source.outputs.ref }}") == 1 assert "TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.ref }}" in workflow assert "ref: ${{ github.workflow_sha }}" not in workflow - assert workflow.count("JOB_CONTEXT_JSON: ${{ toJSON(job) }}") == 2 - assert workflow.count("GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}") == 2 + assert workflow.count("JOB_CONTEXT_JSON: ${{ toJSON(job) }}") == 3 + assert workflow.count("GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}") == 3 assert ( workflow.count( 'job_context.get("workflow_sha") or github_context.get("workflow_sha")' ) - == 2 + == 3 ) - assert workflow.count('workflow_ref.split("@", 1)[1]') == 2 + assert workflow.count('workflow_ref.split("@", 1)[1]') == 3 assert ( workflow.count("Trusted OpenCode workflow ref resolved to an invalid value.") - == 2 + == 3 ) @@ -282,6 +284,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert ( "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in source_job ) + assert "Checkout trusted replay guard" in source_job + assert "persist-credentials: false" in source_job + assert "ref: ${{ steps.coverage_source_trusted.outputs.ref }}" in source_job coverage_start = workflow.index(" coverage-evidence:\n") coverage_end = workflow.index("\n opencode-review-target:", coverage_start) @@ -317,10 +322,44 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' in step ) + assert ( + 'target_visibility="$(gh api "repos/${TARGET_REPOSITORY}" --jq .visibility)"' + in step + ) + assert ( + "Cross-repository coverage artifacts require a public target repository" in step + ) assert "Coverage fetch could not authenticate" in step assert 'merge --no-ff --no-edit "$PR_HEAD_SHA"' in step assert "Coverage merge tree could not be materialized" in step assert "PR_HEAD_SHA:" in step + assert 'validate_coverage_tree_modes "$PR_BASE_SHA"' in step + assert "validate_coverage_tree_modes HEAD" in step + assert 'ls-tree -r -z --full-tree "$treeish"' in step + assert 'ls-tree -r -z --full-tree "$treeish" |' in step + assert 'done < <(git -C "$fetch_dir" ls-tree' not in step + assert "100644 | 100755" in step + assert "TRUSTED_REPLAY_GUARD: ${{ github.workspace }}/trusted-replay-guard/" in step + assert '/usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD"' in step + assert '--repo-root "$fetch_dir"' in step + assert '--base-sha "$PR_BASE_SHA"' in step + assert '--head-sha "$PR_HEAD_SHA"' in step + assert step.index('/usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD"') < step.index( + 'rm -rf -- "$fetch_dir/.git"' + ) + assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step + assert 'rm -rf -- "$fetch_dir/.git"' in step + assert "Coverage source export retained forbidden Git metadata" in step + assert "Coverage source archive contains forbidden Git metadata" in step + assert step.index('rm -rf -- "$fetch_dir/.git"') < step.index( + 'tar -cf "$COVERAGE_SOURCE_ARCHIVE"' + ) + assert '"$(basename "$COVERAGE_BASE_WORKDIR")"' in step + assert '"$(basename "$COVERAGE_SOURCE_WORKDIR")"' in step + assert 'cat "$replay_report"' not in step.split( + 'if [ -n "${GITHUB_STEP_SUMMARY:-}" ]', 1 + )[0] + assert "Replay guard completed with status" in step measure_start = workflow.index( " - name: Measure test and docstring evidence\n" @@ -332,15 +371,43 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "secrets." not in measure_step assert "COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head" in workflow assert ( - 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$COVERAGE_SOURCE_WORKDIR"' in workflow + '/usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' + in coverage_job ) + assert '/usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<' in source_job + assert '"opencode-coverage-base"' in coverage_job + assert '"opencode-coverage-source"' in coverage_job assert "member.isfile() or member.isdir()" in workflow + assert "Coverage source archive contains an unexpected root" in coverage_job + assert "Coverage source archive contains forbidden Git metadata" in coverage_job + assert 'git init "$COVERAGE_SOURCE_WORKDIR"' in coverage_job + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force' in coverage_job + assert "coverage base snapshot" in coverage_job + assert "coverage head snapshot" in coverage_job + assert "COVERAGE_BASE_SHA=%s" in coverage_job + assert "COVERAGE_HEAD_SHA=%s" in coverage_job + assert 'diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA"' in coverage_job + assert "pr_head_replay_guard.py" not in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow + prepare_start = workflow.index( + " - name: Prepare pull request merge tree for coverage measurement\n" + ) + prepare_end = workflow.index("\n - name:", prepare_start + 1) + prepare_step = workflow[prepare_start:prepare_end] + assert 'git init "$COVERAGE_SOURCE_WORKDIR"' in prepare_step + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force' in prepare_step + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign' in prepare_step + assert prepare_step.index("bundle.extractall") < prepare_step.index( + 'git init "$COVERAGE_SOURCE_WORKDIR"' + ) assert "docker.io/library/ubuntu@sha256:" in measure_step assert "apt-get install --no-install-recommends -y" in measure_step assert "--require-hashes" in measure_step - assert 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' in measure_step + assert ( + 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' + in measure_step + ) assert "The networked build context contains only this" in measure_step assert 'install -m 0644 "$trusted_ci_requirements"' in measure_step assert "docker build --pull --no-cache --network=default" in measure_step @@ -417,9 +484,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "uv run --no-project" not in measure_step assert "uv run --no-build" not in measure_step assert "python3 -m coverage run -m pytest tests" in measure_step - trusted_requirements = Path( - "requirements-opencode-review-ci-hashes.txt" - ).read_text(encoding="utf-8") + trusted_requirements = Path("requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) assert "pytest-cov==7.1.0" in trusted_requirements assert ( "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" @@ -433,6 +500,108 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "github.event_name == 'pull_request_target'" not in target_condition +def test_coverage_source_artifact_excludes_git_history(tmp_path): + """The cross-job source archive must not carry target repository history.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + export_start = workflow.index(" # BEGIN_COVERAGE_SOURCE_EXPORT\n") + export_start = workflow.index("\n", export_start) + 1 + export_end = workflow.index(" # END_COVERAGE_SOURCE_EXPORT", export_start) + export_script = textwrap.dedent(workflow[export_start:export_end]) + + repo = tmp_path / "fetch" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.name", "Artifact Boundary Test"], + cwd=repo, + check=True, + ) + subprocess.run( + ["git", "config", "user.email", "artifact@example.invalid"], + cwd=repo, + check=True, + ) + (repo / ".env").write_text("PRIVATE_TOKEN=deleted-history\n", encoding="utf-8") + subprocess.run(["git", "add", ".env"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "historical secret"], cwd=repo, check=True) + (repo / ".env").unlink() + (repo / "safe.txt").write_text("current source\n", encoding="utf-8") + subprocess.run(["git", "add", "-A"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "safe base"], cwd=repo, check=True) + base_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repo, text=True + ).strip() + (repo / "safe.txt").write_text("current head source\n", encoding="utf-8") + subprocess.run(["git", "add", "safe.txt"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "current head"], cwd=repo, check=True) + + base_worktree = tmp_path / "opencode-coverage-base" + source_worktree = tmp_path / "opencode-coverage-source" + archive = tmp_path / "opencode-coverage-source.tar" + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + export_script], + env={ + **os.environ, + "COVERAGE_BASE_WORKDIR": str(base_worktree), + "COVERAGE_SOURCE_WORKDIR": str(source_worktree), + "COVERAGE_SOURCE_ARCHIVE": str(archive), + "PR_BASE_SHA": base_sha, + "RUNNER_TEMP": str(tmp_path), + "fetch_dir": str(repo), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + with tarfile.open(archive) as bundle: + names = [name.removeprefix("./").rstrip("/") for name in bundle.getnames()] + assert "opencode-coverage-base/safe.txt" in names + assert "opencode-coverage-source/safe.txt" in names + assert not any(".git" in name.split("/") for name in names) + assert b"deleted-history" not in archive.read_bytes() + + +def test_coverage_source_symlink_scan_fails_closed_on_find_error(tmp_path): + """Artifact export must stop when its symbolic-link scan cannot complete.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + scan_start = workflow.index( + ' for source_tree in "$COVERAGE_BASE_WORKDIR" ' + '"$COVERAGE_SOURCE_WORKDIR"; do\n' + ) + scan_end = workflow.index( + ' tar -cf "$COVERAGE_SOURCE_ARCHIVE"', scan_start + ) + scan_script = textwrap.dedent(workflow[scan_start:scan_end]) + + base_worktree = tmp_path / "opencode-coverage-base" + source_worktree = tmp_path / "opencode-coverage-source" + base_worktree.mkdir() + source_worktree.mkdir() + fake_bin = tmp_path / "fake-bin" + fake_bin.mkdir() + fake_find = fake_bin / "find" + fake_find.write_text("#!/bin/sh\nexit 7\n", encoding="utf-8") + fake_find.chmod(0o755) + + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + scan_script], + env={ + **os.environ, + "COVERAGE_BASE_WORKDIR": str(base_worktree), + "COVERAGE_SOURCE_WORKDIR": str(source_worktree), + "PATH": f"{fake_bin}:{os.environ['PATH']}", + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 1 + assert "could not scan" in result.stdout + + def test_opencode_repository_dispatch_authorization_is_fail_closed(): """Reject an untrusted dispatcher or a target outside the exact allowlist.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") @@ -507,9 +676,9 @@ def test_opencode_model_exhaustion_retry_stays_owned_by_central_scheduler(): def test_opencode_python_coverage_never_resolves_pr_dependency_manifests(): """Use only the trusted image toolchain during networkless PR execution.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - measure = workflow.split( - " - name: Measure test and docstring evidence\n", 1 - )[1].split("\n - name:", 1)[0] + measure = workflow.split(" - name: Measure test and docstring evidence\n", 1)[ + 1 + ].split("\n - name:", 1)[0] assert "verify_trusted_python_test_toolchain()" in measure assert "PR-selected dependency manifests are never resolved" in measure @@ -1135,6 +1304,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_DYNAMIC_TOTAL_BUDGET_CAP_SECONDS: "11700"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "0"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow + assert 'OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS: "300"' in workflow + assert 'OPENCODE_GITHUB_DEEPSEEK_RUN_TIMEOUT_SECONDS: "300"' not in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow publish_step = workflow.split(" - name: Publish OpenCode review outcome", 1)[ @@ -1192,7 +1363,12 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert "while :" in model_pool_runner assert "should_skip_model_candidate" in model_pool_runner assert "cap_model_run_timeout" in model_pool_runner - assert "constrained request-body limit" in model_pool_runner + assert "configured provider-specific cap" in model_pool_runner + assert ( + "github-models/deepseek/deepseek-r1 | " + "github-models/deepseek/deepseek-r1-0528)" + ) in model_pool_runner + assert "github-models/deepseek/*)" not in model_pool_runner assert "run_central_adversarial_harness" not in model_pool_runner assert "finish_pool_without_model" in model_pool_runner assert "central-current-head-adversarial-harness" not in model_pool_runner @@ -1581,6 +1757,27 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) assert 'cat "$summary_output_file"' in coverage_job assert "Published compact coverage decision output" in coverage_job + assert 'coverage_log_stop_token="$(/usr/bin/python3 -I -c' in coverage_job + assert ( + '[[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]' + in coverage_job + ) + assert ( + '[[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]' in coverage_job + ) + assert "# BEGIN_COVERAGE_LOG_REPLAY" in coverage_job + assert "# END_COVERAGE_LOG_REPLAY" in coverage_job + assert 'grep -Fq "$coverage_log_stop_token" "$summary_file"' in coverage_job + assert ( + "printf '::stop-commands::%s\\n' \"$coverage_log_stop_token\"" in coverage_job + ) + assert 'cat "$summary_file"' in coverage_job + assert "printf '\\n::%s::\\n' \"$coverage_log_stop_token\"" in coverage_job + assert ( + coverage_job.index("printf '::stop-commands::%s\\n'") + < coverage_job.index('cat "$summary_file"') + < coverage_job.index("printf '\\n::%s::\\n'") + ) assert "actions: read" in coverage_job assert "contents: read" not in coverage_job assert 'GITHUB_TOKEN: ""' in coverage_job @@ -1632,6 +1829,15 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) < target_job.index( "Exchange OpenCode app token for target repository review reads" ) + materialize_step = target_job.split( + " - name: Materialize pull request head for OpenCode review data", 1 + )[1].split("\n - name:", 1)[0] + assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z' in materialize_step + assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z |' in materialize_step + assert 'done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files' not in materialize_step + assert "100644 | 100755" in materialize_step + assert 'find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l' in materialize_step + assert "refusing trusted review processing" in materialize_step codegraph_step = target_job.split( " - name: Initialize CodeGraph index for OpenCode", 1 )[1].split("\n - name:", 1)[0] @@ -1652,8 +1858,13 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert '"$CODEGRAPH_BIN" init -i' in codegraph_step assert '"$CODEGRAPH_BIN" status' in codegraph_step assert '"$CODEGRAPH_BIN" --version' in codegraph_step - assert 'cat "$codegraph_status" >&2' in codegraph_step - assert 'cat "$codegraph_raw" >&2' in codegraph_step + assert 'cat "$codegraph_status" >&2' not in codegraph_step + assert 'cat "$codegraph_raw" >&2' not in codegraph_step + assert 'cat "$CODEGRAPH_EVIDENCE_FILE"' not in codegraph_step + assert "CodeGraph status command failed; captured" in codegraph_step + assert "CodeGraph exploration command failed; captured" in codegraph_step + assert "Captured bounded CodeGraph evidence" in codegraph_step + assert 'rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph"' in codegraph_step assert "CodeGraph status failed; approval evidence is incomplete." in codegraph_step assert ( "CodeGraph changed-scope exploration failed; approval evidence is incomplete." @@ -1684,6 +1895,217 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) +def test_coverage_log_replay_disables_runner_commands_and_retries_token_collision( + tmp_path, +): + """Untrusted coverage bytes stay inside a collision-free stop-command envelope.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + replay_start = workflow.index(" # BEGIN_COVERAGE_LOG_REPLAY\n") + replay_start = workflow.index("\n", replay_start) + 1 + replay_end = workflow.index(" # END_COVERAGE_LOG_REPLAY", replay_start) + replay = textwrap.dedent(workflow[replay_start:replay_end]) + + collision_token = "coverage-log-" + "a" * 48 + safe_token = "coverage-log-" + "b" * 48 + summary = tmp_path / "coverage-evidence.md" + summary.write_text( + f"{collision_token}\n" + "::set-output name=coverage_summary::ATTACKER\n" + "::add-path::/tmp/attacker\n", + encoding="utf-8", + ) + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + counter = tmp_path / "counter" + fake_python = fake_bin / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" + 'count="$(cat "$FAKE_COUNTER" 2>/dev/null || printf 0)"\n' + 'printf "%s" "$((count + 1))" >"$FAKE_COUNTER"\n' + 'if [ "$count" -eq 0 ]; then\n' + f" printf '%s\\n' {collision_token}\n" + "else\n" + f" printf '%s\\n' {safe_token}\n" + "fi\n", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + # Replace the absolute trusted interpreter only inside this test harness so + # the collision-retry branch can be deterministic. The production contract + # itself must remain immune to PATH-prepended executables. + replay_with_fixture = replay.replace( + "/usr/bin/python3", shlex.quote(str(fake_python)) + ) + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + replay_with_fixture], + env={ + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "FAKE_COUNTER": str(counter), + "summary_file": str(summary), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + lines = result.stdout.splitlines() + assert lines[0] == f"::stop-commands::{safe_token}" + assert "::set-output name=coverage_summary::ATTACKER" in lines[1:-1] + assert "::add-path::/tmp/attacker" in lines[1:-1] + assert lines[-1] == f"::{safe_token}::" + assert counter.read_text(encoding="utf-8") == "2" + + +def test_coverage_log_replay_rejects_unsafe_stop_token(tmp_path): + """A compromised PATH generator cannot inject a workflow command token.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + replay_start = workflow.index(" # BEGIN_COVERAGE_LOG_REPLAY\n") + replay_end = workflow.index(" # END_COVERAGE_LOG_REPLAY\n", replay_start) + replay = textwrap.dedent(workflow[replay_start:replay_end]) + + summary = tmp_path / "coverage-evidence.md" + summary.write_text("safe log\n", encoding="utf-8") + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_python = fake_bin / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" + "printf 'coverage-log-safe\\n::set-output name=pwned::yes\\n'\n", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + replay_with_fixture = replay.replace( + "/usr/bin/python3", shlex.quote(str(fake_python)) + ) + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + replay_with_fixture], + env={ + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "summary_file": str(summary), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode != 0 + assert "stop-token generation returned an unsafe value" in result.stdout + assert "::stop-commands::" not in result.stdout + + +def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): + """Tracked and untracked symlinks cannot escape into runner credentials.""" + if not hasattr(os, "symlink"): + pytest.skip("symlinks are unavailable") + + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + validation_start = workflow.index( + " # BEGIN_PR_WORKTREE_ENTRY_VALIDATION\n" + ) + validation_start = workflow.index("\n", validation_start) + 1 + validation_end = workflow.index( + " # END_PR_WORKTREE_ENTRY_VALIDATION", validation_start + ) + validation = textwrap.dedent(workflow[validation_start:validation_end]) + + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.name", "Trust Boundary Test"], cwd=repo, check=True + ) + subprocess.run( + ["git", "config", "user.email", "trust@example.invalid"], + cwd=repo, + check=True, + ) + (repo / "safe.txt").write_text("safe\n", encoding="utf-8") + subprocess.run(["git", "add", "safe.txt"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "base"], cwd=repo, check=True) + base_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repo, text=True + ).strip() + + outside = tmp_path / "runner-credential" + outside.write_text("synthetic-secret\n", encoding="utf-8") + (repo / "credential-link").symlink_to(outside) + subprocess.run(["git", "add", "credential-link"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "symlink head"], cwd=repo, check=True) + + clean_worktree = tmp_path / "clean-worktree" + subprocess.run( + ["git", "worktree", "add", "--detach", str(clean_worktree), base_sha], + cwd=repo, + capture_output=True, + check=True, + ) + clean = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, + text=True, + capture_output=True, + check=False, + ) + assert clean.returncode == 0, clean.stderr + + fake_bin = tmp_path / "fake-bin" + fake_bin.mkdir() + fake_find = fake_bin / "find" + fake_find.write_text("#!/bin/sh\nexit 7\n", encoding="utf-8") + fake_find.chmod(0o755) + scan_error = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={ + **os.environ, + "OPENCODE_SOURCE_WORKDIR": str(clean_worktree), + "PATH": f"{fake_bin}:{os.environ['PATH']}", + }, + text=True, + capture_output=True, + check=False, + ) + assert scan_error.returncode == 1 + assert "Could not scan PR worktree for symbolic links" in scan_error.stdout + + untracked_link = clean_worktree / "untracked-credential-link" + untracked_link.symlink_to(outside) + untracked_rejected = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, + text=True, + capture_output=True, + check=False, + ) + assert untracked_rejected.returncode == 1 + assert "PR worktree contains a symbolic link" in untracked_rejected.stdout + assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + untracked_link.unlink() + + malicious_worktree = tmp_path / "malicious-worktree" + subprocess.run( + ["git", "worktree", "add", "--detach", str(malicious_worktree), "HEAD"], + cwd=repo, + capture_output=True, + check=True, + ) + rejected = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(malicious_worktree)}, + text=True, + capture_output=True, + check=False, + ) + + assert rejected.returncode == 1 + assert "refusing trusted review processing" in rejected.stdout + assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + + def test_opencode_pending_peer_checks_hold_blocks_required_workflow_until_approval(): """Pending peer checks cannot satisfy the required gate without a review.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 5e8233487..94cc9222a 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -14,6 +14,8 @@ import pytest +from scripts.ci import opencode_review_normalize_output as normalizer + ROOT = Path(__file__).resolve().parents[1] RUNNER = ROOT / "scripts" / "ci" / "run_opencode_review_model_pool.sh" @@ -29,6 +31,16 @@ } +@pytest.fixture(autouse=True) +def clear_normalizer_artifact_caches(): + """Keep trusted artifact caches isolated from later review-gate tests.""" + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + yield + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + + def bash_command() -> str: """Return a Bash executable that can run repository shell scripts locally.""" if os.name == "nt": @@ -87,6 +99,285 @@ def seal_artifacts( return hashlib.sha256(manifest.read_bytes()).hexdigest() +def prepare_probe_binding_artifacts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> tuple[str, int, str]: + """Create one sealed changed source line for normalizer binding tests.""" + runner_temp = tmp_path / "binding-runner-temp" + source_root = tmp_path / "binding-source" + source_path = source_root / "scripts" / "ci" / "example.py" + runner_temp.mkdir() + source_path.parent.mkdir(parents=True) + source_path.write_text( + "return False\nraise SystemExit(1)\nraise SystemExit(1)\n", + encoding="utf-8", + ) + changed_files = runner_temp / "opencode-changed-files.txt" + changed_files.write_text("scripts/ci/example.py\n", encoding="utf-8") + manifest_digest = seal_artifacts( + runner_temp, + head_sha="binding-head", + run_id="binding-run", + run_attempt="1", + paths=(changed_files,), + ) + monkeypatch.setenv("RUNNER_TEMP", str(runner_temp)) + monkeypatch.setenv("OPENCODE_SOURCE_WORKDIR", str(source_root)) + monkeypatch.setenv("OPENCODE_CHANGED_FILES_FILE", str(changed_files)) + monkeypatch.setenv("OPENCODE_ARTIFACT_MANIFEST_SHA256", manifest_digest) + normalizer.current_changed_files.cache_clear() + normalizer.trusted_execution_receipts.cache_clear() + digest = hashlib.sha256(b"return False").hexdigest() + return "scripts/ci/example.py", 1, f"source-line-sha256={digest}" + + +def test_normalizer_binds_only_a_verified_structured_probe_location( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A matching receipt may restore only redundant path:line evidence text.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + evidence = ( + f"Regression command rejected malformed input with exit code 1; {receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + repaired = normalizer.repair_adversarial_probe_evidence_bindings(value) + + assert repaired is not value + assert repaired["adversarial_validation"]["probes"][0]["evidence"] == ( + f"{path}:{line} {evidence}" + ) + + +@pytest.mark.parametrize("citation_template", ("{path}:2", "{path}#L2")) +def test_normalizer_does_not_add_a_contradictory_redundant_citation( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + citation_template: str, +) -> None: + """A different existing citation for the probe path remains fail-closed.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + evidence = ( + f"Source trace at {citation_template.format(path=path)} rejected malformed input " + f"with exit code 1; {receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + assert normalizer.repair_adversarial_probe_evidence_bindings(value) is value + + +def test_normalizer_rebinds_structured_location_to_unique_receipted_citation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A unique cited changed line may repair redundant structured location drift.""" + path, line, _ = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + cited_line = 2 + receipt = "source-line-sha256=" + hashlib.sha256( + b"raise SystemExit(1)" + ).hexdigest() + evidence = ( + f"Source trace at {path}:{cited_line} rejected malformed input with exit code 1; " + f"{receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + repaired = normalizer.repair_adversarial_probe_evidence_bindings(value) + + assert repaired is not value + repaired_probe = repaired["adversarial_validation"]["probes"][0] + assert repaired_probe["path"] == path + assert repaired_probe["line"] == cited_line + assert repaired_probe["evidence"] == evidence + + +def test_normalizer_does_not_rebind_ambiguous_receipted_citations( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Two cited lines with the same trusted bytes remain ambiguous and fail closed.""" + path, line, _ = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + receipt = "source-line-sha256=" + hashlib.sha256( + b"raise SystemExit(1)" + ).hexdigest() + evidence = ( + f"Source traces at {path}:2 and {path}:3 rejected malformed input with exit code 1; " + f"{receipt}" + ) + value = { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": evidence, + } + ] + } + } + + assert normalizer.repair_adversarial_probe_evidence_bindings(value) is value + + +def test_receipt_verified_location_rejects_unverifiable_evidence( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Missing receipts and invalid cited lines cannot yield a trusted location.""" + path, _, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + changed_files = frozenset({path}) + + assert ( + normalizer.receipt_verified_evidence_location( + f"Source trace at {path}:1 rejected malformed input with exit code 1", + changed_files, + ) + is None + ) + assert ( + normalizer.receipt_verified_evidence_location( + f"Source trace at {path}:99 rejected malformed input with exit code 1; " + f"{receipt}", + changed_files, + ) + is None + ) + + +def test_normalizer_probe_binding_repair_remains_fail_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Malformed, untrusted, circular, and already-bound probes are not rewritten.""" + path, line, receipt = prepare_probe_binding_artifacts(tmp_path, monkeypatch) + assert normalizer.repair_adversarial_probe_evidence_bindings(None) is None + + missing_validation: dict[str, object] = {} + assert ( + normalizer.repair_adversarial_probe_evidence_bindings(missing_validation) + is missing_validation + ) + malformed = {"adversarial_validation": {"probes": "not-a-list"}} + assert normalizer.repair_adversarial_probe_evidence_bindings(malformed) is malformed + + invalid_values = [ + { + "adversarial_validation": { + "probes": [ + "not-an-object", + {"path": path, "line": 0, "evidence": receipt}, + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + "source-line-sha256=" + "0" * 64 + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + f"{receipt} {receipt}" + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": "scripts/ci/not-changed.py", + "line": line, + "evidence": ( + "Regression command rejected malformed input with exit code 1; " + f"{receipt}" + ), + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": receipt, + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": f"Source inspection properly handles all cases; {receipt}", + } + ] + } + }, + { + "adversarial_validation": { + "probes": [ + { + "path": path, + "line": line, + "evidence": ( + f"Regression command at {path}:{line} rejected malformed input " + f"with exit code 1; {receipt}" + ), + } + ] + } + }, + ] + for invalid in invalid_values: + assert normalizer.repair_adversarial_probe_evidence_bindings(invalid) is invalid + + def skip_if_windows_bash_is_unresponsive(command: str) -> None: """Skip with a visible reason when local Git Bash cannot start on Windows.""" if os.name != "nt": @@ -623,12 +914,37 @@ def test_credit_exhausted_402_ends_pool_without_further_spend(tmp_path: Path) -> assert result.returncode == 1 assert "provider credits are exhausted" in result.stdout assert "marking this candidate failed for the rest of the run" in result.stdout - assert "Every OpenCode model candidate is marked failed for this run" in result.stdout + assert "No runnable OpenCode model candidates remain for this run" in result.stdout assert "class=credit-exhausted" in result.stdout assert "Restarting OpenCode model pool" not in result.stdout assert elapsed < 20 +def test_all_credentialless_candidates_end_without_idle_cycles(tmp_path: Path) -> None: + """A fully skipped catalog exits even when cycles and deadlines are unbounded.""" + start = time.monotonic() + result = run_failed_model( + tmp_path, + model_candidates=( + "openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2" + ), + extra_env={ + "OPENAI_API_KEY": "", + "OPENROUTER_API_KEY": "", + "OPENCODE_POOL_MAX_CYCLES": "0", + "OPENCODE_TOTAL_RETRY_BUDGET_SECONDS": "0", + }, + ) + elapsed = time.monotonic() - start + + assert result.returncode == 1 + assert "OPENAI_API_KEY is not configured" in result.stdout + assert "OPENROUTER_API_KEY is not configured" in result.stdout + assert "No runnable OpenCode model candidates remain for this run" in result.stdout + assert "Restarting OpenCode model pool" not in result.stdout + assert elapsed < 10 + + def test_invalid_control_output_cap_marks_candidate_failed(tmp_path: Path) -> None: """Repeated control-rejected output stops retrying at the cap, not the budget.""" result = run_failed_model( @@ -658,7 +974,7 @@ def test_invalid_control_output_cap_marks_candidate_failed(tmp_path: Path) -> No assert result.returncode == 1 assert "produced 2 control-rejected outputs" in result.stdout assert "marking this candidate failed for the rest of the run" in result.stdout - assert "Every OpenCode model candidate is marked failed for this run" in result.stdout + assert "No runnable OpenCode model candidates remain for this run" in result.stdout assert "attempt 3/3" not in result.stdout @@ -713,6 +1029,17 @@ def test_dynamic_review_cadence_uses_small_change_timeout(tmp_path: Path) -> Non assert "retry budget remaining." in result.stdout +def test_review_retry_budget_starts_after_trusted_prompt_preparation() -> None: + """Trusted preflight work cannot consume the provider-attempt retry budget.""" + runner = RUNNER.read_text(encoding="utf-8") + main = runner[runner.index("main() {") :] + prompt = main.index('write_prompt "$model_candidate" "$prompt_file"') + deadline = main.index("deadline=$((SECONDS + budget_seconds))") + first_budget_check = main.index('if [ "$deadline" -gt 0 ]', prompt) + + assert prompt < deadline < first_budget_check + + def test_dynamic_review_cadence_caps_large_change_queue_budget(tmp_path: Path) -> None: """Large PR cadence caps queue time without converting unlimited cycles to one cycle.""" changed_files = [f"backend/changed_{index}.py" for index in range(21)] @@ -759,7 +1086,7 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert result.returncode == 1 assert ( "OpenCode github-models/openai/gpt-5 runtime cap selected 3s instead of 9s " - "because this installation has returned a constrained request-body limit for that endpoint." + "because the configured provider-specific cap is lower than the cadence timeout." ) in result.stdout attempt_budget = re.search( r"OpenCode github-models/openai/gpt-5 attempt 1/1 using (\d+)s run timeout " @@ -772,6 +1099,58 @@ def test_github_gpt5_runtime_cap_preserves_queue_budget(tmp_path: Path) -> None: assert run_timeout <= remaining_budget <= 30 +def test_github_deepseek_r1_runtime_cap_preserves_queue_budget( + tmp_path: Path, +) -> None: + """No-output DeepSeek R1 endpoints cannot consume a full cadence slot.""" + result = run_failed_model( + tmp_path, + model_candidates="github-models/deepseek/deepseek-r1-0528", + extra_env={ + "OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS": "2", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + ) + + assert result.returncode == 1 + assert ( + "OpenCode github-models/deepseek/deepseek-r1-0528 runtime cap selected 2s " + "instead of 9s because the configured provider-specific cap is lower than " + "the cadence timeout." + ) in result.stdout + attempt_budget = re.search( + r"OpenCode github-models/deepseek/deepseek-r1-0528 attempt 1/1 using " + r"(\d+)s run timeout with (\d+)s retry budget remaining\.", + result.stdout, + ) + assert attempt_budget is not None + run_timeout, remaining_budget = map(int, attempt_budget.groups()) + assert run_timeout == 2 + assert run_timeout <= remaining_budget <= 30 + + +def test_github_deepseek_v3_preserves_full_review_cadence(tmp_path: Path) -> None: + """The primary DeepSeek V3 candidate is not subject to the R1 runtime cap.""" + result = run_failed_model( + tmp_path, + model_candidates="github-models/deepseek/deepseek-v3-0324", + extra_env={ + "OPENCODE_GITHUB_DEEPSEEK_R1_RUN_TIMEOUT_SECONDS": "2", + "OPENCODE_RUN_TIMEOUT_SECONDS": "9", + }, + ) + + assert result.returncode == 1 + assert ( + "OpenCode github-models/deepseek/deepseek-v3-0324 attempt 1/1 using 9s " + "run timeout" + ) in result.stdout + assert ( + "OpenCode github-models/deepseek/deepseek-v3-0324 runtime cap selected" + not in result.stdout + ) + + def test_github_models_openai_prompt_references_evidence_without_inlining( tmp_path: Path, ) -> None: diff --git a/tests/test_opencode_python_dependency_lock.py b/tests/test_opencode_python_dependency_lock.py new file mode 100644 index 000000000..33c55d028 --- /dev/null +++ b/tests/test_opencode_python_dependency_lock.py @@ -0,0 +1,144 @@ +"""Contracts for application dependencies trusted by offline OpenCode coverage.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +TRUSTED_SAJU_WHEELS = { + "bcrypt": "5.0.0", + "fastapi": "0.139.2", + "httpx": "0.28.1", + "icalendar": "7.2.0", + "korean-lunar-calendar": "0.4.0", +} + + +def _top_level_requirement(line: str) -> str | None: + """Return a requirement token from one non-indented, non-comment line.""" + + if not line or line[0].isspace() or line.startswith("#"): + return None + header = line.split("\\", 1)[0].strip() + if not header or header.startswith("-"): + return None + return header.split(maxsplit=1)[0] + + +def _top_level_requirements(text: str) -> set[str]: + """Return parsed top-level requirements without comments or stanza details.""" + + return { + requirement + for line in text.splitlines() + if (requirement := _top_level_requirement(line)) is not None + } + + +def _lock_stanza(lock: str, requirement: str) -> list[str]: + """Return one top-level requirement stanza without borrowing later hashes.""" + + lines = lock.splitlines() + start = next( + ( + index + for index, line in enumerate(lines) + if _top_level_requirement(line) == requirement + ), + None, + ) + assert start is not None, f"{requirement} is missing from the hashed dependency lock" + + stanza = [lines[start]] + for line in lines[start + 1 :]: + if not line.strip(): + break + if not line[0].isspace() and not line.startswith("#"): + break + stanza.append(line) + return stanza + + +def test_lock_stanza_accepts_indented_via_comments() -> None: + lock = ( + "demo==1.0 \\\n" + " --hash=sha256:abc123\n" + " # via example\n" + "next-package==2.0 \\\n" + " --hash=sha256:def456\n" + ) + + assert _lock_stanza(lock, "demo==1.0") == [ + "demo==1.0 \\", + " --hash=sha256:abc123", + " # via example", + ] + + +def test_lock_stanza_accepts_attached_line_continuation() -> None: + lock = "demo==1.0\\\n --hash=sha256:abc123\n" + + assert _lock_stanza(lock, "demo==1.0") == [ + "demo==1.0\\", + " --hash=sha256:abc123", + ] + + +def test_lock_stanza_does_not_borrow_a_later_requirement_hash() -> None: + lock = ( + "demo==1.0\n" + "next-package==2.0 \\\n" + " --hash=sha256:def456\n" + ) + + assert not any( + line.lstrip().startswith("--hash=sha256:") + for line in _lock_stanza(lock, "demo==1.0") + ) + + +def test_top_level_requirements_ignore_comments_and_stanza_details() -> None: + requirements = _top_level_requirements( + "# lunar-python==9.9.9 is intentionally not installed\n" + "demo==1.0 \\\n" + " --hash=sha256:abc123\n" + " # via lunar-python==9.9.9\n" + ) + + assert requirements == {"demo==1.0"} + + +def test_saju_caldav_dependencies_are_exact_hash_locked_wheels() -> None: + source = (REPO_ROOT / "requirements-opencode-review-ci.txt").read_text( + encoding="utf-8" + ) + lock = (REPO_ROOT / "requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) + source_requirements = _top_level_requirements(source) + lock_requirements = _top_level_requirements(lock) + + for package, version in TRUSTED_SAJU_WHEELS.items(): + requirement = f"{package}=={version}" + assert requirement in source_requirements + stanza = _lock_stanza(lock, requirement) + assert any( + line.lstrip().startswith("--hash=sha256:") for line in stanza[1:] + ), f"{requirement} has no artifact hash in its lock stanza" + + assert not any( + requirement.startswith("lunar-python==") + for requirement in source_requirements | lock_requirements + ) + + generation_header = "\n".join( + line for line in lock.splitlines() if line.startswith("#") + ) + for fragment in ( + "uv pip compile", + "--generate-hashes", + "--python-version 3.12", + "--python-platform x86_64-manylinux_2_28", + "requirements-opencode-review-ci.txt", + "requirements-opencode-review-ci-hashes.txt", + ): + assert fragment in generation_header