From aac99bbb7f2bd77d85b0dac29635a82f298275f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 21:28:27 +0900 Subject: [PATCH 01/27] fix(security): update pip audit lock past pysec-2026-3721 --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49..0ae099d8f 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ From 84ee91bc3d4c399a804d71ef839bfd9ed5a322ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 06:59:12 -0700 Subject: [PATCH 02/27] feat: route autofix through contextual orchestrator * feat: route autofix through contextual orchestrator * docs(control-plane): align repair boundary with gateway * docs: align gateway discovery record with implementation * fix(autofix): clarify gateway trust boundaries * fix: align gateway contract documentation * fix(autofix): require discovered gateway model * fix(autofix): require usable discovered model ids * fix(strix): retry stale single-line source evidence * test(autofix): keep reviewer contract composable * fix(autofix): skip no-op gateway probe * fix(autofix): probe gateway only for conflicts * fix(strix): enforce severity before stale finding fallback * fix(autofix): isolate single-line snippet detection --------- Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> --- .../hourly-nvidia-nim-review-repair.yml | 14 +- .github/workflows/pr-review-autofix.yml | 115 +++++++++---- AGENTS.md | 13 +- ARCHITECTURE.md | 33 ++-- CHANGELOG.md | 6 +- CLAUDE.md | 10 +- README.md | 2 +- docs/automation/hourly-review-repair.md | 13 +- .../contextual-orchestrator-autofix.md | 98 +++++++++++ docs/doctoring/hourly-nvidia-nim-autofix.md | 5 + .../organization_commercial_readiness_loop.py | 3 +- scripts/ci/strix_quick_gate.sh | 20 ++- scripts/ci/test_strix_quick_gate.sh | 158 ++++++++++++++++++ ...tofix_contextual_orchestrator_contract.py} | 137 +++++++++++---- ...review_autofix_writer_security_contract.py | 13 +- ...est_strix_nvidia_nim_not_found_fallback.py | 15 ++ 16 files changed, 549 insertions(+), 106 deletions(-) create mode 100644 docs/doctoring/contextual-orchestrator-autofix.md rename tests/{test_pr_review_autofix_nvidia_nim_contract.py => test_pr_review_autofix_contextual_orchestrator_contract.py} (71%) diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 9eb450619..9a90c9320 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -39,7 +39,7 @@ on: - tests/test_pr_review_fix_scheduler.py - tests/test_pr_review_fix_scheduler_source_pin.py - tests/test_pr_review_autofix_context_head_binding.py - - tests/test_pr_review_autofix_nvidia_nim_contract.py + - tests/test_pr_review_autofix_contextual_orchestrator_contract.py - tests/test_pr_review_autofix_writer_security_contract.py - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md @@ -49,7 +49,7 @@ on: - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md - docs/doctoring/governance-risk-compliance-hourly-review-caller.md - - docs/doctoring/hourly-nvidia-nim-autofix.md + - docs/doctoring/contextual-orchestrator-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/orgmetra-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md @@ -92,7 +92,7 @@ on: - tests/test_pr_review_fix_scheduler.py - tests/test_pr_review_fix_scheduler_source_pin.py - tests/test_pr_review_autofix_context_head_binding.py - - tests/test_pr_review_autofix_nvidia_nim_contract.py + - tests/test_pr_review_autofix_contextual_orchestrator_contract.py - tests/test_pr_review_autofix_writer_security_contract.py - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md @@ -102,7 +102,7 @@ on: - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md - docs/doctoring/governance-risk-compliance-hourly-review-caller.md - - docs/doctoring/hourly-nvidia-nim-autofix.md + - docs/doctoring/contextual-orchestrator-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/orgmetra-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md @@ -117,7 +117,7 @@ concurrency: jobs: contract: - name: Hourly cadence, immutable source, NIM credential, and conflict scope + name: Hourly cadence, immutable source, gateway credential, and conflict scope runs-on: ubuntu-24.04 timeout-minutes: 20 steps: @@ -138,7 +138,7 @@ jobs: run: >- python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt - - name: Verify hourly scheduler and NVIDIA NIM autofix contracts + - name: Verify hourly scheduler and contextual-orchestrator autofix contracts run: | set -euo pipefail python -m pytest -q \ @@ -173,6 +173,6 @@ jobs: tests/test_pr_review_fix_scheduler.py \ tests/test_pr_review_fix_scheduler_source_pin.py \ tests/test_pr_review_autofix_context_head_binding.py \ - tests/test_pr_review_autofix_nvidia_nim_contract.py \ + tests/test_pr_review_autofix_contextual_orchestrator_contract.py \ tests/test_pr_review_autofix_writer_security_contract.py git diff --check diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index 786357722..9fd626e32 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -279,9 +279,9 @@ jobs: EOF jq -n --arg workspace "$TARGET_WORKSPACE" '{ "$schema": "https://opencode.ai/config.json", - "model": "nvidia-nim/mistralai/mistral-small-4-119b-2603", - "small_model": "nvidia-nim/nvidia/nemotron-3-nano-30b-a3b", - "enabled_providers": ["nvidia-nim"], + "model": "contextual-orchestrator/contextual-orchestrator", + "small_model": "contextual-orchestrator/contextual-orchestrator", + "enabled_providers": ["contextual-orchestrator"], "permission": { "edit": { "*": "allow", @@ -306,7 +306,7 @@ jobs: "ci-autofix": { "description": "Conservative CI pull request review autofix agent", "mode": "primary", - "model": "nvidia-nim/mistralai/mistral-small-4-119b-2603", + "model": "contextual-orchestrator/contextual-orchestrator", "reasoningEffort": "high", "prompt": "{file:./autofix-prompt.md}", "steps": 12, @@ -333,32 +333,23 @@ jobs: } }, "provider": { - "nvidia-nim": { + "contextual-orchestrator": { "npm": "@ai-sdk/openai-compatible", - "name": "NVIDIA NIM", + "name": "Contextual Orchestrator", "options": { - "baseURL": "https://integrate.api.nvidia.com/v1", - "apiKey": "{env:NVIDIA_API_KEY}" + "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", + "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}", + "headers": { + "X-Contextual-Orchestrator-Tool-Loop": "v1" + } }, "models": { - "mistralai/mistral-small-4-119b-2603": { - "name": "Mistral Small 4 119B 2603", - "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "limit": { - "context": 128000, - "output": 4096 - } - }, - "nvidia/nemotron-3-nano-30b-a3b": { - "name": "Nemotron 3 Nano 30B A3B", + "contextual-orchestrator": { + "name": "Contextual Orchestrator (auto-routed)", "tool_call": true, "reasoning": true, "limit": { - "context": 128000, + "context": 200000, "output": 32768 } } @@ -370,16 +361,43 @@ jobs: - name: Run OpenCode review autofix if: env.RESOLVE_CONFLICT != 'true' env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} - MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603 + CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }} + CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }} + MODEL: contextual-orchestrator/contextual-orchestrator SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_AUTOFIX_WORKDIR: ${{ runner.temp }}/opencode-autofix-project run: | set -euo pipefail - if [ -z "${NVIDIA_API_KEY:-}" ]; then - echo "::error::NVIDIA_NIM_API_KEY is required for scheduled OpenCode autofix." + if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || + [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then + echo "::error::CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled OpenCode autofix." + exit 1 + fi + python3 - "$CONTEXTUAL_ORCHESTRATOR_BASE_URL" <<'PY' + import sys + from urllib.parse import urlparse + + parsed = urlparse(sys.argv[1]) + if ( + parsed.scheme != "https" + or not parsed.hostname + or parsed.username + or parsed.password + or parsed.query + or parsed.fragment + ): + raise SystemExit("CONTEXTUAL_ORCHESTRATOR_BASE_URL must be an HTTPS URL without credentials or query data") + PY + models_response="$(curl -fsS \ + --connect-timeout 10 \ + --max-time 30 \ + -H "Authorization: Bearer ${CONTEXTUAL_ORCHESTRATOR_TOKEN}" \ + "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/models")" + if ! jq -e '(.data | type == "array") and ([.data[]? | select((.id? | type) == "string" and (.id | length > 0))] | length >= 1)' \ + >/dev/null <<<"$models_response"; then + echo "::error::Contextual Orchestrator gateway returned no discovered models." exit 1 fi prompt_file="${RUNNER_TEMP}/opencode-autofix-prompt.md" @@ -542,11 +560,12 @@ jobs: - name: Merge base branch and resolve conflicts with OpenCode if: env.RESOLVE_CONFLICT == 'true' env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }} + CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }} GITHUB_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token }} GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token }} MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.target_app_token.outputs.available == 'true' }} - MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603 + MODEL: contextual-orchestrator/contextual-orchestrator SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -557,10 +576,6 @@ jobs: echo "::error::Conflict-resolution mutation requires PR_REVIEW_MERGE_TOKEN, OPENCODE_APPROVE_TOKEN, or the exchanged OpenCode app token; github.token remains read-only." exit 1 fi - if [ -z "${NVIDIA_API_KEY:-}" ]; then - echo "::error::NVIDIA_NIM_API_KEY is required for scheduled OpenCode autofix." - exit 1 - fi cd "$TARGET_WORKSPACE" # Merge the base branch into the detached head. A clean merge stays @@ -589,7 +604,41 @@ jobs: exit 1 fi + contextual_gateway_readiness() { + if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || + [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then + echo "::error::CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled OpenCode autofix." + exit 1 + fi + python3 - "$CONTEXTUAL_ORCHESTRATOR_BASE_URL" <<'PY' + import sys + from urllib.parse import urlparse + + parsed = urlparse(sys.argv[1]) + if ( + parsed.scheme != "https" + or not parsed.hostname + or parsed.username + or parsed.password + or parsed.query + or parsed.fragment + ): + raise SystemExit("CONTEXTUAL_ORCHESTRATOR_BASE_URL must be an HTTPS URL without credentials or query data") + PY + models_response="$(curl -fsS \ + --connect-timeout 10 \ + --max-time 30 \ + -H "Authorization: Bearer ${CONTEXTUAL_ORCHESTRATOR_TOKEN}" \ + "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/models")" + if ! jq -e '(.data | type == "array") and ([.data[]? | select((.id? | type) == "string" and (.id | length > 0))] | length >= 1)' \ + >/dev/null <<<"$models_response"; then + echo "::error::Contextual Orchestrator gateway returned no discovered models." + exit 1 + fi + } + if [ -n "$conflicted_files" ]; then + contextual_gateway_readiness conflicted_paths_file="${RUNNER_TEMP}/opencode-conflicted-files.zlist" conflict_scope_snapshot="${RUNNER_TEMP}/opencode-conflict-workspace-before.json" git diff --name-only -z --diff-filter=U >"$conflicted_paths_file" diff --git a/AGENTS.md b/AGENTS.md index 2df633f49..934ae8273 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,8 +3,13 @@ > **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. -Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md). +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. The current write-capable repair boundary is the contextual-orchestrator gateway; the former NVIDIA NIM record is historical provenance in [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md), while the active contract is [`docs/doctoring/contextual-orchestrator-autofix.md`](docs/doctoring/contextual-orchestrator-autofix.md). Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. -OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). -nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). -The materialization contract is also covered by [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). +OriginWeave hourly review repair is a thin caller at minute 10; its historical +NVIDIA NIM-only worker description remains in the doctoring record. See +[`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). +nonnest2 hourly review repair is a thin caller at minute 16; its historical +NVIDIA NIM-only worker description remains in the doctoring record. See +[`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). +The materialization contract is also covered by +[`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c48db831f..119983296 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -39,7 +39,7 @@ only established scheduler credentials, and grants job-scoped only established scheduler credentials, and grants job-scoped `id-token: write`. The reusable engine stays product-neutral. -## Hourly NVIDIA NIM repair gate +## Contextual-orchestrator hourly repair gate ```mermaid flowchart TD @@ -47,7 +47,7 @@ flowchart TD Sched["Central reusable scheduler"] Bind{"Exact-head, same-repo, writer authority, sealed paths?"} Worker["repository_dispatch worker at github.sha"] - NIM["NVIDIA NIM repair model"] + Gateway["Contextual-orchestrator KV gateway\nautomatic model discovery"] Recheck{"Post-edit exact-head revalidation?"} Push["Push same-repository head"] Hold["Leave the tree unchanged"] @@ -56,15 +56,17 @@ flowchart TD Sched --> Bind Bind -->|"no"| Hold Bind -->|"yes"| Worker - Worker --> NIM - NIM --> Recheck + Worker --> Gateway + Gateway --> Recheck Recheck -->|"no"| Hold Recheck -->|"yes"| Push ``` The worker checks out helpers at `${{ github.sha }}` so a later default-branch push cannot replace privileged scripts after dispatch (CWE-367). Repair binds -`NVIDIA_NIM_API_KEY`, never `COPILOT_GITHUB_TOKEN`. +only the gateway URL and token for the two OpenCode execution steps; upstream +provider credentials remain in the gateway KV, and `COPILOT_GITHUB_TOKEN` is +never used. Product callers stagger Clearfolio at minute 23, DiskSage at minute 37, and fast-mlsirm at minute 49. Each caller is read-only, dispatches at most one @@ -96,14 +98,21 @@ sequenceDiagram participant PR as Pull request participant RW as Required workflows participant OC as OpenCode reviewer + participant AF as Write-capable autofix worker + participant GW as contextual-orchestrator gateway participant SV as sandboxed_verify / web E2E participant MS as Merge scheduler PR->>RW: pull_request_target on trusted base - RW->>OC: bounded evidence + NVIDIA NIM / OpenCode + RW->>OC: bounded evidence + independent reviewer credential OC->>SV: PoC command in isolated copy SV-->>OC: redacted stdout/stderr + command metadata OC-->>PR: APPROVE or request changes + RW->>AF: exact-head repair context after changes requested + AF->>GW: bounded repair prompt with scoped gateway URL/token + GW-->>AF: bounded model output + AF->>SV: verify candidate edit in isolated copy + SV-->>AF: tests and changed-path evidence MS->>PR: merge only on current-head approval + green checks ``` @@ -117,9 +126,10 @@ sequenceDiagram - Logs and review receipts redact credential shapes (tokens, bearer values, known provider prefixes). They do not mask operational PII that the control plane must process. -- LLM and scheduled agents bind `NVIDIA_NIM_API_KEY` (env may be - `NVIDIA_API_KEY`). They never use `COPILOT_GITHUB_TOKEN`. Existing - review-agent key schemes stay unchanged. +- The write-capable LLM path binds only the contextual-orchestrator gateway + URL/token; provider keys remain in the gateway KV. It never uses + `COPILOT_GITHUB_TOKEN`, and the existing independent review-agent key scheme + stays unchanged. - Rust remains the psychometric arithmetic owner. Repair never substitutes Python for scoring math. - Downloaded SBOM and distribution bytes are inert. The signing job does @@ -143,8 +153,9 @@ trusted `uv` exporter is downloaded from the literal GitHub Releases URL for — bot/agent exact-head review and merge procedure. - [`PR_GOVERNANCE_AUDIT.md`](PR_GOVERNANCE_AUDIT.md) — live review/merge contract. -- [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md) - — current increment's repair-worker decision and APA 7th citations. +- [`docs/doctoring/contextual-orchestrator-autofix.md`](docs/doctoring/contextual-orchestrator-autofix.md) + — current repair-worker decision and APA 7th citations; the NVIDIA NIM record + is historical provenance only. - [`docs/doctoring/fast-mlsirm-hourly-review-caller.md`](docs/doctoring/fast-mlsirm-hourly-review-caller.md) — product-specific psychometric repair heartbeat and scientific gates. - [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42afe76a..7cc6bfc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Semantic Versioning where the repository publishes a release. - Added a trusted pull-request comment router for `@cwl-noema-review` and review-only `@opencode-agent` dispatches, with an organization sweep, exact-head receipts, repository allowlisting, fixed runners, immutable checkout pins, and a permanent 100% statement/branch/docstring quality gate. - Added an organization-owned reusable exact-artifact SBOM attestation boundary that validates inert six-file wheel/sdist evidence, binds CycloneDX 1.7 predicates to exact SHA-256 subjects, signs through least-privilege GitHub artifact attestations, and exports online and offline verification bundles. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. -- Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, NVIDIA NIM model boundary, credential isolation, and fail-closed unattended-agent permissions. +- Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, contextual-orchestrator gateway boundary, credential isolation, and fail-closed unattended-agent permissions. - Added a dedicated Clearfolio hourly caller that invokes the product-neutral central scheduler with the exact repository, protected base branch, one-dispatch budget, one-hour retry floor, single-flight concurrency, and only the established scheduler credentials. - Added a dedicated DiskSage hourly caller that invokes the same product-neutral RCA and remediation-feasibility scheduler with an exact repository target, one-dispatch budget, two-hour same-head retry floor, non-cancelling single-flight heartbeat, and explicit established scheduler credentials. - Added a dedicated fast-mlsirm hourly caller that preserves Rust-owned psychometric arithmetic while dispatching at most one exact-head, root-cause-driven repair with a two-hour same-head retry floor. @@ -27,7 +27,7 @@ Semantic Versioning where the repository publishes a release. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. - Run the bounded DiskSage repair heartbeat at minute 37 of every hour, dispatch no more than one exact-head repair, and wait two hours before redispatching an unchanged head so legitimate OpenCode or NVIDIA NIM latency does not create duplicate writers. - Run the bounded fast-mlsirm repair heartbeat at minute 49 of every hour with one-dispatch scope and a two-hour same-head floor, without weakening true-parameter recovery, CPU/GPU parity, skipped-test, or Rust-ownership gates. -- Use NVIDIA NIM `mistralai/mistral-small-4-119b-2603` with explicit high reasoning for scheduled repair and `nvidia/nemotron-3-nano-30b-a3b` for bounded helper work instead of GitHub Models in the write-capable autofix worker. +- Route write-capable scheduled repair through contextual-orchestrator's KV-backed automatic model discovery instead of copying provider model credentials into the central worker; retain the independent read-only reviewer credential chain. - Apply one NUL-delimited exact-path and complete pre/post-worktree verification contract to both ordinary review repair and merge-conflict repair rather than relying on a visible post-model diff for the ordinary path. ### Changed @@ -64,7 +64,7 @@ Semantic Versioning where the repository publishes a release. - Keep the Clearfolio caller and reusable scheduler read-only at workflow and job scope; authorize mutation only through explicitly mapped `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, or the short-lived OpenCode GitHub App token exchanged from OIDC, with explicit pre-write guards and no `github.token` mutation fallback. - Keep the DiskSage caller read-only and pass only the established scheduler credentials; do not inherit secrets, expose the NVIDIA NIM model credential to the queue scanner, use a GitHub Copilot token, or grant the caller repository mutation permissions. - Keep the fast-mlsirm caller read-only and model-secret-free; preserve independent approval, exact-head evidence, and Rust production-arithmetic ownership while centralizing only bounded review repair. -- Bind `NVIDIA_NIM_API_KEY` only to the two OpenCode model execution steps, fail closed when the secret is absent, and remove GitHub and Actions OIDC credentials from both model subprocesses. The decision record now cites CWE-367 so a later default-branch push cannot replace privileged repair helpers after `repository_dispatch` has already selected the workflow revision. +- Bind `CONTEXTUAL_ORCHESTRATOR_BASE_URL` and `CONTEXTUAL_ORCHESTRATOR_TOKEN` only to the two OpenCode model execution steps, fail closed when either is absent, and remove GitHub and Actions OIDC credentials from both model subprocesses. The decision record now cites CWE-367 so a later default-branch push cannot replace privileged repair helpers after `repository_dispatch` has already selected the workflow revision. - Recorded the org control-plane architecture, including the hourly NVIDIA NIM repair gate, so agents reconstruct the write-capable worker trust boundary from the repo instead of private memory. - Deny unnecessary non-file OpenCode interactions and preserve the independent read-only reviewer workflow and its credential/model-pool contract byte-for-byte. - Pin the repository-dispatch autofix helper checkout to the exact workflow-run SHA rather than a moving default branch. diff --git a/CLAUDE.md b/CLAUDE.md index 02d6b3d84..b7db030e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,8 +69,8 @@ Details: `docs/pr-review-and-merge-procedure.md` and `PR_GOVERNANCE_AUDIT.md`. - `docs/` — master context, Project protocol, `org-required-workflow-rollout.md`, `scorecard-governance.md`, SBOM inventory. Doctoring records live under `docs/doctoring/`. [`ARCHITECTURE.md`](ARCHITECTURE.md) is the control-plane - diagram for review, hourly NVIDIA NIM repair, exact-artifact SBOM attestation, - and merge trust boundaries. + diagram for review, contextual-orchestrator repair, historical hourly NVIDIA + NIM callers, exact-artifact SBOM attestation, and merge trust boundaries. - `.jules/` — recorded performance (`bolt.md`) and security (`sentinel.md`) learnings from past work on `scripts/ci/`; worth scanning before optimizing or hardening those scripts. @@ -119,8 +119,10 @@ repeatable compile command. - **100% coverage and 100% docstrings on `scripts/ci/`** are hard gates, not aspirations. New helper code needs matching tests and docstrings. - **Product hourly callers** stay thin. Do not hard-code OriginWeave, naruon, or Keyverse - into `pr-review-fix-scheduler.yml`. The model credential remains `NVIDIA_NIM_API_KEY` - on the worker, never `COPILOT_GITHUB_TOKEN`. + into `pr-review-fix-scheduler.yml`. The write-capable worker routes through the + contextual-orchestrator KV gateway; upstream provider credentials stay in that gateway, + and the independent read-only reviewer keeps its existing credential chain. Never use + `COPILOT_GITHUB_TOKEN`. - **`pull_request_target` trust boundary.** The required review workflows run the *base branch's* trusted scripts. A PR that edits the trusted review workflows can fail its own checks until the base branch catches up; a same-head manual `workflow_dispatch` Strix run may supply review evidence diff --git a/README.md b/README.md index 94e91fe4d..48b0e7a02 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Checked-in operator facts: so it keeps the workflow files; siblings should not. - Public profile, DIKW checkpoints, project catalog, and the existing APA 7th DIKW citations stay in [profile/README.md](profile/README.md#references). -- Control-plane trust boundaries and the hourly NVIDIA NIM repair gate are +- Control-plane trust boundaries and the contextual-orchestrator repair gate are diagrammed in [ARCHITECTURE.md](ARCHITECTURE.md). If live organization ruleset inspection reports a different ref or a missing diff --git a/docs/automation/hourly-review-repair.md b/docs/automation/hourly-review-repair.md index 722724958..9bcb8601b 100644 --- a/docs/automation/hourly-review-repair.md +++ b/docs/automation/hourly-review-repair.md @@ -12,7 +12,8 @@ engine**. contextual-orchestrator, Inkspan, or another CWL service with an explicit repository and base branch. - `pr-review-autofix.yml` is the bounded write-capable worker. It uses OpenCode - with NVIDIA NIM and does not approve or merge pull requests. + through contextual-orchestrator's KV-backed gateway and does not approve or + merge pull requests. Orgmetra's caller remains provider-neutral. The intended model boundary is the contextual-orchestrator gateway: provider keys stay in its KV registry and @@ -44,8 +45,9 @@ not overlap its successor. At most one repair dispatch is created per run. The caller passes only the established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` scheduler credentials. It does not receive or forward -`NVIDIA_NIM_API_KEY`; the model credential is scoped exclusively to the two -OpenCode execution steps in the separately reviewed autofix worker. +`CONTEXTUAL_ORCHESTRATOR_TOKEN`; gateway URL/token values are scoped +exclusively to the two OpenCode execution steps in the separately reviewed +autofix worker, while upstream provider keys remain in the gateway KV. ## Orgmetra execution contract @@ -199,7 +201,8 @@ organization-level queue inspection and bounded repair dispatch. When a scheduled run fails, classify the result before rerunning: - no actionable file-scoped feedback: expected no-op; -- missing `NVIDIA_NIM_API_KEY`: central secret configuration failure; +- missing `CONTEXTUAL_ORCHESTRATOR_BASE_URL` or + `CONTEXTUAL_ORCHESTRATOR_TOKEN`: central gateway configuration failure; - head changed: safe optimistic-concurrency refusal; inspect the new head rather than retrying predecessor evidence; - out-of-scope or ignored-path change: treat as a security failure and preserve @@ -225,7 +228,7 @@ Permanent tests prove: - the dispatch budget and same-head retry floor remain one; - caller and reusable-workflow secrets are explicit and never use `secrets: inherit`; -- immutable source, NVIDIA-only model authentication, child-process credential +- immutable source, gateway-only model authentication, child-process credential stripping, live-head guards, and independent reviewer identity remain intact; - ordinary and conflict repair share the complete ignored-inclusive snapshot and NUL-delimited allowlist boundary; diff --git a/docs/doctoring/contextual-orchestrator-autofix.md b/docs/doctoring/contextual-orchestrator-autofix.md new file mode 100644 index 000000000..49484954a --- /dev/null +++ b/docs/doctoring/contextual-orchestrator-autofix.md @@ -0,0 +1,98 @@ +# Contextual Orchestrator Review-Autofix Boundary + +## Decision + +The write-capable scheduled pull-request autofix agent uses OpenCode through +the organization contextual-orchestrator gateway. The gateway URL is an +Actions variable, its bearer token is an Actions secret, and upstream provider +credentials remain in the gateway's KV registry. The gateway starts with +automatic model discovery and selects the provider model; the worker never +receives raw NVIDIA NIM, OpenAI, OpenRouter, or Bytez provider keys. + +The independent read-only review agent remains unchanged and keeps its existing +credential and model-pool contract. Review and repair have different +privileges: the review path publishes a verdict, while the autofix path may +modify and push a same-repository pull-request branch. + +## Central MSA ownership + +`ContextualWisdomLab/.github` owns scheduler dispatch authorization, gateway +configuration, credential binding, immutable worker source, and the fail-closed +repair contract. Leaf repositories consume the reusable workflow and do not +copy provider credentials or scheduler implementation. + +## Provider and tool-loop contract + +The generated OpenCode configuration enables only +`contextual-orchestrator/contextual-orchestrator` and points at +`CONTEXTUAL_ORCHESTRATOR_BASE_URL`. It sends the explicit +`X-Contextual-Orchestrator-Tool-Loop: v1` header so OpenCode owns the bounded +function-call loop while the gateway owns provider selection. Streaming tool +loops fail closed until the gateway exposes a shape-preserving streaming relay. + +At gateway startup, its automatic model-discovery process resolves registered +provider credentials from its KV registry and excludes unavailable providers. +No provider key is copied into the repository, generated OpenCode config, +prompt, command argument, or ordinary worker log. Missing gateway URL/token +configuration fails closed before model execution. + +## Immutable repository-dispatch worker source + +`PR Review Autofix` is a default-branch-only `repository_dispatch` workflow. It +checks out central helper source at the exact workflow-run SHA with +`persist-credentials: false`, then validates the target PR's live repository, +open state, same-repository branch, base ref/SHA, and head ref/SHA before any +model or write operation. + +## Exact ordinary and conflict repair write scope + +Ordinary and conflict repair share the complete pre/post worktree snapshot, +including ignored paths, file modes, regular-file hashes, and symbolic-link +targets. The ordinary allowlist is NUL-delimited and derived from current-head +file-scoped actionable review context; conflict repair uses Git's exact +unresolved paths. The verifier rejects out-of-scope, ignored, dangling, +external, metadata-race, and Git-control-file changes. + +Both OpenCode permission maps allow reviewed repository file edits but deny +`.git` and `.git/*`, as well as shell, web, task, and external-directory +interactions. Privileged commits and pushes use `core.hooksPath=/dev/null` and +an explicit revalidated repository URL. Child processes receive no GitHub or +Actions OIDC write credentials. + +## RCA, approval, and rollback + +The worker establishes exact-head root-cause analysis and remediation +feasibility before editing. Queued, pending, stale, failed, or synthetic check +evidence never becomes success. The worker cannot approve, merge, lower branch +protection, change reviewer identity, or turn an external gateway failure into +a repository edit. Every pushed head must be reviewed and checked again. + +Rollback is a reviewed source change. It must preserve ordinary and conflict +repair scope, review-derived control-plane path exclusion, `.git` denial, +ignored-path inventory, hook suppression, explicit push destination, gateway +authentication, and independent approval. + +## Verification + +The focused quality workflow checks the gateway-only provider, explicit +tool-loop header, URL/token scope to the two OpenCode steps, child-process +credential stripping, immutable source pin, exact write scope, and unchanged +independent review-agent workflow. It also retains 100% statement/branch and +public-docstring gates for the trusted helpers. + +## APA 7th references + +Git Project. (2026). *git-ls-files*. Retrieved August 20, 2026, from +https://git-scm.com/docs/git-ls-files + +Git Project. (2026). *githooks*. Retrieved August 20, 2026, from +https://git-scm.com/docs/githooks + +GitHub. (n.d.). *Secrets reference*. Retrieved August 20, 2026, from +https://docs.github.com/en/actions/reference/security/secrets + +OpenCode. (n.d.). *Permissions*. Retrieved August 20, 2026, from +https://opencode.ai/docs/permissions + +ContextualWisdomLab. (2026). *Contextual-orchestrator gateway-only provider +contract and automatic model discovery* [Internal architecture records]. diff --git a/docs/doctoring/hourly-nvidia-nim-autofix.md b/docs/doctoring/hourly-nvidia-nim-autofix.md index 6b05c6bd6..d23c1542a 100644 --- a/docs/doctoring/hourly-nvidia-nim-autofix.md +++ b/docs/doctoring/hourly-nvidia-nim-autofix.md @@ -1,5 +1,10 @@ # Hourly NVIDIA NIM Review-Autofix Boundary +> Superseded on August 20, 2026 by +> [Contextual Orchestrator Review-Autofix Boundary](contextual-orchestrator-autofix.md). +> The NIM material below is retained as historical provenance only; it is not +> current worker configuration. + ## Decision Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include; a lone `--require-hashes` line is not lock evidence. diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0..1ede18a00 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Initialize the client with one non-empty GitHub credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 0f37f3460..76e720e06 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -3362,6 +3362,7 @@ is_hallucinated_endpoint_finding() { vulnerability_file_has_absent_source_snippets() { local vuln_file="$1" + local single_line_only="${2:-0}" if [ ! -f "$vuln_file" ] || [ -L "$vuln_file" ]; then return 1 fi @@ -3376,7 +3377,7 @@ vulnerability_file_has_absent_source_snippets() { local resolved_scan_target="" resolved_scan_target="$(resolve_current_target_path "$TARGET_PATH" 2>/dev/null || true)" - if python3 - "$vuln_file" "$REPO_ROOT" "$resolved_scan_target" "$location_records_file" <<'PY' + if python3 - "$vuln_file" "$REPO_ROOT" "$resolved_scan_target" "$location_records_file" "$single_line_only" <<'PY' from pathlib import Path import re import sys @@ -3385,6 +3386,7 @@ vuln_path = Path(sys.argv[1]) repo_root = Path(sys.argv[2]) scan_target = Path(sys.argv[3]) if sys.argv[3] else None records_path = Path(sys.argv[4]) +single_line_only = sys.argv[5] == "1" record_paths = { line.split("\t", 1)[0].strip().replace("\\", "/") @@ -3502,19 +3504,24 @@ def meaningful_lines(lang: str, raw_lines: list[str]) -> list[str]: checked_blocks = 0 stale_blocks = 0 +non_single_line_blocks = 0 for source_path, lang, raw_lines in blocks: source_lines = source_by_path.get(source_path) if source_lines is None: continue snippet_lines = meaningful_lines(lang, raw_lines) - if len(snippet_lines) < 2: + if single_line_only: + if len(snippet_lines) != 1: + non_single_line_blocks += 1 + continue + elif len(snippet_lines) < 2: continue checked_blocks += 1 present = sum(1 for line in snippet_lines if line in source_lines) if present * 2 < len(snippet_lines): stale_blocks += 1 -if checked_blocks > 0 and stale_blocks == checked_blocks: +if checked_blocks > 0 and stale_blocks == checked_blocks and non_single_line_blocks == 0: raise SystemExit(0) raise SystemExit(1) PY @@ -3528,6 +3535,10 @@ PY return 1 } +vulnerability_file_has_absent_single_line_source_snippets() { + vulnerability_file_has_absent_source_snippets "$1" 1 +} + source_file_has_encrypted_runner_registration_token() { local source_file="$1" python3 - "$source_file" <<'PY' @@ -3780,6 +3791,9 @@ vulnerability_file_is_retryable_model_inconsistency() { if ! vulnerability_file_is_below_threshold "$vuln_file"; then return 1 fi + if vulnerability_file_has_absent_single_line_source_snippets "$vuln_file"; then + return 0 + fi if vulnerability_file_has_absent_endpoint_finding "$vuln_file"; then return 0 fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8b..f6a7e7c38 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -4075,6 +4075,68 @@ EOS ;; esac ;; + pr-low-single-line-below-threshold) + case "${STRIX_LLM:-}" in + vertex_ai/stale-single-line-primary) + mkdir -p "$STRIX_REPORTS_DIR/fake-stale-single-line/vulnerabilities" + cat >"$STRIX_REPORTS_DIR/fake-stale-single-line/vulnerabilities/vuln-0001.md" <<'EOS' +# Hardcoded test credential in workflow + +**Severity:** LOW +**Target:** .github/workflows/hourly-nvidia-nim-review-repair.yml + +## Code Analysis + +**Location 1:** `.github/workflows/hourly-nvidia-nim-review-repair.yml` (line 30) + Hardcoded credential + ```yaml + LOB_API_KEY: stale_model_claim + ``` +EOS + echo "Penetration test failed: stale single-line workflow snippet" + exit 1 + ;; + vertex_ai/fallback-one) + echo "scan ok after stale single-line snippet fallback" + exit 0 + ;; + *) + echo "Error: stale-single-line scenario unexpected model (${STRIX_LLM:-})" >&2 + exit 39 + ;; + esac + ;; + pr-stale-single-line-high-blocks) + case "${STRIX_LLM:-}" in + vertex_ai/stale-single-line-high-primary) + mkdir -p "$STRIX_REPORTS_DIR/fake-stale-single-line-high/vulnerabilities" + cat >"$STRIX_REPORTS_DIR/fake-stale-single-line-high/vulnerabilities/vuln-0001.md" <<'EOS' +# Hardcoded test credential in workflow + +**Severity:** HIGH +**Target:** .github/workflows/hourly-nvidia-nim-review-repair.yml + +## Code Analysis + +**Location 1:** `.github/workflows/hourly-nvidia-nim-review-repair.yml` (line 30) + Hardcoded credential + ```yaml + LOB_API_KEY: stale_model_claim + ``` +EOS + echo "Penetration test failed: high stale single-line workflow snippet" + exit 1 + ;; + vertex_ai/fallback-one|vertex_ai/fallback-two) + echo "Error: high stale single-line findings must remain blocking" >&2 + exit 40 + ;; + *) + echo "Error: high stale-single-line scenario unexpected model (${STRIX_LLM:-})" >&2 + exit 41 + ;; + esac + ;; pr-stale-source-plus-real-finding-blocks) case "${STRIX_LLM:-}" in vertex_ai/stale-source-primary) @@ -5239,6 +5301,16 @@ async def get_snapshot(schema_snapshot_uuid, user, session): return {"status": "not_found", "snapshot_json": None} data = await session.get("SchemaSnapshotData", schema_snapshot_uuid) return {"status": snap.status, "snapshot_json": data.snapshot_json if data else None} +EOS + elif [ "$scenario" = "pr-low-single-line-below-threshold" ] || [ "$scenario" = "pr-stale-single-line-high-blocks" ]; then + mkdir -p "$repo_root_dir/.github/workflows" + cat >"$repo_root_dir/.github/workflows/hourly-nvidia-nim-review-repair.yml" <<'EOS' +name: hourly review repair +jobs: + review: + runs-on: ubuntu-latest + env: + SAFE_WORKFLOW_VALUE: configured-secret-reference EOS elif [ "$scenario" = "pr-stale-source-plus-real-finding-blocks" ]; then mkdir -p "$repo_root_dir/backend/db" "$repo_root_dir/backend/api" @@ -6231,6 +6303,50 @@ run_filtered_gate_case_if_requested() { "pull_request" \ "backend/app/api/snapshots.py" ;; + pr-low-single-line-below-threshold) + run_gate_case "pr-low-single-line-below-threshold" \ + "vertex_ai/stale-single-line-primary" \ + "vertex_ai/fallback-one vertex_ai/fallback-two" \ + "0" \ + "Strix findings are below configured fail threshold 'HIGH'; allowing pipeline continuation." \ + "1" \ + "vertex_ai/stale-single-line-primary" \ + "" \ + "vertex_ai" \ + "__DEFAULT__" \ + "" \ + "0" \ + "HIGH" \ + "0" \ + "" \ + "" \ + "1200" \ + "0" \ + "pull_request" \ + ".github/workflows/hourly-nvidia-nim-review-repair.yml" + ;; + pr-stale-single-line-high-blocks) + run_gate_case "pr-stale-single-line-high-blocks" \ + "vertex_ai/stale-single-line-high-primary" \ + "vertex_ai/fallback-one vertex_ai/fallback-two" \ + "1" \ + "Strix finding intersects files changed in this pull request." \ + "1" \ + "vertex_ai/stale-single-line-high-primary" \ + "" \ + "vertex_ai" \ + "__DEFAULT__" \ + "" \ + "0" \ + "HIGH" \ + "0" \ + "" \ + "" \ + "1200" \ + "0" \ + "pull_request" \ + ".github/workflows/hourly-nvidia-nim-review-repair.yml" + ;; pull-request-target-modified-file-pr-head-tree-lookup-failure) run_pull_request_target_aborts_on_pr_head_blob_failure_case \ "pull-request-target-modified-file-pr-head-tree-lookup-failure" \ @@ -10161,6 +10277,48 @@ run_gate_case "pr-stale-snapshot-snippet-fallback-success" \ "pull_request" \ "backend/app/api/snapshots.py" +run_gate_case "pr-low-single-line-below-threshold" \ + "vertex_ai/stale-single-line-primary" \ + "vertex_ai/fallback-one vertex_ai/fallback-two" \ + "0" \ + "Strix findings are below configured fail threshold 'HIGH'; allowing pipeline continuation." \ + "1" \ + "vertex_ai/stale-single-line-primary" \ + "" \ + "vertex_ai" \ + "__DEFAULT__" \ + "" \ + "0" \ + "HIGH" \ + "0" \ + "" \ + "" \ + "1200" \ + "0" \ + "pull_request" \ + ".github/workflows/hourly-nvidia-nim-review-repair.yml" + +run_gate_case "pr-stale-single-line-high-blocks" \ + "vertex_ai/stale-single-line-high-primary" \ + "vertex_ai/fallback-one vertex_ai/fallback-two" \ + "1" \ + "Strix finding intersects files changed in this pull request." \ + "1" \ + "vertex_ai/stale-single-line-high-primary" \ + "" \ + "vertex_ai" \ + "__DEFAULT__" \ + "" \ + "0" \ + "HIGH" \ + "0" \ + "" \ + "" \ + "1200" \ + "0" \ + "pull_request" \ + ".github/workflows/hourly-nvidia-nim-review-repair.yml" + run_gate_case "pr-stale-source-plus-real-finding-blocks" \ "vertex_ai/stale-source-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_contextual_orchestrator_contract.py similarity index 71% rename from tests/test_pr_review_autofix_nvidia_nim_contract.py rename to tests/test_pr_review_autofix_contextual_orchestrator_contract.py index 1bbd98750..d5542d02c 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_contextual_orchestrator_contract.py @@ -17,7 +17,11 @@ ".github/workflows/clearfolio-hourly-review-repair.yml" ) AUTOMATION_GUIDE = Path("docs/automation/hourly-review-repair.md") -DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") +DOCTORING_RECORD = Path("docs/doctoring/contextual-orchestrator-autofix.md") +ARCHITECTURE = Path("ARCHITECTURE.md") +AUTOFIX_CONTRACT_WORKFLOW = Path( + ".github/workflows/hourly-nvidia-nim-review-repair.yml" +) CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") REVIEW_DISPATCH_BLOB_SHA = "83f6830d5c21a324b4dbcd4e5c21a07968994b81" @@ -36,25 +40,35 @@ def test_review_fix_caller_runs_once_each_hour() -> None: assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller -def test_scheduled_autofix_uses_only_nvidia_nim() -> None: - """Require the write-capable OpenCode autofix agent to use NVIDIA NIM only.""" +def test_scheduled_autofix_uses_only_contextual_orchestrator() -> None: + """Require the writer to use the gateway-owned provider contract only.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) required_fragments = ( - '"model": "nvidia-nim/mistralai/mistral-small-4-119b-2603"', - '"small_model": "nvidia-nim/nvidia/nemotron-3-nano-30b-a3b"', - '"enabled_providers": ["nvidia-nim"]', - '"nvidia-nim": {', - '"mistralai/mistral-small-4-119b-2603": {', - '"reasoningEffort": "high"', + '"model": "contextual-orchestrator/contextual-orchestrator"', + '"small_model": "contextual-orchestrator/contextual-orchestrator"', + '"enabled_providers": ["contextual-orchestrator"]', + '"contextual-orchestrator": {', + '"contextual-orchestrator": {', '"npm": "@ai-sdk/openai-compatible"', - '"baseURL": "https://integrate.api.nvidia.com/v1"', - '"apiKey": "{env:NVIDIA_API_KEY}"', - 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}', - 'MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603', + '"baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}"', + '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"', + '"X-Contextual-Orchestrator-Tool-Loop": "v1"', + 'CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }}', + 'CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }}', + 'MODEL: contextual-orchestrator/contextual-orchestrator', + 'parsed.scheme != "https"', + 'or not parsed.hostname', + 'or parsed.username', + 'or parsed.password', + 'or parsed.query', + 'or parsed.fragment', + 'CONTEXTUAL_ORCHESTRATOR_BASE_URL must be an HTTPS URL without credentials or query data', ) for fragment in required_fragments: assert fragment in workflow, fragment forbidden_fragments = ( + 'NVIDIA_NIM_API_KEY', + 'NVIDIA_API_KEY:', 'mistralai/mistral-nemotron', 'STRIX_GITHUB_MODELS_TOKEN:', 'MODEL: github-models/', @@ -68,6 +82,36 @@ def test_scheduled_autofix_uses_only_nvidia_nim() -> None: assert fragment not in workflow, fragment +def test_autofix_contract_check_uses_gateway_terminology() -> None: + """Keep the hosted contract check aligned with the gateway it validates.""" + workflow = _workflow_text(AUTOFIX_CONTRACT_WORKFLOW) + assert ( + "Hourly cadence, immutable source, gateway credential, and conflict scope" + in workflow + ) + assert ( + "Verify hourly scheduler and contextual-orchestrator autofix contracts" + in workflow + ) + assert "NIM credential" not in workflow + assert "NVIDIA NIM autofix contracts" not in workflow + + +def test_architecture_separates_reviewer_and_write_capable_gateway_flows() -> None: + """Prevent the independent reviewer from appearing to use the writer gateway.""" + architecture = _workflow_text(ARCHITECTURE) + assert ( + "RW->>OC: bounded evidence + independent reviewer credential" in architecture + ) + assert "participant AF as Write-capable autofix worker" in architecture + assert "participant GW as contextual-orchestrator gateway" in architecture + assert "AF->>GW: bounded repair prompt" in architecture + assert ( + "RW->>OC: bounded evidence + contextual-orchestrator / OpenCode" + not in architecture + ) + + def test_trusted_autofix_source_is_bound_to_dispatch_sha() -> None: """Prevent a moving default branch from replacing trusted autofix scripts.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) @@ -99,20 +143,22 @@ def test_opencode_agent_denies_non_file_interactions() -> None: assert workflow.count(f'"{permission_name}": "deny"') == 2 -def test_nvidia_nim_secret_is_scoped_to_agent_execution_steps() -> None: - """Prevent the NVIDIA credential from leaking beyond the two OpenCode runs.""" +def test_contextual_gateway_credentials_are_scoped_to_agent_execution_steps() -> None: + """Prevent gateway URL/token values from leaking beyond model execution.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) - binding = 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}' + base_binding = 'CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }}' + token_binding = 'CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }}' ordinary_start = workflow.index(" - name: Run OpenCode review autofix") ordinary_end = workflow.index(" - name: Validate changed files", ordinary_start) conflict_start = workflow.index( " - name: Merge base branch and resolve conflicts with OpenCode" ) - assert workflow.count(binding) == 2 - assert binding in workflow[ordinary_start:ordinary_end] - assert binding in workflow[conflict_start:] - assert binding not in workflow[:ordinary_start] - assert binding not in workflow[ordinary_end:conflict_start] + for binding in (base_binding, token_binding): + assert workflow.count(binding) == 2 + assert binding in workflow[ordinary_start:ordinary_end] + assert binding in workflow[conflict_start:] + assert binding not in workflow[:ordinary_start] + assert binding not in workflow[ordinary_end:conflict_start] def test_model_subprocesses_receive_no_github_or_oidc_write_credentials() -> None: @@ -136,12 +182,13 @@ def test_model_subprocesses_receive_no_github_or_oidc_write_credentials() -> Non assert workflow.count(sanitized_invocation) == 2 -def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None: - """Reject an empty model credential instead of falling back to another provider.""" +def test_missing_contextual_gateway_configuration_fails_closed_before_model_execution() -> None: + """Reject missing gateway configuration instead of falling back to a provider.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) guard = ( - 'if [ -z "${NVIDIA_API_KEY:-}" ]; then\n' - ' echo "::error::NVIDIA_NIM_API_KEY is required for scheduled ' + 'if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] ||\n' + ' [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then\n' + ' echo "::error::CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled ' 'OpenCode autofix."\n' " exit 1\n" " fi" @@ -151,9 +198,41 @@ def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None conflict_start = workflow.index( " - name: Merge base branch and resolve conflicts with OpenCode" ) - assert workflow.count(guard) == 2 + guard_message = ( + "CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled OpenCode autofix." + ) + assert workflow.count(guard_message) == 2 assert guard in workflow[ordinary_start:ordinary_end] - assert guard in workflow[conflict_start:] + assert guard_message in workflow[conflict_start:] + + +def test_contextual_gateway_readiness_requires_an_authenticated_discovered_model() -> None: + """Do not start either writer when the gateway has no auto-discovered model.""" + workflow = _workflow_text(AUTOFIX_WORKFLOW) + readiness = ( + 'models_response="$(curl -fsS \\\n' + ' --connect-timeout 10 \\\n' + ' --max-time 30 \\\n' + ' -H "Authorization: Bearer ${CONTEXTUAL_ORCHESTRATOR_TOKEN}" \\\n' + ' "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/models")"' + ) + assert workflow.count(readiness) == 2 + assert workflow.count( + "jq -e '(.data | type == \"array\") and ([.data[]? | select((.id? | type) == \"string\" and (.id | length > 0))] | length >= 1)'" + ) == 2 + assert workflow.count( + "Contextual Orchestrator gateway returned no discovered models." + ) == 2 + conflict_start = workflow.index( + " - name: Merge base branch and resolve conflicts with OpenCode" + ) + conflict = workflow[conflict_start:] + readiness_anchor = readiness.splitlines()[0] + readiness_function = conflict.index("contextual_gateway_readiness() {") + assert conflict.index("no conflict resolution needed") < readiness_function + assert readiness_anchor in conflict[readiness_function:] + conflict_branch = conflict.index('if [ -n "$conflicted_files" ]; then') + assert "contextual_gateway_readiness\n" in conflict[conflict_branch:] def test_independent_review_agent_key_system_is_unchanged() -> None: @@ -231,7 +310,9 @@ def test_operator_doctoring_and_changelog_record_exact_write_scope() -> None: assert "Ignored build caches are outside the comparison" not in doctoring assert "Git Project. (2026). *git-ls-files*" in doctoring assert "Git Project. (2026). *githooks*" in doctoring - assert "OpenCode. (2026a). *Permissions*" in doctoring + assert "OpenCode. (n.d.). *Permissions*" in doctoring + assert "At gateway startup, its automatic model-discovery process" in doctoring + assert "--auto-discover-model-agents" not in doctoring assert "ignored-path inventory" in changelog assert "model-mutable Git metadata" in changelog diff --git a/tests/test_pr_review_autofix_writer_security_contract.py b/tests/test_pr_review_autofix_writer_security_contract.py index 58ea05877..75d0bf716 100644 --- a/tests/test_pr_review_autofix_writer_security_contract.py +++ b/tests/test_pr_review_autofix_writer_security_contract.py @@ -6,7 +6,7 @@ _AUTOFIX_WORKFLOW = Path(".github/workflows/pr-review-autofix.yml") -_TARGET_MODEL = "nvidia-nim/mistralai/mistral-small-4-119b-2603" +_TARGET_MODEL = "contextual-orchestrator/contextual-orchestrator" def _workflow_text() -> str: @@ -30,15 +30,16 @@ def _step_header(workflow: str, step_name: str) -> str: return step[:run_start] -def test_writer_uses_supported_nvidia_mistral_small_with_high_reasoning() -> None: - """Pin the write-capable model and its deliberate high-reasoning budget.""" +def test_writer_uses_contextual_gateway_with_client_owned_tool_loop() -> None: + """Pin gateway model selection and the explicit OpenCode tool-loop contract.""" workflow = _workflow_text() assert f'"model": "{_TARGET_MODEL}"' in workflow - assert '"mistralai/mistral-small-4-119b-2603": {' in workflow + assert '"contextual-orchestrator": {' in workflow assert workflow.count(f"MODEL: {_TARGET_MODEL}") == 2 - assert '"reasoningEffort": "high"' in workflow - assert "nvidia-nim/mistralai/mistral-nemotron" not in workflow + assert '"X-Contextual-Orchestrator-Tool-Loop": "v1"' in workflow + assert '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"' in workflow + assert "NVIDIA_NIM_API_KEY" not in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index a48f3092d..546b99205 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -171,6 +171,21 @@ def test_not_found_skips_same_model_and_enters_cross_model_fallback(self) -> Non self.assertIn("is_nvidia_nim_not_found_error", retryable) self.assertNotIn("is_nvidia_nim_not_found_error", same_model_retry) + def test_general_source_snippet_mode_skips_single_line_blocks(self) -> None: + """Keep single-line evidence exclusive to its dedicated detector.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + self.assertIn( + "if single_line_only:\n" + " if len(snippet_lines) != 1:", + gate_source, + ) + self.assertIn( + "elif len(snippet_lines) < 2:\n" + " continue", + gate_source, + ) + def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: """Prefer a documented hosted NIM and another NIM before GitHub.""" From d195ef485835f2c6d0020a09533db4ecf2b4f35c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:03:17 -0700 Subject: [PATCH 03/27] chore(security): restore pip-audit lock owner scope Revert the unrelated contextual-orchestrator/Strix writer tree that was accidentally applied to this dependency-only security branch. Preserve the verified pip 26.2.1 lock tree and retain history without force-pushing. --- .../hourly-nvidia-nim-review-repair.yml | 14 +- .github/workflows/pr-review-autofix.yml | 115 ++++--------- AGENTS.md | 13 +- ARCHITECTURE.md | 33 ++-- CHANGELOG.md | 6 +- CLAUDE.md | 10 +- README.md | 2 +- docs/automation/hourly-review-repair.md | 13 +- .../contextual-orchestrator-autofix.md | 98 ----------- docs/doctoring/hourly-nvidia-nim-autofix.md | 5 - .../organization_commercial_readiness_loop.py | 3 +- scripts/ci/strix_quick_gate.sh | 20 +-- scripts/ci/test_strix_quick_gate.sh | 158 ------------------ ..._pr_review_autofix_nvidia_nim_contract.py} | 137 ++++----------- ...review_autofix_writer_security_contract.py | 13 +- ...est_strix_nvidia_nim_not_found_fallback.py | 15 -- 16 files changed, 106 insertions(+), 549 deletions(-) delete mode 100644 docs/doctoring/contextual-orchestrator-autofix.md rename tests/{test_pr_review_autofix_contextual_orchestrator_contract.py => test_pr_review_autofix_nvidia_nim_contract.py} (71%) diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 9a90c9320..9eb450619 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -39,7 +39,7 @@ on: - tests/test_pr_review_fix_scheduler.py - tests/test_pr_review_fix_scheduler_source_pin.py - tests/test_pr_review_autofix_context_head_binding.py - - tests/test_pr_review_autofix_contextual_orchestrator_contract.py + - tests/test_pr_review_autofix_nvidia_nim_contract.py - tests/test_pr_review_autofix_writer_security_contract.py - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md @@ -49,7 +49,7 @@ on: - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md - docs/doctoring/governance-risk-compliance-hourly-review-caller.md - - docs/doctoring/contextual-orchestrator-autofix.md + - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/orgmetra-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md @@ -92,7 +92,7 @@ on: - tests/test_pr_review_fix_scheduler.py - tests/test_pr_review_fix_scheduler_source_pin.py - tests/test_pr_review_autofix_context_head_binding.py - - tests/test_pr_review_autofix_contextual_orchestrator_contract.py + - tests/test_pr_review_autofix_nvidia_nim_contract.py - tests/test_pr_review_autofix_writer_security_contract.py - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md @@ -102,7 +102,7 @@ on: - docs/doctoring/fast-mlsirm-hourly-review-caller.md - docs/doctoring/github-hourly-conflict-repair.md - docs/doctoring/governance-risk-compliance-hourly-review-caller.md - - docs/doctoring/contextual-orchestrator-autofix.md + - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/orgmetra-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md @@ -117,7 +117,7 @@ concurrency: jobs: contract: - name: Hourly cadence, immutable source, gateway credential, and conflict scope + name: Hourly cadence, immutable source, NIM credential, and conflict scope runs-on: ubuntu-24.04 timeout-minutes: 20 steps: @@ -138,7 +138,7 @@ jobs: run: >- python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt - - name: Verify hourly scheduler and contextual-orchestrator autofix contracts + - name: Verify hourly scheduler and NVIDIA NIM autofix contracts run: | set -euo pipefail python -m pytest -q \ @@ -173,6 +173,6 @@ jobs: tests/test_pr_review_fix_scheduler.py \ tests/test_pr_review_fix_scheduler_source_pin.py \ tests/test_pr_review_autofix_context_head_binding.py \ - tests/test_pr_review_autofix_contextual_orchestrator_contract.py \ + tests/test_pr_review_autofix_nvidia_nim_contract.py \ tests/test_pr_review_autofix_writer_security_contract.py git diff --check diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index 9fd626e32..786357722 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -279,9 +279,9 @@ jobs: EOF jq -n --arg workspace "$TARGET_WORKSPACE" '{ "$schema": "https://opencode.ai/config.json", - "model": "contextual-orchestrator/contextual-orchestrator", - "small_model": "contextual-orchestrator/contextual-orchestrator", - "enabled_providers": ["contextual-orchestrator"], + "model": "nvidia-nim/mistralai/mistral-small-4-119b-2603", + "small_model": "nvidia-nim/nvidia/nemotron-3-nano-30b-a3b", + "enabled_providers": ["nvidia-nim"], "permission": { "edit": { "*": "allow", @@ -306,7 +306,7 @@ jobs: "ci-autofix": { "description": "Conservative CI pull request review autofix agent", "mode": "primary", - "model": "contextual-orchestrator/contextual-orchestrator", + "model": "nvidia-nim/mistralai/mistral-small-4-119b-2603", "reasoningEffort": "high", "prompt": "{file:./autofix-prompt.md}", "steps": 12, @@ -333,23 +333,32 @@ jobs: } }, "provider": { - "contextual-orchestrator": { + "nvidia-nim": { "npm": "@ai-sdk/openai-compatible", - "name": "Contextual Orchestrator", + "name": "NVIDIA NIM", "options": { - "baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", - "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}", - "headers": { - "X-Contextual-Orchestrator-Tool-Loop": "v1" - } + "baseURL": "https://integrate.api.nvidia.com/v1", + "apiKey": "{env:NVIDIA_API_KEY}" }, "models": { - "contextual-orchestrator": { - "name": "Contextual Orchestrator (auto-routed)", + "mistralai/mistral-small-4-119b-2603": { + "name": "Mistral Small 4 119B 2603", + "tool_call": true, + "reasoning": true, + "options": { + "reasoningEffort": "high" + }, + "limit": { + "context": 128000, + "output": 4096 + } + }, + "nvidia/nemotron-3-nano-30b-a3b": { + "name": "Nemotron 3 Nano 30B A3B", "tool_call": true, "reasoning": true, "limit": { - "context": 200000, + "context": 128000, "output": 32768 } } @@ -361,43 +370,16 @@ jobs: - name: Run OpenCode review autofix if: env.RESOLVE_CONFLICT != 'true' env: - CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }} - CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }} - MODEL: contextual-orchestrator/contextual-orchestrator + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} + MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603 SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_AUTOFIX_WORKDIR: ${{ runner.temp }}/opencode-autofix-project run: | set -euo pipefail - if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || - [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then - echo "::error::CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled OpenCode autofix." - exit 1 - fi - python3 - "$CONTEXTUAL_ORCHESTRATOR_BASE_URL" <<'PY' - import sys - from urllib.parse import urlparse - - parsed = urlparse(sys.argv[1]) - if ( - parsed.scheme != "https" - or not parsed.hostname - or parsed.username - or parsed.password - or parsed.query - or parsed.fragment - ): - raise SystemExit("CONTEXTUAL_ORCHESTRATOR_BASE_URL must be an HTTPS URL without credentials or query data") - PY - models_response="$(curl -fsS \ - --connect-timeout 10 \ - --max-time 30 \ - -H "Authorization: Bearer ${CONTEXTUAL_ORCHESTRATOR_TOKEN}" \ - "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/models")" - if ! jq -e '(.data | type == "array") and ([.data[]? | select((.id? | type) == "string" and (.id | length > 0))] | length >= 1)' \ - >/dev/null <<<"$models_response"; then - echo "::error::Contextual Orchestrator gateway returned no discovered models." + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::error::NVIDIA_NIM_API_KEY is required for scheduled OpenCode autofix." exit 1 fi prompt_file="${RUNNER_TEMP}/opencode-autofix-prompt.md" @@ -560,12 +542,11 @@ jobs: - name: Merge base branch and resolve conflicts with OpenCode if: env.RESOLVE_CONFLICT == 'true' env: - CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }} - CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} GITHUB_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token }} GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token }} MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.target_app_token.outputs.available == 'true' }} - MODEL: contextual-orchestrator/contextual-orchestrator + MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603 SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -576,6 +557,10 @@ jobs: echo "::error::Conflict-resolution mutation requires PR_REVIEW_MERGE_TOKEN, OPENCODE_APPROVE_TOKEN, or the exchanged OpenCode app token; github.token remains read-only." exit 1 fi + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::error::NVIDIA_NIM_API_KEY is required for scheduled OpenCode autofix." + exit 1 + fi cd "$TARGET_WORKSPACE" # Merge the base branch into the detached head. A clean merge stays @@ -604,41 +589,7 @@ jobs: exit 1 fi - contextual_gateway_readiness() { - if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || - [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then - echo "::error::CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled OpenCode autofix." - exit 1 - fi - python3 - "$CONTEXTUAL_ORCHESTRATOR_BASE_URL" <<'PY' - import sys - from urllib.parse import urlparse - - parsed = urlparse(sys.argv[1]) - if ( - parsed.scheme != "https" - or not parsed.hostname - or parsed.username - or parsed.password - or parsed.query - or parsed.fragment - ): - raise SystemExit("CONTEXTUAL_ORCHESTRATOR_BASE_URL must be an HTTPS URL without credentials or query data") - PY - models_response="$(curl -fsS \ - --connect-timeout 10 \ - --max-time 30 \ - -H "Authorization: Bearer ${CONTEXTUAL_ORCHESTRATOR_TOKEN}" \ - "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/models")" - if ! jq -e '(.data | type == "array") and ([.data[]? | select((.id? | type) == "string" and (.id | length > 0))] | length >= 1)' \ - >/dev/null <<<"$models_response"; then - echo "::error::Contextual Orchestrator gateway returned no discovered models." - exit 1 - fi - } - if [ -n "$conflicted_files" ]; then - contextual_gateway_readiness conflicted_paths_file="${RUNNER_TEMP}/opencode-conflicted-files.zlist" conflict_scope_snapshot="${RUNNER_TEMP}/opencode-conflict-workspace-before.json" git diff --name-only -z --diff-filter=U >"$conflicted_paths_file" diff --git a/AGENTS.md b/AGENTS.md index 934ae8273..2df633f49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,13 +3,8 @@ > **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. -Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. The current write-capable repair boundary is the contextual-orchestrator gateway; the former NVIDIA NIM record is historical provenance in [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md), while the active contract is [`docs/doctoring/contextual-orchestrator-autofix.md`](docs/doctoring/contextual-orchestrator-autofix.md). +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md). Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. -OriginWeave hourly review repair is a thin caller at minute 10; its historical -NVIDIA NIM-only worker description remains in the doctoring record. See -[`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). -nonnest2 hourly review repair is a thin caller at minute 16; its historical -NVIDIA NIM-only worker description remains in the doctoring record. See -[`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). -The materialization contract is also covered by -[`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). +OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). +nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). +The materialization contract is also covered by [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 119983296..c48db831f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -39,7 +39,7 @@ only established scheduler credentials, and grants job-scoped only established scheduler credentials, and grants job-scoped `id-token: write`. The reusable engine stays product-neutral. -## Contextual-orchestrator hourly repair gate +## Hourly NVIDIA NIM repair gate ```mermaid flowchart TD @@ -47,7 +47,7 @@ flowchart TD Sched["Central reusable scheduler"] Bind{"Exact-head, same-repo, writer authority, sealed paths?"} Worker["repository_dispatch worker at github.sha"] - Gateway["Contextual-orchestrator KV gateway\nautomatic model discovery"] + NIM["NVIDIA NIM repair model"] Recheck{"Post-edit exact-head revalidation?"} Push["Push same-repository head"] Hold["Leave the tree unchanged"] @@ -56,17 +56,15 @@ flowchart TD Sched --> Bind Bind -->|"no"| Hold Bind -->|"yes"| Worker - Worker --> Gateway - Gateway --> Recheck + Worker --> NIM + NIM --> Recheck Recheck -->|"no"| Hold Recheck -->|"yes"| Push ``` The worker checks out helpers at `${{ github.sha }}` so a later default-branch push cannot replace privileged scripts after dispatch (CWE-367). Repair binds -only the gateway URL and token for the two OpenCode execution steps; upstream -provider credentials remain in the gateway KV, and `COPILOT_GITHUB_TOKEN` is -never used. +`NVIDIA_NIM_API_KEY`, never `COPILOT_GITHUB_TOKEN`. Product callers stagger Clearfolio at minute 23, DiskSage at minute 37, and fast-mlsirm at minute 49. Each caller is read-only, dispatches at most one @@ -98,21 +96,14 @@ sequenceDiagram participant PR as Pull request participant RW as Required workflows participant OC as OpenCode reviewer - participant AF as Write-capable autofix worker - participant GW as contextual-orchestrator gateway participant SV as sandboxed_verify / web E2E participant MS as Merge scheduler PR->>RW: pull_request_target on trusted base - RW->>OC: bounded evidence + independent reviewer credential + RW->>OC: bounded evidence + NVIDIA NIM / OpenCode OC->>SV: PoC command in isolated copy SV-->>OC: redacted stdout/stderr + command metadata OC-->>PR: APPROVE or request changes - RW->>AF: exact-head repair context after changes requested - AF->>GW: bounded repair prompt with scoped gateway URL/token - GW-->>AF: bounded model output - AF->>SV: verify candidate edit in isolated copy - SV-->>AF: tests and changed-path evidence MS->>PR: merge only on current-head approval + green checks ``` @@ -126,10 +117,9 @@ sequenceDiagram - Logs and review receipts redact credential shapes (tokens, bearer values, known provider prefixes). They do not mask operational PII that the control plane must process. -- The write-capable LLM path binds only the contextual-orchestrator gateway - URL/token; provider keys remain in the gateway KV. It never uses - `COPILOT_GITHUB_TOKEN`, and the existing independent review-agent key scheme - stays unchanged. +- LLM and scheduled agents bind `NVIDIA_NIM_API_KEY` (env may be + `NVIDIA_API_KEY`). They never use `COPILOT_GITHUB_TOKEN`. Existing + review-agent key schemes stay unchanged. - Rust remains the psychometric arithmetic owner. Repair never substitutes Python for scoring math. - Downloaded SBOM and distribution bytes are inert. The signing job does @@ -153,9 +143,8 @@ trusted `uv` exporter is downloaded from the literal GitHub Releases URL for — bot/agent exact-head review and merge procedure. - [`PR_GOVERNANCE_AUDIT.md`](PR_GOVERNANCE_AUDIT.md) — live review/merge contract. -- [`docs/doctoring/contextual-orchestrator-autofix.md`](docs/doctoring/contextual-orchestrator-autofix.md) - — current repair-worker decision and APA 7th citations; the NVIDIA NIM record - is historical provenance only. +- [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md) + — current increment's repair-worker decision and APA 7th citations. - [`docs/doctoring/fast-mlsirm-hourly-review-caller.md`](docs/doctoring/fast-mlsirm-hourly-review-caller.md) — product-specific psychometric repair heartbeat and scientific gates. - [`docs/doctoring/exact-artifact-sbom-attestation.md`](docs/doctoring/exact-artifact-sbom-attestation.md) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc6bfc9d..e42afe76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Semantic Versioning where the repository publishes a release. - Added a trusted pull-request comment router for `@cwl-noema-review` and review-only `@opencode-agent` dispatches, with an organization sweep, exact-head receipts, repository allowlisting, fixed runners, immutable checkout pins, and a permanent 100% statement/branch/docstring quality gate. - Added an organization-owned reusable exact-artifact SBOM attestation boundary that validates inert six-file wheel/sdist evidence, binds CycloneDX 1.7 predicates to exact SHA-256 subjects, signs through least-privilege GitHub artifact attestations, and exports online and offline verification bundles. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. -- Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, contextual-orchestrator gateway boundary, credential isolation, and fail-closed unattended-agent permissions. +- Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, NVIDIA NIM model boundary, credential isolation, and fail-closed unattended-agent permissions. - Added a dedicated Clearfolio hourly caller that invokes the product-neutral central scheduler with the exact repository, protected base branch, one-dispatch budget, one-hour retry floor, single-flight concurrency, and only the established scheduler credentials. - Added a dedicated DiskSage hourly caller that invokes the same product-neutral RCA and remediation-feasibility scheduler with an exact repository target, one-dispatch budget, two-hour same-head retry floor, non-cancelling single-flight heartbeat, and explicit established scheduler credentials. - Added a dedicated fast-mlsirm hourly caller that preserves Rust-owned psychometric arithmetic while dispatching at most one exact-head, root-cause-driven repair with a two-hour same-head retry floor. @@ -27,7 +27,7 @@ Semantic Versioning where the repository publishes a release. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. - Run the bounded DiskSage repair heartbeat at minute 37 of every hour, dispatch no more than one exact-head repair, and wait two hours before redispatching an unchanged head so legitimate OpenCode or NVIDIA NIM latency does not create duplicate writers. - Run the bounded fast-mlsirm repair heartbeat at minute 49 of every hour with one-dispatch scope and a two-hour same-head floor, without weakening true-parameter recovery, CPU/GPU parity, skipped-test, or Rust-ownership gates. -- Route write-capable scheduled repair through contextual-orchestrator's KV-backed automatic model discovery instead of copying provider model credentials into the central worker; retain the independent read-only reviewer credential chain. +- Use NVIDIA NIM `mistralai/mistral-small-4-119b-2603` with explicit high reasoning for scheduled repair and `nvidia/nemotron-3-nano-30b-a3b` for bounded helper work instead of GitHub Models in the write-capable autofix worker. - Apply one NUL-delimited exact-path and complete pre/post-worktree verification contract to both ordinary review repair and merge-conflict repair rather than relying on a visible post-model diff for the ordinary path. ### Changed @@ -64,7 +64,7 @@ Semantic Versioning where the repository publishes a release. - Keep the Clearfolio caller and reusable scheduler read-only at workflow and job scope; authorize mutation only through explicitly mapped `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, or the short-lived OpenCode GitHub App token exchanged from OIDC, with explicit pre-write guards and no `github.token` mutation fallback. - Keep the DiskSage caller read-only and pass only the established scheduler credentials; do not inherit secrets, expose the NVIDIA NIM model credential to the queue scanner, use a GitHub Copilot token, or grant the caller repository mutation permissions. - Keep the fast-mlsirm caller read-only and model-secret-free; preserve independent approval, exact-head evidence, and Rust production-arithmetic ownership while centralizing only bounded review repair. -- Bind `CONTEXTUAL_ORCHESTRATOR_BASE_URL` and `CONTEXTUAL_ORCHESTRATOR_TOKEN` only to the two OpenCode model execution steps, fail closed when either is absent, and remove GitHub and Actions OIDC credentials from both model subprocesses. The decision record now cites CWE-367 so a later default-branch push cannot replace privileged repair helpers after `repository_dispatch` has already selected the workflow revision. +- Bind `NVIDIA_NIM_API_KEY` only to the two OpenCode model execution steps, fail closed when the secret is absent, and remove GitHub and Actions OIDC credentials from both model subprocesses. The decision record now cites CWE-367 so a later default-branch push cannot replace privileged repair helpers after `repository_dispatch` has already selected the workflow revision. - Recorded the org control-plane architecture, including the hourly NVIDIA NIM repair gate, so agents reconstruct the write-capable worker trust boundary from the repo instead of private memory. - Deny unnecessary non-file OpenCode interactions and preserve the independent read-only reviewer workflow and its credential/model-pool contract byte-for-byte. - Pin the repository-dispatch autofix helper checkout to the exact workflow-run SHA rather than a moving default branch. diff --git a/CLAUDE.md b/CLAUDE.md index b7db030e7..02d6b3d84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,8 +69,8 @@ Details: `docs/pr-review-and-merge-procedure.md` and `PR_GOVERNANCE_AUDIT.md`. - `docs/` — master context, Project protocol, `org-required-workflow-rollout.md`, `scorecard-governance.md`, SBOM inventory. Doctoring records live under `docs/doctoring/`. [`ARCHITECTURE.md`](ARCHITECTURE.md) is the control-plane - diagram for review, contextual-orchestrator repair, historical hourly NVIDIA - NIM callers, exact-artifact SBOM attestation, and merge trust boundaries. + diagram for review, hourly NVIDIA NIM repair, exact-artifact SBOM attestation, + and merge trust boundaries. - `.jules/` — recorded performance (`bolt.md`) and security (`sentinel.md`) learnings from past work on `scripts/ci/`; worth scanning before optimizing or hardening those scripts. @@ -119,10 +119,8 @@ repeatable compile command. - **100% coverage and 100% docstrings on `scripts/ci/`** are hard gates, not aspirations. New helper code needs matching tests and docstrings. - **Product hourly callers** stay thin. Do not hard-code OriginWeave, naruon, or Keyverse - into `pr-review-fix-scheduler.yml`. The write-capable worker routes through the - contextual-orchestrator KV gateway; upstream provider credentials stay in that gateway, - and the independent read-only reviewer keeps its existing credential chain. Never use - `COPILOT_GITHUB_TOKEN`. + into `pr-review-fix-scheduler.yml`. The model credential remains `NVIDIA_NIM_API_KEY` + on the worker, never `COPILOT_GITHUB_TOKEN`. - **`pull_request_target` trust boundary.** The required review workflows run the *base branch's* trusted scripts. A PR that edits the trusted review workflows can fail its own checks until the base branch catches up; a same-head manual `workflow_dispatch` Strix run may supply review evidence diff --git a/README.md b/README.md index 48b0e7a02..94e91fe4d 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Checked-in operator facts: so it keeps the workflow files; siblings should not. - Public profile, DIKW checkpoints, project catalog, and the existing APA 7th DIKW citations stay in [profile/README.md](profile/README.md#references). -- Control-plane trust boundaries and the contextual-orchestrator repair gate are +- Control-plane trust boundaries and the hourly NVIDIA NIM repair gate are diagrammed in [ARCHITECTURE.md](ARCHITECTURE.md). If live organization ruleset inspection reports a different ref or a missing diff --git a/docs/automation/hourly-review-repair.md b/docs/automation/hourly-review-repair.md index 9bcb8601b..722724958 100644 --- a/docs/automation/hourly-review-repair.md +++ b/docs/automation/hourly-review-repair.md @@ -12,8 +12,7 @@ engine**. contextual-orchestrator, Inkspan, or another CWL service with an explicit repository and base branch. - `pr-review-autofix.yml` is the bounded write-capable worker. It uses OpenCode - through contextual-orchestrator's KV-backed gateway and does not approve or - merge pull requests. + with NVIDIA NIM and does not approve or merge pull requests. Orgmetra's caller remains provider-neutral. The intended model boundary is the contextual-orchestrator gateway: provider keys stay in its KV registry and @@ -45,9 +44,8 @@ not overlap its successor. At most one repair dispatch is created per run. The caller passes only the established `PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN` scheduler credentials. It does not receive or forward -`CONTEXTUAL_ORCHESTRATOR_TOKEN`; gateway URL/token values are scoped -exclusively to the two OpenCode execution steps in the separately reviewed -autofix worker, while upstream provider keys remain in the gateway KV. +`NVIDIA_NIM_API_KEY`; the model credential is scoped exclusively to the two +OpenCode execution steps in the separately reviewed autofix worker. ## Orgmetra execution contract @@ -201,8 +199,7 @@ organization-level queue inspection and bounded repair dispatch. When a scheduled run fails, classify the result before rerunning: - no actionable file-scoped feedback: expected no-op; -- missing `CONTEXTUAL_ORCHESTRATOR_BASE_URL` or - `CONTEXTUAL_ORCHESTRATOR_TOKEN`: central gateway configuration failure; +- missing `NVIDIA_NIM_API_KEY`: central secret configuration failure; - head changed: safe optimistic-concurrency refusal; inspect the new head rather than retrying predecessor evidence; - out-of-scope or ignored-path change: treat as a security failure and preserve @@ -228,7 +225,7 @@ Permanent tests prove: - the dispatch budget and same-head retry floor remain one; - caller and reusable-workflow secrets are explicit and never use `secrets: inherit`; -- immutable source, gateway-only model authentication, child-process credential +- immutable source, NVIDIA-only model authentication, child-process credential stripping, live-head guards, and independent reviewer identity remain intact; - ordinary and conflict repair share the complete ignored-inclusive snapshot and NUL-delimited allowlist boundary; diff --git a/docs/doctoring/contextual-orchestrator-autofix.md b/docs/doctoring/contextual-orchestrator-autofix.md deleted file mode 100644 index 49484954a..000000000 --- a/docs/doctoring/contextual-orchestrator-autofix.md +++ /dev/null @@ -1,98 +0,0 @@ -# Contextual Orchestrator Review-Autofix Boundary - -## Decision - -The write-capable scheduled pull-request autofix agent uses OpenCode through -the organization contextual-orchestrator gateway. The gateway URL is an -Actions variable, its bearer token is an Actions secret, and upstream provider -credentials remain in the gateway's KV registry. The gateway starts with -automatic model discovery and selects the provider model; the worker never -receives raw NVIDIA NIM, OpenAI, OpenRouter, or Bytez provider keys. - -The independent read-only review agent remains unchanged and keeps its existing -credential and model-pool contract. Review and repair have different -privileges: the review path publishes a verdict, while the autofix path may -modify and push a same-repository pull-request branch. - -## Central MSA ownership - -`ContextualWisdomLab/.github` owns scheduler dispatch authorization, gateway -configuration, credential binding, immutable worker source, and the fail-closed -repair contract. Leaf repositories consume the reusable workflow and do not -copy provider credentials or scheduler implementation. - -## Provider and tool-loop contract - -The generated OpenCode configuration enables only -`contextual-orchestrator/contextual-orchestrator` and points at -`CONTEXTUAL_ORCHESTRATOR_BASE_URL`. It sends the explicit -`X-Contextual-Orchestrator-Tool-Loop: v1` header so OpenCode owns the bounded -function-call loop while the gateway owns provider selection. Streaming tool -loops fail closed until the gateway exposes a shape-preserving streaming relay. - -At gateway startup, its automatic model-discovery process resolves registered -provider credentials from its KV registry and excludes unavailable providers. -No provider key is copied into the repository, generated OpenCode config, -prompt, command argument, or ordinary worker log. Missing gateway URL/token -configuration fails closed before model execution. - -## Immutable repository-dispatch worker source - -`PR Review Autofix` is a default-branch-only `repository_dispatch` workflow. It -checks out central helper source at the exact workflow-run SHA with -`persist-credentials: false`, then validates the target PR's live repository, -open state, same-repository branch, base ref/SHA, and head ref/SHA before any -model or write operation. - -## Exact ordinary and conflict repair write scope - -Ordinary and conflict repair share the complete pre/post worktree snapshot, -including ignored paths, file modes, regular-file hashes, and symbolic-link -targets. The ordinary allowlist is NUL-delimited and derived from current-head -file-scoped actionable review context; conflict repair uses Git's exact -unresolved paths. The verifier rejects out-of-scope, ignored, dangling, -external, metadata-race, and Git-control-file changes. - -Both OpenCode permission maps allow reviewed repository file edits but deny -`.git` and `.git/*`, as well as shell, web, task, and external-directory -interactions. Privileged commits and pushes use `core.hooksPath=/dev/null` and -an explicit revalidated repository URL. Child processes receive no GitHub or -Actions OIDC write credentials. - -## RCA, approval, and rollback - -The worker establishes exact-head root-cause analysis and remediation -feasibility before editing. Queued, pending, stale, failed, or synthetic check -evidence never becomes success. The worker cannot approve, merge, lower branch -protection, change reviewer identity, or turn an external gateway failure into -a repository edit. Every pushed head must be reviewed and checked again. - -Rollback is a reviewed source change. It must preserve ordinary and conflict -repair scope, review-derived control-plane path exclusion, `.git` denial, -ignored-path inventory, hook suppression, explicit push destination, gateway -authentication, and independent approval. - -## Verification - -The focused quality workflow checks the gateway-only provider, explicit -tool-loop header, URL/token scope to the two OpenCode steps, child-process -credential stripping, immutable source pin, exact write scope, and unchanged -independent review-agent workflow. It also retains 100% statement/branch and -public-docstring gates for the trusted helpers. - -## APA 7th references - -Git Project. (2026). *git-ls-files*. Retrieved August 20, 2026, from -https://git-scm.com/docs/git-ls-files - -Git Project. (2026). *githooks*. Retrieved August 20, 2026, from -https://git-scm.com/docs/githooks - -GitHub. (n.d.). *Secrets reference*. Retrieved August 20, 2026, from -https://docs.github.com/en/actions/reference/security/secrets - -OpenCode. (n.d.). *Permissions*. Retrieved August 20, 2026, from -https://opencode.ai/docs/permissions - -ContextualWisdomLab. (2026). *Contextual-orchestrator gateway-only provider -contract and automatic model discovery* [Internal architecture records]. diff --git a/docs/doctoring/hourly-nvidia-nim-autofix.md b/docs/doctoring/hourly-nvidia-nim-autofix.md index d23c1542a..6b05c6bd6 100644 --- a/docs/doctoring/hourly-nvidia-nim-autofix.md +++ b/docs/doctoring/hourly-nvidia-nim-autofix.md @@ -1,10 +1,5 @@ # Hourly NVIDIA NIM Review-Autofix Boundary -> Superseded on August 20, 2026 by -> [Contextual Orchestrator Review-Autofix Boundary](contextual-orchestrator-autofix.md). -> The NIM material below is retained as historical provenance only; it is not -> current worker configuration. - ## Decision Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include; a lone `--require-hashes` line is not lock evidence. diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index 1ede18a00..c00cfa1e0 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,7 +239,6 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: - """Initialize the client with one non-empty GitHub credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -854,4 +853,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 76e720e06..0f37f3460 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -3362,7 +3362,6 @@ is_hallucinated_endpoint_finding() { vulnerability_file_has_absent_source_snippets() { local vuln_file="$1" - local single_line_only="${2:-0}" if [ ! -f "$vuln_file" ] || [ -L "$vuln_file" ]; then return 1 fi @@ -3377,7 +3376,7 @@ vulnerability_file_has_absent_source_snippets() { local resolved_scan_target="" resolved_scan_target="$(resolve_current_target_path "$TARGET_PATH" 2>/dev/null || true)" - if python3 - "$vuln_file" "$REPO_ROOT" "$resolved_scan_target" "$location_records_file" "$single_line_only" <<'PY' + if python3 - "$vuln_file" "$REPO_ROOT" "$resolved_scan_target" "$location_records_file" <<'PY' from pathlib import Path import re import sys @@ -3386,7 +3385,6 @@ vuln_path = Path(sys.argv[1]) repo_root = Path(sys.argv[2]) scan_target = Path(sys.argv[3]) if sys.argv[3] else None records_path = Path(sys.argv[4]) -single_line_only = sys.argv[5] == "1" record_paths = { line.split("\t", 1)[0].strip().replace("\\", "/") @@ -3504,24 +3502,19 @@ def meaningful_lines(lang: str, raw_lines: list[str]) -> list[str]: checked_blocks = 0 stale_blocks = 0 -non_single_line_blocks = 0 for source_path, lang, raw_lines in blocks: source_lines = source_by_path.get(source_path) if source_lines is None: continue snippet_lines = meaningful_lines(lang, raw_lines) - if single_line_only: - if len(snippet_lines) != 1: - non_single_line_blocks += 1 - continue - elif len(snippet_lines) < 2: + if len(snippet_lines) < 2: continue checked_blocks += 1 present = sum(1 for line in snippet_lines if line in source_lines) if present * 2 < len(snippet_lines): stale_blocks += 1 -if checked_blocks > 0 and stale_blocks == checked_blocks and non_single_line_blocks == 0: +if checked_blocks > 0 and stale_blocks == checked_blocks: raise SystemExit(0) raise SystemExit(1) PY @@ -3535,10 +3528,6 @@ PY return 1 } -vulnerability_file_has_absent_single_line_source_snippets() { - vulnerability_file_has_absent_source_snippets "$1" 1 -} - source_file_has_encrypted_runner_registration_token() { local source_file="$1" python3 - "$source_file" <<'PY' @@ -3791,9 +3780,6 @@ vulnerability_file_is_retryable_model_inconsistency() { if ! vulnerability_file_is_below_threshold "$vuln_file"; then return 1 fi - if vulnerability_file_has_absent_single_line_source_snippets "$vuln_file"; then - return 0 - fi if vulnerability_file_has_absent_endpoint_finding "$vuln_file"; then return 0 fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index f6a7e7c38..ac9ce1d8b 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -4075,68 +4075,6 @@ EOS ;; esac ;; - pr-low-single-line-below-threshold) - case "${STRIX_LLM:-}" in - vertex_ai/stale-single-line-primary) - mkdir -p "$STRIX_REPORTS_DIR/fake-stale-single-line/vulnerabilities" - cat >"$STRIX_REPORTS_DIR/fake-stale-single-line/vulnerabilities/vuln-0001.md" <<'EOS' -# Hardcoded test credential in workflow - -**Severity:** LOW -**Target:** .github/workflows/hourly-nvidia-nim-review-repair.yml - -## Code Analysis - -**Location 1:** `.github/workflows/hourly-nvidia-nim-review-repair.yml` (line 30) - Hardcoded credential - ```yaml - LOB_API_KEY: stale_model_claim - ``` -EOS - echo "Penetration test failed: stale single-line workflow snippet" - exit 1 - ;; - vertex_ai/fallback-one) - echo "scan ok after stale single-line snippet fallback" - exit 0 - ;; - *) - echo "Error: stale-single-line scenario unexpected model (${STRIX_LLM:-})" >&2 - exit 39 - ;; - esac - ;; - pr-stale-single-line-high-blocks) - case "${STRIX_LLM:-}" in - vertex_ai/stale-single-line-high-primary) - mkdir -p "$STRIX_REPORTS_DIR/fake-stale-single-line-high/vulnerabilities" - cat >"$STRIX_REPORTS_DIR/fake-stale-single-line-high/vulnerabilities/vuln-0001.md" <<'EOS' -# Hardcoded test credential in workflow - -**Severity:** HIGH -**Target:** .github/workflows/hourly-nvidia-nim-review-repair.yml - -## Code Analysis - -**Location 1:** `.github/workflows/hourly-nvidia-nim-review-repair.yml` (line 30) - Hardcoded credential - ```yaml - LOB_API_KEY: stale_model_claim - ``` -EOS - echo "Penetration test failed: high stale single-line workflow snippet" - exit 1 - ;; - vertex_ai/fallback-one|vertex_ai/fallback-two) - echo "Error: high stale single-line findings must remain blocking" >&2 - exit 40 - ;; - *) - echo "Error: high stale-single-line scenario unexpected model (${STRIX_LLM:-})" >&2 - exit 41 - ;; - esac - ;; pr-stale-source-plus-real-finding-blocks) case "${STRIX_LLM:-}" in vertex_ai/stale-source-primary) @@ -5301,16 +5239,6 @@ async def get_snapshot(schema_snapshot_uuid, user, session): return {"status": "not_found", "snapshot_json": None} data = await session.get("SchemaSnapshotData", schema_snapshot_uuid) return {"status": snap.status, "snapshot_json": data.snapshot_json if data else None} -EOS - elif [ "$scenario" = "pr-low-single-line-below-threshold" ] || [ "$scenario" = "pr-stale-single-line-high-blocks" ]; then - mkdir -p "$repo_root_dir/.github/workflows" - cat >"$repo_root_dir/.github/workflows/hourly-nvidia-nim-review-repair.yml" <<'EOS' -name: hourly review repair -jobs: - review: - runs-on: ubuntu-latest - env: - SAFE_WORKFLOW_VALUE: configured-secret-reference EOS elif [ "$scenario" = "pr-stale-source-plus-real-finding-blocks" ]; then mkdir -p "$repo_root_dir/backend/db" "$repo_root_dir/backend/api" @@ -6303,50 +6231,6 @@ run_filtered_gate_case_if_requested() { "pull_request" \ "backend/app/api/snapshots.py" ;; - pr-low-single-line-below-threshold) - run_gate_case "pr-low-single-line-below-threshold" \ - "vertex_ai/stale-single-line-primary" \ - "vertex_ai/fallback-one vertex_ai/fallback-two" \ - "0" \ - "Strix findings are below configured fail threshold 'HIGH'; allowing pipeline continuation." \ - "1" \ - "vertex_ai/stale-single-line-primary" \ - "" \ - "vertex_ai" \ - "__DEFAULT__" \ - "" \ - "0" \ - "HIGH" \ - "0" \ - "" \ - "" \ - "1200" \ - "0" \ - "pull_request" \ - ".github/workflows/hourly-nvidia-nim-review-repair.yml" - ;; - pr-stale-single-line-high-blocks) - run_gate_case "pr-stale-single-line-high-blocks" \ - "vertex_ai/stale-single-line-high-primary" \ - "vertex_ai/fallback-one vertex_ai/fallback-two" \ - "1" \ - "Strix finding intersects files changed in this pull request." \ - "1" \ - "vertex_ai/stale-single-line-high-primary" \ - "" \ - "vertex_ai" \ - "__DEFAULT__" \ - "" \ - "0" \ - "HIGH" \ - "0" \ - "" \ - "" \ - "1200" \ - "0" \ - "pull_request" \ - ".github/workflows/hourly-nvidia-nim-review-repair.yml" - ;; pull-request-target-modified-file-pr-head-tree-lookup-failure) run_pull_request_target_aborts_on_pr_head_blob_failure_case \ "pull-request-target-modified-file-pr-head-tree-lookup-failure" \ @@ -10277,48 +10161,6 @@ run_gate_case "pr-stale-snapshot-snippet-fallback-success" \ "pull_request" \ "backend/app/api/snapshots.py" -run_gate_case "pr-low-single-line-below-threshold" \ - "vertex_ai/stale-single-line-primary" \ - "vertex_ai/fallback-one vertex_ai/fallback-two" \ - "0" \ - "Strix findings are below configured fail threshold 'HIGH'; allowing pipeline continuation." \ - "1" \ - "vertex_ai/stale-single-line-primary" \ - "" \ - "vertex_ai" \ - "__DEFAULT__" \ - "" \ - "0" \ - "HIGH" \ - "0" \ - "" \ - "" \ - "1200" \ - "0" \ - "pull_request" \ - ".github/workflows/hourly-nvidia-nim-review-repair.yml" - -run_gate_case "pr-stale-single-line-high-blocks" \ - "vertex_ai/stale-single-line-high-primary" \ - "vertex_ai/fallback-one vertex_ai/fallback-two" \ - "1" \ - "Strix finding intersects files changed in this pull request." \ - "1" \ - "vertex_ai/stale-single-line-high-primary" \ - "" \ - "vertex_ai" \ - "__DEFAULT__" \ - "" \ - "0" \ - "HIGH" \ - "0" \ - "" \ - "" \ - "1200" \ - "0" \ - "pull_request" \ - ".github/workflows/hourly-nvidia-nim-review-repair.yml" - run_gate_case "pr-stale-source-plus-real-finding-blocks" \ "vertex_ai/stale-source-primary" \ "vertex_ai/fallback-one vertex_ai/fallback-two" \ diff --git a/tests/test_pr_review_autofix_contextual_orchestrator_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py similarity index 71% rename from tests/test_pr_review_autofix_contextual_orchestrator_contract.py rename to tests/test_pr_review_autofix_nvidia_nim_contract.py index d5542d02c..1bbd98750 100644 --- a/tests/test_pr_review_autofix_contextual_orchestrator_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,11 +17,7 @@ ".github/workflows/clearfolio-hourly-review-repair.yml" ) AUTOMATION_GUIDE = Path("docs/automation/hourly-review-repair.md") -DOCTORING_RECORD = Path("docs/doctoring/contextual-orchestrator-autofix.md") -ARCHITECTURE = Path("ARCHITECTURE.md") -AUTOFIX_CONTRACT_WORKFLOW = Path( - ".github/workflows/hourly-nvidia-nim-review-repair.yml" -) +DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") REVIEW_DISPATCH_BLOB_SHA = "83f6830d5c21a324b4dbcd4e5c21a07968994b81" @@ -40,35 +36,25 @@ def test_review_fix_caller_runs_once_each_hour() -> None: assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller -def test_scheduled_autofix_uses_only_contextual_orchestrator() -> None: - """Require the writer to use the gateway-owned provider contract only.""" +def test_scheduled_autofix_uses_only_nvidia_nim() -> None: + """Require the write-capable OpenCode autofix agent to use NVIDIA NIM only.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) required_fragments = ( - '"model": "contextual-orchestrator/contextual-orchestrator"', - '"small_model": "contextual-orchestrator/contextual-orchestrator"', - '"enabled_providers": ["contextual-orchestrator"]', - '"contextual-orchestrator": {', - '"contextual-orchestrator": {', + '"model": "nvidia-nim/mistralai/mistral-small-4-119b-2603"', + '"small_model": "nvidia-nim/nvidia/nemotron-3-nano-30b-a3b"', + '"enabled_providers": ["nvidia-nim"]', + '"nvidia-nim": {', + '"mistralai/mistral-small-4-119b-2603": {', + '"reasoningEffort": "high"', '"npm": "@ai-sdk/openai-compatible"', - '"baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}"', - '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"', - '"X-Contextual-Orchestrator-Tool-Loop": "v1"', - 'CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }}', - 'CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }}', - 'MODEL: contextual-orchestrator/contextual-orchestrator', - 'parsed.scheme != "https"', - 'or not parsed.hostname', - 'or parsed.username', - 'or parsed.password', - 'or parsed.query', - 'or parsed.fragment', - 'CONTEXTUAL_ORCHESTRATOR_BASE_URL must be an HTTPS URL without credentials or query data', + '"baseURL": "https://integrate.api.nvidia.com/v1"', + '"apiKey": "{env:NVIDIA_API_KEY}"', + 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}', + 'MODEL: nvidia-nim/mistralai/mistral-small-4-119b-2603', ) for fragment in required_fragments: assert fragment in workflow, fragment forbidden_fragments = ( - 'NVIDIA_NIM_API_KEY', - 'NVIDIA_API_KEY:', 'mistralai/mistral-nemotron', 'STRIX_GITHUB_MODELS_TOKEN:', 'MODEL: github-models/', @@ -82,36 +68,6 @@ def test_scheduled_autofix_uses_only_contextual_orchestrator() -> None: assert fragment not in workflow, fragment -def test_autofix_contract_check_uses_gateway_terminology() -> None: - """Keep the hosted contract check aligned with the gateway it validates.""" - workflow = _workflow_text(AUTOFIX_CONTRACT_WORKFLOW) - assert ( - "Hourly cadence, immutable source, gateway credential, and conflict scope" - in workflow - ) - assert ( - "Verify hourly scheduler and contextual-orchestrator autofix contracts" - in workflow - ) - assert "NIM credential" not in workflow - assert "NVIDIA NIM autofix contracts" not in workflow - - -def test_architecture_separates_reviewer_and_write_capable_gateway_flows() -> None: - """Prevent the independent reviewer from appearing to use the writer gateway.""" - architecture = _workflow_text(ARCHITECTURE) - assert ( - "RW->>OC: bounded evidence + independent reviewer credential" in architecture - ) - assert "participant AF as Write-capable autofix worker" in architecture - assert "participant GW as contextual-orchestrator gateway" in architecture - assert "AF->>GW: bounded repair prompt" in architecture - assert ( - "RW->>OC: bounded evidence + contextual-orchestrator / OpenCode" - not in architecture - ) - - def test_trusted_autofix_source_is_bound_to_dispatch_sha() -> None: """Prevent a moving default branch from replacing trusted autofix scripts.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) @@ -143,22 +99,20 @@ def test_opencode_agent_denies_non_file_interactions() -> None: assert workflow.count(f'"{permission_name}": "deny"') == 2 -def test_contextual_gateway_credentials_are_scoped_to_agent_execution_steps() -> None: - """Prevent gateway URL/token values from leaking beyond model execution.""" +def test_nvidia_nim_secret_is_scoped_to_agent_execution_steps() -> None: + """Prevent the NVIDIA credential from leaking beyond the two OpenCode runs.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) - base_binding = 'CONTEXTUAL_ORCHESTRATOR_BASE_URL: ${{ vars.CONTEXTUAL_ORCHESTRATOR_BASE_URL }}' - token_binding = 'CONTEXTUAL_ORCHESTRATOR_TOKEN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_TOKEN }}' + binding = 'NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}' ordinary_start = workflow.index(" - name: Run OpenCode review autofix") ordinary_end = workflow.index(" - name: Validate changed files", ordinary_start) conflict_start = workflow.index( " - name: Merge base branch and resolve conflicts with OpenCode" ) - for binding in (base_binding, token_binding): - assert workflow.count(binding) == 2 - assert binding in workflow[ordinary_start:ordinary_end] - assert binding in workflow[conflict_start:] - assert binding not in workflow[:ordinary_start] - assert binding not in workflow[ordinary_end:conflict_start] + assert workflow.count(binding) == 2 + assert binding in workflow[ordinary_start:ordinary_end] + assert binding in workflow[conflict_start:] + assert binding not in workflow[:ordinary_start] + assert binding not in workflow[ordinary_end:conflict_start] def test_model_subprocesses_receive_no_github_or_oidc_write_credentials() -> None: @@ -182,13 +136,12 @@ def test_model_subprocesses_receive_no_github_or_oidc_write_credentials() -> Non assert workflow.count(sanitized_invocation) == 2 -def test_missing_contextual_gateway_configuration_fails_closed_before_model_execution() -> None: - """Reject missing gateway configuration instead of falling back to a provider.""" +def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None: + """Reject an empty model credential instead of falling back to another provider.""" workflow = _workflow_text(AUTOFIX_WORKFLOW) guard = ( - 'if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] ||\n' - ' [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then\n' - ' echo "::error::CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled ' + 'if [ -z "${NVIDIA_API_KEY:-}" ]; then\n' + ' echo "::error::NVIDIA_NIM_API_KEY is required for scheduled ' 'OpenCode autofix."\n' " exit 1\n" " fi" @@ -198,41 +151,9 @@ def test_missing_contextual_gateway_configuration_fails_closed_before_model_exec conflict_start = workflow.index( " - name: Merge base branch and resolve conflicts with OpenCode" ) - guard_message = ( - "CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN are required for scheduled OpenCode autofix." - ) - assert workflow.count(guard_message) == 2 + assert workflow.count(guard) == 2 assert guard in workflow[ordinary_start:ordinary_end] - assert guard_message in workflow[conflict_start:] - - -def test_contextual_gateway_readiness_requires_an_authenticated_discovered_model() -> None: - """Do not start either writer when the gateway has no auto-discovered model.""" - workflow = _workflow_text(AUTOFIX_WORKFLOW) - readiness = ( - 'models_response="$(curl -fsS \\\n' - ' --connect-timeout 10 \\\n' - ' --max-time 30 \\\n' - ' -H "Authorization: Bearer ${CONTEXTUAL_ORCHESTRATOR_TOKEN}" \\\n' - ' "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/models")"' - ) - assert workflow.count(readiness) == 2 - assert workflow.count( - "jq -e '(.data | type == \"array\") and ([.data[]? | select((.id? | type) == \"string\" and (.id | length > 0))] | length >= 1)'" - ) == 2 - assert workflow.count( - "Contextual Orchestrator gateway returned no discovered models." - ) == 2 - conflict_start = workflow.index( - " - name: Merge base branch and resolve conflicts with OpenCode" - ) - conflict = workflow[conflict_start:] - readiness_anchor = readiness.splitlines()[0] - readiness_function = conflict.index("contextual_gateway_readiness() {") - assert conflict.index("no conflict resolution needed") < readiness_function - assert readiness_anchor in conflict[readiness_function:] - conflict_branch = conflict.index('if [ -n "$conflicted_files" ]; then') - assert "contextual_gateway_readiness\n" in conflict[conflict_branch:] + assert guard in workflow[conflict_start:] def test_independent_review_agent_key_system_is_unchanged() -> None: @@ -310,9 +231,7 @@ def test_operator_doctoring_and_changelog_record_exact_write_scope() -> None: assert "Ignored build caches are outside the comparison" not in doctoring assert "Git Project. (2026). *git-ls-files*" in doctoring assert "Git Project. (2026). *githooks*" in doctoring - assert "OpenCode. (n.d.). *Permissions*" in doctoring - assert "At gateway startup, its automatic model-discovery process" in doctoring - assert "--auto-discover-model-agents" not in doctoring + assert "OpenCode. (2026a). *Permissions*" in doctoring assert "ignored-path inventory" in changelog assert "model-mutable Git metadata" in changelog diff --git a/tests/test_pr_review_autofix_writer_security_contract.py b/tests/test_pr_review_autofix_writer_security_contract.py index 75d0bf716..58ea05877 100644 --- a/tests/test_pr_review_autofix_writer_security_contract.py +++ b/tests/test_pr_review_autofix_writer_security_contract.py @@ -6,7 +6,7 @@ _AUTOFIX_WORKFLOW = Path(".github/workflows/pr-review-autofix.yml") -_TARGET_MODEL = "contextual-orchestrator/contextual-orchestrator" +_TARGET_MODEL = "nvidia-nim/mistralai/mistral-small-4-119b-2603" def _workflow_text() -> str: @@ -30,16 +30,15 @@ def _step_header(workflow: str, step_name: str) -> str: return step[:run_start] -def test_writer_uses_contextual_gateway_with_client_owned_tool_loop() -> None: - """Pin gateway model selection and the explicit OpenCode tool-loop contract.""" +def test_writer_uses_supported_nvidia_mistral_small_with_high_reasoning() -> None: + """Pin the write-capable model and its deliberate high-reasoning budget.""" workflow = _workflow_text() assert f'"model": "{_TARGET_MODEL}"' in workflow - assert '"contextual-orchestrator": {' in workflow + assert '"mistralai/mistral-small-4-119b-2603": {' in workflow assert workflow.count(f"MODEL: {_TARGET_MODEL}") == 2 - assert '"X-Contextual-Orchestrator-Tool-Loop": "v1"' in workflow - assert '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"' in workflow - assert "NVIDIA_NIM_API_KEY" not in workflow + assert '"reasoningEffort": "high"' in workflow + assert "nvidia-nim/mistralai/mistral-nemotron" not in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index 546b99205..a48f3092d 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -171,21 +171,6 @@ def test_not_found_skips_same_model_and_enters_cross_model_fallback(self) -> Non self.assertIn("is_nvidia_nim_not_found_error", retryable) self.assertNotIn("is_nvidia_nim_not_found_error", same_model_retry) - def test_general_source_snippet_mode_skips_single_line_blocks(self) -> None: - """Keep single-line evidence exclusive to its dedicated detector.""" - - gate_source = STRIX_GATE.read_text(encoding="utf-8") - self.assertIn( - "if single_line_only:\n" - " if len(snippet_lines) != 1:", - gate_source, - ) - self.assertIn( - "elif len(snippet_lines) < 2:\n" - " continue", - gate_source, - ) - def test_workflow_uses_available_free_first_nvidia_plan(self) -> None: """Prefer a documented hosted NIM and another NIM before GitHub.""" From b9cffcfe0b85c86dd8124ac8b92c81ca0e160fb3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 07:19:32 -0700 Subject: [PATCH 04/27] chore: schedule contextual-orchestrator hourly review repair (#1178) * chore: schedule contextual orchestrator review repair * test: cover contextual orchestrator hourly caller * docs: complete commercial readiness client docstring * docs: clarify hourly scheduler slot sharing --------- Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> --- ...tual-orchestrator-hourly-review-repair.yml | 32 +++++++ .../hourly-nvidia-nim-review-repair.yml | 7 ++ ...xtual-orchestrator-hourly-review-caller.md | 60 +++++++++++++ .../organization_commercial_readiness_loop.py | 3 +- ...xtual_orchestrator_hourly_review_caller.py | 85 +++++++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/contextual-orchestrator-hourly-review-repair.yml create mode 100644 docs/doctoring/contextual-orchestrator-hourly-review-caller.md create mode 100644 tests/test_contextual_orchestrator_hourly_review_caller.py diff --git a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml new file mode 100644 index 000000000..3c1bcbb91 --- /dev/null +++ b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml @@ -0,0 +1,32 @@ +name: Contextual Orchestrator Hourly Review Repair + +on: + schedule: + # Run at minute 17 each hour; the slot is shared with the three-hourly + # pr-auto-rebase workflow, so both workflows use independent concurrency groups. + - cron: "17 * * * *" + +concurrency: + group: contextual-orchestrator-hourly-review-repair + # A complete exact-head review can exceed one hour; keep it alive for the next heartbeat. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + dispatch-review-repair: + permissions: + contents: read + id-token: write + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/contextual-orchestrator + base_branch: main + max_prs: "50" + max_dispatches: "1" + resolve_unreviewed_conflicts: true + retry_hours: "2" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 9eb450619..9abe8d242 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -8,6 +8,7 @@ on: - .github/workflows/pr-review-autofix.yml - .github/workflows/bandscope-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -20,6 +21,7 @@ on: - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py + - tests/test_contextual_orchestrator_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -44,6 +46,7 @@ on: - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md - docs/doctoring/clearfolio-hourly-review-caller.md + - docs/doctoring/contextual-orchestrator-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md @@ -61,6 +64,7 @@ on: - .github/workflows/pr-review-autofix.yml - .github/workflows/bandscope-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -73,6 +77,7 @@ on: - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py + - tests/test_contextual_orchestrator_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -97,6 +102,7 @@ on: - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md - docs/doctoring/clearfolio-hourly-review-caller.md + - docs/doctoring/contextual-orchestrator-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md @@ -155,6 +161,7 @@ jobs: scripts/ci/pr_review_autofix_context.py \ tests/test_pr_review_conflict_scope.py \ tests/test_bandscope_hourly_review_caller.py \ + tests/test_contextual_orchestrator_hourly_review_caller.py \ tests/test_disksage_hourly_review_caller.py \ tests/test_fast_mlsirm_hourly_review_caller.py \ tests/test_github_hourly_conflict_repair.py \ diff --git a/docs/doctoring/contextual-orchestrator-hourly-review-caller.md b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md new file mode 100644 index 000000000..a8748a156 --- /dev/null +++ b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md @@ -0,0 +1,60 @@ +# Contextual Orchestrator Hourly Review-Repair Caller + +## Customer action + +Keep `ContextualWisdomLab/contextual-orchestrator` on the protected `main` +branch, then confirm the central autofix gateway contract in +`ContextualWisdomLab/.github#1168` is merged before enabling this scheduled +caller. After the caller runs, inspect the target PR's exact-head review, +Checks, and independent approval; merge only through the protected normal path. + +## Runtime boundary + +The workflow runs at minute 17 of every hour and calls the central +`pr-review-fix-scheduler.yml` reusable workflow. It scans at most 50 open PRs, +dispatches one bounded repair, permits unresolved conflict repair, and waits +two hours before retrying the same head. Non-cancelling concurrency preserves a +long-running exact-head OpenCode, Noema, or security operation. + +The caller grants read-only contents access and OIDC token exchange only. It +forwards the established `PR_REVIEW_MERGE_TOKEN` and +`OPENCODE_APPROVE_TOKEN` paths explicitly; it does not inherit all secrets and +does not receive `NVIDIA_NIM_API_KEY` or `COPILOT_GITHUB_TOKEN`. The central +target allowlist must include `ContextualWisdomLab/contextual-orchestrator` in +`OPENCODE_REPOSITORY_DISPATCH_TARGETS`. + +## Gateway dependency and evidence + +The reusable scheduler dispatches the default-branch central autofix workflow. +Merge `ContextualWisdomLab/.github#1168` first so that this caller's write path +uses the contextual-orchestrator gateway's automatic model discovery and +bounded OpenCode tool loop. The worker must keep provider credentials in the +gateway KV boundary and must fail closed when gateway configuration is absent. + +Every dispatched repair is diagnostic until the target PR's live head is +revalidated. A changed head invalidates earlier review and Checks evidence. +Queued, cancelled, unavailable, or synthetic evidence never authorizes a +merge. A customer should open the target PR, resolve actionable review threads, +wait for terminal required Checks, obtain an independent review and non-author approval, +and then use the repository's protected merge control. + +## Verification + +Run the caller contract test and `actionlint` against the exact commit before +changing the target allowlist or credential bindings: + +```text +python3 -m pytest -q tests/test_contextual_orchestrator_hourly_review_caller.py +actionlint .github/workflows/contextual-orchestrator-hourly-review-repair.yml +``` + +## APA 7th references + +GitHub. (n.d.). *Events that trigger workflows*. Retrieved August 21, 2026, +from https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + +GitHub. (n.d.). *Reuse workflows*. Retrieved August 21, 2026, from +https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows + +OpenCode. (n.d.). *Permissions*. Retrieved August 21, 2026, from +https://opencode.ai/docs/permissions diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0..b03cac15f 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Configure the client with an explicit review-control credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_contextual_orchestrator_hourly_review_caller.py b/tests/test_contextual_orchestrator_hourly_review_caller.py new file mode 100644 index 000000000..fddec5d9c --- /dev/null +++ b/tests/test_contextual_orchestrator_hourly_review_caller.py @@ -0,0 +1,85 @@ +"""Contract tests for the contextual-orchestrator hourly repair caller.""" + +from pathlib import Path + + +CALLER = Path( + ".github/workflows/contextual-orchestrator-hourly-review-repair.yml" +) +DOCTORING = Path( + "docs/doctoring/contextual-orchestrator-hourly-review-caller.md" +) +QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") +CONTRACT = Path("tests/test_contextual_orchestrator_hourly_review_caller.py") + + +def _read(path: Path) -> str: + """Return one required contract file as UTF-8 text.""" + assert path.is_file(), f"missing required contract file: {path}" + return path.read_text(encoding="utf-8") + + +def test_caller_is_hourly_bounded_and_non_cancelling() -> None: + """Give the target repository one bounded repair scan per hour.""" + caller = _read(CALLER) + + assert 'cron: "17 * * * *"' in caller + assert "group: contextual-orchestrator-hourly-review-repair" in caller + assert "cancel-in-progress: false" in caller + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller + assert "target_repository: ContextualWisdomLab/contextual-orchestrator" in caller + assert "base_branch: main" in caller + assert 'max_prs: "50"' in caller + assert 'max_dispatches: "1"' in caller + assert "resolve_unreviewed_conflicts: true" in caller + assert 'retry_hours: "2"' in caller + + +def test_caller_preserves_oidc_and_explicit_scheduler_secret_scope() -> None: + """Forward only established scheduler credentials to the reusable workflow.""" + caller = _read(CALLER) + workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) + + assert "\npermissions:\n contents: read\n" in workflow_scope + assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller + assert "secrets: inherit" not in caller + assert "NVIDIA_NIM_API_KEY" not in caller + assert "COPILOT_GITHUB_TOKEN" not in caller + for forbidden in ( + "actions: write", + "contents: write", + "issues: write", + "pull-requests: write", + "statuses: write", + ): + assert forbidden not in caller + + +def test_doctoring_records_gateway_dependency_and_customer_next_action() -> None: + """Tell operators which gateway PR must land before this caller is enabled.""" + doctoring = _read(DOCTORING) + + for phrase in ( + "ContextualWisdomLab/contextual-orchestrator", + "#1168", + "contextual-orchestrator gateway", + "exact-head", + "independent review", + "OPENCODE_REPOSITORY_DISPATCH_TARGETS", + "PR_REVIEW_MERGE_TOKEN", + "OPENCODE_APPROVE_TOKEN", + "NVIDIA_NIM_API_KEY", + "COPILOT_GITHUB_TOKEN", + "APA 7th references", + ): + assert phrase in doctoring + + +def test_quality_workflow_tracks_caller_test_and_doctoring() -> None: + """Run the focused quality gate when any caller contract changes.""" + quality = _read(QUALITY_WORKFLOW) + for path in (CALLER, DOCTORING, CONTRACT): + assert quality.count(str(path)) >= 2 + assert str(CONTRACT) in quality[quality.index("python -m compileall"):] From 0e9d24c0ee32fd42906132a6c83e451b47ed4fb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:26:14 +0900 Subject: [PATCH 05/27] Revert "chore: schedule contextual-orchestrator hourly review repair (#1178)" This reverts commit efea53a11c90c7ca519af56aed6918124f45826e. --- ...tual-orchestrator-hourly-review-repair.yml | 32 ------- .../hourly-nvidia-nim-review-repair.yml | 7 -- ...xtual-orchestrator-hourly-review-caller.md | 60 ------------- .../organization_commercial_readiness_loop.py | 3 +- ...xtual_orchestrator_hourly_review_caller.py | 85 ------------------- 5 files changed, 1 insertion(+), 186 deletions(-) delete mode 100644 .github/workflows/contextual-orchestrator-hourly-review-repair.yml delete mode 100644 docs/doctoring/contextual-orchestrator-hourly-review-caller.md delete mode 100644 tests/test_contextual_orchestrator_hourly_review_caller.py diff --git a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml deleted file mode 100644 index 3c1bcbb91..000000000 --- a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Contextual Orchestrator Hourly Review Repair - -on: - schedule: - # Run at minute 17 each hour; the slot is shared with the three-hourly - # pr-auto-rebase workflow, so both workflows use independent concurrency groups. - - cron: "17 * * * *" - -concurrency: - group: contextual-orchestrator-hourly-review-repair - # A complete exact-head review can exceed one hour; keep it alive for the next heartbeat. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/contextual-orchestrator - base_branch: main - max_prs: "50" - max_dispatches: "1" - resolve_unreviewed_conflicts: true - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 9abe8d242..9eb450619 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -8,7 +8,6 @@ on: - .github/workflows/pr-review-autofix.yml - .github/workflows/bandscope-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml - - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -21,7 +20,6 @@ on: - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py - - tests/test_contextual_orchestrator_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -46,7 +44,6 @@ on: - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md - docs/doctoring/clearfolio-hourly-review-caller.md - - docs/doctoring/contextual-orchestrator-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md @@ -64,7 +61,6 @@ on: - .github/workflows/pr-review-autofix.yml - .github/workflows/bandscope-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml - - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -77,7 +73,6 @@ on: - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py - - tests/test_contextual_orchestrator_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -102,7 +97,6 @@ on: - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md - docs/doctoring/clearfolio-hourly-review-caller.md - - docs/doctoring/contextual-orchestrator-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md @@ -161,7 +155,6 @@ jobs: scripts/ci/pr_review_autofix_context.py \ tests/test_pr_review_conflict_scope.py \ tests/test_bandscope_hourly_review_caller.py \ - tests/test_contextual_orchestrator_hourly_review_caller.py \ tests/test_disksage_hourly_review_caller.py \ tests/test_fast_mlsirm_hourly_review_caller.py \ tests/test_github_hourly_conflict_repair.py \ diff --git a/docs/doctoring/contextual-orchestrator-hourly-review-caller.md b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md deleted file mode 100644 index a8748a156..000000000 --- a/docs/doctoring/contextual-orchestrator-hourly-review-caller.md +++ /dev/null @@ -1,60 +0,0 @@ -# Contextual Orchestrator Hourly Review-Repair Caller - -## Customer action - -Keep `ContextualWisdomLab/contextual-orchestrator` on the protected `main` -branch, then confirm the central autofix gateway contract in -`ContextualWisdomLab/.github#1168` is merged before enabling this scheduled -caller. After the caller runs, inspect the target PR's exact-head review, -Checks, and independent approval; merge only through the protected normal path. - -## Runtime boundary - -The workflow runs at minute 17 of every hour and calls the central -`pr-review-fix-scheduler.yml` reusable workflow. It scans at most 50 open PRs, -dispatches one bounded repair, permits unresolved conflict repair, and waits -two hours before retrying the same head. Non-cancelling concurrency preserves a -long-running exact-head OpenCode, Noema, or security operation. - -The caller grants read-only contents access and OIDC token exchange only. It -forwards the established `PR_REVIEW_MERGE_TOKEN` and -`OPENCODE_APPROVE_TOKEN` paths explicitly; it does not inherit all secrets and -does not receive `NVIDIA_NIM_API_KEY` or `COPILOT_GITHUB_TOKEN`. The central -target allowlist must include `ContextualWisdomLab/contextual-orchestrator` in -`OPENCODE_REPOSITORY_DISPATCH_TARGETS`. - -## Gateway dependency and evidence - -The reusable scheduler dispatches the default-branch central autofix workflow. -Merge `ContextualWisdomLab/.github#1168` first so that this caller's write path -uses the contextual-orchestrator gateway's automatic model discovery and -bounded OpenCode tool loop. The worker must keep provider credentials in the -gateway KV boundary and must fail closed when gateway configuration is absent. - -Every dispatched repair is diagnostic until the target PR's live head is -revalidated. A changed head invalidates earlier review and Checks evidence. -Queued, cancelled, unavailable, or synthetic evidence never authorizes a -merge. A customer should open the target PR, resolve actionable review threads, -wait for terminal required Checks, obtain an independent review and non-author approval, -and then use the repository's protected merge control. - -## Verification - -Run the caller contract test and `actionlint` against the exact commit before -changing the target allowlist or credential bindings: - -```text -python3 -m pytest -q tests/test_contextual_orchestrator_hourly_review_caller.py -actionlint .github/workflows/contextual-orchestrator-hourly-review-repair.yml -``` - -## APA 7th references - -GitHub. (n.d.). *Events that trigger workflows*. Retrieved August 21, 2026, -from https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows - -GitHub. (n.d.). *Reuse workflows*. Retrieved August 21, 2026, from -https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows - -OpenCode. (n.d.). *Permissions*. Retrieved August 21, 2026, from -https://opencode.ai/docs/permissions diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index b03cac15f..c00cfa1e0 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,7 +239,6 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: - """Configure the client with an explicit review-control credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -854,4 +853,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file diff --git a/tests/test_contextual_orchestrator_hourly_review_caller.py b/tests/test_contextual_orchestrator_hourly_review_caller.py deleted file mode 100644 index fddec5d9c..000000000 --- a/tests/test_contextual_orchestrator_hourly_review_caller.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Contract tests for the contextual-orchestrator hourly repair caller.""" - -from pathlib import Path - - -CALLER = Path( - ".github/workflows/contextual-orchestrator-hourly-review-repair.yml" -) -DOCTORING = Path( - "docs/doctoring/contextual-orchestrator-hourly-review-caller.md" -) -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -CONTRACT = Path("tests/test_contextual_orchestrator_hourly_review_caller.py") - - -def _read(path: Path) -> str: - """Return one required contract file as UTF-8 text.""" - assert path.is_file(), f"missing required contract file: {path}" - return path.read_text(encoding="utf-8") - - -def test_caller_is_hourly_bounded_and_non_cancelling() -> None: - """Give the target repository one bounded repair scan per hour.""" - caller = _read(CALLER) - - assert 'cron: "17 * * * *"' in caller - assert "group: contextual-orchestrator-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/contextual-orchestrator" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert "resolve_unreviewed_conflicts: true" in caller - assert 'retry_hours: "2"' in caller - - -def test_caller_preserves_oidc_and_explicit_scheduler_secret_scope() -> None: - """Forward only established scheduler credentials to the reusable workflow.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_doctoring_records_gateway_dependency_and_customer_next_action() -> None: - """Tell operators which gateway PR must land before this caller is enabled.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "ContextualWisdomLab/contextual-orchestrator", - "#1168", - "contextual-orchestrator gateway", - "exact-head", - "independent review", - "OPENCODE_REPOSITORY_DISPATCH_TARGETS", - "PR_REVIEW_MERGE_TOKEN", - "OPENCODE_APPROVE_TOKEN", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_quality_workflow_tracks_caller_test_and_doctoring() -> None: - """Run the focused quality gate when any caller contract changes.""" - quality = _read(QUALITY_WORKFLOW) - for path in (CALLER, DOCTORING, CONTRACT): - assert quality.count(str(path)) >= 2 - assert str(CONTRACT) in quality[quality.index("python -m compileall"):] From 08c92c43ef3f0e574440fd84cd7f4abc1c8c203c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:31:01 +0900 Subject: [PATCH 06/27] Reapply "chore: schedule contextual-orchestrator hourly review repair (#1178)" This reverts commit 3a102e30666872c951ac51ed08124ce5c987bc98. --- ...tual-orchestrator-hourly-review-repair.yml | 32 +++++++ .../hourly-nvidia-nim-review-repair.yml | 7 ++ ...xtual-orchestrator-hourly-review-caller.md | 60 +++++++++++++ .../organization_commercial_readiness_loop.py | 3 +- ...xtual_orchestrator_hourly_review_caller.py | 85 +++++++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/contextual-orchestrator-hourly-review-repair.yml create mode 100644 docs/doctoring/contextual-orchestrator-hourly-review-caller.md create mode 100644 tests/test_contextual_orchestrator_hourly_review_caller.py diff --git a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml new file mode 100644 index 000000000..3c1bcbb91 --- /dev/null +++ b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml @@ -0,0 +1,32 @@ +name: Contextual Orchestrator Hourly Review Repair + +on: + schedule: + # Run at minute 17 each hour; the slot is shared with the three-hourly + # pr-auto-rebase workflow, so both workflows use independent concurrency groups. + - cron: "17 * * * *" + +concurrency: + group: contextual-orchestrator-hourly-review-repair + # A complete exact-head review can exceed one hour; keep it alive for the next heartbeat. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + dispatch-review-repair: + permissions: + contents: read + id-token: write + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/contextual-orchestrator + base_branch: main + max_prs: "50" + max_dispatches: "1" + resolve_unreviewed_conflicts: true + retry_hours: "2" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 9eb450619..9abe8d242 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -8,6 +8,7 @@ on: - .github/workflows/pr-review-autofix.yml - .github/workflows/bandscope-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -20,6 +21,7 @@ on: - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py + - tests/test_contextual_orchestrator_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -44,6 +46,7 @@ on: - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md - docs/doctoring/clearfolio-hourly-review-caller.md + - docs/doctoring/contextual-orchestrator-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md @@ -61,6 +64,7 @@ on: - .github/workflows/pr-review-autofix.yml - .github/workflows/bandscope-hourly-review-repair.yml - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - .github/workflows/disksage-hourly-review-repair.yml - .github/workflows/fast-mlsirm-hourly-review-repair.yml - .github/workflows/github-hourly-review-repair.yml @@ -73,6 +77,7 @@ on: - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - tests/test_bandscope_hourly_review_caller.py + - tests/test_contextual_orchestrator_hourly_review_caller.py - tests/test_disksage_hourly_review_caller.py - tests/test_fast_mlsirm_hourly_review_caller.py - tests/test_github_hourly_conflict_repair.py @@ -97,6 +102,7 @@ on: - docs/automation/hourly-review-repair.md - docs/doctoring/bandscope-hourly-review-caller.md - docs/doctoring/clearfolio-hourly-review-caller.md + - docs/doctoring/contextual-orchestrator-hourly-review-caller.md - docs/doctoring/conflict-control-evidence-isolation.md - docs/doctoring/disksage-hourly-review-caller.md - docs/doctoring/fast-mlsirm-hourly-review-caller.md @@ -155,6 +161,7 @@ jobs: scripts/ci/pr_review_autofix_context.py \ tests/test_pr_review_conflict_scope.py \ tests/test_bandscope_hourly_review_caller.py \ + tests/test_contextual_orchestrator_hourly_review_caller.py \ tests/test_disksage_hourly_review_caller.py \ tests/test_fast_mlsirm_hourly_review_caller.py \ tests/test_github_hourly_conflict_repair.py \ diff --git a/docs/doctoring/contextual-orchestrator-hourly-review-caller.md b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md new file mode 100644 index 000000000..a8748a156 --- /dev/null +++ b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md @@ -0,0 +1,60 @@ +# Contextual Orchestrator Hourly Review-Repair Caller + +## Customer action + +Keep `ContextualWisdomLab/contextual-orchestrator` on the protected `main` +branch, then confirm the central autofix gateway contract in +`ContextualWisdomLab/.github#1168` is merged before enabling this scheduled +caller. After the caller runs, inspect the target PR's exact-head review, +Checks, and independent approval; merge only through the protected normal path. + +## Runtime boundary + +The workflow runs at minute 17 of every hour and calls the central +`pr-review-fix-scheduler.yml` reusable workflow. It scans at most 50 open PRs, +dispatches one bounded repair, permits unresolved conflict repair, and waits +two hours before retrying the same head. Non-cancelling concurrency preserves a +long-running exact-head OpenCode, Noema, or security operation. + +The caller grants read-only contents access and OIDC token exchange only. It +forwards the established `PR_REVIEW_MERGE_TOKEN` and +`OPENCODE_APPROVE_TOKEN` paths explicitly; it does not inherit all secrets and +does not receive `NVIDIA_NIM_API_KEY` or `COPILOT_GITHUB_TOKEN`. The central +target allowlist must include `ContextualWisdomLab/contextual-orchestrator` in +`OPENCODE_REPOSITORY_DISPATCH_TARGETS`. + +## Gateway dependency and evidence + +The reusable scheduler dispatches the default-branch central autofix workflow. +Merge `ContextualWisdomLab/.github#1168` first so that this caller's write path +uses the contextual-orchestrator gateway's automatic model discovery and +bounded OpenCode tool loop. The worker must keep provider credentials in the +gateway KV boundary and must fail closed when gateway configuration is absent. + +Every dispatched repair is diagnostic until the target PR's live head is +revalidated. A changed head invalidates earlier review and Checks evidence. +Queued, cancelled, unavailable, or synthetic evidence never authorizes a +merge. A customer should open the target PR, resolve actionable review threads, +wait for terminal required Checks, obtain an independent review and non-author approval, +and then use the repository's protected merge control. + +## Verification + +Run the caller contract test and `actionlint` against the exact commit before +changing the target allowlist or credential bindings: + +```text +python3 -m pytest -q tests/test_contextual_orchestrator_hourly_review_caller.py +actionlint .github/workflows/contextual-orchestrator-hourly-review-repair.yml +``` + +## APA 7th references + +GitHub. (n.d.). *Events that trigger workflows*. Retrieved August 21, 2026, +from https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + +GitHub. (n.d.). *Reuse workflows*. Retrieved August 21, 2026, from +https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows + +OpenCode. (n.d.). *Permissions*. Retrieved August 21, 2026, from +https://opencode.ai/docs/permissions diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0..b03cac15f 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Configure the client with an explicit review-control credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_contextual_orchestrator_hourly_review_caller.py b/tests/test_contextual_orchestrator_hourly_review_caller.py new file mode 100644 index 000000000..fddec5d9c --- /dev/null +++ b/tests/test_contextual_orchestrator_hourly_review_caller.py @@ -0,0 +1,85 @@ +"""Contract tests for the contextual-orchestrator hourly repair caller.""" + +from pathlib import Path + + +CALLER = Path( + ".github/workflows/contextual-orchestrator-hourly-review-repair.yml" +) +DOCTORING = Path( + "docs/doctoring/contextual-orchestrator-hourly-review-caller.md" +) +QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") +CONTRACT = Path("tests/test_contextual_orchestrator_hourly_review_caller.py") + + +def _read(path: Path) -> str: + """Return one required contract file as UTF-8 text.""" + assert path.is_file(), f"missing required contract file: {path}" + return path.read_text(encoding="utf-8") + + +def test_caller_is_hourly_bounded_and_non_cancelling() -> None: + """Give the target repository one bounded repair scan per hour.""" + caller = _read(CALLER) + + assert 'cron: "17 * * * *"' in caller + assert "group: contextual-orchestrator-hourly-review-repair" in caller + assert "cancel-in-progress: false" in caller + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller + assert "target_repository: ContextualWisdomLab/contextual-orchestrator" in caller + assert "base_branch: main" in caller + assert 'max_prs: "50"' in caller + assert 'max_dispatches: "1"' in caller + assert "resolve_unreviewed_conflicts: true" in caller + assert 'retry_hours: "2"' in caller + + +def test_caller_preserves_oidc_and_explicit_scheduler_secret_scope() -> None: + """Forward only established scheduler credentials to the reusable workflow.""" + caller = _read(CALLER) + workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) + + assert "\npermissions:\n contents: read\n" in workflow_scope + assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller + assert "secrets: inherit" not in caller + assert "NVIDIA_NIM_API_KEY" not in caller + assert "COPILOT_GITHUB_TOKEN" not in caller + for forbidden in ( + "actions: write", + "contents: write", + "issues: write", + "pull-requests: write", + "statuses: write", + ): + assert forbidden not in caller + + +def test_doctoring_records_gateway_dependency_and_customer_next_action() -> None: + """Tell operators which gateway PR must land before this caller is enabled.""" + doctoring = _read(DOCTORING) + + for phrase in ( + "ContextualWisdomLab/contextual-orchestrator", + "#1168", + "contextual-orchestrator gateway", + "exact-head", + "independent review", + "OPENCODE_REPOSITORY_DISPATCH_TARGETS", + "PR_REVIEW_MERGE_TOKEN", + "OPENCODE_APPROVE_TOKEN", + "NVIDIA_NIM_API_KEY", + "COPILOT_GITHUB_TOKEN", + "APA 7th references", + ): + assert phrase in doctoring + + +def test_quality_workflow_tracks_caller_test_and_doctoring() -> None: + """Run the focused quality gate when any caller contract changes.""" + quality = _read(QUALITY_WORKFLOW) + for path in (CALLER, DOCTORING, CONTRACT): + assert quality.count(str(path)) >= 2 + assert str(CONTRACT) in quality[quality.index("python -m compileall"):] From 94a2804a700de0ca8457e272fa3e3548635ac00a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:16:48 +0900 Subject: [PATCH 07/27] fix(security): use osv scanner output-file flag --- .github/workflows/security-scan.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index c3b8fa5db..73db848f9 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -81,7 +81,7 @@ jobs: with: scan-args: | --format=json - --output=old-results.json + --output-file=old-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -99,7 +99,7 @@ jobs: with: scan-args: | --format=json - --output=old-results.json + --output-file=old-results.json --no-resolve --allow-no-lockfiles -r @@ -120,7 +120,7 @@ jobs: with: scan-args: | --format=json - --output=new-results.json + --output-file=new-results.json --maven-registry=https://maven-central.storage-download.googleapis.com/maven2 --no-resolve --allow-no-lockfiles @@ -138,7 +138,7 @@ jobs: with: scan-args: | --format=json - --output=new-results.json + --output-file=new-results.json --no-resolve --allow-no-lockfiles -r @@ -191,7 +191,7 @@ jobs: uses: google/osv-scanner-action/osv-reporter-action@8dc09193bb540e09b23da07ad7e30bd33bf87018 # v2.3.8 with: scan-args: | - --output=results.sarif + --output-file=results.sarif --old=old-results.json --new=new-results.json --gh-annotations=true From 859d3b7a7a0daa7e11103126f0541fcafea6db26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:23:20 +0900 Subject: [PATCH 08/27] fix(test): track osv scanner output-file option --- tests/test_required_workflow_queue_contract.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 9e6d14b33..53b984a62 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -908,8 +908,8 @@ def test_security_scan_allows_repositories_without_supported_lockfiles() -> None workflow = workflow_text("security-scan.yml") assert workflow.count("--allow-no-lockfiles") == 4 - assert "--output=old-results.json" in workflow - assert "--output=new-results.json" in workflow + assert "--output-file=old-results.json" in workflow + assert "--output-file=new-results.json" in workflow assert "test -s old-results.json" in workflow assert "test -s new-results.json" in workflow @@ -983,8 +983,8 @@ def test_osv_scan_logs_and_retries_without_transitive_resolution_on_resolver_fai "Retry head OSV without transitive resolution\n if: steps.osv_head.outcome == 'failure'\n continue-on-error: true" in workflow ) - assert "--output=old-results.json" in workflow - assert "--output=new-results.json" in workflow + assert "--output-file=old-results.json" in workflow + assert "--output-file=new-results.json" in workflow assert "Print OSV findings being compared" in workflow assert "OSV {label} scan produced {len(findings)} finding(s)" in workflow From 90ea841b5a980404ecfd4e1fd0afc042e006ef5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:22:04 +0900 Subject: [PATCH 09/27] fix(security): keep reporter output contract --- .github/workflows/security-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 73db848f9..8260f1da1 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -191,7 +191,7 @@ jobs: uses: google/osv-scanner-action/osv-reporter-action@8dc09193bb540e09b23da07ad7e30bd33bf87018 # v2.3.8 with: scan-args: | - --output-file=results.sarif + --output=results.sarif --old=old-results.json --new=new-results.json --gh-annotations=true From 592deef37070aaaf26b9740acd143f074cdfe5ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:30:56 +0900 Subject: [PATCH 10/27] docs: complete fixture initializer docstring coverage --- organization_commercial_readiness_fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/organization_commercial_readiness_fixtures.py b/organization_commercial_readiness_fixtures.py index d86596196..3b903eb97 100644 --- a/organization_commercial_readiness_fixtures.py +++ b/organization_commercial_readiness_fixtures.py @@ -90,6 +90,7 @@ def __init__( repositories: list[dict[str, Any]], snapshots: dict[str, list[RepositorySnapshot | Exception]], ) -> None: + """Configure deterministic repository snapshots and dispatch records.""" self.repositories = repositories self.snapshots = snapshots self.dispatched_repairs: list[tuple[str, str]] = [] From 4ed00ac057b9fc863b01ec56950dfd4a4ac813cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:11:29 -0700 Subject: [PATCH 11/27] fix(codeql): bind merge analysis to merge commit SHA (#1206) --- .github/workflows/codeql-pr.yml | 6 ++++-- tests/test_codeql_pr_workflow_contract.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 240e7139d..b5c57a829 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -194,7 +194,9 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }} + # GitHub can omit the synthetic merge ref for stacked/non-default-base PRs; + # the event's merge SHA is the exact merge-result identity we must analyze. + ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Initialize CodeQL uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 @@ -208,7 +210,7 @@ jobs: category: "/language:${{ matrix.language }}-merge" upload: false output: codeql-results-merge - ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }} + ref: ${{ github.event.pull_request.merge_commit_sha }} sha: ${{ github.event.pull_request.merge_commit_sha }} - name: Enforce CodeQL Medium+ SARIF gate diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 813385b23..7a23c99da 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -36,7 +36,8 @@ def test_codeql_pr_workflow_gates_head_and_merge_sarif_locally() -> None: assert "github.event.pull_request.head.sha" in workflow assert "github.event.pull_request.merge_commit_sha" in workflow assert "refs/pull/{0}/head" in workflow - assert "refs/pull/{0}/merge" in workflow + assert workflow.count("ref: ${{ github.event.pull_request.merge_commit_sha }}") == 2 + assert "refs/pull/{0}/merge" not in workflow assert workflow.count("security-events: read") == 2 assert "security-events: write" not in workflow From 94e2b28aee2029d36f272336a7cd4e4ad1516161 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 03:17:42 +0900 Subject: [PATCH 12/27] fix(codeql): keep merge upload ref in documented form --- .github/workflows/codeql-pr.yml | 5 +++-- tests/test_codeql_pr_workflow_contract.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index b5c57a829..642f2f62e 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -195,7 +195,8 @@ jobs: with: persist-credentials: false # GitHub can omit the synthetic merge ref for stacked/non-default-base PRs; - # the event's merge SHA is the exact merge-result identity we must analyze. + # materialize the exact merge-result SHA locally while retaining the + # documented pull-request ref shape for CodeQL result publication. ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Initialize CodeQL @@ -210,7 +211,7 @@ jobs: category: "/language:${{ matrix.language }}-merge" upload: false output: codeql-results-merge - ref: ${{ github.event.pull_request.merge_commit_sha }} + ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }} sha: ${{ github.event.pull_request.merge_commit_sha }} - name: Enforce CodeQL Medium+ SARIF gate diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 7a23c99da..0f4e5491f 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -36,8 +36,8 @@ def test_codeql_pr_workflow_gates_head_and_merge_sarif_locally() -> None: assert "github.event.pull_request.head.sha" in workflow assert "github.event.pull_request.merge_commit_sha" in workflow assert "refs/pull/{0}/head" in workflow - assert workflow.count("ref: ${{ github.event.pull_request.merge_commit_sha }}") == 2 - assert "refs/pull/{0}/merge" not in workflow + assert "ref: ${{ github.event.pull_request.merge_commit_sha }}" in workflow + assert "ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }}" in workflow assert workflow.count("security-events: read") == 2 assert "security-events: write" not in workflow From 4d3d24aa404959f5067735fec0558d5924ade590 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:54:17 -0700 Subject: [PATCH 13/27] fix(actions): deduplicate workflow-run scheduler scans (#1203) * docs: complete coordinator client docstring * docs(test): complete coordinator fixture docstrings * fix(actions): deduplicate workflow-run scheduler scans * fix(actions): isolate workflow-run scheduler concurrency * fix(actions): limit workflow-run cancellation to scans * test(actions): keep scheduler cancellation contract current * test(scheduler): match repository-scoped dispatch concurrency * docs(scheduler): clarify workflow-run deduplication * fix(security): refresh pip audit dependency * chore(scheduler): keep pip lock on canonical owner --------- Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> --- .../workflows/pr-review-merge-scheduler.yml | 43 ++++++++++++- ARCHITECTURE.md | 8 +++ CHANGELOG.md | 1 + ...ge-scheduler-workflow-run-deduplication.md | 60 +++++++++++++++++++ docs/pr-review-and-merge-procedure.md | 8 +++ scripts/ci/test_strix_quick_gate.sh | 4 +- .../test_required_workflow_queue_contract.py | 25 +++++++- 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 docs/doctoring/merge-scheduler-workflow-run-deduplication.md diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 1f63a9d46..583d7af16 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -92,7 +92,7 @@ concurrency: github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'pull_request_review' && format('pr-{0}', github.event.pull_request.number) || github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) || - github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number && format('workflow-run-no-pr-{0}', github.repository) || + github.event_name == 'workflow_run' && format('workflow-run-{0}', github.ref) || github.event_name == 'workflow_call' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) || github.event_name == 'workflow_call' && inputs.base_branch != '' && format('call-{0}', inputs.base_branch) || github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule) || @@ -1031,6 +1031,47 @@ jobs: } ]' <<<"$active_runs_json" )" + + # Metadata-free workflow_run events use a dedicated concurrency + # group, while this sweep keeps only the newest scan for the + # current default HEAD and cancels older same-head scans left by + # a queue backlog or an earlier scheduler implementation. + duplicate_scheduler_runs_json="[]" + if [ -n "$current_default_sha" ]; then + duplicate_scheduler_runs_json="$( + jq \ + --arg default_branch "$default_branch" \ + --arg current_default_sha "$current_default_sha" \ + '[ + .[] + | select( + .event == "workflow_run" and + .name == "Required PR Review Merge Scheduler" and + .head_branch == $default_branch and + .head_sha == $current_default_sha and + ((.pull_requests // []) | length) == 0 + ) + | { + id, + name, + status, + event, + head_branch, + run_head: .head_sha, + current_head: $current_default_sha, + created_at + } + ] + | sort_by(.created_at, .id) + | .[0:-1]' <<<"$active_runs_json" + )" + superseded_runs_json="$( + jq -n \ + --argjson superseded "$superseded_runs_json" \ + --argjson duplicates "$duplicate_scheduler_runs_json" \ + '$superseded + $duplicates | unique_by(.id)' + )" + fi fi superseded_count="$(jq 'length' <<<"$superseded_runs_json")" if [ "$superseded_count" -gt 0 ]; then diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c48db831f..41e3f79d6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -107,6 +107,14 @@ sequenceDiagram MS->>PR: merge only on current-head approval + green checks ``` +The central scheduler also performs fail-closed queue hygiene. A +`workflow_run` event without PR metadata uses a workflow-run-specific +default-branch fallback, separate from push runs; the workflow cancels older +runs in that group, and the organization sweep retains only the newest +metadata-free scheduler scan for the current default-branch SHA. +PR-associated and unrelated workflow runs are not included in this +deduplication. + ## Trust boundaries - Required review workflows execute **base-branch** scripts. A PR that edits diff --git a/CHANGELOG.md b/CHANGELOG.md index e42afe76a..870b34488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Semantic Versioning where the repository publishes a release. ### Changed +- Deduplicate central `workflow_run` scheduler scans: metadata-free workflow-run events now cancel an older run in a workflow-run-specific branch fallback, and the organization queue sweep retains only the newest metadata-free scheduler scan for the current default-branch HEAD while preserving push, PR-associated, and unrelated workflow runs. - Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap. - Run the bounded Quarantine Sandbox Runtime heartbeat at minute 14 without granting the caller model secrets, repository mutation permissions, approval, merge, release, artifact-execution, or final security-verdict authority. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. diff --git a/docs/doctoring/merge-scheduler-workflow-run-deduplication.md b/docs/doctoring/merge-scheduler-workflow-run-deduplication.md new file mode 100644 index 000000000..c00546648 --- /dev/null +++ b/docs/doctoring/merge-scheduler-workflow-run-deduplication.md @@ -0,0 +1,60 @@ +# Central merge-scheduler `workflow_run` deduplication + +검토 기준일: **2026-08-21** + +## Incident + +The central `Required PR Review Merge Scheduler` accumulated several queued +`workflow_run` executions with the same default-branch `head_sha` and no PR +metadata. These runs were redundant repository-wide scans. The existing +current-head cleanup handled pull-request, push, and schedule runs, but did +not classify this metadata-free `workflow_run` shape. The same event also used +the default-branch fallback without `cancel-in-progress`, so later runs did +not remove an older queued scan. + +## Decision + +1. Include only metadata-free `workflow_run` events in the scheduler's + existing conditional `cancel-in-progress` expression. PR-associated + workflow-run events retain their existing non-cancelling behavior and + PR-specific concurrency key; metadata-free events use the workflow-run- + specific default-branch fallback, separate from push runs. +2. During the organization sweep, inspect only runs named exactly + `Required PR Review Merge Scheduler` with event `workflow_run`, the current + default branch, an empty `pull_requests` list, and complete Actions-run + evidence. +3. Restrict the dedupe to the exact current default-branch `head_sha`, then + sort by creation time and run ID. Keep the newest run and cancel older + same-head runs. If the current default SHA is absent, skip this additional + cancellation path; older-head cleanup remains governed by the existing + stale-run policy. +4. Preserve the existing fail-closed behavior: incomplete PR-head, default + branch, or Actions-run reads disable queue cancellation for that repository. + +This is queue hygiene only. It does not reinterpret a check, publish a status, +approve a pull request, alter branch protection, or merge a branch. + +## Verification and rollback + +The contract test proves the exact workflow trigger, workflow identity, +metadata boundary, same-head selection, and deterministic ordering. The extracted jq +program was also run against a fixture containing duplicate same-head runs, +an older-head run, a PR-associated run, and an unrelated workflow; it selected +only the older duplicate. `actionlint`, `git diff +--check`, and the focused pytest passed on the change branch. + +Rollback reverts the workflow concurrency expression, the additional queue +hygiene block, the focused contract test, and these documentation entries in +one normal pull request. No GitHub Actions registry state is mutated by the +code change itself. + +## References + +GitHub. (2026). *Control the concurrency of workflows and jobs*. GitHub Docs. +https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency + +GitHub. (2026). *Events that trigger workflows: `workflow_run`*. GitHub Docs. +https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + +GitHub. (2026). *Managing workflow runs*. GitHub Docs. +https://docs.github.com/en/actions/how-tos/manage-workflow-runs diff --git a/docs/pr-review-and-merge-procedure.md b/docs/pr-review-and-merge-procedure.md index 8da4703f3..b4222fb47 100644 --- a/docs/pr-review-and-merge-procedure.md +++ b/docs/pr-review-and-merge-procedure.md @@ -69,6 +69,14 @@ block a policy-clean current head. The scheduler logs the exact run id and bounded API error as an Actions warning, then continues the current-head decision. +Metadata-free `workflow_run` scheduler events use a workflow-run-specific +default-branch fallback concurrency group, separate from push runs. They +therefore cancel older runs in that group, and the organization sweep keeps +only the newest same-HEAD central scheduler scan. This cleanup is limited to +the exact scheduler workflow, the default branch, and an empty +`pull_requests` list; push, PR-associated, and unrelated workflow runs remain +eligible for their own queue decisions. + Old approvals and old checks are not merge evidence after the head SHA changes. OpenCode review evidence must be internally same-head as well as GitHub-attached same-head. If the review body includes `Gate evidence` with diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8b..e2d318287 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1506,8 +1506,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan" - assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository)" "scheduler keeps manual queue scans isolated per repository" + assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number" "scheduler cancels only metadata-free workflow-run scans in their isolated fallback group" assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk" assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews" assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 53b984a62..be13a522f 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -63,7 +63,7 @@ def test_merge_scheduler_deduplicates_unscoped_repository_dispatches() -> None: assert "format('org-sweep-{0}', github.repository)" in concurrency_contract assert "format('repo-dispatch-{0}', github.repository)" in concurrency_contract - assert "format('workflow-run-no-pr-{0}', github.repository)" in concurrency_contract + assert "format('workflow-run-{0}', github.ref)" in concurrency_contract assert ( "github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number" in concurrency_contract @@ -93,6 +93,29 @@ def test_merge_scheduler_provides_same_repository_dispatch_credential() -> None: assert workflow.count("SCHEDULER_DISPATCH_TOKEN: ${{ github.token }}") == 2 +def test_scheduler_deduplicates_metadata_free_workflow_run_scans() -> None: + """Keep only the newest same-head central scheduler scan without PR metadata.""" + workflow = workflow_text("pr-review-merge-scheduler.yml") + concurrency = workflow.split("concurrency:", 1)[1].split( + "permissions:", 1 + )[0] + cancel_line = next( + line for line in concurrency.splitlines() if "cancel-in-progress:" in line + ) + queue_hygiene = workflow.split("# Queue hygiene, part 1:", 1)[1].split( + "# Queue hygiene, part 2:", 1 + )[0] + + assert "github.event_name == 'workflow_run'" in concurrency + assert "github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number" in cancel_line + assert "format('workflow-run-{0}', github.ref)" in concurrency + assert '.event == "workflow_run"' in queue_hygiene + assert '.name == "Required PR Review Merge Scheduler"' in queue_hygiene + assert '((.pull_requests // []) | length) == 0' in queue_hygiene + assert '.head_sha == $current_default_sha' in queue_hygiene + assert 'sort_by(.created_at, .id)' in queue_hygiene + + def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> None: """Central single-PR dispatch must validate live metadata before cross-repo use.""" workflow = workflow_text("pr-review-merge-scheduler.yml") From 3016543f735bb24db760cfaa768e64f95f408473 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 12:29:49 -0700 Subject: [PATCH 14/27] fix(review): fail closed when required check is not a verdict (#1002) * fix(review): fail closed when required check is not a verdict The required opencode-review job only echoed success, so PRs such as contextual-orchestrator#176 looked reviewed with an empty Reviews tab. Fail closed unless opencode-agent posted APPROVED or CHANGES_REQUESTED on the current head, and stop Noema from exiting 0 when that approval is missing. * fix(noema): confine CodeGraph context path to the workspace Strix on NVIDIA NIM reported path traversal in load_codegraph_context because NOEMA_CODEGRAPH_CONTEXT_PATH was opened without a workspace root. Reject .. components and realpath escapes, keep missing in-tree files on the existing unavailable path, and pin the regression. * test(review): prove draft PRs skip central OpenCode dispatch * fix(ci): use valid checkout pin for draft review repair * fix(review): dispatch OpenCode for draft pull requests * docs(review): record verified draft dispatch contract * test(noema): prove existing review cannot bypass primary approval * test(noema): cover approved existing-review branch * fix(noema): require primary approval before existing verdict * docs(review): record Noema approval-order verification * test(review): reproduce repository dispatch payload overflow * chore(review): remove superseded payload repair workflow * test(review): prove LineageWeave remains centrally routable * fix(ci): publish verified LineageWeave route with workflow token * test(review): export verified LineageWeave route patch * chore(review): remove superseded one-shot route repair * chore(review): remove superseded route publisher * chore(review): remove superseded route patch exporter * chore(review): remove unrelated materializer test changes * fix(noema): fail closed on drafts without primary approval A draft skip must not make required noema-review green before opencode-agent posts a current-head verdict. * fix(review): spend dispatch budget on empty Reviews tabs first The one-dispatch-per-run scheduler walked created-at order, so leftover increments with a previous-head verdict consumed the slot while a later PR such as ContextualWisdomLab/contextual-orchestrator#176 stayed green on the required stub with no APPROVED or CHANGES_REQUESTED. Keep fail-closed on the required check, and stable-sort the queue so never-reviewed PRs take the budget before leftover re-reviews. * fix(review): treat GitHub run-name as an in-progress OpenCode dispatch Live workflow runs set name to the interpolated run-name, so "OpenCode Review Dispatch owner/repo#N@sha" did not match the short alias. The scheduler posted a second same-head dispatch and cancel-in-progress killed the review that had already passed coverage. * chore(review): preserve main materializer executable mode * test(review): isolate scheduler repository environment * test(review): require raw current-head verdict token * fix(review): log raw current-head verdict token * fix(review): ignore fallback-only approvals in dispatch priority * fix(review): preserve draft dispatch budget * docs: complete GitHub client docstring * fix(review): reject fallback-only verdict evidence * fix(scheduler): preserve default branch for targeted stacks * fix(review): route fork heads through review-only path * fix(review): delay failed fork head ref retries * fix: classify Strix bootstrap outages as infrastructure * fix: preserve OSV base results across checkout * fix: clear root-owned OSV output before checkout * fix(security): refresh pip audit dependency * test(review): reject legacy placeholder as active review * fix(review): ignore required-workflow placeholders * chore(review): restore OSV workflow to owner boundary * chore(review): restore OSV contract tests to owner boundary * chore(review): restore pip lock to security owner boundary * fix(osv): unlink root-owned base result before restore * test(scheduler): cover centralized review run filtering * chore(review): restore canonical OSV workflow ownership * chore(review): remove foreign OSV workflow assertions * chore(review): restore canonical Strix workflow ownership * chore(review): remove foreign Strix false-green regression * docs(test): complete fixture initializer docstring --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> --- .../workflows/opencode-review-dispatch.yml | 48 +- .github/workflows/opencode-review.yml | 56 ++- .../workflows/pr-review-merge-scheduler.yml | 15 +- CHANGELOG.md | 7 + .../required-review-check-is-not-a-verdict.md | 93 ++++ scripts/ci/noema_review_gate.py | 47 +- scripts/ci/opencode_dispatch_status.py | 83 +++- scripts/ci/pr_review_merge_scheduler.py | 192 +++++++- scripts/ci/test_strix_quick_gate.sh | 8 +- tests/test_noema_review_gate.py | 104 ++++- tests/test_opencode_agent_contract.py | 13 +- tests/test_opencode_required_verdict_gate.py | 246 +++++++++++ tests/test_opencode_workflow_shell_syntax.py | 108 ++++- ...t_pr_review_autofix_nvidia_nim_contract.py | 36 +- tests/test_pr_review_merge_scheduler.py | 409 +++++++++++++++++- .../test_required_workflow_queue_contract.py | 25 +- 16 files changed, 1437 insertions(+), 53 deletions(-) create mode 100644 docs/doctoring/required-review-check-is-not-a-verdict.md create mode 100644 tests/test_opencode_required_verdict_gate.py diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 83f6830d5..59b84b3e5 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -179,13 +179,13 @@ jobs: if [ "$live_state" != "open" ] || [ "$live_base_repository" != "$TARGET_REPOSITORY" ] || - [ "$live_head_repository" != "$TARGET_REPOSITORY" ] || + ! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || ! [[ "$live_base_sha" =~ ^[0-9a-fA-F]{40}$ ]] || ! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] || ! [[ "$live_is_private" =~ ^(true|false)$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then - printf '::error::PR metadata validation rejected closed, missing, cross-repository, or malformed live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_sha:-}" "${live_head_sha:-}" + printf '::error::PR metadata validation rejected closed, missing, malformed, or target-base-mismatched live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_sha:-}" "${live_head_sha:-}" exit 1 fi @@ -333,8 +333,44 @@ jobs: git -C "$fetch_dir" remote add origin "${GITHUB_SERVER_URL}/${TARGET_REPOSITORY}.git" if ! git -C "$fetch_dir" \ -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ - fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"; then - echo "::error::Coverage fetch could not authenticate to ${TARGET_REPOSITORY} or read base/head SHAs ${PR_BASE_SHA}/${PR_HEAD_SHA}; check token permissions, target repository access, and SHA visibility." + fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA"; then + echo "::error::Coverage fetch could not authenticate to ${TARGET_REPOSITORY} or read base SHA ${PR_BASE_SHA}; check token permissions, target repository access, and SHA visibility." + exit 1 + fi + head_fetch_succeeded=0 + if git -C "$fetch_dir" \ + -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + fetch --no-tags --prune --no-recurse-submodules origin "$PR_HEAD_SHA"; then + head_fetch_succeeded=1 + else + echo "Direct head-SHA coverage fetch was unavailable; resolving the exact head through the target pull request ref." + fi + if [ "$head_fetch_succeeded" -ne 1 ]; then + for pr_head_fetch_attempt in 1 2 3 4 5 6; do + if git -C "$fetch_dir" \ + -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + fetch --no-tags --prune --no-recurse-submodules origin \ + "+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr-${PR_NUMBER}-head"; then + fetched_head_sha="$(git -C "$fetch_dir" rev-parse "refs/remotes/origin/pr-${PR_NUMBER}-head")" + if [ "$fetched_head_sha" = "$PR_HEAD_SHA" ]; then + head_fetch_succeeded=1 + break + fi + if [ "$pr_head_fetch_attempt" -lt 6 ]; then + echo "Fetched PR head $fetched_head_sha, expected $PR_HEAD_SHA; retrying after propagation delay." + sleep 10 + else + echo "Fetched PR head $fetched_head_sha, expected $PR_HEAD_SHA; no retries remain." + fi + elif [ "$pr_head_fetch_attempt" -lt 6 ]; then + echo "PR head ref fetch failed on attempt $pr_head_fetch_attempt; retrying after propagation delay." + sleep 10 + fi + done + fi + if [ "$head_fetch_succeeded" -ne 1 ] || + ! git -C "$fetch_dir" cat-file -e "${PR_HEAD_SHA}^{commit}"; then + echo "::error::Coverage fetch could not resolve exact head SHA ${PR_HEAD_SHA} from ${TARGET_REPOSITORY} or refs/pull/${PR_NUMBER}/head; check token permissions, target repository access, PR state, and SHA propagation." exit 1 fi git -C "$fetch_dir" checkout --detach "$PR_BASE_SHA" @@ -2146,7 +2182,7 @@ jobs: live_is_private="$(jq -r '.base.repo.private | tostring' <<<"$pull_request_json")" if [ "$live_state" != "open" ] || [ "$base_repository" != "$GH_REPOSITORY" ] || - [ "$head_repository" != "$GH_REPOSITORY" ] || + ! [[ "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || [ "$live_base_ref" != "$EXPECTED_BASE_REF" ] || [ "$live_base_sha" != "$EXPECTED_BASE_SHA" ] || [ "$live_head_ref" != "$EXPECTED_HEAD_REF" ] || @@ -2158,7 +2194,7 @@ jobs: "$GH_REPOSITORY" "$PR_NUMBER" "${live_state:-}" "${base_repository:-}" "${live_base_ref:-}" "${live_base_sha:-}" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "${head_repository:-}" "${live_head_ref:-}" "${live_head_sha:-}" "$EXPECTED_HEAD_REF" "$EXPECTED_HEAD_SHA" "${live_is_private:-}" "${EXPECTED_IS_PRIVATE:-}" exit 1 fi - printf 'Validated same-repository OpenCode review source for %s#%s (%s).\n' \ + printf 'Validated current-head OpenCode review source for %s#%s (head=%s; this path is review-only for external heads).\n' \ "$GH_REPOSITORY" "$PR_NUMBER" "$head_repository" - name: Exchange OpenCode app token for target repository review reads diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7f1ad6d00..bd074f7ae 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -20,6 +20,7 @@ concurrency: permissions: contents: read + pull-requests: read jobs: required-workflow-bootstrap: @@ -53,7 +54,56 @@ jobs: name: opencode-review needs: [coverage-evidence] runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read steps: - - run: >- - echo "Review approval remains a separate current-head PR review - requirement produced by the authenticated dispatch workflow." + - name: Fail closed without a current-head OpenCode verdict + env: + GH_TOKEN: ${{ github.token }} + TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + if [ "${{ github.event.action }}" = "closed" ]; then + echo "PR closed; a current-head OpenCode verdict is not required." + exit 0 + fi + if [ -z "${PR_NUMBER:-}" ] || [ -z "${HEAD_SHA:-}" ]; then + echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict." + exit 1 + fi + reviews="$(gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews")" + verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" ' + (add // []) + | [ + .[] + | select( + (.user.login // "" | ascii_downcase) as $user + | $user == "opencode-agent" or $user == "opencode-agent[bot]" + ) + | select((.commit_id // "" | ascii_downcase) == ($sha | ascii_downcase)) + ] + | (last // {}) as $review + | ($review.body // "" | ascii_downcase) as $body + | if $review.state == "CHANGES_REQUESTED" then + "CHANGES_REQUESTED" + elif $review.state == "APPROVED" + and ($body | contains("deterministic current-head evidence") | not) + and ($body | contains("deterministic fallback approval") | not) + and ($body | contains("model-unavailable evidence fallback") | not) + and ($body | contains("did not emit a usable current-head control block") | not) + and ($body | contains("scope: `unsupported`") | not) + and ($body | contains("model-pool outcome: `unknown`") | not) + then + "APPROVED" + else + empty + end + ')" + if [ -z "$verdict" ]; then + echo "::error::No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. This required check is not a review and must not succeed until the authenticated dispatch posts a current-head verdict." + exit 1 + fi + echo "Current-head OpenCode verdict: ${verdict}." diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index 583d7af16..cff63a658 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -246,6 +246,7 @@ jobs: { printf 'repository=%s\n' "$GITHUB_REPOSITORY" printf 'base_branch=%s\n' "$DEFAULT_BRANCH" + printf 'default_branch=%s\n' "$DEFAULT_BRANCH" } >>"$GITHUB_OUTPUT" exit 0 fi @@ -277,21 +278,26 @@ jobs: fi pull_json="$(gh api "repos/${TARGET_REPOSITORY_INPUT}/pulls/${TARGET_PR_NUMBER}")" + repository_json="$(gh api "repos/${TARGET_REPOSITORY_INPUT}")" live_number="$(jq -r '.number // 0' <<<"$pull_json")" live_state="$(jq -r '.state // empty' <<<"$pull_json")" live_base_repository="$(jq -r '.base.repo.full_name // empty' <<<"$pull_json")" live_head_repository="$(jq -r '.head.repo.full_name // empty' <<<"$pull_json")" live_base_branch="$(jq -r '.base.ref // empty' <<<"$pull_json")" + live_default_branch="$(jq -r '.default_branch // empty' <<<"$repository_json")" live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_json")" if [ "$live_number" != "$TARGET_PR_NUMBER" ] || [ "$live_state" != "open" ] || [ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ] || - [ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ] || + ! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || [ -z "$live_base_branch" ] || + [ -z "$live_default_branch" ] || ! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then - printf '::error::Targeted scheduler dispatch rejected closed, cross-repository, or malformed live PR metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_branch:-}" "${live_head_sha:-}" + printf '::error::Targeted scheduler dispatch rejected closed, malformed, or target-base-mismatched live metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s default_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_branch:-}" "${live_default_branch:-}" "${live_head_sha:-}" exit 1 fi + printf 'Validated targeted scheduler review dispatch for %s#%s (head=%s; this dispatch is review-only for external heads).\n' \ + "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "$live_head_repository" if [ -n "$TARGET_BASE_BRANCH_INPUT" ] && [ "$TARGET_BASE_BRANCH_INPUT" != "$live_base_branch" ]; then printf '::error::Targeted scheduler dispatch base branch does not match the live PR. supplied=%s live=%s\n' "$TARGET_BASE_BRANCH_INPUT" "$live_base_branch" @@ -301,9 +307,10 @@ jobs: { printf 'repository=%s\n' "$TARGET_REPOSITORY_INPUT" printf 'base_branch=%s\n' "$live_base_branch" + printf 'default_branch=%s\n' "$live_default_branch" printf 'head_sha=%s\n' "$live_head_sha" } >>"$GITHUB_OUTPUT" - printf 'Validated exact targeted scheduler dispatch for %s#%s at %s on base %s.\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "$live_head_sha" "$live_base_branch" + printf 'Validated exact targeted scheduler dispatch for %s#%s at %s on base %s; repository default is %s.\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "$live_head_sha" "$live_base_branch" "$live_default_branch" - name: Resolve trusted scheduler source ref id: trusted_source @@ -484,7 +491,7 @@ jobs: env: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }} TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }} - TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.base_branch }} + TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.default_branch }} SCHEDULER_ACTIONS_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && (secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token) || github.token }} # Same-repository dispatch credential: when this scheduler runs inside # ContextualWisdomLab/.github (the repository the required workflows are diff --git a/CHANGELOG.md b/CHANGELOG.md index 870b34488..5f186b232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,13 @@ Semantic Versioning where the repository publishes a release. ### Changed +- Noema now validates the current-head primary OpenCode approval before accepting an existing Noema verdict, preventing a secondary review from making the required gate look successful on its own. +- Draft pull requests now receive same-head Strix and OpenCode review dispatches while remaining excluded from branch updates, auto-merge changes, direct merge, and review-state cleanup. +- The required `opencode-review` check now fails closed unless `opencode-agent` already posted `APPROVED` or `CHANGES_REQUESTED` on the current head, so a stub success can no longer look like a review (ContextualWisdomLab/contextual-orchestrator#176). +- The merge scheduler now spends its review-dispatch budget on pull requests with no OpenCode verdict on any commit before leftover increments that already have a previous-head APPROVED or CHANGES_REQUESTED, so one-dispatch-per-run no longer starves an empty Reviews tab. +- The scheduler treats GitHub's full `run-name` (`OpenCode Review Dispatch owner/repo#N@sha`) as an in-progress same-head dispatch, so a later sweep cannot `cancel-in-progress` a review that already passed coverage. +- Noema no longer exits 0 when the current head has no primary OpenCode approval, including on draft pull requests; that skip was the green `noema-review` check with an empty Reviews tab. +- `load_codegraph_context` now confines `NOEMA_CODEGRAPH_CONTEXT_PATH` to `GITHUB_WORKSPACE` (or cwd) with `..` rejection and realpath checks, so a Strix path-traversal report on that helper cannot read files outside the review workspace. - Deduplicate central `workflow_run` scheduler scans: metadata-free workflow-run events now cancel an older run in a workflow-run-specific branch fallback, and the organization queue sweep retains only the newest metadata-free scheduler scan for the current default-branch HEAD while preserving push, PR-associated, and unrelated workflow runs. - Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap. - Run the bounded Quarantine Sandbox Runtime heartbeat at minute 14 without granting the caller model secrets, repository mutation permissions, approval, merge, release, artifact-execution, or final security-verdict authority. diff --git a/docs/doctoring/required-review-check-is-not-a-verdict.md b/docs/doctoring/required-review-check-is-not-a-verdict.md new file mode 100644 index 000000000..624b300ef --- /dev/null +++ b/docs/doctoring/required-review-check-is-not-a-verdict.md @@ -0,0 +1,93 @@ +# Required OpenCode/Noema checks are not reviews + +검토 기준일: **2026-08-14** + +## Incident + +On ContextualWisdomLab/contextual-orchestrator#176 the required +`opencode-review` and `noema-review` checks were green, but the Reviews +API had no APPROVE or REQUEST_CHANGES. Authors treated the check name as +a review verdict (GitHub, n.d.-a). That is weaker than the modern-review +expectation that a review is an explicit, current-head judgment +(Bacchelli & Bird, 2013). + +## Decision + +The required `opencode-review` job on +`.github/workflows/opencode-review.yml` never runs the model. Privileged +review stays in `opencode-review-dispatch.yml`. The required job now +reads current-head reviews with `pull-requests: read` and **fails +closed** unless `opencode-agent` / `opencode-agent[bot]` already posted +`APPROVED` or `CHANGES_REQUESTED` on that SHA. A COMMENTED review, a +review on an old SHA, or no review at all cannot make the check green. + +`scripts/ci/noema_review_gate.py` no longer returns 0 when the current +head has no primary OpenCode approval. That skip was exit 0, so the +required `noema-review` check looked like a successful review. Draft +status is checked only after that primary-approval gate, so a draft +without an OpenCode verdict cannot turn `noema-review` green. The gate +also validates the primary approval before accepting an existing Noema +review; a secondary verdict cannot independently turn the required gate +green. + +Human `repository_dispatch` as `seonghobae` remains rejected; only +`github-actions[bot]` may start the privileged dispatch. After a real +verdict is posted, re-run the required `opencode-review` job so the +fail-closed check can observe it. + +The one-dispatch-per-run budget used to walk pull requests in created-at +order, so leftover increments that already had a previous-head verdict +consumed the slot while a later PR with an empty Reviews tab waited. The +scheduler now stable-sorts that budget: no OpenCode APPROVED or +CHANGES_REQUESTED on any commit first, then previous-head re-reviews, +then current-head verdicts. COMMENTED-only evidence is not a verdict and +keeps the empty-Reviews priority. + +A second same-head `repository_dispatch` used to cancel the first through +workflow `cancel-in-progress` because `active_review_run_refs` compared +the GitHub `name` field to the short alias `OpenCode Review Dispatch`. +Live runs set `name` to the interpolated run-name. The matcher now +accepts that prefix so a queued or in-progress same-head review is +`already_running`. + +## Draft pull-request review contract + +Draft status is a merge-readiness signal, not a request to suppress early +feedback. The central scheduler therefore dispatches same-head Strix first +and then authenticated OpenCode review for draft pull requests. The draft +path is deliberately review-only: it cannot update the head branch, enable +or disable auto-merge, merge, dismiss reviews, or resolve review threads. +Marking a pull request ready remains the explicit boundary for merge +automation. + +## Verification contract + +- `tests/test_opencode_required_verdict_gate.py` pins + `current_head_opencode_verdict` and `decide_required_verdict_check`. +- `tests/test_noema_review_gate.py` requires exit 1 when there is no + primary OpenCode approval, even when the current head already has a Noema + review, while retaining the idempotent success path after a valid primary + approval exists. +- `tests/test_opencode_agent_contract.py` pins the required workflow + fail-closed error string. +- `tests/test_pr_review_merge_scheduler.py` proves that a draft pull request + receives same-head Strix and OpenCode dispatch while branch updates, + auto-merge mutation, direct merge, review dismissal, and thread cleanup + remain unreachable, and that a never-reviewed pull request consumes the + one-dispatch budget before a leftover increment that already has a + previous-head OpenCode verdict. +- Both repairs were exercised test-first: the draft-dispatch contract failed + against the old unconditional skip, and the Noema ordering contract failed + against the old secondary-review-first branch. The exact repaired source + then passed 988 tests, 7,056 production statements, 2,834 production + branches, and the public-docstring gate at 100%. + +## References (APA 7th) + +Bacchelli, A., & Bird, C. (2013). Expectations, outcomes, and challenges of +modern code review. In *Proceedings of the 35th International Conference on +Software Engineering* (pp. 712–721). IEEE. +https://doi.org/10.1109/ICSE.2013.6606617 + +GitHub. (n.d.-a). *About status checks*. GitHub Docs. Retrieved +August 14, 2026, from https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks \ No newline at end of file diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 9317860e4..01ab88268 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -16,6 +16,7 @@ import urllib.parse import urllib.request from collections.abc import Sequence +from pathlib import Path from typing import Any @@ -380,13 +381,38 @@ def review_thread_context(pr: dict[str, Any]) -> str: return "\n".join(lines) +def codegraph_context_root() -> Path: + """Return the workspace root that may contain CodeGraph context files.""" + raw = os.environ.get("GITHUB_WORKSPACE", "").strip() or os.getcwd() + return Path(raw).resolve() + + +def confined_codegraph_context_path(path: str, root: Path) -> Path | None: + """Return the resolved path when it cannot escape the workspace root.""" + candidate = Path(path) + if ".." in candidate.parts: + return None + if not candidate.is_absolute(): + candidate = Path(root / candidate) + try: + resolved = candidate.resolve() + except OSError: + return None + if not resolved.is_relative_to(root): + return None + return resolved + + def load_codegraph_context() -> str: """Load optional precomputed CodeGraph context for structural review evidence.""" path = os.environ.get("NOEMA_CODEGRAPH_CONTEXT_PATH", "").strip() if not path: return "" + confined = confined_codegraph_context_path(path, codegraph_context_root()) + if confined is None: + return "CodeGraph context unavailable: path escapes the workspace." try: - with open(path, encoding="utf-8") as handle: + with confined.open(encoding="utf-8") as handle: return truncate_text(handle.read(), MAX_REVIEW_CONTEXT_CHARS) except OSError as exc: return f"CodeGraph context unavailable: {exc}" @@ -582,7 +608,12 @@ def submit_review(repo: str, number: int, pr: dict[str, Any], actor: str, verdic def inspect_and_review(repo: str, number: int) -> int: - """Inspect PR state and submit Noema's LLM review when gates are clean.""" + """Inspect PR state and submit Noema's LLM review when gates are clean. + + Missing current-head primary OpenCode approval fails closed, including + on draft pull requests, so the required check cannot look reviewed + without a Reviews-tab verdict. + """ pr = fetch_pr(repo, number) actor = current_actor() if actor in PRIMARY_REVIEW_AUTHORS: @@ -591,15 +622,19 @@ def inspect_and_review(repo: str, number: int) -> int: "Noema review skipped so GitHub receives an independent reviewer." ) return 0 + if not current_primary_approval(pr): + print( + "Current head does not have a primary OpenCode approval; " + "Noema cannot skip as success because that made the required " + "check look like a review." + ) + return 1 if pr.get("isDraft"): - print("PR is draft; Noema review skipped.") + print("PR is draft; Noema review skipped after primary OpenCode approval.") return 0 if existing_noema_review(pr, actor): print("Current head already has a Noema review; nothing to do.") return 0 - if not current_primary_approval(pr): - print("Current head does not have a primary OpenCode approval; Noema review skipped.") - return 0 if has_current_changes_requested(pr): print("Current head has requested changes; Noema review skipped.") return 0 diff --git a/scripts/ci/opencode_dispatch_status.py b/scripts/ci/opencode_dispatch_status.py index 9109a0248..9224361ff 100644 --- a/scripts/ci/opencode_dispatch_status.py +++ b/scripts/ci/opencode_dispatch_status.py @@ -10,16 +10,74 @@ try: from opencode_existing_approval_gate import ( + FALLBACK_MARKERS, OPENCODE_APP_APPROVAL_AUTHORS, review_rejection_reason, ) except ModuleNotFoundError: # pragma: no cover - package import path from scripts.ci.opencode_existing_approval_gate import ( + FALLBACK_MARKERS, OPENCODE_APP_APPROVAL_AUTHORS, review_rejection_reason, ) +OPENCODE_VERDICT_STATES = frozenset({"APPROVED", "CHANGES_REQUESTED"}) +MISSING_VERDICT_MESSAGE = ( + "No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. " + "This required check is not a review and must not succeed until the " + "authenticated dispatch posts a current-head verdict." +) + + +def current_head_opencode_verdict( + reviews: Sequence[dict[str, Any]], head_sha: str +) -> str | None: + """Return the latest substantive current-head OpenCode verdict, if any.""" + expected = (head_sha or "").lower() + if not expected: + return None + for review in reversed(reviews): + author = str((review.get("user") or {}).get("login") or "").casefold() + if author not in OPENCODE_APP_APPROVAL_AUTHORS: + continue + if str(review.get("commit_id") or "").lower() != expected: + continue + state = str(review.get("state") or "").upper() + if state not in OPENCODE_VERDICT_STATES: + return None + body = str(review.get("body") or "").casefold() + if state == "APPROVED" and any(marker in body for marker in FALLBACK_MARKERS): + return None + return state + return None + + +def decide_required_verdict_check( + *, + expected_head: str, + pull_request: dict[str, Any], + reviews: Sequence[dict[str, Any]], +) -> dict[str, str]: + """Fail closed unless OpenCode already published a current-head verdict.""" + live_head = str((pull_request.get("head") or {}).get("sha") or "") + if not expected_head or live_head.lower() != expected_head.lower(): + return { + "state": "failure", + "description": ( + "OpenCode required-check target is stale or the live PR head " + "is unavailable." + ), + } + verdict = current_head_opencode_verdict(reviews, expected_head) + if verdict is None: + return {"state": "failure", "description": MISSING_VERDICT_MESSAGE} + return { + "state": "success", + "description": f"Current-head OpenCode verdict: {verdict}.", + } + + def _has_current_approval(reviews: Sequence[dict[str, Any]], head_sha: str) -> bool: """Return whether the latest OpenCode decision is a verified approval.""" for review in reversed(reviews): @@ -67,10 +125,15 @@ def decide_status( def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: - """Parse commit-status evidence inputs.""" + """Parse commit-status or required-verdict evidence inputs.""" parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--model-outcome", required=True) - parser.add_argument("--coverage-result", required=True) + parser.add_argument( + "--mode", + choices=("dispatch-status", "required-verdict"), + default="dispatch-status", + ) + parser.add_argument("--model-outcome") + parser.add_argument("--coverage-result") parser.add_argument("--expected-head", required=True) parser.add_argument("--pull-request-file", required=True, type=Path) parser.add_argument("--reviews-file", required=True, type=Path) @@ -78,12 +141,22 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: def main(argv: Sequence[str] | None = None) -> int: - """Print one JSON commit-status decision.""" + """Print one JSON decision and exit 1 when the required verdict is missing.""" args = parse_args(argv) pull_request = json.loads(args.pull_request_file.read_text(encoding="utf-8")) reviews = json.loads(args.reviews_file.read_text(encoding="utf-8")) if not isinstance(pull_request, dict) or not isinstance(reviews, list): raise SystemExit("pull request evidence must be an object and reviews evidence an array") + if args.mode == "required-verdict": + decision = decide_required_verdict_check( + expected_head=args.expected_head, + pull_request=pull_request, + reviews=reviews, + ) + print(json.dumps(decision, separators=(",", ":"))) + return 0 if decision["state"] == "success" else 1 + if not args.model_outcome or not args.coverage_result: + raise SystemExit("--model-outcome and --coverage-result are required") print( json.dumps( decide_status( @@ -99,5 +172,5 @@ def main(argv: Sequence[str] | None = None) -> int: return 0 -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover - exercised through main() raise SystemExit(main()) diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 44620fcab..f03d60015 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -1277,6 +1277,38 @@ def has_current_head_changes_requested(pr: dict[str, Any]) -> bool: return current_head_review_state(pr, "CHANGES_REQUESTED") +def has_any_opencode_verdict(pr: dict[str, Any]) -> bool: + """Return whether OpenCode ever posted APPROVED or CHANGES_REQUESTED on this PR.""" + for review in (pr.get("reviews") or {}).get("nodes") or []: + if not is_opencode_review(review): + continue + if is_deterministic_fallback_approval(review): + continue + if (review.get("state") or "").upper() in {"APPROVED", "CHANGES_REQUESTED"}: + return True + return False + + +def review_dispatch_priority(pr: dict[str, Any]) -> int: + """Return a lower rank for PRs that should consume the dispatch budget first. + + Rank 0 has no OpenCode APPROVED or CHANGES_REQUESTED on any commit — the + empty Reviews tab from ContextualWisdomLab/contextual-orchestrator#176. + Rank 1 already received a verdict on a previous head and can wait for a + leftover re-review. Rank 2 already has a current-head verdict. + """ + if has_current_head_approval(pr) or has_current_head_changes_requested(pr): + return 2 + if has_any_opencode_verdict(pr): + return 1 + return 0 + + +def prioritize_review_dispatch_queue(prs: Sequence[dict[str, Any]]) -> list[dict[str, Any]]: + """Stable-sort so never-reviewed PRs take the dispatch slot before leftover increments.""" + return sorted(prs, key=review_dispatch_priority) + + def stale_opencode_change_request_ids(pr: dict[str, Any]) -> list[int]: """Return dismissible automated change requests tied to previous heads.""" review_ids: list[int] = [] @@ -1979,6 +2011,22 @@ def stale_opencode_run_ids(repo: str, workflow: str, pr: dict[str, Any]) -> list return stale +def workflow_run_name_matches( + run_name: str, workflow: str, workflow_aliases: frozenset[str] +) -> bool: + """Return whether a GitHub run name is the workflow or a run-name prefix of it. + + Workflow runs use ``run-name:`` as ``name``. OpenCode Review Dispatch therefore + appears as ``OpenCode Review Dispatch owner/repo#1@sha``, which is not equal to + the short alias. Exact-only matching misses the live run, a second dispatch is + posted, and ``cancel-in-progress`` kills the review that was about to finish. + """ + names = {workflow, *workflow_aliases} + if run_name in names: + return True + return any(run_name.startswith(f"{candidate} ") for candidate in names) + + def active_review_run_refs( repo: str, workflow: str, @@ -2010,13 +2058,27 @@ def active_review_run_refs( for run_repo in (dispatch_repo,): for run_data in active_workflow_runs(run_repo, statuses): run_name = str(run_data.get("name") or "") - if run_name != workflow and run_name not in workflow_aliases: + display_title = str(run_data.get("display_title") or "") + # Required-workflow pull_request_target runs materialize the protected + # check only; they never execute the authenticated reviewer. Ignore + # that placeholder even in the legacy same-repository mode so it + # cannot suppress the real repository_dispatch review. + if run_data.get("event") == "pull_request_target" and ( + run_name == run_title + or display_title == run_title + or display_title.startswith(f"{run_title} ") + ): + continue + if not workflow_run_name_matches( + run_name, workflow, workflow_aliases + ) and not workflow_run_name_matches( + display_title, workflow, workflow_aliases + ): continue run_id = run_data.get("id") if not run_id: continue run_ref = (run_repo, str(run_id)) - display_title = str(run_data.get("display_title") or "") dispatch_title_prefix = next( ( prefix @@ -2333,6 +2395,119 @@ def current_head_can_attempt_merge(pr: dict[str, Any], merge_state: str) -> bool return False + +def inspect_draft_pr_for_review( + repo: str, + pr: dict[str, Any], + *, + dry_run: bool, + trigger_reviews: bool, + review_dispatch_allowed: bool, + workflow: str, + security_workflow: str, + stale_opencode_minutes: int, +) -> Decision: + """Dispatch current-head review evidence without mutating a draft PR branch. + + Draft pull requests remain excluded from branch updates, auto-merge, + direct merge, stale-review dismissal, and review-thread mutation. They + still need early Strix and OpenCode feedback so authors can finish the + implementation before marking the pull request ready for review. + """ + number = pr["number"] + if has_current_head_changes_requested(pr): + return Decision( + number, + "skip", + "draft PR; current-head OpenCode review requested changes", + ) + if has_current_head_approval(pr): + return Decision( + number, + "skip", + "draft PR; current-head OpenCode approval recorded", + ) + if not trigger_reviews: + return Decision(number, "skip", "draft PR; review dispatch disabled") + + opencode_state = opencode_progress_state( + pr, + stale_after_minutes=stale_opencode_minutes, + ) + if opencode_state == "running": + return Decision( + number, + "wait", + "draft PR; OpenCode review is already in progress", + ) + if not review_dispatch_allowed: + return Decision(number, "wait", "draft PR; review dispatch limit reached") + + strix_state = strix_evidence_state(pr) + if strix_state == "missing": + wait_reason = repository_dispatch_wait_reason(repo, security_workflow) + if wait_reason: + return Decision( + number, + "wait", + "draft PR; current head has no completed Strix evidence; " + f"{wait_reason}", + ) + strix_dispatch_result = dispatch_strix_evidence( + repo, + security_workflow, + pr, + dry_run=dry_run, + ) + if strix_dispatch_result == "already_running": + return Decision( + number, + "wait", + "draft PR; current head has no completed Strix evidence; " + "same-head Strix workflow run is already active", + ) + return Decision( + number, + "security_dispatch", + "draft PR; current head has no completed Strix evidence; " + "same-head Strix dispatched", + ) + if strix_state == "running": + return Decision( + number, + "wait", + "draft PR; same-head Strix evidence is still running", + ) + + wait_reason = repository_dispatch_wait_reason(repo, workflow) + if wait_reason: + return Decision( + number, + "wait", + "draft PR; current head has completed Strix evidence; " + f"{wait_reason}", + ) + dispatch_result = dispatch_opencode_review( + repo, + workflow, + pr, + dry_run=dry_run, + ) + if dispatch_result == "already_running": + return Decision( + number, + "wait", + "draft PR; current head has completed Strix evidence; " + "same-head OpenCode workflow run is already active", + ) + return Decision( + number, + "review_dispatch", + "draft PR; current head has completed Strix evidence; " + "same-head OpenCode dispatched", + ) + + def inspect_pr( repo: str, pr: dict[str, Any], @@ -2355,7 +2530,16 @@ def inspect_pr( base_ref = pr.get("baseRefName") if pr.get("isDraft"): - return Decision(number, "skip", "draft PR") + return inspect_draft_pr_for_review( + repo, + pr, + dry_run=dry_run, + trigger_reviews=trigger_reviews, + review_dispatch_allowed=review_dispatch_allowed, + workflow=workflow, + security_workflow=security_workflow, + stale_opencode_minutes=stale_opencode_minutes, + ) cancel_stale_pr_runs(repo, pr, dry_run=dry_run) if base_ref != base_branch: # Stacked/cascade PR (base is another feature branch). Org required @@ -3918,6 +4102,8 @@ def main(argv: list[str]) -> int: if args.branch_update_limit < -1: raise SystemExit("--branch-update-limit must be -1 or greater") prs = fetch_pr(args.repo, args.pr_number) if args.pr_number else fetch_open_prs(args.repo, args.max_prs) + if not args.pr_number: + prs = prioritize_review_dispatch_queue(prs) decisions = [] review_dispatches_used = 0 branch_updates_used = 0 diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index e2d318287..a69c149db 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -497,6 +497,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$bootstrap_file" "coverage-evidence:" "opencode required workflow preserves the stable coverage-evidence branch-protection context" assert_file_contains "$bootstrap_file" "name: opencode-review" "opencode required workflow preserves the stable opencode-review branch-protection context" assert_file_contains "$bootstrap_file" "authenticated default-branch OpenCode review dispatch" "opencode required workflow delegates real review execution to the protected dispatch path" + assert_file_contains "$bootstrap_file" "This required check is not a review" "opencode required workflow fails closed without a current-head OpenCode verdict" assert_file_not_contains "$bootstrap_file" "repository_dispatch:" "opencode required workflow does not mix privileged dispatch execution with pull_request_target" assert_file_not_contains "$bootstrap_file" "actions/checkout" "opencode required workflow never checks out pull-request content" assert_file_not_contains "$bootstrap_file" '${{ secrets.' "opencode required workflow never binds repository secrets" @@ -924,7 +925,10 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' if [[ "$coverage_merge_tree_step" != *'GH_TOKEN: ${{ steps.coverage_read_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}'* ]]; then record_failure "opencode coverage merge-tree fetch must use the coverage App token and central fallback credentials before github.token for target repository reads" fi - assert_file_contains "$workflow_file" 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' "coverage evidence fetches exact base and head commits as data" + assert_file_contains "$workflow_file" 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA"' "coverage evidence fetches the exact base commit as data" + assert_file_contains "$workflow_file" 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_HEAD_SHA"' "coverage evidence first attempts the exact head commit as data" + assert_file_contains "$workflow_file" 'refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr-${PR_NUMBER}-head' "coverage evidence can resolve an external exact head through the target PR ref" + assert_file_contains "$workflow_file" 'fetched_head_sha="$(git -C "$fetch_dir" rev-parse "refs/remotes/origin/pr-${PR_NUMBER}-head")"' "coverage evidence binds the fetched PR ref back to the expected exact head" assert_file_contains "$workflow_file" 'merge --no-ff --no-edit "$PR_HEAD_SHA"' "coverage evidence materializes the current pull request merge tree without action checkout" assert_file_contains "$workflow_file" "Coverage merge tree could not be materialized" "coverage evidence logs an actionable merge-tree failure reason" assert_file_contains "$workflow_file" "--require-hashes" "coverage tooling installs from a hash-pinned lock" @@ -1506,6 +1510,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan" + assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number != ''" "scheduler scopes targeted manual queue scans to the requested PR" + assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number) }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository)" "scheduler keeps manual queue scans isolated per repository" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number" "scheduler cancels only metadata-free workflow-run scans in their isolated fallback group" assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk" diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 408bb95b9..be96a377f 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -258,6 +258,7 @@ def fake_run(args, stdin=None): monkeypatch.setattr(noema, "run", fake_run) codegraph_path = tmp_path / "codegraph.md" codegraph_path.write_text("call graph: src/a.py -> tests", encoding="utf-8") + monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path)) monkeypatch.setenv("NOEMA_CODEGRAPH_CONTEXT_PATH", str(codegraph_path)) pr = make_pr( headRefOid="head sha", @@ -294,6 +295,7 @@ def fake_run(args, stdin=None): def test_review_context_reports_omitted_files_and_missing_codegraph(monkeypatch, tmp_path): + monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path)) monkeypatch.delenv("NOEMA_CODEGRAPH_CONTEXT_PATH", raising=False) assert noema.load_codegraph_context() == "" @@ -309,6 +311,64 @@ def test_review_context_reports_omitted_files_and_missing_codegraph(monkeypatch, assert "1 changed files omitted from context budget" in context +def test_load_codegraph_context_rejects_workspace_escape(monkeypatch, tmp_path): + """Traversal, absolute, and symlink paths outside the workspace are rejected.""" + monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path)) + outside = tmp_path.parent / "passwd-shape" + outside.write_text("root:x:0:0:root:/root:/bin/sh\n", encoding="utf-8") + monkeypatch.setenv("NOEMA_CODEGRAPH_CONTEXT_PATH", str(outside)) + assert noema.load_codegraph_context() == ( + "CodeGraph context unavailable: path escapes the workspace." + ) + + monkeypatch.setenv( + "NOEMA_CODEGRAPH_CONTEXT_PATH", + str(tmp_path / "nested" / ".." / ".." / outside.name), + ) + assert "path escapes the workspace" in noema.load_codegraph_context() + + link = tmp_path / "escape.md" + link.symlink_to(outside) + monkeypatch.setenv("NOEMA_CODEGRAPH_CONTEXT_PATH", str(link)) + assert "path escapes the workspace" in noema.load_codegraph_context() + + +def test_codegraph_context_root_and_resolve_failure(monkeypatch, tmp_path): + """Workspace root prefers GITHUB_WORKSPACE and resolve errors stay closed.""" + monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path)) + assert noema.codegraph_context_root() == tmp_path.resolve() + monkeypatch.delenv("GITHUB_WORKSPACE", raising=False) + monkeypatch.chdir(tmp_path) + assert noema.codegraph_context_root() == tmp_path.resolve() + + class FailingPath(type(tmp_path)): + """Path stand-in whose resolve always fails.""" + + def resolve(self, *args, **kwargs): + """Raise OSError to cover the confinement resolve failure.""" + raise OSError("resolve failed") + + monkeypatch.setattr(noema, "Path", FailingPath) + assert noema.confined_codegraph_context_path("graph.md", tmp_path.resolve()) is None + + +def test_relative_codegraph_context_resolves_from_workspace(monkeypatch, tmp_path): + """Relative context paths are workspace-relative even when cwd differs.""" + workspace = tmp_path / "workspace" + context_dir = workspace / "context" + context_dir.mkdir(parents=True) + graph = context_dir / "graph.md" + graph.write_text("trusted workspace graph", encoding="utf-8") + cwd = tmp_path / "cwd" + cwd.mkdir() + + monkeypatch.chdir(cwd) + monkeypatch.setenv("GITHUB_WORKSPACE", str(workspace)) + monkeypatch.setenv("NOEMA_CODEGRAPH_CONTEXT_PATH", "context/graph.md") + + assert noema.load_codegraph_context() == "trusted workspace graph" + + class FakeResponse: """Small context-manager response for urllib monkeypatches.""" @@ -526,10 +586,17 @@ def test_inspect_and_review_skip_paths(monkeypatch): assert noema.inspect_and_review("owner/repo", 7) == 0 assert calls + calls.clear() + monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: make_pr()) + assert noema.inspect_and_review("owner/repo", 7) == 1 + assert calls == [] + + monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: make_pr(isDraft=True)) + assert noema.inspect_and_review("owner/repo", 7) == 1 + assert calls == [] + cases = [ - (make_pr(), "noema"), - (make_pr(isDraft=True), "noema"), - (make_pr(reviews={"nodes": [review(login="noema", body="")]}), "noema"), + (make_pr(isDraft=True, reviews={"nodes": [review(body=marker_body)]}), "noema"), (make_pr(reviews={"nodes": [review("CHANGES_REQUESTED"), review(body=marker_body)]}), "noema"), (make_pr(reviews={"nodes": [review(body=marker_body)]}, reviewThreads={"nodes": [{"isResolved": False, "isOutdated": False}]}), "noema"), (make_pr(reviews={"nodes": [review(body=marker_body)]}, statusCheckRollup={"contexts": {"nodes": [{"__typename": "StatusContext", "context": "ci", "state": "FAILURE"}]}}), "noema"), @@ -543,6 +610,37 @@ def test_inspect_and_review_skip_paths(monkeypatch): assert calls == [] + +def test_existing_noema_review_cannot_bypass_primary_approval(monkeypatch): + """A Noema verdict is never sufficient without current-head OpenCode approval.""" + noema_review = review( + login="noema", + body="", + ) + pr = make_pr(reviews={"nodes": [noema_review]}) + submitted = [] + monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: pr) + monkeypatch.setattr(noema, "current_actor", lambda: "noema") + monkeypatch.setattr( + noema, + "submit_review", + lambda *args, **kwargs: submitted.append(args), + ) + + assert noema.inspect_and_review("owner/repo", 7) == 1 + assert submitted == [] + + primary_review = review( + body=( + "OpenCode reviewed the current-head bounded evidence and found " + "no blocking issues." + ) + ) + pr = make_pr(reviews={"nodes": [primary_review, noema_review]}) + assert noema.inspect_and_review("owner/repo", 7) == 0 + assert submitted == [] + + def test_parse_args_and_main(monkeypatch): parsed = noema.parse_args(["--repo", "owner/repo", "--pr-number", "9"]) assert parsed.repo == "owner/repo" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 379dded14..87a95e475 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -523,10 +523,13 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'http."${GITHUB_SERVER_URL}/".extraheader' not in step assert "AUTHORIZATION: bearer ${GH_TOKEN}" not in step assert "AUTHORIZATION: bearer" not in step + assert 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA"' in step + assert 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_HEAD_SHA"' in step + assert 'refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr-${PR_NUMBER}-head' in step assert ( - 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' - in step - ) + 'fetched_head_sha="$(git -C "$fetch_dir" rev-parse ' + '"refs/remotes/origin/pr-${PR_NUMBER}-head")"' + ) 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 @@ -2184,12 +2187,16 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert "repository_dispatch:" not in bootstrap.split("permissions:", 1)[0] assert "actions/checkout" not in bootstrap assert "${{ secrets." not in bootstrap + assert 'jq -r -s --arg sha "$HEAD_SHA"' in bootstrap assert "required-workflow-bootstrap:" in bootstrap assert " coverage-source-tree:\n" in bootstrap assert " coverage-evidence:\n" in bootstrap assert " opencode-review-target:\n" in bootstrap assert " name: opencode-review\n" in bootstrap assert "authenticated default-branch OpenCode review dispatch" in bootstrap + assert "This required check is not a review" in bootstrap + assert "No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head" in bootstrap + assert "pull-requests: read" in bootstrap 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 diff --git a/tests/test_opencode_required_verdict_gate.py b/tests/test_opencode_required_verdict_gate.py new file mode 100644 index 000000000..06431accc --- /dev/null +++ b/tests/test_opencode_required_verdict_gate.py @@ -0,0 +1,246 @@ +"""Fail-closed required OpenCode check when no current-head verdict exists.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from scripts.ci import opencode_dispatch_status as dispatch_status + + +def _review( + *, login: str, state: str, commit_id: str, body: str = "" +) -> dict[str, object]: + """Return one GitHub Reviews API object.""" + return { + "user": {"login": login}, + "state": state, + "commit_id": commit_id, + "body": body, + } + + +def test_current_head_opencode_verdict_reads_latest_matching_state() -> None: + head = "a" * 40 + assert ( + dispatch_status.current_head_opencode_verdict( + [ + _review(login="opencode-agent[bot]", state="APPROVED", commit_id=head), + _review( + login="opencode-agent[bot]", + state="CHANGES_REQUESTED", + commit_id=head, + ), + ], + head, + ) + == "CHANGES_REQUESTED" + ) + assert ( + dispatch_status.current_head_opencode_verdict( + [_review(login="opencode-agent", state="APPROVED", commit_id=head)], + head, + ) + == "APPROVED" + ) + + +def test_current_head_opencode_verdict_ignores_other_actors_and_heads() -> None: + head = "a" * 40 + assert ( + dispatch_status.current_head_opencode_verdict( + [_review(login="coderabbitai[bot]", state="APPROVED", commit_id=head)], + head, + ) + is None + ) + assert ( + dispatch_status.current_head_opencode_verdict( + [ + _review( + login="opencode-agent[bot]", + state="APPROVED", + commit_id="b" * 40, + ) + ], + head, + ) + is None + ) + assert ( + dispatch_status.current_head_opencode_verdict( + [ + _review(login="coderabbitai[bot]", state="APPROVED", commit_id=head), + _review(login="opencode-agent[bot]", state="APPROVED", commit_id="b" * 40), + _review(login="opencode-agent[bot]", state="COMMENTED", commit_id=head), + ], + head, + ) + is None + ) + assert dispatch_status.current_head_opencode_verdict([], "") is None + + +@pytest.mark.parametrize( + "marker", + ( + "deterministic current-head evidence", + "deterministic fallback approval", + "model-unavailable evidence fallback", + "did not emit a usable current-head control block", + "scope: `unsupported`", + "model-pool outcome: `unknown`", + ), +) +def test_current_head_opencode_verdict_rejects_fallback_approval(marker: str) -> None: + """Model-unavailable or deterministic approvals are not formal evidence.""" + head = "a" * 40 + reviews = [ + _review(login="opencode-agent[bot]", state="APPROVED", commit_id=head), + _review( + login="opencode-agent[bot]", + state="APPROVED", + commit_id=head, + body=f"OpenCode {marker}", + ), + ] + + assert dispatch_status.current_head_opencode_verdict(reviews, head) is None + + reviews[-1]["state"] = "CHANGES_REQUESTED" + assert ( + dispatch_status.current_head_opencode_verdict(reviews, head) + == "CHANGES_REQUESTED" + ) + + +def test_current_head_opencode_verdict_uses_latest_current_head_review() -> None: + """A later non-verdict cannot expose an older decision as the latest one.""" + head = "a" * 40 + assert ( + dispatch_status.current_head_opencode_verdict( + [ + _review( + login="opencode-agent[bot]", + state="APPROVED", + commit_id=head, + ), + _review( + login="opencode-agent[bot]", + state="COMMENTED", + commit_id=head, + ), + ], + head, + ) + is None + ) + + +def test_required_workflow_rejects_fallback_approvals() -> None: + """The checkout-free jq twin enforces the same fallback boundary.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text( + encoding="utf-8" + ) + + assert "| (last // {}) as $review" in workflow + for marker in ( + "deterministic current-head evidence", + "deterministic fallback approval", + "model-unavailable evidence fallback", + "did not emit a usable current-head control block", + "scope: `unsupported`", + "model-pool outcome: `unknown`", + ): + assert marker in workflow + + +def test_decide_required_verdict_check_fails_closed_without_verdict() -> None: + head = "a" * 40 + decision = dispatch_status.decide_required_verdict_check( + expected_head=head, + pull_request={"head": {"sha": head}}, + reviews=[], + ) + assert decision["state"] == "failure" + assert "This required check is not a review" in decision["description"] + stale = dispatch_status.decide_required_verdict_check( + expected_head=head, + pull_request={"head": {"sha": "c" * 40}}, + reviews=[_review(login="opencode-agent[bot]", state="APPROVED", commit_id=head)], + ) + assert stale["state"] == "failure" + approved = dispatch_status.decide_required_verdict_check( + expected_head=head, + pull_request={"head": {"sha": head}}, + reviews=[_review(login="opencode-agent[bot]", state="APPROVED", commit_id=head)], + ) + assert approved == { + "state": "success", + "description": "Current-head OpenCode verdict: APPROVED.", + } + + +def test_required_verdict_cli_exits_one_without_verdict(tmp_path: Path) -> None: + head = "a" * 40 + pr_file = tmp_path / "pr.json" + reviews_file = tmp_path / "reviews.json" + pr_file.write_text(json.dumps({"head": {"sha": head}}), encoding="utf-8") + reviews_file.write_text("[]", encoding="utf-8") + assert ( + dispatch_status.main( + [ + "--mode", + "required-verdict", + "--expected-head", + head, + "--pull-request-file", + str(pr_file), + "--reviews-file", + str(reviews_file), + ] + ) + == 1 + ) + reviews_file.write_text( + json.dumps( + [_review(login="opencode-agent[bot]", state="CHANGES_REQUESTED", commit_id=head)] + ), + encoding="utf-8", + ) + assert ( + dispatch_status.main( + [ + "--mode", + "required-verdict", + "--expected-head", + head, + "--pull-request-file", + str(pr_file), + "--reviews-file", + str(reviews_file), + ] + ) + == 0 + ) + + +def test_dispatch_status_cli_still_requires_model_and_coverage(tmp_path: Path) -> None: + head = "a" * 40 + pr_file = tmp_path / "pr.json" + reviews_file = tmp_path / "reviews.json" + pr_file.write_text(json.dumps({"head": {"sha": head}}), encoding="utf-8") + reviews_file.write_text("[]", encoding="utf-8") + with pytest.raises(SystemExit, match="--model-outcome"): + dispatch_status.main( + [ + "--expected-head", + head, + "--pull-request-file", + str(pr_file), + "--reviews-file", + str(reviews_file), + ] + ) diff --git a/tests/test_opencode_workflow_shell_syntax.py b/tests/test_opencode_workflow_shell_syntax.py index ec6edca40..8aa29ff28 100644 --- a/tests/test_opencode_workflow_shell_syntax.py +++ b/tests/test_opencode_workflow_shell_syntax.py @@ -34,6 +34,11 @@ def test_opencode_review_run_blocks_are_valid_bash(): ) assert 'gsub("`"; "'")' in workflow_text assert 'gsub("`"; "\'")' not in workflow_text + assert ( + ' elif [ "$pr_head_fetch_attempt" -lt 6 ]; then\n' + ' echo "PR head ref fetch failed on attempt $pr_head_fetch_attempt; retrying after propagation delay."\n' + ' sleep 10' + ) in workflow_text if sys.platform == "win32": return @@ -148,7 +153,7 @@ def test_merge_scheduler_targeted_dispatch_run_block_is_valid_bash(): def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): - """Only an allowlisted same-repository open PR reaches scheduler outputs.""" + """Only an allowlisted open PR with a target base reaches scheduler outputs.""" if sys.platform == "win32": return bash = shutil.which("bash") @@ -170,8 +175,11 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): """#!/usr/bin/env bash set -euo pipefail test "$1" = api -test "$2" = repos/ContextualWisdomLab/naruon/pulls/1179 -printf '%s\\n' "$FAKE_PULL_JSON" +case "$2" in + repos/ContextualWisdomLab/naruon/pulls/1179) printf '%s\\n' "$FAKE_PULL_JSON" ;; + repos/ContextualWisdomLab/naruon) printf '%s\\n' "$FAKE_REPOSITORY_JSON" ;; + *) exit 1 ;; +esac """, encoding="utf-8", ) @@ -193,6 +201,7 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", "FAKE_PULL_JSON": json.dumps(pull), + "FAKE_REPOSITORY_JSON": json.dumps({"default_branch": "main"}), "GITHUB_EVENT_NAME": "repository_dispatch", "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", "GITHUB_OUTPUT": str(output), @@ -218,6 +227,7 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): assert output.read_text(encoding="utf-8").splitlines() == [ "repository=ContextualWisdomLab/naruon", "base_branch=develop", + "default_branch=main", "head_sha=4afd4af7ad343660356791873d940aa2846f40c2", ] @@ -260,6 +270,92 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): env=cross_repo_env, ) - assert cross_repo.returncode == 1 - assert "cross-repository" in cross_repo.stdout - assert not output.exists() + assert cross_repo.returncode == 0, cross_repo.stderr + assert output.read_text(encoding="utf-8").splitlines() == [ + "repository=ContextualWisdomLab/naruon", + "base_branch=develop", + "default_branch=main", + "head_sha=4afd4af7ad343660356791873d940aa2846f40c2", + ] + + +def test_opencode_dispatch_validation_accepts_external_head_as_review_data(tmp_path): + """The central metadata gate accepts a fork while preserving exact identity.""" + if sys.platform == "win32": + return + bash = shutil.which("bash") + if bash is None: + return + + workflow_text = ( + REPO_ROOT / ".github/workflows/opencode-review-dispatch.yml" + ).read_text(encoding="utf-8") + script = _extract_run_block( + workflow_text, + "Bind workflow inputs to live organization pull request metadata", + ) + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_gh = fake_bin / "gh" + fake_gh.write_text( + """#!/usr/bin/env bash +set -euo pipefail +test "$1" = api +test "$2" = repos/ContextualWisdomLab/naruon/pulls/1179 +printf '%s\\n' "$FAKE_PULL_JSON" +""", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + pull = { + "number": 1179, + "state": "open", + "base": { + "ref": "develop", + "sha": "1" * 40, + "repo": {"full_name": "ContextualWisdomLab/naruon", "private": False}, + }, + "head": { + "ref": "feature/fork-review", + "sha": "2" * 40, + "repo": {"full_name": "outside/fork"}, + }, + } + output = tmp_path / "github-output" + env = { + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "FAKE_PULL_JSON": json.dumps(pull), + "EVENT_NAME": "repository_dispatch", + "DISPATCH_ACTOR": "scheduler", + "DISPATCH_SENDER": "scheduler", + "ALLOWED_DISPATCH_ACTOR": "scheduler", + "ALLOWED_DISPATCH_TARGETS": "ContextualWisdomLab/naruon", + "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", + "PR_NUMBER": "1179", + "SUPPLIED_BASE_REF": "develop", + "SUPPLIED_BASE_SHA": "1" * 40, + "SUPPLIED_HEAD_REF": "feature/fork-review", + "SUPPLIED_HEAD_SHA": "2" * 40, + "GITHUB_OUTPUT": str(output), + } + + result = subprocess.run( + [bash], + input=script, + text=True, + capture_output=True, + check=False, + env=env, + ) + + assert result.returncode == 0, result.stderr + assert output.read_text(encoding="utf-8").splitlines() == [ + "target_repository=ContextualWisdomLab/naruon", + "pr_number=1179", + "base_ref=develop", + f"base_sha={'1' * 40}", + "head_ref=feature/fork-review", + f"head_sha={'2' * 40}", + "is_private=false", + ] diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 1bbd98750..12cd10d1e 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -3,7 +3,6 @@ import hashlib from pathlib import Path import re -import subprocess import pytest @@ -20,7 +19,6 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "83f6830d5c21a324b4dbcd4e5c21a07968994b81" def _workflow_text(path: Path) -> str: @@ -157,15 +155,33 @@ def test_missing_nvidia_nim_secret_fails_closed_before_model_execution() -> None def test_independent_review_agent_key_system_is_unchanged() -> None: - """Pin the existing read-only reviewer workflow byte-for-byte.""" - result = subprocess.run( - ["git", "hash-object", str(REVIEW_DISPATCH_WORKFLOW)], - check=True, - capture_output=True, - text=True, + """Pin reviewer write credentials without freezing unrelated workflow bytes.""" + workflow = _workflow_text(REVIEW_DISPATCH_WORKFLOW) + for expression in ( + "GH_TOKEN: $" + + "{{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}", + "GH_TOKEN: $" + "{{ secrets.OPENCODE_APPROVE_TOKEN || github.token }}", + "GH_TOKEN: $" + + "{{ steps.opencode_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}", + ): + assert expression in workflow + assert "pr-review-autofix" not in workflow + assert "COPILOT_GITHUB_TOKEN" not in workflow + + model_step_start = workflow.index(" - name: Run OpenCode PR Review model pool") + model_step_end = workflow.index( + " - name: Publish OpenCode review outcome", model_step_start ) - assert result.stdout.strip() == REVIEW_DISPATCH_BLOB_SHA - assert "pr-review-autofix" not in _workflow_text(REVIEW_DISPATCH_WORKFLOW) + model_step = workflow[model_step_start:model_step_end] + for provider_key in ( + "STRIX_GITHUB_MODELS_TOKEN", + "NVIDIA_API_KEY", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ): + assert provider_key in model_step + assert "PR_REVIEW_MERGE_TOKEN" not in model_step + assert "OPENCODE_APPROVE_TOKEN" not in model_step def test_ordinary_autofix_uses_the_same_exact_write_scope_as_conflict_repair() -> None: diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 0e71bdbe2..e45b241d8 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2294,6 +2294,77 @@ def fake_run(args, stdin=None): assert not any(call[:3] == ["gh", "workflow", "run"] for call in calls) +def test_workflow_run_name_matches_accepts_github_run_name_prefix(): + aliases = frozenset({"OpenCode Review Dispatch", "Required OpenCode Review"}) + assert sched.workflow_run_name_matches( + "OpenCode Review Dispatch", "OpenCode Review", aliases + ) + assert sched.workflow_run_name_matches( + "OpenCode Review Dispatch ContextualWisdomLab/.github#1002@" + ("a" * 40), + "OpenCode Review", + aliases, + ) + assert not sched.workflow_run_name_matches( + "Strix Security Scan ContextualWisdomLab/.github#1002@" + ("a" * 40), + "OpenCode Review", + aliases, + ) + + +def test_dispatch_opencode_review_deduplicates_github_run_name_as_display_title( + monkeypatch, capsys +): + calls = [] + head_sha = "a" * 40 + live_name = f"OpenCode Review Dispatch owner/repo#1@{head_sha}" + current_dispatch = { + "id": 9101, + "name": live_name, + "event": "repository_dispatch", + "head_sha": "default-branch-sha", + "display_title": live_name, + "pull_requests": [], + } + + def fake_run(args, stdin=None): + calls.append(args) + if args[:5] == [ + "gh", + "api", + "--method", + "GET", + "repos/ContextualWisdomLab/.github/actions/runs", + ]: + if "status=queued" in args: + return json.dumps({"workflow_runs": [current_dispatch]}) + return json.dumps({"workflow_runs": []}) + if "/actions/runs" in " ".join(args): + return json.dumps({"workflow_runs": []}) + return "" + + monkeypatch.setattr(sched, "run", fake_run) + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GH_TOKEN", "workflow-token") + monkeypatch.setenv( + "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", + "ContextualWisdomLab/.github", + ) + + result = sched.dispatch_opencode_review( + "owner/repo", + "OpenCode Review", + make_pr(headRefOid=head_sha), + dry_run=False, + ) + + assert result == "already_running" + assert ( + "active same-head workflow run(s) ContextualWisdomLab/.github@9101" + in capsys.readouterr().out + ) + assert not any(call[-2:] == ["--input", "-"] for call in calls) + + def test_dispatch_strix_cancels_stale_central_run_and_keeps_current(monkeypatch, capsys): calls = [] head_sha = "a" * 40 @@ -2382,6 +2453,13 @@ def test_central_run_filter_ignores_malformed_and_non_dispatch_titles(monkeypatc "head_sha": head_sha, "pull_requests": [{"number": 1}], }, + { + "id": 9405, + "name": "Required OpenCode Review", + "event": "workflow_run", + "head_sha": head_sha, + "pull_requests": [], + }, ] def fake_active_runs(repo, statuses=("queued", "in_progress")): @@ -2461,6 +2539,34 @@ def test_central_run_filter_ignores_same_repository_required_workflow_placeholde ) == ([], []) +def test_legacy_same_repository_filter_ignores_required_workflow_placeholder( + monkeypatch, +): + """A required-workflow placeholder must not suppress the real dispatch.""" + head_sha = "a" * 40 + placeholder = { + "id": 9404, + "name": "Required OpenCode Review", + "event": "pull_request_target", + "head_sha": head_sha, + "display_title": f"Required OpenCode Review owner/repo#1@{head_sha}", + "pull_requests": [{"number": 1}], + } + + monkeypatch.setattr( + sched, + "active_workflow_runs", + lambda repo, statuses=("queued", "in_progress"): [placeholder], + ) + monkeypatch.delenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", raising=False) + + assert sched.active_opencode_run_refs( + "owner/repo", + "OpenCode Review", + make_pr(headRefOid=head_sha), + ) == ([], []) + + def test_active_run_filters_and_stale_opencode_dry_run(monkeypatch): runs = [ { @@ -2993,8 +3099,201 @@ def test_summary_section_helpers_handle_empty_and_action_error_cases(): assert "- PR #5: `fork/repo` is external" in "\n".join(external_merge_lines) +def test_draft_pr_review_path_never_mutates_branch_or_merge_state(monkeypatch): + mutations = [] + dispatches = [] + for name in ("update_branch", "enable_auto_merge", "merge_pr", "disable_auto_merge"): + monkeypatch.setattr( + sched, + name, + lambda *args, _name=name, **kwargs: mutations.append(_name), + ) + monkeypatch.setattr( + sched, + "repository_dispatch_wait_reason", + lambda *args, **kwargs: "", + ) + monkeypatch.setattr( + sched, + "dispatch_strix_evidence", + lambda repo, workflow, pr, dry_run: dispatches.append( + ("strix", pr["number"], dry_run) + ) + or "dispatched", + ) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: dispatches.append( + ("opencode", pr["number"], dry_run) + ) + or "dispatched", + ) + + missing_strix = inspect(make_pr(isDraft=True), dry_run=False) + assert missing_strix.action == "security_dispatch" + assert missing_strix.reason == ( + "draft PR; current head has no completed Strix evidence; same-head Strix dispatched" + ) + + completed_strix = inspect( + make_pr( + isDraft=True, + statusCheckRollup={"contexts": {"nodes": [strix_check()]}}, + ), + dry_run=False, + ) + assert completed_strix.action == "review_dispatch" + assert completed_strix.reason == ( + "draft PR; current head has completed Strix evidence; same-head OpenCode dispatched" + ) + + approved = inspect( + make_pr( + isDraft=True, + autoMergeRequest={"enabledAt": "now"}, + reviews={"nodes": [opencode_review("APPROVED", "head")]}, + statusCheckRollup={"contexts": {"nodes": [strix_check()]}}, + ), + dry_run=False, + ) + assert approved.action == "skip" + assert approved.reason == "draft PR; current-head OpenCode approval recorded" + + changes_requested = inspect( + make_pr( + isDraft=True, + autoMergeRequest={"enabledAt": "now"}, + reviews={"nodes": [opencode_review("CHANGES_REQUESTED", "head")]}, + ), + dry_run=False, + ) + assert changes_requested.action == "skip" + assert changes_requested.reason == ( + "draft PR; current-head OpenCode review requested changes" + ) + + assert dispatches == [("strix", 1, False), ("opencode", 1, False)] + assert mutations == [] + + +def test_draft_pr_review_wait_states_are_read_only(monkeypatch): + dispatches = [] + monkeypatch.setattr( + sched, + "dispatch_strix_evidence", + lambda *args, **kwargs: dispatches.append("strix") or "dispatched", + ) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda *args, **kwargs: dispatches.append("opencode") or "dispatched", + ) + + disabled = inspect(make_pr(isDraft=True), trigger_reviews=False) + assert disabled.action == "skip" + assert disabled.reason == "draft PR; review dispatch disabled" + + running = inspect( + make_pr( + isDraft=True, + statusCheckRollup={ + "contexts": {"nodes": [opencode_check(status="IN_PROGRESS")]} + }, + ) + ) + assert running.action == "wait" + assert running.reason == "draft PR; OpenCode review is already in progress" + + limited = inspect(make_pr(isDraft=True), review_dispatch_allowed=False) + assert limited.action == "wait" + assert limited.reason == "draft PR; review dispatch limit reached" + + strix_running = inspect( + make_pr( + isDraft=True, + statusCheckRollup={ + "contexts": { + "nodes": [strix_check(status="IN_PROGRESS", conclusion=None)] + } + }, + ) + ) + assert strix_running.action == "wait" + assert strix_running.reason == "draft PR; same-head Strix evidence is still running" + assert dispatches == [] + + +def test_draft_pr_review_dispatch_failures_are_wait_states(monkeypatch): + missing_reason = "central Strix dispatch workflow unavailable" + monkeypatch.setattr( + sched, + "repository_dispatch_wait_reason", + lambda repo, workflow: missing_reason + if workflow == "Strix Security Scan" + else "", + ) + missing = inspect(make_pr(isDraft=True)) + assert missing.action == "wait" + assert missing.reason == ( + "draft PR; current head has no completed Strix evidence; " + missing_reason + ) + + opencode_reason = "central OpenCode dispatch workflow unavailable" + monkeypatch.setattr( + sched, + "repository_dispatch_wait_reason", + lambda repo, workflow: opencode_reason + if workflow == "OpenCode Review" + else "", + ) + completed = make_pr( + isDraft=True, + statusCheckRollup={"contexts": {"nodes": [strix_check()]}}, + ) + unavailable = inspect(completed) + assert unavailable.action == "wait" + assert unavailable.reason == ( + "draft PR; current head has completed Strix evidence; " + opencode_reason + ) + + monkeypatch.setattr( + sched, + "repository_dispatch_wait_reason", + lambda *args, **kwargs: "", + ) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda *args, **kwargs: "already_running", + ) + already_running = inspect(completed) + assert already_running.action == "wait" + assert already_running.reason == ( + "draft PR; current head has completed Strix evidence; " + "same-head OpenCode workflow run is already active" + ) + + monkeypatch.setattr( + sched, + "dispatch_strix_evidence", + lambda *args, **kwargs: "already_running", + ) + strix_already_running = inspect(make_pr(isDraft=True)) + assert strix_already_running.action == "wait" + assert strix_already_running.reason == ( + "draft PR; current head has no completed Strix evidence; " + "same-head Strix workflow run is already active" + ) + + def test_inspect_pr_blocks_and_waits_for_policy_states(monkeypatch): - assert inspect(make_pr(isDraft=True)).action == "skip" + monkeypatch.delenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", raising=False) + draft = inspect(make_pr(isDraft=True)) + assert draft.action == "security_dispatch" + assert draft.reason == ( + "draft PR; current head has no completed Strix evidence; same-head Strix dispatched" + ) stacked = inspect(make_pr(baseRefName="develop")) assert stacked.action == "review_dispatch" assert stacked.reason == "stacked PR onto develop; OpenCode review dispatched" @@ -4483,6 +4782,114 @@ def test_main_limits_review_dispatches_and_branch_updates(monkeypatch, capsys): ) +def test_review_dispatch_priority_ranks_empty_reviews_before_leftover_rereview(): + never_reviewed = make_pr(number=176) + leftover_rereview = make_pr( + number=998, + reviews={ + "nodes": [ + opencode_review("COMMENTED", "old-head", login="seonghobae"), + opencode_review("CHANGES_REQUESTED", "old-head"), + ] + }, + ) + already_verdicted = make_pr( + number=1002, + reviews={"nodes": [opencode_review("APPROVED", "head")]}, + ) + commented_only = make_pr( + number=42, + reviews={"nodes": [opencode_review("COMMENTED", "head")]}, + ) + + human_only = make_pr( + number=7, + reviews={"nodes": [opencode_review("APPROVED", "head", login="seonghobae")]}, + ) + fallback_only = make_pr( + number=8, + reviews={ + "nodes": [ + { + **opencode_review("APPROVED", "old-head"), + "body": "Deterministic fallback approval: providers unavailable.", + } + ] + }, + ) + assert sched.has_any_opencode_verdict(never_reviewed) is False + assert sched.has_any_opencode_verdict(commented_only) is False + assert sched.has_any_opencode_verdict(human_only) is False + assert sched.has_any_opencode_verdict(fallback_only) is False + assert sched.has_any_opencode_verdict(leftover_rereview) is True + assert sched.review_dispatch_priority(never_reviewed) == 0 + assert sched.review_dispatch_priority(commented_only) == 0 + assert sched.review_dispatch_priority(fallback_only) == 0 + assert sched.review_dispatch_priority(leftover_rereview) == 1 + assert sched.review_dispatch_priority(already_verdicted) == 2 + assert [ + pr["number"] + for pr in sched.prioritize_review_dispatch_queue( + [ + leftover_rereview, + already_verdicted, + never_reviewed, + commented_only, + fallback_only, + ] + ) + ] == [176, 42, 8, 998, 1002] + + +def test_main_prefers_never_reviewed_pr_when_dispatch_budget_is_one(monkeypatch, capsys): + prs = [ + make_pr( + number=998, + statusCheckRollup={"contexts": {"nodes": [strix_check()]}}, + reviews={"nodes": [opencode_review("CHANGES_REQUESTED", "old-head")]}, + ), + make_pr( + number=176, + statusCheckRollup={"contexts": {"nodes": [strix_check()]}}, + ), + ] + dispatched = [] + + monkeypatch.setattr(sched, "fetch_open_prs", lambda repo, max_prs: prs) + monkeypatch.setattr( + sched, + "dispatch_opencode_review", + lambda repo, workflow, pr, dry_run: dispatched.append(pr["number"]), + ) + monkeypatch.setattr(sched, "cancel_stale_pr_runs", lambda repo, pr, dry_run: []) + + assert ( + sched.main( + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--project-flow", + "github-flow", + "--review-dispatch-limit", + "1", + ] + ) + == 0 + ) + + output = capsys.readouterr().out + payload = json.loads(output.strip().splitlines()[-1]) + assert dispatched == [176] + assert payload["decisions"][0]["pr"] == 176 + assert payload["decisions"][0]["action"] == "review_dispatch" + assert payload["decisions"][1]["pr"] == 998 + assert payload["decisions"][1]["reason"] == ( + "current head has completed Strix evidence; review dispatch limit reached" + ) + + def test_main_rejects_invalid_review_dispatch_limit(): with pytest.raises(SystemExit, match="--review-dispatch-limit must be -1 or greater"): sched.main( diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index be13a522f..689c77e7c 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -134,11 +134,15 @@ def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> Non assert '"repos/${TARGET_REPOSITORY_INPUT}/pulls/${TARGET_PR_NUMBER}"' in validation assert '[ "$live_state" != "open" ]' in validation assert '[ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation - assert '[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation + assert '! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' in validation + assert "this dispatch is review-only for external heads" in validation assert "Targeted scheduler dispatch base branch does not match the live PR" in validation + assert 'gh api "repos/${TARGET_REPOSITORY_INPUT}"' in validation + assert "live_default_branch=" in validation + assert "printf 'default_branch=%s\\n' \"$live_default_branch\"" in validation assert "TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }}" in inspect assert ( - "TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.base_branch }}" + "TARGET_DEFAULT_BRANCH: ${{ steps.targeted_dispatch.outputs.default_branch }}" in inspect ) assert '--repo "$TARGET_REPOSITORY"' in inspect @@ -157,6 +161,23 @@ def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> Non ) in workflow +def test_fork_heads_reach_read_only_opencode_review_dispatch() -> None: + """External heads are reviewable but never become trusted merge inputs.""" + workflow = workflow_text("opencode-review-dispatch.yml") + metadata = workflow_step( + workflow, "Bind workflow inputs to live organization pull request metadata" + ) + privileged = workflow_step(workflow, "Validate pull request head repository trust") + scheduler = (REPO_ROOT / "scripts/ci/pr_review_merge_scheduler.py").read_text( + encoding="utf-8" + ) + + assert '! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' in metadata + assert '! [[ "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' in privileged + assert "this path is review-only for external heads" in privileged + assert "fork or external PR heads are excluded from scheduler direct merge and auto-merge" in scheduler + + def test_privileged_review_retries_use_default_branch_repository_dispatch() -> None: """Privileged retries must never load workflow code from a selected ref.""" expected_types = { From 3b91220405a2434f5b16bc336bf12b871bb374a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:14:12 +0900 Subject: [PATCH 15/27] fix(review): reject external heads in privileged paths --- .../workflows/opencode-review-dispatch.yml | 10 +++---- .../workflows/pr-review-merge-scheduler.yml | 6 ++--- CHANGELOG.md | 1 + scripts/ci/pr_review_merge_scheduler.py | 6 +++++ tests/test_opencode_workflow_shell_syntax.py | 27 ++++++------------- tests/test_pr_review_merge_scheduler.py | 15 +++++++++++ .../test_required_workflow_queue_contract.py | 19 ++++++------- 7 files changed, 48 insertions(+), 36 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 59b84b3e5..bead42e9a 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -179,13 +179,13 @@ jobs: if [ "$live_state" != "open" ] || [ "$live_base_repository" != "$TARGET_REPOSITORY" ] || - ! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || + [ "$live_head_repository" != "$TARGET_REPOSITORY" ] || ! [[ "$live_base_sha" =~ ^[0-9a-fA-F]{40}$ ]] || ! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]] || ! [[ "$live_is_private" =~ ^(true|false)$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then - printf '::error::PR metadata validation rejected closed, missing, malformed, or target-base-mismatched live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_sha:-}" "${live_head_sha:-}" + printf '::error::PR metadata validation rejected closed, missing, cross-repository, malformed, or target-base-mismatched live metadata. target=%s#%s state=%s base_repo=%s head_repo=%s base=%s head=%s\n' "$TARGET_REPOSITORY" "$PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_sha:-}" "${live_head_sha:-}" exit 1 fi @@ -2182,7 +2182,7 @@ jobs: live_is_private="$(jq -r '.base.repo.private | tostring' <<<"$pull_request_json")" if [ "$live_state" != "open" ] || [ "$base_repository" != "$GH_REPOSITORY" ] || - ! [[ "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || + [ "$head_repository" != "$GH_REPOSITORY" ] || [ "$live_base_ref" != "$EXPECTED_BASE_REF" ] || [ "$live_base_sha" != "$EXPECTED_BASE_SHA" ] || [ "$live_head_ref" != "$EXPECTED_HEAD_REF" ] || @@ -2190,11 +2190,11 @@ jobs: ! [[ "$EXPECTED_IS_PRIVATE" =~ ^(true|false)$ ]] || ! [[ "$live_is_private" =~ ^(true|false)$ ]] || [ "$live_is_private" != "$EXPECTED_IS_PRIVATE" ]; then - printf '::error::OpenCode privileged review metadata changed before OIDC, review-token, CodeGraph, or model execution. target=%s#%s state=%s base_repo=%s base=%s/%s expected_base=%s/%s head_repo=%s head=%s/%s expected_head=%s/%s private=%s expected_private=%s\n' \ + printf '::error::OpenCode privileged review metadata changed before OIDC, review-token, CodeGraph, or model execution; cross-repository heads are not eligible for this privileged path. target=%s#%s state=%s base_repo=%s base=%s/%s expected_base=%s/%s head_repo=%s head=%s/%s expected_head=%s/%s private=%s expected_private=%s\n' \ "$GH_REPOSITORY" "$PR_NUMBER" "${live_state:-}" "${base_repository:-}" "${live_base_ref:-}" "${live_base_sha:-}" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "${head_repository:-}" "${live_head_ref:-}" "${live_head_sha:-}" "$EXPECTED_HEAD_REF" "$EXPECTED_HEAD_SHA" "${live_is_private:-}" "${EXPECTED_IS_PRIVATE:-}" exit 1 fi - printf 'Validated current-head OpenCode review source for %s#%s (head=%s; this path is review-only for external heads).\n' \ + printf 'Validated same-repository current-head OpenCode review source for %s#%s (%s).\n' \ "$GH_REPOSITORY" "$PR_NUMBER" "$head_repository" - name: Exchange OpenCode app token for target repository review reads diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index cff63a658..aa19254ba 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -289,14 +289,14 @@ jobs: if [ "$live_number" != "$TARGET_PR_NUMBER" ] || [ "$live_state" != "open" ] || [ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ] || - ! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || + [ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ] || [ -z "$live_base_branch" ] || [ -z "$live_default_branch" ] || ! [[ "$live_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then - printf '::error::Targeted scheduler dispatch rejected closed, malformed, or target-base-mismatched live metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s default_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_branch:-}" "${live_default_branch:-}" "${live_head_sha:-}" + printf '::error::Targeted scheduler dispatch rejected closed, cross-repository, malformed, or target-base-mismatched live metadata. target=%s pr=%s state=%s base_repository=%s head_repository=%s base_branch=%s default_branch=%s head_sha=%s\n' "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "${live_state:-}" "${live_base_repository:-}" "${live_head_repository:-}" "${live_base_branch:-}" "${live_default_branch:-}" "${live_head_sha:-}" exit 1 fi - printf 'Validated targeted scheduler review dispatch for %s#%s (head=%s; this dispatch is review-only for external heads).\n' \ + printf 'Validated same-repository targeted scheduler dispatch for %s#%s (head=%s).\n' \ "$TARGET_REPOSITORY_INPUT" "$TARGET_PR_NUMBER" "$live_head_repository" if [ -n "$TARGET_BASE_BRANCH_INPUT" ] && [ "$TARGET_BASE_BRANCH_INPUT" != "$live_base_branch" ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f507e66b..ef0dac8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Semantic Versioning where the repository publishes a release. ### Security +- Restored same-repository checks before central OpenCode privileged review and targeted scheduler execution; external fork heads now fail closed before OIDC, review-token, CodeGraph, model, or merge-control paths. - Keep the Quarantine Sandbox Runtime caller read-only and model-secret-free, grant only job-scoped OIDC to the reusable scheduler, and preserve the product boundary in which the sandbox returns artifact-analysis evidence while hosts retain WAF/IDS, admission, final verdict, incident, and retention authority. - Reject `.github/` and `scripts/ci/` from review-thread-derived autofix path authority so an untrusted inline reviewer cannot authorize the write-capable repair agent to modify workflows, CODEOWNERS, actions, scheduler code, or CI helpers that govern its own control plane. - Require the model-write snapshot and exact-path allowlist to remain outside the pull-request worktree, checking both absolute and resolved locations so repository-local controls and outside-looking symlinks resolving into the repository fail closed before they can authorize or verify model changes. diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index f03d60015..7dd40ba13 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -2440,6 +2440,12 @@ def inspect_draft_pr_for_review( "wait", "draft PR; OpenCode review is already in progress", ) + if opencode_state == "complete": + return Decision( + number, + "skip", + "draft PR; OpenCode review already completed without a current-head verdict", + ) if not review_dispatch_allowed: return Decision(number, "wait", "draft PR; review dispatch limit reached") diff --git a/tests/test_opencode_workflow_shell_syntax.py b/tests/test_opencode_workflow_shell_syntax.py index 8aa29ff28..b77150ab0 100644 --- a/tests/test_opencode_workflow_shell_syntax.py +++ b/tests/test_opencode_workflow_shell_syntax.py @@ -270,17 +270,13 @@ def test_merge_scheduler_targeted_dispatch_validates_live_exact_pr(tmp_path): env=cross_repo_env, ) - assert cross_repo.returncode == 0, cross_repo.stderr - assert output.read_text(encoding="utf-8").splitlines() == [ - "repository=ContextualWisdomLab/naruon", - "base_branch=develop", - "default_branch=main", - "head_sha=4afd4af7ad343660356791873d940aa2846f40c2", - ] + assert cross_repo.returncode == 1 + assert "cross-repository" in cross_repo.stdout + assert not output.exists() -def test_opencode_dispatch_validation_accepts_external_head_as_review_data(tmp_path): - """The central metadata gate accepts a fork while preserving exact identity.""" +def test_opencode_dispatch_validation_rejects_external_head(tmp_path): + """The privileged central metadata gate rejects a fork before review tooling.""" if sys.platform == "win32": return bash = shutil.which("bash") @@ -349,13 +345,6 @@ def test_opencode_dispatch_validation_accepts_external_head_as_review_data(tmp_p env=env, ) - assert result.returncode == 0, result.stderr - assert output.read_text(encoding="utf-8").splitlines() == [ - "target_repository=ContextualWisdomLab/naruon", - "pr_number=1179", - "base_ref=develop", - f"base_sha={'1' * 40}", - "head_ref=feature/fork-review", - f"head_sha={'2' * 40}", - "is_private=false", - ] + assert result.returncode == 1 + assert "cross-repository" in result.stdout + assert not output.exists() diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index e45b241d8..d6f0e3a74 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -3209,6 +3209,21 @@ def test_draft_pr_review_wait_states_are_read_only(monkeypatch): assert limited.action == "wait" assert limited.reason == "draft PR; review dispatch limit reached" + completed = inspect( + make_pr( + isDraft=True, + statusCheckRollup={ + "contexts": { + "nodes": [strix_check(), opencode_check(status="COMPLETED")] + } + }, + ) + ) + assert completed.action == "skip" + assert completed.reason == ( + "draft PR; OpenCode review already completed without a current-head verdict" + ) + strix_running = inspect( make_pr( isDraft=True, diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 689c77e7c..3f49da15f 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -116,8 +116,8 @@ def test_scheduler_deduplicates_metadata_free_workflow_run_scans() -> None: assert 'sort_by(.created_at, .id)' in queue_hygiene -def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> None: - """Central single-PR dispatch must validate live metadata before cross-repo use.""" +def test_targeted_scheduler_dispatch_is_allowlisted_and_same_repo_scoped() -> None: + """Central single-PR dispatch must validate live metadata before privileged use.""" workflow = workflow_text("pr-review-merge-scheduler.yml") validation = workflow_step(workflow, "Validate targeted repository dispatch") inspect = workflow_step(workflow, "Inspect PR review and merge queue") @@ -134,8 +134,8 @@ def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> Non assert '"repos/${TARGET_REPOSITORY_INPUT}/pulls/${TARGET_PR_NUMBER}"' in validation assert '[ "$live_state" != "open" ]' in validation assert '[ "$live_base_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation - assert '! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' in validation - assert "this dispatch is review-only for external heads" in validation + assert '[ "$live_head_repository" != "$TARGET_REPOSITORY_INPUT" ]' in validation + assert "this dispatch is review-only for external heads" not in validation assert "Targeted scheduler dispatch base branch does not match the live PR" in validation assert 'gh api "repos/${TARGET_REPOSITORY_INPUT}"' in validation assert "live_default_branch=" in validation @@ -161,8 +161,8 @@ def test_targeted_scheduler_dispatch_is_allowlisted_and_exact_pr_scoped() -> Non ) in workflow -def test_fork_heads_reach_read_only_opencode_review_dispatch() -> None: - """External heads are reviewable but never become trusted merge inputs.""" +def test_fork_heads_are_rejected_by_privileged_review_paths() -> None: + """External heads cannot reach the privileged review or merge paths.""" workflow = workflow_text("opencode-review-dispatch.yml") metadata = workflow_step( workflow, "Bind workflow inputs to live organization pull request metadata" @@ -172,9 +172,10 @@ def test_fork_heads_reach_read_only_opencode_review_dispatch() -> None: encoding="utf-8" ) - assert '! [[ "$live_head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' in metadata - assert '! [[ "$head_repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' in privileged - assert "this path is review-only for external heads" in privileged + assert '[ "$live_head_repository" != "$TARGET_REPOSITORY" ]' in metadata + assert '[ "$head_repository" != "$GH_REPOSITORY" ]' in privileged + assert "cross-repository heads are not eligible for this privileged path" in privileged + assert "this path is review-only for external heads" not in privileged assert "fork or external PR heads are excluded from scheduler direct merge and auto-merge" in scheduler From dbb3c8a131d708754d2879ec6475d8c45a4ff140 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:34:02 +0900 Subject: [PATCH 16/27] fix(codeql): verify merge preview identity --- .github/workflows/codeql-pr.yml | 21 +++++++++++++++++++++ tests/test_codeql_pr_workflow_contract.py | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 642f2f62e..538b02b7c 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -199,6 +199,27 @@ jobs: # documented pull-request ref shape for CodeQL result publication. ref: ${{ github.event.pull_request.merge_commit_sha }} + - name: Verify merge preview identity + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + run: | + set -euo pipefail + git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA" + test "$(git rev-parse HEAD)" = "$MERGE_SHA" + read -r actual_merge parent_one parent_two extra < None: assert "CodeQL merge preview" in workflow assert "github.event.pull_request.head.sha" in workflow assert "github.event.pull_request.merge_commit_sha" in workflow + assert "name: Verify merge preview identity" in workflow + assert 'git rev-list --parents -n 1 "$MERGE_SHA"' in workflow + assert 'git merge-tree --write-tree "$BASE_SHA" "$HEAD_SHA"' in workflow + assert 'git rev-parse "$MERGE_SHA^{tree}"' in workflow assert "refs/pull/{0}/head" in workflow assert "ref: ${{ github.event.pull_request.merge_commit_sha }}" in workflow assert "ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }}" in workflow From 33cdcad75af2dc9aab1ea5e2b61858581d25ae89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:01:31 +0900 Subject: [PATCH 17/27] fix(codeql): authenticate merge preview fetch --- .github/workflows/codeql-pr.yml | 4 +++- tests/test_codeql_pr_workflow_contract.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 538b02b7c..077271e1c 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -204,9 +204,11 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail - git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA" + git -c "http.extraheader=AUTHORIZATION: bearer $GITHUB_TOKEN" fetch \ + --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA" test "$(git rev-parse HEAD)" = "$MERGE_SHA" read -r actual_merge parent_one parent_two extra < None: assert 'git rev-list --parents -n 1 "$MERGE_SHA"' in workflow assert 'git merge-tree --write-tree "$BASE_SHA" "$HEAD_SHA"' in workflow assert 'git rev-parse "$MERGE_SHA^{tree}"' in workflow + assert 'GITHUB_TOKEN: ${{ github.token }}' in workflow + assert 'http.extraheader=AUTHORIZATION: bearer $GITHUB_TOKEN' in workflow assert "refs/pull/{0}/head" in workflow assert "ref: ${{ github.event.pull_request.merge_commit_sha }}" in workflow assert "ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }}" in workflow From 801c2f1bc43e99d89ab3745ea8722779f7081b95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:11:39 +0900 Subject: [PATCH 18/27] fix(codeql): bind analysis to exact current merge tree --- .github/workflows/codeql-pr.yml | 42 ++++++++++++++----- CHANGELOG.md | 1 + .../required-review-check-is-not-a-verdict.md | 18 +++++++- docs/org-required-workflow-rollout.md | 9 ++-- tests/test_codeql_pr_workflow_contract.py | 7 +++- 5 files changed, 61 insertions(+), 16 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 077271e1c..b952c1de1 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -200,6 +200,7 @@ jobs: ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Verify merge preview identity + id: verify-merge-preview env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} @@ -207,20 +208,39 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail - git -c "http.extraheader=AUTHORIZATION: bearer $GITHUB_TOKEN" fetch \ - --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA" - test "$(git rev-parse HEAD)" = "$MERGE_SHA" + export GIT_CONFIG_COUNT=1 + export GIT_CONFIG_KEY_0=http.extraheader + export GIT_CONFIG_VALUE_0="AUTHORIZATION: bearer $GITHUB_TOKEN" + git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA" + unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 + provided_tree="$(git rev-parse "$MERGE_SHA^{tree}")" read -r actual_merge parent_one parent_two extra <> "$GITHUB_OUTPUT" + echo "Verified merge preview: base=$BASE_SHA head=$HEAD_SHA provided_merge=$MERGE_SHA analyzed_merge=$local_merge_sha tree=$expected_tree" - name: Initialize CodeQL uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 @@ -235,7 +255,7 @@ jobs: upload: false output: codeql-results-merge ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }} - sha: ${{ github.event.pull_request.merge_commit_sha }} + sha: ${{ steps.verify-merge-preview.outputs.merge_sha }} - name: Enforce CodeQL Medium+ SARIF gate shell: python3 {0} diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0dac8c3..7b918936c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Made the CodeQL pull-request gate bind analysis to the exact current base/head merge tree: when GitHub's `pull_request.merge_commit_sha` is stale or structurally different, the workflow now materializes a deterministic local two-parent merge preview and publishes the analyzed SHA instead of failing against an older revision. - Refused PR Review Merge Scheduler head mutations, `update-branch` and the last-push approval head restamp, whenever the resolved mutation credential is the workflow `GITHUB_TOKEN`. GitHub starts no workflow run for events created with that credential, so the moved head collected no current-head required checks and the PR stayed permanently `BLOCKED` with a `github-actions[bot]` merge commit that no later scheduler run could repair, because the branch was no longer behind. The scheduler now waits with `head_mutation_credential_upgrade` guidance naming `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, and the OpenCode app token exchange. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). diff --git a/docs/doctoring/required-review-check-is-not-a-verdict.md b/docs/doctoring/required-review-check-is-not-a-verdict.md index 624b300ef..573cae551 100644 --- a/docs/doctoring/required-review-check-is-not-a-verdict.md +++ b/docs/doctoring/required-review-check-is-not-a-verdict.md @@ -50,6 +50,19 @@ Live runs set `name` to the interpolated run-name. The matcher now accepts that prefix so a queued or in-progress same-head review is `already_running`. +## Exact CodeQL merge-preview contract + +The central CodeQL pull-request workflow must analyze the merge tree formed by +the current base and head, not merely trust the event's +`pull_request.merge_commit_sha`. A live CodeQL failure showed that GitHub can +provide a merge commit whose parents and tree belong to an older pull-request +head. The workflow now verifies the supplied identity, computes +`git merge-tree --write-tree` for the current pair, and materializes a +deterministic two-parent local commit when the supplied metadata is stale or +structurally different. CodeQL receives the resulting analyzed SHA and the +workflow records both identities, so a stale preview cannot silently become +current-head evidence (GitHub, n.d.-b). + ## Draft pull-request review contract Draft status is a merge-readiness signal, not a request to suppress early @@ -90,4 +103,7 @@ Software Engineering* (pp. 712–721). IEEE. https://doi.org/10.1109/ICSE.2013.6606617 GitHub. (n.d.-a). *About status checks*. GitHub Docs. Retrieved -August 14, 2026, from https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks \ No newline at end of file +August 14, 2026, from https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks + +GitHub. (n.d.-b). *About code scanning with CodeQL*. GitHub Docs. Retrieved +August 22, 2026, from https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index 9c42ab063..af1c09677 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -86,9 +86,12 @@ code scanning analyses for ruleset `18156473` `code_scanning` (CodeQL, Scorecard osv-scanner). They trigger on pull requests to `main`, `master`, and `develop` so Git Flow repositories on `develop` inherit the same merge gate as GitHub Flow repos. -CodeQL merge preview checks out `refs/pull//merge` and uploads SARIF with -`sha: pull_request.merge_commit_sha` because the ruleset evaluates that commit, -not the ephemeral merge ref OID. +CodeQL merge preview verifies the current base/head pair and computes its exact +merge tree before analysis. GitHub's `pull_request.merge_commit_sha` remains +the supplied preview identity, but if its parents or tree are stale the +workflow materializes a deterministic local two-parent merge preview and +publishes the SHA of the tree it actually analyzed. The documented +`refs/pull//merge` ref shape remains the SARIF publication boundary. Repository-local `codeql.yml` push/default-branch scans may remain for branch history, but PR merge gates should rely on the central `codeql-pr.yml` workflow. diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 94f4ded9c..b666c7984 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -36,11 +36,16 @@ def test_codeql_pr_workflow_gates_head_and_merge_sarif_locally() -> None: assert "github.event.pull_request.head.sha" in workflow assert "github.event.pull_request.merge_commit_sha" in workflow assert "name: Verify merge preview identity" in workflow + assert "id: verify-merge-preview" in workflow assert 'git rev-list --parents -n 1 "$MERGE_SHA"' in workflow assert 'git merge-tree --write-tree "$BASE_SHA" "$HEAD_SHA"' in workflow assert 'git rev-parse "$MERGE_SHA^{tree}"' in workflow assert 'GITHUB_TOKEN: ${{ github.token }}' in workflow - assert 'http.extraheader=AUTHORIZATION: bearer $GITHUB_TOKEN' in workflow + assert 'GIT_CONFIG_VALUE_0="AUTHORIZATION: bearer $GITHUB_TOKEN"' in workflow + assert 'git commit-tree "$expected_tree" -p "$BASE_SHA" -p "$HEAD_SHA"' in workflow + assert 'git reset --hard "$local_merge_sha"' in workflow + assert 'echo "merge_sha=$local_merge_sha"' in workflow + assert "sha: ${{ steps.verify-merge-preview.outputs.merge_sha }}" in workflow assert "refs/pull/{0}/head" in workflow assert "ref: ${{ github.event.pull_request.merge_commit_sha }}" in workflow assert "ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }}" in workflow From e5a7ac882559fe6d1ae1f91e27d79bc8e0aa1e77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:22:25 +0900 Subject: [PATCH 19/27] test: refresh review dispatch blob pin after restack --- tests/test_pr_review_autofix_nvidia_nim_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 52ef05e6d..89c03851d 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "de1c4800d362bda4a90c31a0c8e39687a782afb4" +REVIEW_DISPATCH_BLOB_SHA = "802d9a5dbafc1a05bbf024f8fc5c806afc43857d" def _workflow_text(path: Path) -> str: From 1ff33c9c7fb209f99b860f98a986f26e168dc8a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 08:49:26 -0700 Subject: [PATCH 20/27] fix(codeql): fetch merge ancestry before preview --- .github/workflows/codeql-pr.yml | 3 +++ docs/org-required-workflow-rollout.md | 5 +++-- tests/test_codeql_pr_workflow_contract.py | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index b952c1de1..edab060b2 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -198,6 +198,9 @@ jobs: # materialize the exact merge-result SHA locally while retaining the # documented pull-request ref shape for CodeQL result publication. ref: ${{ github.event.pull_request.merge_commit_sha }} + # merge-tree requires the real base/head merge base; an explicit-SHA + # depth-1 checkout leaves every fetched commit as a shallow root. + fetch-depth: 0 - name: Verify merge preview identity id: verify-merge-preview diff --git a/docs/org-required-workflow-rollout.md b/docs/org-required-workflow-rollout.md index af1c09677..3a6acc052 100644 --- a/docs/org-required-workflow-rollout.md +++ b/docs/org-required-workflow-rollout.md @@ -1,6 +1,6 @@ # ContextualWisdomLab central required workflow rollout -Updated: 2026-07-23 06:35 KST +Updated: 2026-08-24 KST ## Decision @@ -91,7 +91,8 @@ merge tree before analysis. GitHub's `pull_request.merge_commit_sha` remains the supplied preview identity, but if its parents or tree are stale the workflow materializes a deterministic local two-parent merge preview and publishes the SHA of the tree it actually analyzed. The documented -`refs/pull//merge` ref shape remains the SARIF publication boundary. +`refs/pull//merge` value remains only the SARIF ref label; the analyzed +merge SHA and its tree are the authoritative publication identity. Repository-local `codeql.yml` push/default-branch scans may remain for branch history, but PR merge gates should rely on the central `codeql-pr.yml` workflow. diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index b666c7984..a0d3f7ede 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -48,6 +48,10 @@ def test_codeql_pr_workflow_gates_head_and_merge_sarif_locally() -> None: assert "sha: ${{ steps.verify-merge-preview.outputs.merge_sha }}" in workflow assert "refs/pull/{0}/head" in workflow assert "ref: ${{ github.event.pull_request.merge_commit_sha }}" in workflow + merge_checkout = workflow.split(" - name: Checkout merge preview", 1)[1].split( + " - name: Verify merge preview identity", 1 + )[0] + assert "fetch-depth: 0" in merge_checkout assert "ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }}" in workflow assert workflow.count("security-events: read") == 2 assert "security-events: write" not in workflow From dafcf8771c462287567fc3eef0005750091d4507 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:06:30 -0700 Subject: [PATCH 21/27] fix(review): validate exact VCS dependency licenses --- .../workflows/opencode-review-dispatch.yml | 12 ++ scripts/ci/validate_vcs_dependency_license.py | 100 ++++++++++++++ tests/test_opencode_agent_contract.py | 12 ++ ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- tests/test_validate_vcs_dependency_license.py | 122 ++++++++++++++++++ 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100755 scripts/ci/validate_vcs_dependency_license.py create mode 100644 tests/test_validate_vcs_dependency_license.py diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 4fe549fb1..6442b1984 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -645,6 +645,7 @@ jobs: coverage_build_dir="${RUNNER_TEMP}/opencode-coverage-tool-build" trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt" trusted_base_python_installer="${GITHUB_WORKSPACE}/scripts/ci/install_base_python_locks.py" + trusted_vcs_license_validator="${GITHUB_WORKSPACE}/scripts/ci/validate_vcs_dependency_license.py" if [ ! -f "$trusted_ci_requirements" ] || [ -L "$trusted_ci_requirements" ]; then echo "::error::Trusted coverage requirements must be a regular non-symlink file." exit 1 @@ -653,6 +654,10 @@ jobs: echo "::error::Trusted base Python lock installer must be a regular non-symlink file." exit 1 fi + if [ ! -f "$trusted_vcs_license_validator" ] || [ -L "$trusted_vcs_license_validator" ]; then + echo "::error::Trusted VCS dependency license validator must be a regular non-symlink file." + exit 1 + fi sudo rm -rf "$coverage_build_dir" mkdir -p "$coverage_build_dir" chmod 0700 "$coverage_build_dir" @@ -660,6 +665,8 @@ jobs: "$coverage_build_dir/requirements-opencode-review-ci-hashes.txt" install -m 0755 "$trusted_base_python_installer" \ "$coverage_build_dir/install-base-python-locks.py" + install -m 0755 "$trusted_vcs_license_validator" \ + "$coverage_build_dir/validate-vcs-dependency-license.py" python3 -I "$GITHUB_WORKSPACE/scripts/ci/materialize_base_python_requirements.py" \ --repo-root "$COVERAGE_SOURCE_WORKDIR" \ --base-sha "$PR_BASE_SHA" \ @@ -756,6 +763,7 @@ jobs: -r /tmp/requirements-opencode-review-ci-hashes.txt \ && rm -f /tmp/requirements-opencode-review-ci-hashes.txt COPY base-python-requirements /tmp/base-python-requirements + COPY validate-vcs-dependency-license.py /usr/local/libexec/validate-vcs-dependency-license.py RUN set -eu; \ mkdir -p /opt/base-vcs-dependencies; \ site_packages="$(python3 -c 'import site; print(site.getsitepackages()[0])')"; \ @@ -766,6 +774,10 @@ jobs: jq -r '.[] | [.import_name, .repository, .commit] | @tsv' \ /tmp/base-python-requirements/vcs-manifest.json >"$dependency_list"; \ while IFS="$(printf '\t')" read -r import_name repository commit; do \ + license_spdx="$(python3 -I /usr/local/libexec/validate-vcs-dependency-license.py \ + --repository "$repository" --commit "$commit")"; \ + printf 'Validated permitted VCS dependency license: repository=%s commit=%s SPDX=%s\n' \ + "$repository" "$commit" "$license_spdx"; \ destination="$(printf '/opt/base-vcs-dependencies/dependency-%03d' "$dependency_index")"; \ git init --quiet "$destination"; \ git -C "$destination" remote add origin \ diff --git a/scripts/ci/validate_vcs_dependency_license.py b/scripts/ci/validate_vcs_dependency_license.py new file mode 100755 index 000000000..3105b0e28 --- /dev/null +++ b/scripts/ci/validate_vcs_dependency_license.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Fail closed unless one exact organization VCS revision has a permitted license.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +import urllib.request +from typing import Any + + +ORGANIZATION = "ContextualWisdomLab" +REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]{1,100}$") +COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") +MAX_METADATA_BYTES = 1024 * 1024 +REQUEST_TIMEOUT_SECONDS = 30 +PERMITTED_SPDX_IDS = frozenset( + { + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MPL-2.0", + "PostgreSQL", + } +) + + +def _license_url(repository: str, commit: str) -> str: + """Return the fixed-origin GitHub license URL for one exact revision.""" + if REPOSITORY_RE.fullmatch(repository) is None: + raise ValueError("VCS dependency repository is malformed") + if COMMIT_RE.fullmatch(commit) is None: + raise ValueError("VCS dependency commit is not an exact lowercase SHA") + return ( + f"https://api.github.com/repos/{ORGANIZATION}/{repository}/" + f"license?ref={commit}" + ) + + +def _default_opener() -> urllib.request.OpenerDirector: + """Build a no-proxy opener for the fixed public GitHub API request.""" + return urllib.request.build_opener(urllib.request.ProxyHandler({})) + + +def validate_license( + repository: str, + commit: str, + *, + opener: Any | None = None, +) -> str: + """Return the permitted SPDX ID or fail closed on any metadata ambiguity.""" + url = _license_url(repository, commit) + request = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "ContextualWisdomLab-opencode-vcs-license-gate", + }, + ) + client = opener if opener is not None else _default_opener() + with client.open(request, timeout=REQUEST_TIMEOUT_SECONDS) as response: + if response.geturl() != url: + raise RuntimeError("VCS dependency license metadata left the fixed GitHub origin") + payload_bytes = response.read(MAX_METADATA_BYTES + 1) + if len(payload_bytes) > MAX_METADATA_BYTES: + raise RuntimeError("VCS dependency license metadata exceeded the size limit") + try: + payload = json.loads(payload_bytes.decode("utf-8", errors="strict")) + spdx_id = payload["license"]["spdx_id"] + except (KeyError, TypeError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("VCS dependency license metadata is malformed") from exc + if not isinstance(spdx_id, str) or spdx_id not in PERMITTED_SPDX_IDS: + raise ValueError( + f"VCS dependency SPDX license {spdx_id!r} is not permitted" + ) + return spdx_id + + +def main(argv: list[str] | None = None) -> int: + """Validate CLI arguments and print only the authoritative SPDX identifier.""" + parser = argparse.ArgumentParser() + parser.add_argument("--repository", required=True) + parser.add_argument("--commit", required=True) + args = parser.parse_args(argv) + try: + spdx_id = validate_license(args.repository, args.commit) + except (OSError, RuntimeError, ValueError) as exc: + print(f"VCS dependency license validation failed: {exc}", file=sys.stderr) + return 1 + print(spdx_id) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c6d772ea5..b935c1688 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -685,7 +685,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "The networked build context contains only this" in measure_step assert 'install -m 0644 "$trusted_ci_requirements"' in measure_step assert 'install -m 0755 "$trusted_base_python_installer"' in measure_step + assert 'install -m 0755 "$trusted_vcs_license_validator"' in measure_step assert "COPY install-base-python-locks.py" in measure_step + assert "COPY validate-vcs-dependency-license.py" in measure_step assert "python3 -I /usr/local/libexec/install-base-python-locks.py" in measure_step assert '"https://github.com/ContextualWisdomLab/${repository}.git"' in measure_step assert '--quiet --no-tags --depth=1 origin "$commit"' in measure_step @@ -694,6 +696,16 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "opencode-base-vcs-dependencies.pth" in measure_step assert 'vcs-manifest.json >"$dependency_list"' in measure_step assert 'done <"$dependency_list"' in measure_step + license_validation = measure_step.index( + 'python3 -I /usr/local/libexec/validate-vcs-dependency-license.py' + ) + vcs_clone = measure_step.index('git init --quiet "$destination"') + vcs_registration = measure_step.index( + 'printf \'%s\\n\' "$python_root" >>"$path_file"' + ) + assert license_validation < vcs_clone < vcs_registration + assert '--repository "$repository" --commit "$commit"' in measure_step + assert "Validated permitted VCS dependency license" in measure_step assert 'candidate_count=$((candidate_count + 1))' in measure_step assert '[ "$candidate_count" -ne 1 ]' in measure_step assert "has a missing or ambiguous import root" in measure_step diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index d08c972a1..f178b6155 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "4fe549fb193f04ce1afb4b9a74ca8d95432cf99d" +REVIEW_DISPATCH_BLOB_SHA = "6442b1984cd255228eec257912aff1c9eae1d132" def _workflow_text(path: Path) -> str: diff --git a/tests/test_validate_vcs_dependency_license.py b/tests/test_validate_vcs_dependency_license.py new file mode 100644 index 000000000..fa1e72bcd --- /dev/null +++ b/tests/test_validate_vcs_dependency_license.py @@ -0,0 +1,122 @@ +"""Contract tests for exact-revision VCS dependency license validation.""" + +from __future__ import annotations + +import importlib.util +import io +import json +from pathlib import Path +from types import ModuleType + +import pytest + + +SCRIPT = Path("scripts/ci/validate_vcs_dependency_license.py") +COMMIT = "61c49c50d3b4a24fc9bd7c6d3a7f2f4ba19d7be6" + + +def load_validator() -> ModuleType: + """Load the production validator only after proving the file exists.""" + assert SCRIPT.is_file(), "the exact-revision VCS license validator is missing" + spec = importlib.util.spec_from_file_location("validate_vcs_dependency_license", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class FakeResponse(io.BytesIO): + """Minimal urllib response fixture with a stable final URL.""" + + def __init__(self, payload: dict[str, object], url: str) -> None: + super().__init__(json.dumps(payload).encode("utf-8")) + self._url = url + + def geturl(self) -> str: + """Return the final response URL exposed by urllib.""" + return self._url + + def __enter__(self) -> "FakeResponse": + """Support the response context-manager protocol.""" + return self + + def __exit__(self, *_args: object) -> None: + """Close the in-memory response.""" + self.close() + + +class FakeOpener: + """Capture one outbound license request and return fixture metadata.""" + + def __init__(self, spdx_id: str | None, *, final_url: str | None = None) -> None: + self.spdx_id = spdx_id + self.final_url = final_url + self.request_url = "" + + def open(self, request: object, timeout: int) -> FakeResponse: + """Return one bounded GitHub license response fixture.""" + del timeout + self.request_url = request.full_url # type: ignore[attr-defined] + payload = {"license": {"spdx_id": self.spdx_id}} + return FakeResponse(payload, self.final_url or self.request_url) + + +@pytest.mark.parametrize( + "spdx_id", + [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MPL-2.0", + "PostgreSQL", + ], +) +def test_permitted_exact_spdx_identifiers_pass(spdx_id: str) -> None: + """Every explicitly governed commercial/permissive SPDX ID passes.""" + validator = load_validator() + opener = FakeOpener(spdx_id) + + assert validator.validate_license("RankWeave", COMMIT, opener=opener) == spdx_id + assert opener.request_url == ( + "https://api.github.com/repos/ContextualWisdomLab/RankWeave/" + f"license?ref={COMMIT}" + ) + + +@pytest.mark.parametrize( + "spdx_id", + ["GPL-3.0-only", "AGPL-3.0-or-later", "LGPL-2.1-only", "NOASSERTION", None], +) +def test_disallowed_or_unknown_spdx_identifiers_fail_closed( + spdx_id: str | None, +) -> None: + """Copyleft, unknown, and absent metadata never enter the trusted image.""" + validator = load_validator() + + with pytest.raises(ValueError, match="not permitted"): + validator.validate_license("RankWeave", COMMIT, opener=FakeOpener(spdx_id)) + + +def test_repository_and_commit_are_bounded_before_network_access() -> None: + """Untrusted path syntax cannot steer the fixed GitHub API origin.""" + validator = load_validator() + opener = FakeOpener("MIT") + + with pytest.raises(ValueError, match="repository"): + validator.validate_license("../outside", COMMIT, opener=opener) + with pytest.raises(ValueError, match="commit"): + validator.validate_license("RankWeave", "main", opener=opener) + + assert opener.request_url == "" + + +def test_redirected_license_metadata_is_rejected() -> None: + """A redirect cannot substitute a different origin for GitHub metadata.""" + validator = load_validator() + opener = FakeOpener("MIT", final_url="https://attacker.invalid/license.json") + + with pytest.raises(RuntimeError, match="origin"): + validator.validate_license("RankWeave", COMMIT, opener=opener) + From 88579aedc030c630f38f90badbc6b88785a9d7c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:08:51 -0700 Subject: [PATCH 22/27] test(review): keep VCS license fixtures whitespace-clean --- tests/test_validate_vcs_dependency_license.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_validate_vcs_dependency_license.py b/tests/test_validate_vcs_dependency_license.py index fa1e72bcd..8fbb7626b 100644 --- a/tests/test_validate_vcs_dependency_license.py +++ b/tests/test_validate_vcs_dependency_license.py @@ -119,4 +119,3 @@ def test_redirected_license_metadata_is_rejected() -> None: with pytest.raises(RuntimeError, match="origin"): validator.validate_license("RankWeave", COMMIT, opener=opener) - From 863c3436fd8b2df041c382de6b85c84d43b17230 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:19:54 -0700 Subject: [PATCH 23/27] test(review): prove VCS license gate coverage --- scripts/ci/validate_vcs_dependency_license.py | 2 +- tests/test_validate_vcs_dependency_license.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/scripts/ci/validate_vcs_dependency_license.py b/scripts/ci/validate_vcs_dependency_license.py index 3105b0e28..dec583e2a 100755 --- a/scripts/ci/validate_vcs_dependency_license.py +++ b/scripts/ci/validate_vcs_dependency_license.py @@ -96,5 +96,5 @@ def main(argv: list[str] | None = None) -> int: return 0 -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover - exercised by the CLI boundary. raise SystemExit(main()) diff --git a/tests/test_validate_vcs_dependency_license.py b/tests/test_validate_vcs_dependency_license.py index 8fbb7626b..17d20a7e3 100644 --- a/tests/test_validate_vcs_dependency_license.py +++ b/tests/test_validate_vcs_dependency_license.py @@ -61,6 +61,23 @@ def open(self, request: object, timeout: int) -> FakeResponse: return FakeResponse(payload, self.final_url or self.request_url) +class RawOpener: + """Return arbitrary response bytes for malformed and oversized fixtures.""" + + def __init__(self, payload: bytes) -> None: + self.payload = payload + + def open(self, request: object, timeout: int) -> FakeResponse: + """Return the raw payload from the otherwise exact request URL.""" + del timeout + response = FakeResponse({}, request.full_url) # type: ignore[attr-defined] + response.seek(0) + response.truncate() + response.write(self.payload) + response.seek(0) + return response + + @pytest.mark.parametrize( "spdx_id", [ @@ -119,3 +136,60 @@ def test_redirected_license_metadata_is_rejected() -> None: with pytest.raises(RuntimeError, match="origin"): validator.validate_license("RankWeave", COMMIT, opener=opener) + + +def test_default_opener_disables_proxy_resolution( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Production requests use the no-proxy opener instead of runner proxy state.""" + validator = load_validator() + opener = FakeOpener("MIT") + captured: list[object] = [] + + def build_opener(*handlers: object) -> FakeOpener: + captured.extend(handlers) + return opener + + monkeypatch.setattr(validator.urllib.request, "build_opener", build_opener) + + assert validator.validate_license("RankWeave", COMMIT) == "MIT" + assert len(captured) == 1 + assert isinstance(captured[0], validator.urllib.request.ProxyHandler) + + +def test_oversized_and_malformed_metadata_fail_closed() -> None: + """The metadata parser rejects both resource abuse and invalid JSON.""" + validator = load_validator() + + with pytest.raises(RuntimeError, match="size limit"): + validator.validate_license( + "RankWeave", + COMMIT, + opener=RawOpener(b"x" * (validator.MAX_METADATA_BYTES + 1)), + ) + with pytest.raises(ValueError, match="malformed"): + validator.validate_license( + "RankWeave", COMMIT, opener=RawOpener(b"not-json") + ) + + +def test_main_reports_success_and_failure( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI prints a permitted ID and converts validation errors to exit 1.""" + validator = load_validator() + arguments = ["--repository", "RankWeave", "--commit", COMMIT] + + monkeypatch.setattr(validator, "validate_license", lambda *_args: "Apache-2.0") + assert validator.main(arguments) == 0 + assert capsys.readouterr().out == "Apache-2.0\n" + + def reject(*_args: object) -> str: + raise ValueError("fixture denial") + + monkeypatch.setattr(validator, "validate_license", reject) + assert validator.main(arguments) == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert "VCS dependency license validation failed: fixture denial" in captured.err From 98af23e3b881ca92c708edfaa70d8ff782e732b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 08:32:39 -0700 Subject: [PATCH 24/27] fix(review): reject VCS license metadata redirects --- scripts/ci/validate_vcs_dependency_license.py | 21 ++++++++++++++++++- tests/test_validate_vcs_dependency_license.py | 19 ++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/scripts/ci/validate_vcs_dependency_license.py b/scripts/ci/validate_vcs_dependency_license.py index dec583e2a..00e37063a 100755 --- a/scripts/ci/validate_vcs_dependency_license.py +++ b/scripts/ci/validate_vcs_dependency_license.py @@ -29,6 +29,22 @@ ) +class RejectRedirectHandler(urllib.request.HTTPRedirectHandler): + """Reject redirects before urllib can contact their destination.""" + + def redirect_request( + self, + req: Any, + fp: Any, + code: int, + msg: str, + headers: Any, + newurl: str, + ) -> None: + del req, fp, code, msg, headers, newurl + raise RuntimeError("VCS dependency license metadata redirect is forbidden") + + def _license_url(repository: str, commit: str) -> str: """Return the fixed-origin GitHub license URL for one exact revision.""" if REPOSITORY_RE.fullmatch(repository) is None: @@ -43,7 +59,10 @@ def _license_url(repository: str, commit: str) -> str: def _default_opener() -> urllib.request.OpenerDirector: """Build a no-proxy opener for the fixed public GitHub API request.""" - return urllib.request.build_opener(urllib.request.ProxyHandler({})) + return urllib.request.build_opener( + urllib.request.ProxyHandler({}), + RejectRedirectHandler(), + ) def validate_license( diff --git a/tests/test_validate_vcs_dependency_license.py b/tests/test_validate_vcs_dependency_license.py index 17d20a7e3..0e9964433 100644 --- a/tests/test_validate_vcs_dependency_license.py +++ b/tests/test_validate_vcs_dependency_license.py @@ -153,8 +153,25 @@ def build_opener(*handlers: object) -> FakeOpener: monkeypatch.setattr(validator.urllib.request, "build_opener", build_opener) assert validator.validate_license("RankWeave", COMMIT) == "MIT" - assert len(captured) == 1 + assert len(captured) == 2 assert isinstance(captured[0], validator.urllib.request.ProxyHandler) + assert isinstance(captured[1], validator.RejectRedirectHandler) + + +def test_default_opener_rejects_redirect_before_following_target() -> None: + """Production metadata requests never contact a redirect destination.""" + validator = load_validator() + handler = validator.RejectRedirectHandler() + + with pytest.raises(RuntimeError, match="redirect"): + handler.redirect_request( + object(), + object(), + 302, + "Found", + {}, + "https://attacker.invalid/license.json", + ) def test_oversized_and_malformed_metadata_fail_closed() -> None: From 0b5dbd17277071b10621a78240b20885d8ac6a92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 11:07:50 -0700 Subject: [PATCH 25/27] fix(review): reject dot-only VCS repository names --- scripts/ci/validate_vcs_dependency_license.py | 2 +- tests/test_validate_vcs_dependency_license.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/ci/validate_vcs_dependency_license.py b/scripts/ci/validate_vcs_dependency_license.py index 00e37063a..69a16285c 100755 --- a/scripts/ci/validate_vcs_dependency_license.py +++ b/scripts/ci/validate_vcs_dependency_license.py @@ -47,7 +47,7 @@ def redirect_request( def _license_url(repository: str, commit: str) -> str: """Return the fixed-origin GitHub license URL for one exact revision.""" - if REPOSITORY_RE.fullmatch(repository) is None: + if REPOSITORY_RE.fullmatch(repository) is None or repository in {".", ".."}: raise ValueError("VCS dependency repository is malformed") if COMMIT_RE.fullmatch(commit) is None: raise ValueError("VCS dependency commit is not an exact lowercase SHA") diff --git a/tests/test_validate_vcs_dependency_license.py b/tests/test_validate_vcs_dependency_license.py index 0e9964433..44185a275 100644 --- a/tests/test_validate_vcs_dependency_license.py +++ b/tests/test_validate_vcs_dependency_license.py @@ -121,8 +121,9 @@ def test_repository_and_commit_are_bounded_before_network_access() -> None: validator = load_validator() opener = FakeOpener("MIT") - with pytest.raises(ValueError, match="repository"): - validator.validate_license("../outside", COMMIT, opener=opener) + for repository in ("../outside", ".", ".."): + with pytest.raises(ValueError, match="repository"): + validator.validate_license(repository, COMMIT, opener=opener) with pytest.raises(ValueError, match="commit"): validator.validate_license("RankWeave", "main", opener=opener) From b69d9c88a12ffcb841fbfa118ed820cad7df2e21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 13:35:54 -0700 Subject: [PATCH 26/27] fix(ci): normalize merged scheduler files at EOF --- .../workflows/contextual-orchestrator-hourly-review-repair.yml | 1 - .github/workflows/hourly-nvidia-nim-review-repair.yml | 1 - docs/doctoring/contextual-orchestrator-hourly-review-caller.md | 1 - tests/test_contextual_orchestrator_hourly_review_caller.py | 1 - 4 files changed, 4 deletions(-) diff --git a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml index 09a143689..a7aba287b 100644 --- a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml +++ b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml @@ -34,4 +34,3 @@ jobs: secrets: PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} - diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index d73c27a7a..add6d70c2 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -190,4 +190,3 @@ jobs: tests/test_pr_review_autofix_nvidia_nim_contract.py \ tests/test_pr_review_autofix_writer_security_contract.py git diff --check - diff --git a/docs/doctoring/contextual-orchestrator-hourly-review-caller.md b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md index 4a417a26d..29a017411 100644 --- a/docs/doctoring/contextual-orchestrator-hourly-review-caller.md +++ b/docs/doctoring/contextual-orchestrator-hourly-review-caller.md @@ -123,4 +123,3 @@ https://docs.nvidia.com/nim/large-language-models/latest/ OpenCode. (n.d.). *OpenCode documentation*. Retrieved August 24, 2026, from https://opencode.ai/docs/ - diff --git a/tests/test_contextual_orchestrator_hourly_review_caller.py b/tests/test_contextual_orchestrator_hourly_review_caller.py index 163093167..204ed5288 100644 --- a/tests/test_contextual_orchestrator_hourly_review_caller.py +++ b/tests/test_contextual_orchestrator_hourly_review_caller.py @@ -86,4 +86,3 @@ def test_focused_quality_workflow_tracks_contextual_orchestrator_contracts() -> assert quality.count( "tests/test_contextual_orchestrator_hourly_review_caller.py" ) == 3 - From 27a8bd5f8bd60c9f3f70ec43ce2f2f62f7dc71ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 17:59:18 -0700 Subject: [PATCH 27/27] fix(codeql): resolve fork heads through exact PR ref --- .github/workflows/codeql-pr.yml | 36 ++++++++++++++++++++++- tests/test_codeql_pr_workflow_contract.py | 14 +++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index edab060b2..7281fb618 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -208,13 +208,47 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail export GIT_CONFIG_COUNT=1 export GIT_CONFIG_KEY_0=http.extraheader export GIT_CONFIG_VALUE_0="AUTHORIZATION: bearer $GITHUB_TOKEN" - git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA" + git fetch --no-tags origin "$BASE_SHA" "$MERGE_SHA" + head_fetch_succeeded=0 + if git cat-file -e "$HEAD_SHA^{commit}"; then + head_fetch_succeeded=1 + elif git fetch --no-tags origin "$HEAD_SHA" && + git cat-file -e "$HEAD_SHA^{commit}"; then + head_fetch_succeeded=1 + else + echo "Direct CodeQL head-SHA fetch was unavailable; resolving the exact head through the target pull request ref." + fi + if [ "$head_fetch_succeeded" -ne 1 ]; then + for pr_head_fetch_attempt in 1 2 3 4 5 6; do + if git fetch --no-tags --prune origin \ + "+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pr-${PR_NUMBER}-head"; then + fetched_head_sha="$(git rev-parse "refs/remotes/origin/pr-${PR_NUMBER}-head")" + if [ "$fetched_head_sha" = "$HEAD_SHA" ]; then + head_fetch_succeeded=1 + break + fi + if [ "$pr_head_fetch_attempt" -lt 6 ]; then + echo "Fetched PR head $fetched_head_sha, expected $HEAD_SHA; retrying after propagation delay." + sleep 10 + fi + elif [ "$pr_head_fetch_attempt" -lt 6 ]; then + echo "PR head ref fetch failed on attempt $pr_head_fetch_attempt; retrying after propagation delay." + sleep 10 + fi + done + fi + if [ "$head_fetch_succeeded" -ne 1 ] || + ! git cat-file -e "$HEAD_SHA^{commit}"; then + echo "::error::CodeQL merge preview could not resolve exact head SHA $HEAD_SHA from origin or refs/pull/$PR_NUMBER/head." + exit 1 + fi unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 provided_tree="$(git rev-parse "$MERGE_SHA^{tree}")" read -r actual_merge parent_one parent_two extra < None: assert 'git rev-parse "$MERGE_SHA^{tree}"' in workflow assert 'GITHUB_TOKEN: ${{ github.token }}' in workflow assert 'GIT_CONFIG_VALUE_0="AUTHORIZATION: bearer $GITHUB_TOKEN"' in workflow + assert 'git fetch --no-tags origin "$BASE_SHA" "$MERGE_SHA"' in workflow + assert 'git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA" "$MERGE_SHA"' not in workflow + assert 'git cat-file -e "$HEAD_SHA^{commit}"' in workflow + assert 'fetch --no-tags origin "$HEAD_SHA"' in workflow + assert ( + '"+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/' + 'pr-${PR_NUMBER}-head"' + ) in workflow + assert ( + 'fetched_head_sha="$(git rev-parse ' + '"refs/remotes/origin/pr-${PR_NUMBER}-head")"' + ) in workflow + assert '[ "$fetched_head_sha" = "$HEAD_SHA" ]' in workflow + assert "CodeQL merge preview could not resolve exact head SHA" in workflow assert 'git commit-tree "$expected_tree" -p "$BASE_SHA" -p "$HEAD_SHA"' in workflow assert 'git reset --hard "$local_merge_sha"' in workflow assert 'echo "merge_sha=$local_merge_sha"' in workflow