diff --git a/.github/actions/orchestrator-free-sidecar/action.yml b/.github/actions/orchestrator-free-sidecar/action.yml index edddfe1bc3..c6a6cc3919 100644 --- a/.github/actions/orchestrator-free-sidecar/action.yml +++ b/.github/actions/orchestrator-free-sidecar/action.yml @@ -23,9 +23,16 @@ runs: ref: ${{ github.action_ref }} path: ${{ runner.temp }}/cwl-control-plane persist-credentials: false + - name: Set up lock-compatible sidecar Python + id: sidecar_python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + update-environment: false - name: Provision contextual-orchestrator orchestrator/free shell: bash --noprofile --norc -e -o pipefail {0} env: + SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ inputs.require_zdr }} ORCHESTRATOR_CATALOG_LIMIT: ${{ inputs.catalog_limit }} ORCHESTRATOR_CATALOG_ACCOUNT_CAP: ${{ inputs.catalog_account_cap }} diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 44db0c662a..95f1ecc288 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -6,8 +6,8 @@ # runner, then one coordinator POSTs repository_dispatch to # codeql-scan-dispatch.yml (native, unrestricted, in # ContextualWisdomLab/.github) with the remaining language matrix. The -# handler publishes codeql-dispatch/ and reruns only that exact -# failed job. On rerun the shard reads the terminal status once. Design: +# handler publishes a base/run/source-bound codeql-dispatch receipt and reruns +# only that exact failed job. On rerun the shard reads the terminal status once. Design: # docs/adr/0025-codeql-required-workflow-dispatch-architecture.md. The # merge-preview scan (analyze-merge) is required nowhere (PR #1766) and was # dropped, not migrated. @@ -153,6 +153,7 @@ jobs: # closed PRs need no required check. runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} permissions: + actions: read contents: read id-token: write pull-requests: read @@ -163,7 +164,7 @@ jobs: steps: - name: Read current-head CodeQL dispatch verdict # Shards never dispatch. They re-check the live head, consume an - # authenticated codeql-dispatch/ verdict when one exists, + # authenticated base/run/source-bound CodeQL verdict when one exists, # and otherwise fail pending so the runner is released. One # coordinator job POSTs the remaining language matrix after every # shard has a job id. @@ -182,6 +183,7 @@ jobs: live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')" + live_merge="$(printf '%s' "$live_pr" | jq -r '.merge_commit_sha // empty')" live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ && "$live_head" =~ ^[0-9a-fA-F]{40}$ ]] || [[ "$live_state" != "open" && "$live_state" != "closed" ]]; then echo "::error::Could not validate live pull request state before CodeQL dispatch." @@ -206,8 +208,9 @@ jobs: echo "verdict=obsolete" >>"$GITHUB_OUTPUT" exit 0 fi - if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "::error::Could not validate live pull request base SHA before CodeQL verdict read." + if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] || + ! [[ "$live_merge" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "::error::Could not validate live pull request base/source SHA before CodeQL verdict read." exit 1 fi if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then @@ -216,13 +219,17 @@ jobs: fi statuses="$(gh api "repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses")" - verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${LANGUAGE}" ' + expected_context="codeql-dispatch/${LANGUAGE}/${live_base}" + expected_description="cwl1;h=${PR_HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${live_merge}" + verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "$expected_context" --arg description "$expected_description" ' [ .[] | select(.context == $ctx) + | select(.description == $description) | select( (.creator.login // "" | ascii_downcase) as $creator | $creator == "opencode-agent" or $creator == "opencode-agent[bot]" + or $creator == "cwl-noema-review" or $creator == "cwl-noema-review[bot]" ) ] | first // {} | .state // empty @@ -235,9 +242,15 @@ jobs: ;; esac - expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}" + expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}/${live_merge}" expected_job="CodeQL dispatch scan (${LANGUAGE})" - runs_json="$(gh api --paginate --slurp "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs")" + # A dispatch bound to this required run cannot predate its creation. + required_created_at="$(gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}" --jq .created_at)" + if ! [[ "$required_created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + echo "::error::Could not validate required run creation time before CodeQL verdict lookup." + exit 1 + fi + runs_json="$(gh api --method GET --paginate --slurp -f per_page=100 -f event=repository_dispatch -f created=">=${required_created_at}" "repos/ContextualWisdomLab/.github/actions/workflows/codeql-scan-dispatch.yml/runs")" run_id="$(printf '%s' "$runs_json" | jq -r --arg title "$expected_title" --arg path ".github/workflows/codeql-scan-dispatch.yml" ' [ .[] | .workflow_runs[] @@ -259,11 +272,20 @@ jobs: gate_conclusion="$(printf '%s' "$dispatch_job" | jq -r ' (.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate") | .conclusion) // empty ')" + ghas_identity_conclusion="$(printf '%s' "$dispatch_job" | jq -r ' + (.steps[]? | select(.name == "Verify GHAS base/head CodeQL configuration identity") | .conclusion) // empty + ')" + sarif_upload_conclusion="$(printf '%s' "$dispatch_job" | jq -r ' + (.steps[]? | select(.name == "Preserve CodeQL SARIF evidence") | .conclusion) // empty + ')" case "$gate_conclusion" in success) - echo "verdict=success" >>"$GITHUB_OUTPUT" - echo "Found completed CodeQL dispatch scan gate for ${LANGUAGE}: success." - exit 0 + if [ "$ghas_identity_conclusion" = "success" ] && + [ "$sarif_upload_conclusion" = "success" ]; then + echo "verdict=success" >>"$GITHUB_OUTPUT" + echo "Found completed CodeQL dispatch proof for ${LANGUAGE}: gate, GHAS identity, and SARIF evidence succeeded." + exit 0 + fi ;; failure|cancelled|skipped) echo "verdict=failure" >>"$GITHUB_OUTPUT" @@ -283,7 +305,7 @@ jobs: fi if [ "$RUN_ATTEMPT" != "1" ]; then - echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict." + echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict; GHAS identity and preserved SARIF are required for authenticated terminal proof." exit 1 fi echo "verdict=pending" >>"$GITHUB_OUTPUT" @@ -356,6 +378,7 @@ jobs: live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')" live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')" + live_merge="$(printf '%s' "$live_pr" | jq -r '.merge_commit_sha // empty')" live_base_ref="$(printf '%s' "$live_pr" | jq -r '.base.ref // empty')" live_head_ref="$(printf '%s' "$live_pr" | jq -r '.head.ref // empty')" live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')" @@ -375,8 +398,10 @@ jobs: echo "::error::CodeQL dispatch requires a canonical current run id." exit 1 fi - if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then - echo "::error::Could not validate live pull request base identity before CodeQL dispatch." + if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] || + ! [[ "$live_merge" =~ ^[0-9a-fA-F]{40}$ ]] || + [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then + echo "::error::Could not validate live pull request base/source identity before CodeQL dispatch." exit 1 fi @@ -413,13 +438,17 @@ jobs: pending_matrix='[]' while IFS= read -r entry; do language="$(printf '%s' "$entry" | jq -r '.language // empty')" - verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${language}" ' + expected_context="codeql-dispatch/${language}/${live_base}" + expected_description="cwl1;h=${PR_HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${live_merge}" + verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "$expected_context" --arg description "$expected_description" ' [ .[] | select(.context == $ctx) + | select(.description == $description) | select( (.creator.login // "" | ascii_downcase) as $creator | $creator == "opencode-agent" or $creator == "opencode-agent[bot]" + or $creator == "cwl-noema-review" or $creator == "cwl-noema-review[bot]" ) ] | first // {} | .state // empty @@ -477,5 +506,6 @@ jobs: --argjson matrix "$pending_matrix" \ --arg required_run_id "$REQUIRED_RUN_ID" \ --argjson required_jobs "$required_jobs" \ - '{event_type:"codeql-scan",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head_ref:$pr_head_ref,pr_head_sha:$pr_head_sha,matrix:$matrix,required_run_id:$required_run_id,required_jobs:$required_jobs}}' | + --arg producer_source_sha "$live_merge" \ + '{event_type:"codeql-scan-v2",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head:{schema:"1",ref:$pr_head_ref,sha:$pr_head_sha},producer_source_sha:$producer_source_sha,matrix:$matrix,required_run_id:$required_run_id,required_jobs:$required_jobs}}' | GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input - diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 7f6e717db0..9c02817198 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -589,7 +589,7 @@ jobs: - name: Detect optional Noema analysis-read credential id: noema_analysis_config - if: steps.gate.outcome == 'success' + if: always() && steps.live_metadata.outcome == 'success' env: TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} NOEMA_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID || '' }} @@ -701,10 +701,23 @@ jobs: if-no-files-found: error retention-days: 7 + - name: Mint target-scoped Noema CodeQL status token + id: noema_status_token + if: always() && steps.noema_analysis_config.outputs.available == 'true' + continue-on-error: true + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} + owner: ContextualWisdomLab + repositories: ${{ steps.noema_analysis_config.outputs.repository }} + permission-statuses: write + - name: Publish CodeQL dispatch status id: publish_status if: always() && steps.live_metadata.outcome == 'success' env: + NOEMA_STATUS_TOKEN: ${{ steps.noema_status_token.outputs.token || '' }} TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} GITHUB_STATUS_READ_TOKEN: ${{ github.token }} PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} @@ -783,6 +796,11 @@ jobs: actual_creator="$(jq -r '.creator.login // "" | ascii_downcase' "$status_response" 2>/dev/null || true)" creator_trusted=false case "$token_label" in + noema-status-token) + case "$actual_creator" in + cwl-noema-review|cwl-noema-review\[bot\]) creator_trusted=true ;; + esac + ;; target-app-token|pr-review-merge-token|opencode-approve-token) case "$actual_creator" in opencode-agent|opencode-agent\[bot\]) creator_trusted=true ;; @@ -815,6 +833,9 @@ jobs: return 1 } + if post_status "noema-status-token" "${NOEMA_STATUS_TOKEN:-}"; then + exit 0 + fi if post_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then exit 0 fi @@ -920,8 +941,34 @@ jobs: echo "token=$app_token" } >>"$GITHUB_OUTPUT" + - name: Resolve Noema settlement token configuration + id: noema_settlement_config + env: + NOEMA_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID || '' }} + NOEMA_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY || '' }} + TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} + run: | + set -euo pipefail + if [ -n "$NOEMA_APP_CLIENT_ID" ] && [ -n "$NOEMA_APP_PRIVATE_KEY" ]; then + printf 'repository=%s\n' "${TARGET_REPOSITORY#*/}" >>"$GITHUB_OUTPUT" + echo "available=true" >>"$GITHUB_OUTPUT" + fi + + - name: Mint target-scoped Noema CodeQL settlement token + id: noema_settlement_token + if: steps.noema_settlement_config.outputs.available == 'true' + continue-on-error: true + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} + owner: ContextualWisdomLab + repositories: ${{ steps.noema_settlement_config.outputs.repository }} + permission-actions: write + - name: Settle exact CodeQL required run env: + NOEMA_WAKE_TOKEN: ${{ steps.noema_settlement_token.outputs.token || '' }} TARGET_APP_WAKE_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} PR_REVIEW_MERGE_WAKE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} OPENCODE_APPROVE_WAKE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} @@ -960,7 +1007,8 @@ jobs: } github_api() { - run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" || + run_api "noema-settlement-token" "${NOEMA_WAKE_TOKEN:-}" "$@" || + run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" || run_api "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" "$@" || run_api "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" "$@" || run_api "github-token" "$GITHUB_WAKE_TOKEN" "$@" @@ -1074,6 +1122,26 @@ jobs: echo "::error::CodeQL settlement rejected incomplete handler gate or SARIF evidence for ${language}." exit 1 fi + clean_gate_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" ' + [.[] | select( + .name == $name + and .status == "completed" + and .run_attempt == $attempt + and ([.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate" and .conclusion == "success")] | length) == 1 + )] | length + ')" + ghas_identity_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" ' + [.[] | select( + .name == $name + and .status == "completed" + and .run_attempt == $attempt + and ([.steps[]? | select(.name == "Verify GHAS base/head CodeQL configuration identity" and .conclusion == "success")] | length) == 1 + )] | length + ')" + if [ "$clean_gate_count" -eq 1 ] && [ "$ghas_identity_count" -ne 1 ]; then + echo "::error::CodeQL settlement rejected missing GHAS configuration identity proof for ${language}." + exit 1 + fi done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]') case "$RERUN_MODE" in @@ -1099,7 +1167,8 @@ jobs: return 1 } - if post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" || + if post_wake "noema-settlement-token" "${NOEMA_WAKE_TOKEN:-}" || + post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" || post_wake "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" || post_wake "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" || post_wake "github-token" "$GITHUB_WAKE_TOKEN"; then diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 58a00b547c..80d3d96cb3 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -41,7 +41,7 @@ jobs: && github.event.action != 'converted_to_draft' && github.event.pull_request.head.repo.full_name == github.repository ) - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator') && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 5 outputs: admitted: ${{ steps.live_head.outputs.admitted }} @@ -99,7 +99,7 @@ jobs: # PR/REPO lookup naturally falls through to "scan everything" for that # path, matching strix.yml's identical repository_dispatch behavior. if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator') && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 5 permissions: contents: read @@ -155,7 +155,7 @@ jobs: if: >- github.event_name == 'pull_request_target' && (github.event.action == 'closed' || github.event.action == 'converted_to_draft') - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator') && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} # Bound this job well short of GitHub's 360-minute platform default. Its # only step is a single-repository, status-filtered gh api --paginate # list-and-cancel sweep (up to 3 passes x 5 statuses), no branch update @@ -333,7 +333,7 @@ jobs: noema-review: name: noema-review needs: [admit-current-head, changed-scope] - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator') && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} # No job-level timeout-minutes here, deliberately. This job's "Prepare # Noema model verdict" step calls two_phase.py's call_llm synchronously # via the contextual-orchestrator gateway and blocks on the model's own @@ -368,12 +368,12 @@ jobs: id-token: write pull-requests: read outputs: - transport_capacity_unavailable: ${{ steps.noema_prepare.outputs.transport_capacity_unavailable }} - transport_retry_eligible: ${{ steps.noema_prepare.outputs.transport_retry_eligible }} - transport_retry_delay_seconds: ${{ steps.noema_prepare.outputs.transport_retry_delay_seconds }} - transport_retry_next_attempt: ${{ steps.noema_prepare.outputs.transport_retry_next_attempt }} - provider_attempt_count: ${{ steps.noema_prepare.outputs.provider_attempt_count }} - transport_http_status: ${{ steps.noema_prepare.outputs.transport_http_status }} + transport_capacity_unavailable: ${{ steps.noema_prepare.outputs.transport_capacity_unavailable || steps.noema_sidecar_failure.outputs.transport_capacity_unavailable }} + transport_retry_eligible: ${{ steps.noema_prepare.outputs.transport_retry_eligible || steps.noema_sidecar_failure.outputs.transport_retry_eligible }} + transport_retry_delay_seconds: ${{ steps.noema_prepare.outputs.transport_retry_delay_seconds || steps.noema_sidecar_failure.outputs.transport_retry_delay_seconds }} + transport_retry_next_attempt: ${{ steps.noema_prepare.outputs.transport_retry_next_attempt || steps.noema_sidecar_failure.outputs.transport_retry_next_attempt }} + provider_attempt_count: ${{ steps.noema_prepare.outputs.provider_attempt_count || steps.noema_sidecar_failure.outputs.provider_attempt_count }} + transport_http_status: ${{ steps.noema_prepare.outputs.transport_http_status || steps.noema_sidecar_failure.outputs.transport_http_status }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }} @@ -734,9 +734,19 @@ jobs: with: node-version: "22.23.3" + - name: Set up lock-compatible sidecar Python + if: env.PR_NUMBER != '' + id: sidecar_python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + update-environment: false + - name: Provision contextual-orchestrator review sidecar + id: noema_sidecar if: env.PR_NUMBER != '' env: + SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} @@ -745,8 +755,20 @@ jobs: CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.require_zdr }} run: | set -euo pipefail + test ! -L "$GITHUB_WORKSPACE/strix_runs" + rm -f "$GITHUB_WORKSPACE/strix_runs/contextual-orchestrator-preflight.json" bash "$GITHUB_WORKSPACE/scripts/ci/contextual_orchestrator_review_sidecar.sh" + - name: Classify sidecar provider-capacity failure + id: noema_sidecar_failure + if: failure() && steps.noema_sidecar.outcome == 'failure' + env: + NOEMA_TRANSPORT_RETRY_ATTEMPT: ${{ toJSON(github.event.client_payload.transport_retry_attempt) }} + run: | + python3 "$GITHUB_WORKSPACE/scripts/ci/noema_preflight_capacity.py" \ + --expected-head "$EXPECTED_HEAD_SHA" \ + --preflight-report "$GITHUB_WORKSPACE/strix_runs/contextual-orchestrator-preflight.json" + - name: Provision local reviewed HWP document reader if: env.PR_NUMBER != '' env: @@ -871,13 +893,15 @@ jobs: && needs.noema-review.result == 'failure' && needs.noema-review.outputs.transport_capacity_unavailable == 'true' && needs.noema-review.outputs.transport_retry_eligible == 'true' - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator') && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 10 permissions: contents: write pull-requests: read env: - GH_TOKEN: ${{ github.token }} + # Consumer required workflows need the existing central dispatch credential. + # The central handler can use its repository-scoped token as fallback. + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || github.token }} TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.client_payload.pr_number || '' }} EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }} @@ -890,12 +914,13 @@ jobs: - name: Schedule bounded Noema transport re-dispatch run: | set -euo pipefail - if [ "$TARGET_REPOSITORY" != "$GITHUB_REPOSITORY" ] || + if { [ "$GITHUB_REPOSITORY" != "ContextualWisdomLab/.github" ] && + [ "$TARGET_REPOSITORY" != "$GITHUB_REPOSITORY" ]; } || ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] || ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] || ! [[ "$EXPECTED_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then - echo "::error::Noema transport re-dispatch rejected a target outside its repository or malformed PR identity." + echo "::error::Noema transport re-dispatch rejected an unrelated origin or malformed PR identity." exit 1 fi if ! [[ "$DELAY_SECONDS" =~ ^[1-9][0-9]*$ ]] || [ "$DELAY_SECONDS" -gt 300 ] || @@ -907,10 +932,12 @@ jobs: sleep "$DELAY_SECONDS" live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")" live_head="$(jq -r '.head.sha // empty' <<<"$live_pr")" + live_head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$live_pr")" live_base="$(jq -r '.base.sha // empty' <<<"$live_pr")" live_base_repo="$(jq -r '.base.repo.full_name // empty' <<<"$live_pr")" live_state="$(jq -r '.state // empty' <<<"$live_pr")" if [ "$live_head" != "$EXPECTED_HEAD_SHA" ] || + [ "$live_head_repo" != "$TARGET_REPOSITORY" ] || [ "$live_base" != "$EXPECTED_BASE_SHA" ] || [ "$live_base_repo" != "$TARGET_REPOSITORY" ] || [ "$live_state" != "open" ]; then @@ -930,5 +957,5 @@ jobs: pr_head_sha: $pr_head_sha, transport_retry_attempt: $transport_retry_attempt } - }' | gh api -X POST "repos/${TARGET_REPOSITORY}/dispatches" --input - + }' | gh api -X POST "repos/ContextualWisdomLab/.github/dispatches" --input - echo "::notice::Scheduled Noema transport continuation re-dispatch for ${TARGET_REPOSITORY}#${PR_NUMBER} at ${EXPECTED_HEAD_SHA} (attempt ${NEXT_ATTEMPT})." diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index e691911e6f..135fca5f84 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -2479,8 +2479,16 @@ jobs: printf 'Validated exact-head OpenCode review source for %s#%s (%s).\n' \ "$GH_REPOSITORY" "$PR_NUMBER" "$head_repository" + - name: Set up lock-compatible sidecar Python + id: sidecar_python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + update-environment: false + - name: Provision contextual-orchestrator review sidecar env: + SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index c087e98ca3..a0cf3642d4 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -263,8 +263,16 @@ jobs: python3 "$GITHUB_WORKSPACE/trusted-autofix-source/scripts/ci/pr_review_autofix_context.py" \ "${context_args[@]}" + - name: Set up lock-compatible sidecar Python + id: sidecar_python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + update-environment: false + - name: Provision contextual-orchestrator review sidecar env: + SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} diff --git a/.github/workflows/release-dependency-license-strix-gate.yml b/.github/workflows/release-dependency-license-strix-gate.yml index 6996d239f3..ad9f25d255 100644 --- a/.github/workflows/release-dependency-license-strix-gate.yml +++ b/.github/workflows/release-dependency-license-strix-gate.yml @@ -182,7 +182,7 @@ jobs: with: repository: ContextualWisdomLab/.github # Reviewed helper revision; intentionally distinct from workflow revision. - ref: d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3 + ref: 64bb4e7d32667980223c70afda81ffac97209630 path: trusted-gate persist-credentials: false # The whole scripts/ci tree, not an enumerated file list: the trusted @@ -202,14 +202,14 @@ jobs: CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | - expected=d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3 + expected=64bb4e7d32667980223c70afda81ffac97209630 test "$(git -C "$HELPER_ROOT" rev-parse HEAD)" = "$expected" origin="$(git -C "$HELPER_ROOT" remote get-url origin)" case "$origin" in https://github.com/ContextualWisdomLab/.github|https://github.com/ContextualWisdomLab/.github.git) ;; *) echo "Foreign helper repository" >&2; exit 1 ;; esac - test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = 0c0ca6079716c1c69ef8ba02605ac9b34a8dca25 + test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = b15b746d2a1c38501c2ecd6f6c5962c27c9d11af test "$(git -C "$HELPER_ROOT" rev-parse HEAD:requirements-strix-ci-hashes.txt)" = eb83beda177c9d2e4ca9b7e2888a1ccb55a123ac git -C "$HELPER_ROOT" diff --exit-code HEAD -- scripts/ci requirements-strix-ci-hashes.txt test -f "$HELPER_ROOT/scripts/ci/release_dependency_gate.py" @@ -474,7 +474,7 @@ jobs: with: repository: ContextualWisdomLab/.github # Reviewed helper revision; intentionally distinct from workflow revision. - ref: d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3 + ref: 64bb4e7d32667980223c70afda81ffac97209630 path: trusted-gate persist-credentials: false # The whole scripts/ci tree, not an enumerated file list: the trusted @@ -494,14 +494,14 @@ jobs: CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | - expected=d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3 + expected=64bb4e7d32667980223c70afda81ffac97209630 test "$(git -C "$HELPER_ROOT" rev-parse HEAD)" = "$expected" origin="$(git -C "$HELPER_ROOT" remote get-url origin)" case "$origin" in https://github.com/ContextualWisdomLab/.github|https://github.com/ContextualWisdomLab/.github.git) ;; *) echo "Foreign helper repository" >&2; exit 1 ;; esac - test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = 0c0ca6079716c1c69ef8ba02605ac9b34a8dca25 + test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = b15b746d2a1c38501c2ecd6f6c5962c27c9d11af test "$(git -C "$HELPER_ROOT" rev-parse HEAD:requirements-strix-ci-hashes.txt)" = eb83beda177c9d2e4ca9b7e2888a1ccb55a123ac git -C "$HELPER_ROOT" diff --exit-code HEAD -- scripts/ci requirements-strix-ci-hashes.txt test -f "$HELPER_ROOT/scripts/ci/release_dependency_gate.py" @@ -762,7 +762,7 @@ jobs: with: repository: ContextualWisdomLab/.github # Reviewed helper revision; intentionally distinct from workflow revision. - ref: d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3 + ref: 64bb4e7d32667980223c70afda81ffac97209630 path: trusted-gate persist-credentials: false # The whole scripts/ci tree, not an enumerated file list: the trusted @@ -782,14 +782,14 @@ jobs: CALLER_WORKFLOW_SHA: ${{ github.workflow_sha }} shell: bash --noprofile --norc -e -o pipefail {0} run: | - expected=d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3 + expected=64bb4e7d32667980223c70afda81ffac97209630 test "$(git -C "$HELPER_ROOT" rev-parse HEAD)" = "$expected" origin="$(git -C "$HELPER_ROOT" remote get-url origin)" case "$origin" in https://github.com/ContextualWisdomLab/.github|https://github.com/ContextualWisdomLab/.github.git) ;; *) echo "Foreign helper repository" >&2; exit 1 ;; esac - test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = 0c0ca6079716c1c69ef8ba02605ac9b34a8dca25 + test "$(git -C "$HELPER_ROOT" rev-parse HEAD:scripts/ci)" = b15b746d2a1c38501c2ecd6f6c5962c27c9d11af test "$(git -C "$HELPER_ROOT" rev-parse HEAD:requirements-strix-ci-hashes.txt)" = eb83beda177c9d2e4ca9b7e2888a1ccb55a123ac git -C "$HELPER_ROOT" diff --exit-code HEAD -- scripts/ci requirements-strix-ci-hashes.txt test -f "$HELPER_ROOT/scripts/ci/release_dependency_gate.py" diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index ef0966cf4d..817bef4d26 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -130,7 +130,7 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 5 permissions: contents: read @@ -187,7 +187,7 @@ jobs: if: >- github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} timeout-minutes: 5 permissions: contents: read @@ -260,7 +260,7 @@ jobs: github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} # Bound this gh-api-only cleanup job so a stuck call (rate limit, hung # `gh api --paginate`) cannot silently occupy a runner for GitHub's # 360-minute platform default -- exactly the window when a busy PR is @@ -770,9 +770,18 @@ jobs: echo 'provider_mode=contextual_orchestrator' } >> "$GITHUB_OUTPUT" + - name: Set up lock-compatible sidecar Python + if: steps.gate.outputs.enabled == 'true' + id: sidecar_python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + update-environment: false + - name: Provision contextual-orchestrator Strix sidecar if: steps.gate.outputs.enabled == 'true' env: + SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }} @@ -1172,7 +1181,7 @@ jobs: name: publish-manual-pr-evidence-status needs: strix if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }} - runs-on: ubuntu-24.04 + runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} # Single-shot OIDC exchange plus a handful of curl/gh api calls, no loop # or pagination -- same shape as the agent-mention-*-dispatch.yml # validate-and-forward jobs, which bound at timeout-minutes: 5. Without diff --git a/CHANGELOG.d/20260927-codeql-terminal-proof.md b/CHANGELOG.d/20260927-codeql-terminal-proof.md new file mode 100644 index 0000000000..ea4154fcec --- /dev/null +++ b/CHANGELOG.d/20260927-codeql-terminal-proof.md @@ -0,0 +1,9 @@ +## Fixed + +- Require a successful GHAS base/head configuration-identity proof and preserved + SARIF before a clean central CodeQL gate may settle or satisfy an exact required + run. A failed post-gate identity check can no longer be promoted to GREEN by a + wake-only fallback. +- Bind CodeQL terminal receipts to the live base, required run, head, and merge + source through the v2 dispatch protocol, preventing a trusted but stale commit + status from satisfying a retargeted or later required run. diff --git a/CHANGELOG.md b/CHANGELOG.md index b4aba1b7f8..d90fa0c899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ - `release_dependency_gate.py` now has behavior-level coverage for bounded archive reads, unsafe or absent declared licence files, symlink/special members, archive-member limits, raw-capture and destination symlinks, Cargo workspace identity, Strix fanout identity/fixture/runtime-report validation, and install-time licence rebinding. The no-caller `parse_member_listing` helper and its isolated test were removed; immutable archive bytes remain the sole member authority. Focused evidence is 442 passed with 1,126/1,126 statements and 472/472 branches; the warnings-as-errors repository suite is 3,976 passed and 28 skipped. Repository-wide coverage rises from 98% to 99%, so the overall 100% release gate remains RED and the PR stays Draft. +### Pingora declared binary artifacts reject readable runtime directives + +- A file under a base-owned declared research/data prefix no longer gains binary admission merely by adding an invalid UTF-8 byte to readable Nginx runtime content. For suffixes without recognized format magic, the bounded replacement-decoded bytes must also contain no prohibited runtime pattern; `.github#2386` covers `.sh`, `.dat`, and `.txt` names through the production evaluation boundary. ### Queue-health permission contract rejects aggregate token grants - The queue-health workflow contract now pins both workflow-level and collector-job permissions to exactly `contents: read` plus `actions: read`, rejecting scalar `read-all`/`write-all`, quoting/spacing variants, inline maps, and unexpected write scopes. diff --git a/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md b/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md new file mode 100644 index 0000000000..50c6392edc --- /dev/null +++ b/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md @@ -0,0 +1,50 @@ +--- +title: "ADR-0032: Owned CodeQL status and settlement authority" +status: Proposed +date: "2026-09-27" +authors: "Codex" +tags: [architecture, ci, security] +supersedes: "" +superseded_by: "" +--- + +# ADR-0032: Owned CodeQL status and settlement authority + +## Status + +Proposed. Requires #2405 complete terminal-proof foundation, owned-app installation permission acceptance, and an unchanged-head live canary before protected deployment is accepted. + +## Context + +DiskSage #473 dispatch 36305375849 encountered cross-repository HTTP403 during status publication and required-run settlement. Public app and organization installation metadata confirm opencode-agent is owned by anomalyco and has Actions/read and statuses/read. A consumer cannot change the external owner's app permissions. The organization-owned cwl-noema-review (app4291520) is already installed on all repositories with security_events/read; its private-key organization secret is available to central workflows. The existing target-scoped analysis-read token remains the GHAS reader. + +## Decision + +Use the existing owned Noema app for separate target-repository tokens: statuses/write solely for authenticated CodeQL receipt publication, and Actions/write solely for exact required-run settlement. Keep security_events/read in its existing separate read token. The installation must authorize those two write permissions; credentials cannot mint permissions the installation lacks. Optional mint failures retain existing fallback credentials and never create validation success. + +The owned status writer must publish as cwl-noema-review or cwl-noema-review[bot]; another returned creator is rejected. No arbitrary actor is added. Complete base/head/run/source/workflow receipt and terminal SARIF/GHAS proof from #2405 remain prerequisites; do not deploy the new receiver trust before that foundation. Preserve exact-run identity, supersession, rerun budget, SARIF preservation and Medium+ gates. + +## Consequences + +- POS-001: Removes dependence on an external app owner's unavailable write grants. +- POS-002: Reuses an installed app and keeps analysis, publication and lifecycle tokens separate and target scoped. +- NEG-001: Expands the owned installation's capabilities and therefore the impact of its private-key compromise. Restrict key access and retain the trusted default-branch workflow boundary; never export keys into reviewed source or logs. +- NEG-002: Needs owner-authenticated app settings and installation acceptance plus live verification. Unit contracts do not prove deployment or permission availability. + +## Alternatives Considered + +- ALT-001: Change the external OpenCode app. Rejected because anomalyco owns that app and its current grants cannot satisfy writes. +- ALT-002: Transfer a user's CLI token into CI. Rejected: broad personal credentials are unnecessary and not copied. +- ALT-003: Bypass identity/receipt checks or synthesize success. Rejected because that removes the security proof. +- ALT-004: Reuse the analysis-read token for mutations. Rejected because its read-only contract must remain unchanged. + +## Implementation Notes + +- IMP-001: Pin the existing create-github-app-token action and request exactly one target repository and one write permission per writer token. +- IMP-002: Grant Actions/write and Commit statuses/write to the owned app and accept the installation update; do not add Code Scanning writes. +- IMP-003: Accept deployment only after real current-head scan, GHAS identity, receipt creator, one exact run-wide wake and terminal required verdict are verified. References: ContextualWisdomLab/.github#2276, #1929 and #2405. + +## References + +GitHub. (n.d.). *Create GitHub App token*. https://github.com/actions/create-github-app-token +GitHub. (n.d.). *Choosing permissions for a GitHub App*. https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/choosing-permissions-for-a-github-app diff --git a/docs/doctoring/central-dedicated-runner-routing-20260927.md b/docs/doctoring/central-dedicated-runner-routing-20260927.md index 0b76a62ccc..aa84465589 100644 --- a/docs/doctoring/central-dedicated-runner-routing-20260927.md +++ b/docs/doctoring/central-dedicated-runner-routing-20260927.md @@ -85,3 +85,49 @@ include `ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/hea Preserve every existing allowlist entry, repository restriction, and external contributor approval. Do not allow a feature-branch ref. Until that grant is verified, the source change is not an operational routing repair. + + +## Issue 1565 review admission follow-up + +The SDK and naruon consumer Noema jobs still selected hosted Ubuntu after the +initial runner rollout. Extend the existing repository allowlist to +`ContextualWisdomLab/cwl-telemetry` and `ContextualWisdomLab/naruon`, only when +`github.workflow_ref` is exactly the central Noema workflow at `refs/heads/main`. +Metadata and continuation use the control pool; model review uses MCP remediation. +PR-authored workflow refs retain hosted execution and existing fork admission, +credentials, review publication, concurrency and inference-time policy remain. + +At 2026-09-27 12:12 UTC, group 3 repository membership was verified after two +repository-specific PUT requests. Its selected-workflow restrictions remain; +group 6 already allows repositories subject to its selected-workflow restrictions. +This is runner admission, not approval or evidence of a completed model review. +Existing queued runs retain their original workflow revision and may still wait +until event-driven current-head recovery creates a new run. + +The allocation calculation from the initial rollout is reused; no new host +capacity or independent service-time measurement justifies another solver. +The routing regression fails against the unchanged baseline. Workflow syntax +and affected contracts passed: 238 passed, 2 skipped with `GITHUB_ACTIONS=true`; +`actionlint` and `git diff --check` passed. + + +## fast-mlsirm Strix control admission + +Current fast-mlsirm PR #2220 head `4eaeb799a6647ea29f3f4902d9ca79a1377e795c` +queued Strix admission job `108617323217` with `ubuntu-24.04`, despite the +self-hosted rollout. Route only changed-scope, current-head admission, +superseded-run cleanup and manual status publication through group 6 when +the source is exactly central `strix.yml@refs/heads/main` and the caller is +the central repository or fast-mlsirm. These jobs do not check out PR code. +The model scan keeps its existing hosted image and all evidence, credentials, +fork handling and live-head validation remain intact. + +Reuse the deployed allocation; no new service-time or capacity measurement +justifies a different solver result. Deployment requires adding only central +`strix.yml@refs/heads/main` to group 6's selected workflows, preserving all +existing restrictions and grants. Old queued jobs keep their original source. + +The routing test failed on the unmodified workflow. The affected runner, +changed-scope and dependency-hash tests passed (21 tests); actionlint and +diff whitespace checks passed. This is local source proof, not completed +consumer gate evidence. diff --git a/docs/doctoring/codeql-terminal-proof-2352.md b/docs/doctoring/codeql-terminal-proof-2352.md new file mode 100644 index 0000000000..cfd2b356a3 --- /dev/null +++ b/docs/doctoring/codeql-terminal-proof-2352.md @@ -0,0 +1,66 @@ +# CodeQL terminal-proof settlement (#2352) + +## Incident + +On `.github#2352@f1a8dc813e6dba4e4905bf3e1b770b6d44344944`, required CodeQL +run `35805450471` initially failed pending and was later rerun. Attempt 2 jobs +`107353895415` (Actions) and `107353895562` (Python) became GREEN by reading +the successful `Enforce CodeQL Medium+ SARIF gate` step from producer run +`35841640640`. + +The producer jobs were nevertheless terminal failures: the later +`Verify GHAS base/head CodeQL configuration identity` step received HTTP 403. +The gate-only fallback therefore hid the exact credential/permission defect +tracked by `#2275` and `#2276`. + +## Root cause and boundary + +The required receiver and settlement contract treated one successful SARIF +gate step as terminal success even when a later mandatory proof failed. This +was originally allowed so a wake-only API failure could not invalidate an +otherwise complete scan, but the contract did not distinguish that harmless +late failure from GHAS identity or SARIF-preservation failure. + +A clean result recovered from a producer job whose overall conclusion is +failure now requires the same three proof units in both paths: + +1. `Enforce CodeQL Medium+ SARIF gate` succeeds; +2. `Verify GHAS base/head CodeQL configuration identity` succeeds; and +3. `Preserve CodeQL SARIF evidence` succeeds. + +A later failure confined to waking the exact required job remains outside the +scan verdict and may still be reconciled. A Medium+ gate failure remains a +terminal security failure and does not require a successful GHAS identity +step. A producer job whose overall conclusion is success remains authenticated +terminal proof because GitHub completed its non-optional steps successfully. +Missing, duplicate, skipped, cancelled, or failed proof on the failed-job clean +fallback stays fail-closed. + +An independent review found a second boundary defect before merge: the +required receiver and coordinator trusted the legacy +`codeql-dispatch/` commit status using only head SHA and publisher. +GitHub retains statuses on a commit, so the same head could reuse a success +from an earlier base, required run, or producer protocol after a PR retarget. +The current producer and consumers now use the v2 receipt exclusively: + +- context: `codeql-dispatch//`; +- description: exact head SHA, required run ID, workflow identity, and live + merge-source SHA; and +- publisher: the existing allowlisted app identity. + +The coordinator dispatches `codeql-scan-v2` with the versioned `pr_head` +envelope and live merge source. A legacy or otherwise stale status is ignored, +so the exact run performs or reuses only its own base/source-bound scan. + +## Verification and ownership + +Executable regressions reproduce the direct receiver and run-wide settlement +false-GREEN surfaces plus stale trusted-status reuse. They are RED on protected +`main` and GREEN with the proof contract. Focused workflow tests pass 91/91; +the complete repository suite passes 3,372 tests with 28 skips and 40 subtests. + +The central `.github` workflow remains the canonical owner. Do not copy the +workflow into a consumer, synthesize a status, accept clean SARIF alone, or +weaken the GHAS identity proof. `#2275`/`#2276` still own the real credential +and target permission repair; this change prevents that missing authority from +being mislabeled as a successful required check. diff --git a/docs/doctoring/codeql-verdict-history-scope.md b/docs/doctoring/codeql-verdict-history-scope.md new file mode 100644 index 0000000000..c25f395608 --- /dev/null +++ b/docs/doctoring/codeql-verdict-history-scope.md @@ -0,0 +1,21 @@ +# CodeQL verdict history scope + +## Structure and gap + +Required CodeQL shards consume an authenticated status or an exact completed dispatch bound to target repository, PR, head, base and required run ID. The fallback previously paginated the complete central dispatch history. On 2026-09-27 the public workflow API reported 10,311 runs; contextual-orchestrator#1031 Python verdict job 108609837545 was executing the lookup on cwlab-s1-05. This proves the lookup workload, not that it alone caused all queue delay. + +## Repair and invariant + +Read the canonical required run creation timestamp with Actions read permission. Fail closed if it is missing or malformed. Query repository_dispatch runs created at or after that timestamp, retaining pagination and exact identity and terminal gate checks. A producer bound to the required run cannot exist before the required run. No elapsed-time model verdict, synthetic approval, runner-group relaxation or security exemption is introduced. + +The same live API query with created >= 2026-09-27T11:08:00Z returned 3 runs. This is query cardinality evidence, not deployed latency or completed CodeQL proof. + +## Verification + +Real extracted Bash verdict scripts retain successful completed-dispatch recovery, later-page recovery, stale base/run rejection, unknown-state rejection and no-dispatch pending behavior. Added invalid timestamp failure coverage. The focused contract suite passed 29 tests before the explicit Actions read grant. Final combined verification is recorded in the PR. + +The original extended runner-image oracle expected three literal ubuntu-24.04 jobs while protected main routes trusted workflow jobs to the central control group. The exact oracle failed on unmodified base c3e86141c. It now requires all three jobs to compare the exact trusted main workflow ref, select the control group with self-hosted/linux/x64 labels, and retain the explicit ubuntu-24.04 fallback for other refs. The six runner-image tests pass locally; combined final receipt is recorded in the PR. + +## Reference + +GitHub. (n.d.). *REST API endpoints for workflow runs*. Retrieved September 27, 2026, from https://docs.github.com/en/rest/actions/workflow-runs#list-workflow-runs-for-a-workflow . The created filter uses date-time search syntax; per_page supports 100. Filtered searches return up to 1,000 runs; overflow cannot authorize a false success because exact receipt matching remains mandatory. diff --git a/docs/doctoring/noema-central-transport-continuation.md b/docs/doctoring/noema-central-transport-continuation.md new file mode 100644 index 0000000000..f53277998d --- /dev/null +++ b/docs/doctoring/noema-central-transport-continuation.md @@ -0,0 +1,34 @@ +# Noema central transport continuation + +## Failure + +The 429-capacity continuation sent repository dispatch to the product repository. +Organization-required workflows do not supply a local repository-dispatch handler +there. A central review of another repository also failed its same-repository +origin guard, even though the review itself had admitted that target. + +## Repair + +Send the existing `noema-review` event to the central `.github` handler. Preserve +its target repository, PR, exact head and retry count. Permit only the central +origin or the target repository's own required workflow. Re-fetch an open PR and +require matching head, base, base repository and head repository before sending. +Fork, stale, closed and unrelated-origin continuations retire or fail closed. + +The central handler can dispatch with its repository-scoped GitHub token. +A consumer continuation requires the existing `PR_REVIEW_MERGE_TOKEN` to read the +product PR and create a central repository dispatch; absence or insufficient +permission fails explicitly. This change does not assert that every consumer has +that credential. Native trusted-main runner restrictions, independent review, +publication fencing and the existing post-failure retry bound remain unchanged. +No model inference deadline is added. + +## Evidence + +The shell regression fails on the baseline because the dispatch endpoint is the +consumer repository. It executes the actual workflow step against a fake API, +checks the central endpoint and preserved payload, permits central-origin retry, +rejects unrelated origin and fork or changed-base evidence, and proves a rejected +POST cannot report successful continuation. The unchanged reviewer contracts +are run alongside this regression. Live provider recovery and approval still +require successful current-head hosted execution. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index e7fd78c563..f7a6e6a5ee 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -98,9 +98,11 @@ UTF-8 is still fully content-scanned, never silently admitted. A file whose suffix has a known magic byte (`.hwpx`, `.pdf`, `.png`) is verified by that format's structural evidence; a file with no known magic entry (most research-data formats) is admitted only on the stricter combination of "no -diff patch" and "the fetched bytes are not valid UTF-8" -- a text file can -never be mistaken for a binary artefact merely by sitting under a declared -prefix. +diff patch", "the fetched bytes are not valid UTF-8", and "the +replacement-decoded content contains no prohibited runtime pattern". A text +file cannot be mistaken for a binary artefact merely by sitting under a +declared prefix, and a stray invalid byte cannot hide a readable runtime +directive. **Bounds.** The declaration is capped at 64 entries and 8 path segments of depth per entry (`MAX_DECLARED_ARTIFACT_PREFIXES` / diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 5164d0c825..6e5f1c549a 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -18,6 +18,7 @@ | Gap ID | 상태 | exact-head evidence | causal owner / next gate | |---|---|---|---| | CONTROL-OPENCODE-VCS-PYROOT-01 | **Source repaired on `main` (#2123 `ebc69a401`); image-path helper extracted + offline-proven under #2157 follow-up; hosted consumer step-#17 link still required to close the issue** | `ContextualWisdomLab/contextual-orchestrator#1149@684cf28f`의 중앙 [OpenCode run 34701472466](https://github.com/ContextualWisdomLab/.github/actions/runs/34701472466) `coverage-evidence` job `103574547257`은 PR 코드를 실행하기 전에 immutable `ContextualWisdomLab/fast-mlsirm@09f762d`의 `python/fast_mlsirm` import root를 찾지 못해 종료했다. 같은 head의 제품 테스트는 `3602 passed, 2 skipped`, native CodeQL·fuzz·SBOM·SAST·Strix는 성공했다. | `.github`의 `opencode-review-dispatch.yml`이 root/`src/`만 허용한 계약 drift를 소유했다. #2123이 `python/` candidates를 추가해 `main`에 병합했고, #2157 follow-up은 동일 로직을 `scripts/ci/resolve_opencode_base_vcs_import_root.sh`로 추출해 `tests/test_opencode_vcs_python_source_root_contract.py` fixture로 증명한다. Issue #2157 종료는 post-`ebc69a401` consumer `coverage-evidence`가 docker step #17을 통과한 job id를 문서에 링크한 뒤에만 한다. | +| CONTROL-PINGORA-DECLARED-BINARY-RUNTIME-01 | **Source repaired on `.github#2386@dea7532e`; protected integration pending** | A base-owned artifact-prefix declaration admitted a no-patch file after any non-UTF-8 byte, even when readable bytes contained `nginx -c /etc/nginx/nginx.conf`. The production-bound regression covers `.sh`, `.dat`, and `.txt`; the focused suite is the exact-head acceptance target. | `.github` owns `scripts/ci/pingora_edge_policy.py`. Replacement-decoded content must contain no `CONTENT_RULES` match before an unrecognized binary suffix is admitted. Current-head hosted security Checks, qualifying independent approval, ordinary protected merge, and downstream `late-life-anxiety-reanalysis#269` revalidation remain required. | ### 2026-09-27 CodeQL compatibility retirement delta @@ -3619,6 +3620,15 @@ statements and 7,098 branches covered. Status stays Proposed/Draft and release admission remains HOLD pending terminal GREEN hosted Checks, downstream verdict-shape acceptance, and qualifying independent approval. +## 2026-09-27 Strix AnyIO security-lock carryover + +**Status:** Proposed on `ContextualWisdomLab/.github#2386`; fresh exact-head hosted Checks and qualifying independent approval remain mandatory. + +**Context Map / owner.** The central `.github` security/review bounded context owns the hash-locked Strix CI runtime. PyPI packages and the vulnerability advisory service are upstream evidence; product repositories consume only the released central workflow contract. + +**Gap / RCA.** Exact-head Python Security run [36236245577](https://github.com/ContextualWisdomLab/.github/actions/runs/36236245577), job `108402877544`, found AnyIO `4.14.0` vulnerable to `CVE-2026-63374`, `CVE-2026-64847`, and `CVE-2026-63349`; all three list `4.14.2` as fixed. The generated lock had no explicit AnyIO source constraint, so unrelated PR #2386 inherited a known-vulnerable transitive selection. + +**RED → GREEN / carryover.** RED `761be5b0f63422505b37e28a367a4c5170f302ba` imports #2385's source↔lock contract and fails `1 failed, 1 passed` because the source input lacks `anyio==4.14.2`. GREEN `c59ef9aed32ab4c5138c2b7770ddcc10d7ee8393` adds that exact source constraint; `a895dc5aec775076c3819679eadf0b50a563aa2e` adopts #2385's generated lock blob `eb83beda177c9d2e4ca9b7e2888a1ccb55a123ac`, whose only predecessor differences are version line 143 and hash lines 144–145. Exact remote blobs pass the focused contract `2 passed`. This is complete three-file delta integration, not a claim that #2385 or #2386 is accepted. Completion still requires fresh exact-head pip-audit/other required Checks, no unresolved actionable review, qualifying independent approval, and ordinary protected-main integration. ## 2026-09-27 Git blob protocol-hash SAST authority **Status:** Proposed on `ContextualWisdomLab/.github#2396`; fresh exact-head hosted Checks and qualifying independent approval remain mandatory. @@ -3628,3 +3638,13 @@ verdict-shape acceptance, and qualifying independent approval. **Gap / RCA.** Exact-head SAST run [36243375994](https://github.com/ContextualWisdomLab/.github/actions/runs/36243375994), job `108407968534`, reported `python.lang.security.insecure-hash-algorithms.insecure-hash-algorithm-sha1` at `scripts/ci/pingora_edge_policy.py:602`. The call recomputes Git's protocol-defined `blob \\0` object ID with `usedforsecurity=False`; it is equality evidence for the exact GitHub blob, not a cryptographic signature. Replacing it with SHA-256 would contradict the upstream 40-hex blob identifier and remove tamper detection. **Action / evidence.** RED is the exact hosted failure above. Commit `53f447f73f0ef33eb708bf44202ec4d5954ade66`, formatted by `d00cdff974f5ac665a5f7481620d550735bd26c8`, adds one rule-scoped `nosemgrep` annotation plus the protocol rationale without changing the hash input, comparison, download bound, or failure behavior. Existing executable cases still require exact byte count and reject altered bytes by Git blob-ID mismatch. Completion requires fresh exact-head SAST GREEN, the remaining protected checks, no unresolved actionable review thread, qualifying independent approval, and ordinary merge. + +## 2026-09-27 CodeQL terminal-proof fallback run identity + +**Status:** Proposed on `ContextualWisdomLab/.github#2405`; direct repair parent `5a77a8c711bc93330c24a4821dff7439f600a264`, tree `5ce8ba7448cb878a5b130ed1acaba1578e4940fd`. This documentation-only successor preserves that executable tree; the PR body is the authority for the current exact head and hosted-run IDs. Merge and required-workflow admission remain HOLD. + +**Context Map / owner.** The central `.github` CodeQL required-workflow and dispatch bounded context owns dispatch identity, terminal evidence, and exact job recovery. Product repositories consume the protected workflow contract; they do not copy the producer or manufacture success receipts. + +**Gap / failure scene.** The v2 handler names a run with `head/base/required-run/producer-source`, but its required-workflow fallback looked up only `head/base/required-run`. When authenticated status publication is unavailable, a completed clean handler job could not be found and a rerun ended false RED. Omitting the producer source would also allow a regenerated live merge revision to reuse predecessor evidence. + +**Action / evidence.** Correct the fallback lookup to include the live merge source and retain fail-closed base, head, required-run, workflow-path, job-name, GHAS-identity, and SARIF checks. The test-first repair reproduced two failures, then passed 96 focused workflow-contract tests; the new edge case rejects a stale merge-source title. Ruff E9/F/I on the changed dispatch-contract file and `git diff --check` pass. Fresh hosted Checks and a qualifying independent approval are still required on the unchanged executable delta before merge. diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 838fe464ea..a83becf67b 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -43,7 +43,7 @@ CATALOG_LIMIT="${ORCHESTRATOR_CATALOG_LIMIT:-24}" # equivalence relation. CATALOG_ACCOUNT_CAP="${ORCHESTRATOR_CATALOG_ACCOUNT_CAP:-8}" ORCHESTRATOR_GITHUB_ENV="${GITHUB_ENV:-}" -sidecar_python="$(command -v python3)" +sidecar_python="${SIDECAR_PYTHON:-$(command -v python3)}" log() { printf '[contextual-orchestrator-sidecar] %s\n' "$*"; } @@ -101,6 +101,10 @@ requirements_lock="$ORCHESTRATOR_SOURCE/requirements.lock" if [ ! -f "$requirements_lock" ]; then fail "vendored orchestrator is missing its hash-pinned requirements.lock" fi +# The pinned lock includes CPython 3.12 wheels; isolate them from consumer runtimes. +"$sidecar_python" -c 'import sys; sys.exit(0 if sys.version_info[:2] == (3, 12) else "sidecar requires Python 3.12 for its pinned wheel hashes")' +"$sidecar_python" -m venv "$ORCHESTRATOR_WORK/.venv" +sidecar_python="$ORCHESTRATOR_WORK/.venv/bin/python" log "installing hash-pinned orchestrator dependencies at ${checked_out}" "$sidecar_python" -m pip install --quiet --disable-pip-version-check --no-cache-dir \ --require-hashes \ diff --git a/scripts/ci/noema_preflight_capacity.py b/scripts/ci/noema_preflight_capacity.py new file mode 100644 index 0000000000..b9bed218b1 --- /dev/null +++ b/scripts/ci/noema_preflight_capacity.py @@ -0,0 +1,163 @@ +"""Classify an all-429 review-sidecar preflight as provider capacity (#2148). + +The sidecar launcher writes ``strix_runs/contextual-orchestrator-preflight.json`` +(contract ``strix-plain-chat-preflight-v2``) before it exits on a failed +preflight. When every probed route was refused with HTTP 429 and none is ready, +the private-target ZDR pool is rate-limited rather than broken, which is the same +``provider_capacity_unavailable`` class ADR-0031 already re-dispatches after a +gateway failure. This module emits the same step outputs as +``two_phase._emit_transport_capacity_outputs`` (via the stdlib-only +``noema_transport_redispatch`` helpers) so the existing bounded +re-dispatch step can consume them. It never changes the job result: the +provisioning step has already failed and review remains required. +""" + +from __future__ import annotations + +import argparse +import json +import os +import stat +import re +import sys +from pathlib import Path +from typing import Any + +if __package__ in (None, ""): # pragma: no cover - executed as a workflow script + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +# Stdlib-only on purpose: this runs on the runner's bare python3 after the +# sidecar step failed, before the HWP reader step installs defusedxml, so it +# must not import noema_review_gate (whose document import needs it). +from scripts.ci import noema_transport_redispatch as gate # noqa: E402 + +PREFLIGHT_CONTRACT = "strix-plain-chat-preflight-v2" +PREFLIGHT_CAPACITY_HTTP_STATUS = 429 +PREFLIGHT_CAPACITY_ROUTE_STATUSES = frozenset({"rejected", "deferred"}) +MAX_PREFLIGHT_REPORT_BYTES = 256 * 1024 + + +def _exact_int(value: Any) -> int | None: + """Return ``value`` only when it is a real ``int`` (``bool`` is rejected).""" + return value if type(value) is int else None + + +def _stage_retry_after(report: Any) -> list[int] | None: + """Return one all-429 stage's in-cap ``retry_after_s`` values, or None if not all-429. + + A stage qualifies only when ``ready_count`` is 0, ``probed_count`` is at + least 1, ``routes`` holds exactly ``probed_count`` rows, and every row is a + rejected or deferred route whose ``http_status`` is the integer 429. + """ + if not isinstance(report, dict) or report.get("contract") != PREFLIGHT_CONTRACT: + return None + probed = _exact_int(report.get("probed_count")) + routes = report.get("routes") + if _exact_int(report.get("ready_count")) != 0 or probed is None or probed < 1: + return None + if not isinstance(routes, list) or len(routes) != probed: + return None + waits: list[int] = [] + for row in routes: + if not isinstance(row, dict): + return None + if row.get("status") not in PREFLIGHT_CAPACITY_ROUTE_STATUSES: + return None + if _exact_int(row.get("http_status")) != PREFLIGHT_CAPACITY_HTTP_STATUS: + return None + wait = _exact_int(row.get("retry_after_s")) + if wait is not None and 1 <= wait <= gate.TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS: + waits.append(wait) + return waits + + +def classify_preflight_report(report: Any) -> tuple[int, int | None] | None: + """Return ``(probed_count, retry_after_seconds)`` for an all-429 report, else None. + + A nested ``primary_attempt`` (a fallback stage also ran) must itself be + all-429. ``retry_after_seconds`` is the longest provider-stated wait inside + ADR-0031's existing cap, or None so the deterministic jitter applies. + """ + waits = _stage_retry_after(report) + if waits is None: + return None + probed = report["probed_count"] + if "primary_attempt" in report: + primary_waits = _stage_retry_after(report["primary_attempt"]) + if primary_waits is None: + return None + waits.extend(primary_waits) + probed += report["primary_attempt"]["probed_count"] + return probed, (max(waits) if waits else None) + + +def load_preflight_report(path: Path) -> Any: + """Return the parsed report, or None when it is missing, oversized, or not JSON.""" + try: + if path.parent.is_symlink(): + return None + flags = os.O_RDONLY | os.O_NONBLOCK | getattr(os, "O_NOFOLLOW", 0) + with os.fdopen(os.open(path, flags), "rb") as handle: + metadata = os.fstat(handle.fileno()) + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + return None + raw = handle.read(MAX_PREFLIGHT_REPORT_BYTES + 1) + if len(raw) > MAX_PREFLIGHT_REPORT_BYTES: + return None + return json.loads(raw.decode("utf-8")) + except (OSError, UnicodeDecodeError, ValueError, RecursionError): + return None + + +def emit_preflight_capacity_outputs(path: Path, *, expected_head: str) -> dict[str, str]: + """Write the ADR-0031 transport outputs for one failed sidecar preflight.""" + classified = classify_preflight_report(load_preflight_report(path)) + if classified is None: + outputs = {"transport_capacity_unavailable": "false", "transport_retry_eligible": "false"} + gate.append_github_output(outputs) + return outputs + probed, retry_after = classified + retry_attempt = gate.current_transport_retry_attempt() + delay = gate.transport_redispatch_delay_seconds( + transport_retry_attempt=retry_attempt, + head_sha=expected_head, + retry_after_seconds=retry_after, + ) + outputs = { + "transport_capacity_unavailable": "true", + "transport_retry_eligible": "true" if delay is not None else "false", + "transport_http_status": str(PREFLIGHT_CAPACITY_HTTP_STATUS), + "provider_attempt_count": str(probed), + } + if delay is not None: + outputs["transport_retry_delay_seconds"] = str(delay) + outputs["transport_retry_next_attempt"] = str(retry_attempt + 1) + print( + "::notice::Noema sidecar preflight was all-429 (provider capacity unavailable); " + f"bounded continuation re-dispatch is eligible in {delay}s " + f"(attempt {retry_attempt + 1}/{gate.MAX_TRANSPORT_REDISPATCH_ATTEMPTS})." + ) + else: + print( + "::error::Noema sidecar preflight was all-429 (provider capacity unavailable); " + "automatic re-dispatch budget is exhausted. Review remains required." + ) + gate.append_github_output(outputs) + return outputs + + +def main(argv: list[str]) -> int: + """Classify one preflight report; always exit 0 because the job already failed.""" + parser = argparse.ArgumentParser() + parser.add_argument("--preflight-report", required=True, type=Path) + parser.add_argument("--expected-head", required=True) + args = parser.parse_args(argv) + if not re.fullmatch(r"[0-9a-f]{40}", args.expected_head): + print("::error::--expected-head must be a canonical lowercase 40-character Git SHA.") + return 0 + emit_preflight_capacity_outputs(args.preflight_report, expected_head=args.expected_head) + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 877ff6ce37..258f0e7155 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -26,6 +26,15 @@ from scripts.ci.opencode_review_normalize_output import changed_file_is_material from scripts.ci.noema_review_document import DocumentReadError, extract_review_document +from scripts.ci.noema_transport_redispatch import ( # noqa: F401 + MAX_TRANSPORT_REDISPATCH_ATTEMPTS, + TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS, + TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS, + TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS, + append_github_output, + current_transport_retry_attempt, + transport_redispatch_delay_seconds, +) PRIMARY_REVIEW_AUTHORS = { @@ -65,10 +74,6 @@ MAX_HTTP_ERROR_BODY_BYTES = 16 * 1024 # ADR-0031: transport-capacity class after gateway failover (not caller retries). TRANSPORT_CAPACITY_HTTP_STATUSES = frozenset({429, 500, 502, 503, 504}) -MAX_TRANSPORT_REDISPATCH_ATTEMPTS = 2 -TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS = 60 -TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS = 180 -TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS = 300 DIFF_HUNK_RE = re.compile(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@") SAFE_MODEL_IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$") @@ -258,64 +263,6 @@ def parse_http_retry_after_seconds(headers: Any) -> int | None: return seconds -def transport_redispatch_delay_seconds( - *, - transport_retry_attempt: int, - head_sha: str, - retry_after_seconds: int | None = None, -) -> int | None: - """Return the post-failure scheduling delay, or None when the re-dispatch bound is spent. - - ``transport_retry_attempt`` is the number of automatic capacity re-dispatches - already performed for this head (0 on the first failure). Prefer a capped - gateway ``Retry-After`` when present; otherwise use deterministic jitter in - ``[TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS, TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS]`` - keyed by head SHA and attempt so concurrent failures do not stampede. - """ - if transport_retry_attempt < 0 or transport_retry_attempt >= MAX_TRANSPORT_REDISPATCH_ATTEMPTS: - return None - if retry_after_seconds is not None: - if ( - type(retry_after_seconds) is int - and 1 <= retry_after_seconds <= TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS - ): - return retry_after_seconds - return None - digest = hashlib.sha256( - f"{head_sha.strip().lower()}:{transport_retry_attempt}".encode("utf-8") - ).digest() - span = ( - TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS - TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS + 1 - ) - offset = int.from_bytes(digest[:4], "big") % span - return TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS + offset - - -def current_transport_retry_attempt() -> int: - """Parse the retry counter; invalid values exhaust the automatic budget.""" - raw = os.environ.get("NOEMA_TRANSPORT_RETRY_ATTEMPT") - if raw is None or raw == "null": - return 0 - if not re.fullmatch(r"[0-9]{1,2}", raw): - return MAX_TRANSPORT_REDISPATCH_ATTEMPTS - value = int(raw) - return min(value, MAX_TRANSPORT_REDISPATCH_ATTEMPTS) - - -def append_github_output(values: dict[str, str]) -> None: - """Append allowlisted step outputs when running under GitHub Actions.""" - path = (os.environ.get("GITHUB_OUTPUT") or "").strip() - if not path or not values: - return - with open(path, "a", encoding="utf-8") as handle: - for key, value in values.items(): - if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key): - continue - if any(ch in value for ch in ("\n", "\r", "\0")): - continue - handle.write(f"{key}={value}\n") - - def _stable_failure_diagnostic(exc: BaseException) -> str: """Return actionable trusted diagnostics without reflecting model values.""" message = scrub_sensitive_data(str(exc)) or type(exc).__name__ diff --git a/scripts/ci/noema_transport_redispatch.py b/scripts/ci/noema_transport_redispatch.py new file mode 100644 index 0000000000..0af2ac63d4 --- /dev/null +++ b/scripts/ci/noema_transport_redispatch.py @@ -0,0 +1,72 @@ +"""Stdlib-only Noema continuation helpers for startup and model failures.""" + +from __future__ import annotations + +import hashlib +import os +import re + +MAX_TRANSPORT_REDISPATCH_ATTEMPTS = 2 +TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS = 60 +TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS = 180 +TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS = 300 + + +def transport_redispatch_delay_seconds( + *, + transport_retry_attempt: int, + head_sha: str, + retry_after_seconds: int | None = None, +) -> int | None: + """Return the post-failure scheduling delay, or None when the re-dispatch bound is spent. + + ``transport_retry_attempt`` is the number of automatic capacity re-dispatches + already performed for this head (0 on the first failure). Prefer a capped + gateway ``Retry-After`` when present; otherwise use deterministic jitter in + ``[TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS, TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS]`` + keyed by head SHA and attempt so concurrent failures do not stampede. + """ + if transport_retry_attempt < 0 or transport_retry_attempt >= MAX_TRANSPORT_REDISPATCH_ATTEMPTS: + return None + if retry_after_seconds is not None: + if ( + type(retry_after_seconds) is int + and 1 <= retry_after_seconds <= TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS + ): + return retry_after_seconds + return None + digest = hashlib.sha256( + f"{head_sha.strip().lower()}:{transport_retry_attempt}".encode("utf-8") + ).digest() + span = ( + TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS - TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS + 1 + ) + offset = int.from_bytes(digest[:4], "big") % span + return TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS + offset + + +def current_transport_retry_attempt() -> int: + """Parse the retry counter; invalid values exhaust the automatic budget.""" + raw = os.environ.get("NOEMA_TRANSPORT_RETRY_ATTEMPT") + if raw is None or raw == "null": + return 0 + if not re.fullmatch(r"[0-9]{1,2}", raw): + return MAX_TRANSPORT_REDISPATCH_ATTEMPTS + value = int(raw) + return min(value, MAX_TRANSPORT_REDISPATCH_ATTEMPTS) + + +def append_github_output(values: dict[str, str]) -> None: + """Append allowlisted step outputs when running under GitHub Actions.""" + path = (os.environ.get("GITHUB_OUTPUT") or "").strip() + if not path or not values: + return + with open(path, "a", encoding="utf-8") as handle: + for key, value in values.items(): + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key): + continue + if any(ch in value for ch in ("\n", "\r", "\0")): + continue + handle.write(f"{key}={value}\n") + + diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 518ef84d3c..0d3a2c0948 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -24,14 +24,14 @@ ``.npz``, ...) have no entry in ``BINARY_DOCUMENT_MAGIC``, which only knows ``.hwpx``/``.pdf``/``.png``. Rather than grow that registry for every such format, a file under a declared prefix whose suffix has no magic entry is -admitted on the stricter complement of the UTF-8 decode this module already -performs for every ordinarily-scanned file: no diff patch available, *and* the -fetched bytes fail to decode as UTF-8. That keeps the module's central -guarantee honest -- a file that decodes as valid UTF-8 is never treated as a -binary artifact, since scanning exactly that content is what this module -exists to do -- while still admitting genuinely opaque research binaries -without maintaining an open-ended magic-byte catalog. A suffix that *does* -have a magic entry keeps that entry's existing structural evidence check +admitted only when no diff patch is available, the fetched bytes fail to decode +as UTF-8, and their replacement-decoded text contains no prohibited runtime +pattern. That keeps the module's central guarantee honest -- a file that +decodes as valid UTF-8 is never treated as a binary artifact, and one stray +invalid byte cannot conceal a readable runtime command -- while still +admitting genuinely opaque research binaries without maintaining an open-ended +magic-byte catalog. A suffix that *does* have a magic entry keeps that entry's +existing structural evidence check (``_is_complete_png``, ``_is_complete_hwpx``, or the raw magic-prefix check for ``.pdf``) even under a declared prefix. """ @@ -672,6 +672,8 @@ def _binary_documentation_evidence_confirms( bytes that decode cleanly are never admitted this way, so a valid-UTF-8 file cannot be mistaken for a binary artifact merely by sitting under a declared prefix -- it still reaches the normal content scan instead. + Inspect readable text even when other bytes are invalid UTF-8, so a stray + binary byte cannot conceal an active runtime command. """ try: @@ -687,7 +689,8 @@ def _binary_documentation_evidence_confirms( try: raw.decode("utf-8") except UnicodeDecodeError: - return True + readable = raw.decode("utf-8", errors="replace") + return not any(pattern.search(readable) for _, pattern in CONTENT_RULES) return False return raw.startswith(BINARY_DOCUMENT_MAGIC[suffix]) diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 4489ee62a3..5a86bd24c8 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -3905,14 +3905,8 @@ def is_strix_scan_check_run(node: dict[str, Any]) -> bool: def dispatch_strix_evidence(repo: str, workflow: str, pr: dict[str, Any], *, dry_run: bool) -> str: """Dispatch same-head Strix workflow evidence before OpenCode reviews.""" - job_id = matching_actions_job_id(pr, is_strix_scan_check_run) - if job_id: - if not dry_run and not review_dispatch_admitted("strix", repo, pr): - return "admission_deferred" - if not dry_run and not live_dispatch_head_matches(repo, pr): - return "stale_head" - rerun_actions_job(repo, job_id, dry_run=dry_run, action="rerun-strix-evidence") - return "rerun" if not dry_run else "dry_run" + # A job rerun retains its original trusted workflow revision. Fresh dispatch + # selects the default-branch runtime and still enforces admission and live head. if dry_run: return "dry_run" require_github_actions_control_actor("inspect-active-strix-evidence") diff --git a/scripts/ci/release_dependency_gate.py b/scripts/ci/release_dependency_gate.py index 2c23446d90..cafa10a777 100644 --- a/scripts/ci/release_dependency_gate.py +++ b/scripts/ci/release_dependency_gate.py @@ -2255,7 +2255,8 @@ def gate(capture_root: Path, stage: str = FULL_STAGE, *, except GateError as error: report.failures.append(Failure(error.code, subject, error.detail)) continue - license_evidence = {**evidence, "license_texts": {**evidence["license_texts"], **source_texts}} + license_evidence = {**evidence, "license_texts": { + **_require_mapping(evidence, "license_texts", subject), **source_texts}} license_failures, decision, license_source = evaluate_dependency_license( license_evidence, subject, selections.get(subject) ) @@ -2807,13 +2808,11 @@ def install_is_authorized(report: Path) -> None: if payload.get("stage") != LICENSE_STAGE: raise GateError( LICENSE_MISSING, - LICENSE_SELECTION_INVALID, f"prescreen report records stage {payload.get('stage')!r}, not {LICENSE_STAGE!r}", ) if payload.get("result") != "PASS": raise GateError( LICENSE_MISSING, - LICENSE_SELECTION_INVALID, f"prescreen report records result {payload.get('result')!r}, not 'PASS'", ) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 938f717363..28cf2e4e36 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -14,6 +14,16 @@ REPO_ROOT="$( GATE_SCRIPT="$REPO_ROOT/scripts/ci/strix_quick_gate.sh" FAILURES=0 + +materialize_trusted_gate_fixture() { + local fixture_script_dir="$1" + + mkdir -p "$fixture_script_dir" + cp "$GATE_SCRIPT" "$fixture_script_dir/strix_quick_gate.sh" + cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$fixture_script_dir/strix_model_utils.sh" + cp "$REPO_ROOT/scripts/ci/strix_evidence_binding.py" "$fixture_script_dir/strix_evidence_binding.py" + chmod +x "$fixture_script_dir/strix_quick_gate.sh" +} TIMEOUT_TEST_PROCESS_SECONDS="${STRIX_TEST_PROCESS_TIMEOUT_SECONDS:-30}" TIMEOUT_TEST_FAKE_SLEEP_SECONDS="${STRIX_TEST_FAKE_SLEEP_SECONDS:-60}" @@ -640,7 +650,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$workflow_file" 'ref: ${{ github.workflow_sha }}' "opencode trusted checkout never bypasses the validated ref output" assert_file_contains "$workflow_file" "target_repository:" "opencode repository_dispatch can target a repository whose PR does not inherit required workflows" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "opencode coverage measures the PR merge tree instead of exposing secrets to untrusted checkout actions" - assert_file_contains "$workflow_file" 'TARGET_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }}' "opencode coverage fetches exact validated base/head commits from the target repository" + assert_file_contains "$workflow_file" 'TARGET_REPOSITORY: ${{ steps.validate.outputs.target_repository }}' "opencode coverage fetches exact validated base/head commits from the target repository" assert_file_contains "$workflow_file" "Exchange OpenCode app token for target repository review reads" "opencode review can read private target repositories through the OpenCode app token before materializing review data" assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ steps.review_read_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }}' "opencode materialization prefers the OpenCode app token for private target repository reads" assert_file_contains "$workflow_file" '[ "${GH_REPOSITORY:-}" != "${GITHUB_REPOSITORY:-}" ]' "opencode approval uses the app token for target-repository check lookup" @@ -968,13 +978,13 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" assert_file_contains "$workflow_file" '"enabled_providers": ["contextual-orchestrator"]' "opencode review keeps the generated provider set gateway-only" assert_file_contains "$workflow_file" '"model": "contextual-orchestrator/orchestrator/free"' "opencode review keeps the generated model on orchestrator/free" - assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests" + assert_file_contains "$workflow_file" "validate-pr-metadata:" "opencode admission job materializes coverage source before running PR-head tests" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "required OpenCode reviews measure coverage instead of approving skipped coverage evidence" assert_file_contains "$workflow_file" "Exchange OpenCode app token for target repository coverage reads" "coverage source materialization can read private target repositories during central manual dispatch" assert_file_contains "$workflow_file" "Upload materialized pull request merge tree" "coverage source materialization passes only a prepared merge tree artifact to the PR-head coverage job" assert_file_contains "$workflow_file" "Download materialized pull request merge tree" "coverage evidence consumes the prepared merge tree artifact without target-repository credentials" - assert_file_contains "$workflow_file" "Report coverage source materialization failure" "coverage evidence logs source materialization failures as the coverage blocker" + assert_file_contains "$workflow_file" "needs.validate-pr-metadata.result == 'success'" "coverage evidence requires successful source materialization in the admission job" local coverage_merge_tree_step coverage_merge_tree_step="$( awk ' @@ -3293,10 +3303,14 @@ run_gate_case() { local repo_root_dir="$workspace_dir/smart-crawling-server" mkdir -p "$bin_dir" "$untrusted_bin_dir" "$repo_root_dir/src" mkdir -p "$repo_root_dir/scripts/ci" - local gate_under_test="$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$GATE_SCRIPT" "$gate_under_test" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$gate_under_test" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + local gate_under_test="$trusted_script_dir/strix_quick_gate.sh" + materialize_trusted_gate_fixture "$trusted_script_dir" + if [ "$scenario" = "pr-changed-scope-includes-ci-dependency" ]; then + # Consumer source under scan; execution still uses the separate trusted runtime. + cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" + fi local fake_strix="$bin_dir/strix" local path_hijack_log="$tmp_dir/path-hijack.log" cat >"$untrusted_bin_dir/strix" <<'EOF' @@ -5947,7 +5961,7 @@ PY -u STRIX_OPENAI_FALLBACK_KEY_FILE \ -u STRIX_OPENAI_FALLBACK_API_BASE_FILE \ "${env_cmd[@]}" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$gate_under_test" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7024,9 +7038,8 @@ run_pull_request_target_head_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7152,7 +7165,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="$target_path" \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7172,9 +7185,8 @@ run_pull_request_target_plaintext_runner_token_fails_closed_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7272,7 +7284,7 @@ EOS LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7294,9 +7306,8 @@ run_pull_request_target_bounded_head_context_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7382,7 +7393,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7399,9 +7410,8 @@ run_pull_request_target_changed_context_scope_uses_pr_head_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7525,7 +7535,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7561,7 +7571,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) rc=$? set -e @@ -7578,9 +7588,8 @@ run_pull_request_target_changed_backend_context_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7812,7 +7821,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7837,9 +7846,8 @@ run_pull_request_target_frontend_email_context_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -8008,7 +8016,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8027,9 +8035,8 @@ run_pull_request_target_shallow_head_merge_base_fallback_case() { local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$origin_repo_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -8103,7 +8110,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8142,9 +8149,8 @@ run_pull_request_target_aborts_on_pr_head_blob_failure_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local real_git real_git="$(command -v git)" @@ -8241,7 +8247,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8266,9 +8272,8 @@ run_pull_request_target_rejects_invalid_sha_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8333,7 +8338,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8359,9 +8364,8 @@ run_pull_request_target_irregular_head_entry_fails_closed_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8420,7 +8424,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8442,9 +8446,8 @@ run_pull_request_target_gitlink_is_explicitly_skipped_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8494,7 +8497,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8524,9 +8527,8 @@ run_full_head_scope_skips_gitlink_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -8617,7 +8619,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8638,9 +8640,8 @@ run_pull_request_target_rejects_unsafe_changed_path_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8684,7 +8685,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8730,9 +8731,8 @@ run_timeout_cleanup_case() { local workspace_dir="$tmp_dir/workspace" local repo_root_dir="$workspace_dir/smart-crawling-server" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local child_pid_file="$tmp_dir/child.pid" local output_log="$tmp_dir/output.log" @@ -8768,7 +8768,7 @@ EOF STRIX_VERTEX_FALLBACK_MODELS="" \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ STRIX_TARGET_PATH="." \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8812,9 +8812,8 @@ run_vertex_model_ignores_untrusted_llm_api_base_file_case() { local llm_api_base_file="$outside_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -8845,7 +8844,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8864,9 +8863,8 @@ run_total_timeout_case() { local workspace_dir="$tmp_dir/workspace" local repo_root_dir="$workspace_dir/smart-crawling-server" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" local call_count_file="$tmp_dir/calls.log" @@ -8902,7 +8900,7 @@ EOF STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS="0" \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ STRIX_TARGET_PATH="." \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9191,9 +9189,8 @@ run_llm_api_base_file_outside_input_root_fails_closed_case() { local llm_api_base_file="$outside_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9218,7 +9215,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9246,9 +9243,8 @@ run_pr_scoped_llm_api_base_file_config_failure_exits_2_case() { local llm_api_base_file="$outside_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$repo_root_dir/src" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" printf '%s\n' 'print("one")' >"$repo_root_dir/src/one.py" printf '%s\n' 'print("two")' >"$repo_root_dir/src/two.py" @@ -9277,7 +9273,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9307,9 +9303,8 @@ run_required_input_file_outside_input_root_fails_closed_case() { local outside_file="$outside_dir/${file_env}.txt" mkdir -p "$repo_root_dir/scripts/ci" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9349,7 +9344,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9377,9 +9372,8 @@ run_input_file_root_override_takes_precedence_over_runner_temp_case() { local llm_api_base_file="$explicit_input_root/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$explicit_input_root" "$inherited_runner_temp" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9405,7 +9399,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9431,9 +9425,8 @@ run_stale_report_case() { local llm_api_base_file="$tmp_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" mkdir -p "$stale_report_dir" cat >"$stale_report_dir/vuln-0001.md" <<'EOF' @@ -9463,7 +9456,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_REPORTS_DIR="strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9486,9 +9479,8 @@ run_symlink_report_case() { local llm_api_base_file="$tmp_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" mkdir -p "$external_report_dir" "$repo_root_dir/strix_runs" cat >"$external_report_dir/vuln-0001.md" <<'EOF' @@ -9519,7 +9511,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_REPORTS_DIR="strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9542,9 +9534,8 @@ run_unsafe_target_path_case() { local llm_api_base_file="$tmp_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9570,7 +9561,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_TARGET_PATH="../../../../../etc/passwd" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9590,9 +9581,8 @@ run_absolute_outside_target_path_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/workspace/smart-crawling-server" mkdir -p "$bin_dir" "$repo_root_dir/src" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" local output_log="$tmp_dir/output.log" @@ -9622,7 +9612,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_TARGET_PATH="$tmp_dir/strix-pr-scope.attacker" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e diff --git a/tests/test_code_scanning_required_workflow_contract.py b/tests/test_code_scanning_required_workflow_contract.py index 19933303d8..dbabff9705 100644 --- a/tests/test_code_scanning_required_workflow_contract.py +++ b/tests/test_code_scanning_required_workflow_contract.py @@ -45,4 +45,6 @@ def test_ruleset_requires_dispatch_safe_codeql_pr() -> None: assert workflow_path in audit.REQUIRED_WORKFLOW_PATHS assert "uses: github/codeql-action" not in workflow - assert "event_type:\"codeql-scan\"" in workflow + assert "event_type:\"codeql-scan-v2\"" in workflow + assert 'pr_head:{schema:"1",ref:$pr_head_ref,sha:$pr_head_sha}' in workflow + assert "producer_source_sha:$producer_source_sha" in workflow diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index e1360b9b78..977a47deed 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -51,11 +51,12 @@ def test_codeql_pr_workflow_structure() -> None: assert "analyze-merge:" not in workflow assert "CodeQL merge preview" not in workflow assert "refs/pull/{0}/merge" not in workflow - assert "event_type:\"codeql-scan\"" in workflow + assert "event_type:\"codeql-scan-v2\"" in workflow assert "repos/ContextualWisdomLab/.github/dispatches" in workflow - # Reads the authenticated context codeql-scan-dispatch.yml publishes; it - # never publishes that status from the required workflow. - assert '--arg ctx "codeql-dispatch/${LANGUAGE}"' in workflow + # Reads the authenticated, base-bound context that + # codeql-scan-dispatch.yml publishes; the required workflow never writes it. + assert 'expected_context="codeql-dispatch/${LANGUAGE}/${live_base}"' in workflow + assert ".description == $description" in workflow assert "commits/${PR_HEAD_SHA}/statuses" in workflow @@ -76,7 +77,7 @@ def test_codeql_pr_shards_do_not_dispatch_and_coordinator_sends_the_full_matrix_ assert "id: dispatch" in analyze_head assert "repos/ContextualWisdomLab/.github/dispatches" not in analyze_head - assert 'event_type:"codeql-scan"' not in analyze_head + assert 'event_type:"codeql-scan-v2"' not in analyze_head assert 'matrix:[{language:$language,"build-mode":$build_mode}]' not in workflow assert "required_job_id:$required_job_id" not in analyze_head assert "required_language:$required_language" not in analyze_head @@ -109,7 +110,9 @@ def test_codeql_coordinator_dispatches_later_attempts_when_no_terminal_verdict() assert "github.run_attempt == 1" not in coordinator_if assert "All detected CodeQL languages already have authenticated terminal verdicts" in coordinator - assert 'event_type:"codeql-scan"' in coordinator + assert 'event_type:"codeql-scan-v2"' in coordinator + assert 'pr_head:{schema:"1",ref:$pr_head_ref,sha:$pr_head_sha}' in coordinator + assert "producer_source_sha:$producer_source_sha" in coordinator assert "required_jobs:$required_jobs" in coordinator assert "required_run_id:$required_run_id" in coordinator assert "required_job_id:$required_job_id" not in coordinator @@ -152,19 +155,42 @@ def test_codeql_pr_dispatch_and_release_run_blocks_are_valid_bash() -> None: COORDINATOR_STEP_NAME = "Dispatch current-head CodeQL scan" _TEST_HEAD_SHA = "b" * 40 _TEST_BASE_SHA = "a" * 40 +_TEST_PRODUCER_SOURCE_SHA = "c" * 40 _TEST_REQUIRED_RUN_ID = "42" +def _bound_status( + language: str, + state: str, + *, + head_sha: str = _TEST_HEAD_SHA, + base_sha: str = _TEST_BASE_SHA, + required_run_id: str = _TEST_REQUIRED_RUN_ID, + producer_source_sha: str = _TEST_PRODUCER_SOURCE_SHA, +) -> dict: + """Return a v2 receipt bound to the exact base, run, and merge source.""" + return { + "context": f"codeql-dispatch/{language}/{base_sha}", + "state": state, + "description": ( + f"cwl1;h={head_sha};w=codeql-scan-dispatch;" + f"r={required_run_id};s={producer_source_sha}" + ), + "creator": {"login": "opencode-agent[bot]"}, + } + + def _dispatch_scan_title( *, head_sha: str = _TEST_HEAD_SHA, base_sha: str = _TEST_BASE_SHA, required_run_id: str = _TEST_REQUIRED_RUN_ID, + producer_source_sha: str = _TEST_PRODUCER_SOURCE_SHA, ) -> str: """Return the immutable CodeQL dispatch run-name for one required shard.""" return ( "CodeQL Scan Dispatch ContextualWisdomLab/naruon#42@" - f"{head_sha}/{base_sha}/{required_run_id}" + f"{head_sha}/{base_sha}/{required_run_id}/{producer_source_sha}" ) @@ -194,6 +220,7 @@ def _run_verdict_read( live_state: str = "open", live_head: str = _TEST_HEAD_SHA, comparison: dict | None = None, + required_created_at: str = "2026-09-27T11:08:00Z", ) -> tuple[subprocess.CompletedProcess[str], subprocess.CompletedProcess[str]]: """Execute the real one-shot status read and verdict enforcement blocks.""" bash = shutil.which("bash") @@ -208,6 +235,7 @@ def _run_verdict_read( live_pr = { "head": {"sha": live_head}, "base": {"sha": _TEST_BASE_SHA}, + "merge_commit_sha": _TEST_PRODUCER_SOURCE_SHA, "state": live_state, } @@ -218,6 +246,7 @@ def _run_verdict_read( "#!/usr/bin/env bash\n" "set -euo pipefail\n" 'test "$1" = api\n' + 'if [[ "$*" == *"/actions/runs/42 --jq .created_at" ]]; then printf \'%s\\n\' "$FAKE_REQUIRED_CREATED_AT"; exit 0; fi\n' 'endpoint="${@: -1}"\n' 'case "$endpoint" in\n' " */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n" @@ -235,6 +264,7 @@ def _run_verdict_read( dispatch_env = { **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", + "FAKE_REQUIRED_CREATED_AT": required_created_at, "FAKE_PULL_JSON": json.dumps(live_pr), "FAKE_COMPARE_JSON": json.dumps(comparison if comparison is not None else { "status": "ahead", "behind_by": 0, "ahead_by": 1, @@ -290,9 +320,8 @@ def _run_verdict_read( def test_codeql_pr_one_shot_read_ignores_status_forged_by_non_opencode_creator(tmp_path: Path) -> None: """A PR-forged 'codeql-dispatch/: success' status must not stand in for the real verdict. - Only a status published by codeql-scan-dispatch.yml's own app identity - (opencode-agent[bot], minted via the same OIDC exchange - opencode-review-dispatch.yml uses) may satisfy the verdict read -- matching the + Only a status published by the handler's explicitly trusted app identities + (OpenCode or the organization-owned Noema status writer) may satisfy the verdict read -- matching the context string alone is not enough, since anyone with statuses:write on the repository can publish an arbitrary context (ADR 0025, "Poll target cannot be spoofed by the PR author"). This proves the forged success is @@ -303,11 +332,7 @@ def test_codeql_pr_one_shot_read_ignores_status_forged_by_non_opencode_creator(t tmp_path, statuses=[ {"context": "codeql-dispatch/python", "state": "success", "creator": {"login": "attacker"}}, - { - "context": "codeql-dispatch/python", - "state": "failure", - "creator": {"login": "opencode-agent[bot]"}, - }, + _bound_status("python", "failure"), ], ) assert dispatch_result.returncode == 0, dispatch_result.stderr @@ -317,6 +342,17 @@ def test_codeql_pr_one_shot_read_ignores_status_forged_by_non_opencode_creator(t def test_codeql_pr_one_shot_read_accepts_the_opencode_agent_creator(tmp_path: Path) -> None: """The legitimate handler's own success status is accepted once creator identity matches.""" + dispatch_result, verdict_result = _run_verdict_read( + tmp_path, + statuses=[_bound_status("python", "success")], + ) + assert dispatch_result.returncode == 0, dispatch_result.stderr + assert verdict_result.returncode == 0, verdict_result.stderr + assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout + + +def test_codeql_pr_one_shot_read_rejects_stale_unbound_status(tmp_path: Path) -> None: + """A trusted creator cannot make a status from an earlier base/run current.""" dispatch_result, verdict_result = _run_verdict_read( tmp_path, statuses=[ @@ -327,9 +363,9 @@ def test_codeql_pr_one_shot_read_accepts_the_opencode_agent_creator(tmp_path: Pa } ], ) - assert dispatch_result.returncode == 0, dispatch_result.stderr - assert verdict_result.returncode == 0, verdict_result.stderr - assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout + assert dispatch_result.returncode == 1, dispatch_result.stdout + assert verdict_result.returncode == 1 + assert "without an authenticated terminal verdict" in dispatch_result.stdout def test_codeql_pr_one_shot_read_accepts_clean_gate_when_wake_step_failed_job( @@ -352,6 +388,14 @@ def test_codeql_pr_one_shot_read_accepts_clean_gate_when_wake_step_failed_job( "name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success", }, + { + "name": "Verify GHAS base/head CodeQL configuration identity", + "conclusion": "success", + }, + { + "name": "Preserve CodeQL SARIF evidence", + "conclusion": "success", + }, { "name": "Wake exact CodeQL required job", "conclusion": "failure", @@ -363,10 +407,49 @@ def test_codeql_pr_one_shot_read_accepts_clean_gate_when_wake_step_failed_job( ) assert dispatch_result.returncode == 0, dispatch_result.stderr + dispatch_result.stdout assert verdict_result.returncode == 0, verdict_result.stderr + verdict_result.stdout - assert "completed CodeQL dispatch scan gate for python: success" in dispatch_result.stdout + assert "completed CodeQL dispatch proof for python" in dispatch_result.stdout assert "Current-head CodeQL dispatch verdict for python: success." in verdict_result.stdout +def test_codeql_pr_one_shot_read_rejects_clean_gate_when_ghas_identity_failed( + tmp_path: Path, +) -> None: + """A clean SARIF gate cannot hide a later GHAS identity proof failure.""" + head_sha = _TEST_HEAD_SHA + title = _dispatch_scan_title(head_sha=head_sha) + dispatch_result, verdict_result = _run_verdict_read( + tmp_path, + statuses=[], + dispatch_runs={"workflow_runs": [_completed_dispatch_run(title=title)]}, + dispatch_jobs={ + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "failure", + "steps": [ + { + "name": "Enforce CodeQL Medium+ SARIF gate", + "conclusion": "success", + }, + { + "name": "Verify GHAS base/head CodeQL configuration identity", + "conclusion": "failure", + }, + { + "name": "Preserve CodeQL SARIF evidence", + "conclusion": "success", + }, + ], + } + ] + }, + ) + + assert dispatch_result.returncode == 1 + assert "authenticated terminal proof" in dispatch_result.stdout + assert verdict_result.returncode == 1 + + def test_codeql_pr_one_shot_read_accepts_completed_dispatch_scan_job_when_status_unpublishable( tmp_path: Path, ) -> None: @@ -490,8 +573,32 @@ def test_codeql_pr_rejects_completed_dispatch_scan_from_a_different_required_run assert "completed CodeQL dispatch scan job for python: success" not in dispatch_result.stdout +def test_codeql_pr_rejects_completed_dispatch_scan_from_a_stale_merge_source( + tmp_path: Path, +) -> None: + """A regenerated live merge source cannot reuse its predecessor's scan.""" + stale_title = _dispatch_scan_title(producer_source_sha="d" * 40) + dispatch_result, _verdict_result = _run_verdict_read( + tmp_path, + statuses=[], + dispatch_runs={"workflow_runs": [_completed_dispatch_run(title=stale_title)]}, + dispatch_jobs={ + "jobs": [ + { + "name": "CodeQL dispatch scan (python)", + "conclusion": "success", + } + ] + }, + ) + + assert dispatch_result.returncode == 1, dispatch_result.stderr + dispatch_result.stdout + assert "without an authenticated terminal verdict" in dispatch_result.stdout + assert "completed CodeQL dispatch scan job for python: success" not in dispatch_result.stdout + + def test_codeql_pr_fallback_binds_live_base_and_required_run_identity() -> None: - """The required shard looks up the public dispatch run by immutable identity.""" + """The shard binds fallback proof to base, run, and live merge source.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") shard = workflow.split(" analyze-head:\n", 1)[1].split( " dispatch-current-head:\n", 1 @@ -501,9 +608,9 @@ def test_codeql_pr_fallback_binds_live_base_and_required_run_identity() -> None: assert 'live_base="$(printf' in shard assert ( 'expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}' - '@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}"' + '@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}/${live_merge}"' ) in shard - assert "Could not validate live pull request base SHA before CodeQL verdict read." in shard + assert "Could not validate live pull request base/source SHA before CodeQL verdict read." in shard def test_codeql_action_steps_use_one_version_per_workflow() -> None: @@ -616,6 +723,7 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( ' printf \'%s\\n\' "$4" >>"$FAKE_POST_LOG"\n' " exit 0\n" "fi\n" + 'if [[ "$*" == *"/actions/runs/42 --jq .created_at" ]]; then printf \'%s\\n\' "$FAKE_REQUIRED_CREATED_AT"; exit 0; fi\n' 'endpoint="${@: -1}"\n' 'case "$endpoint" in\n' " */pulls/*) printf '%s\\n' \"$FAKE_PULL_JSON\" ;;\n" @@ -635,12 +743,14 @@ def test_codeql_pr_attempt_one_without_verdict_fails_pending_without_dispatch( { "head": {"sha": head_sha}, "base": {"sha": _TEST_BASE_SHA}, + "merge_commit_sha": _TEST_PRODUCER_SOURCE_SHA, "state": "open", } ), "FAKE_STATUSES_JSON": json.dumps([]), "FAKE_DISPATCH_RUNS_JSON": json.dumps([{"workflow_runs": []}]), "FAKE_DISPATCH_JOBS_JSON": json.dumps([{"jobs": []}]), + "FAKE_REQUIRED_CREATED_AT": "2026-09-27T11:08:00Z", "FAKE_POST_LOG": str(post_log), "GH_TOKEN": "fake-token", "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", @@ -761,6 +871,7 @@ def _run_coordinator( "state": "open", "head": {"sha": head_sha, "ref": "feature"}, "base": {"sha": "a" * 40, "ref": "main"}, + "merge_commit_sha": "c" * 40, } jobs = jobs or { "total_count": 2, @@ -836,10 +947,12 @@ def test_codeql_coordinator_posts_one_dispatch_for_every_pending_language( "repos/ContextualWisdomLab/.github/dispatches" ] payload = json.loads(post_body.read_text(encoding="utf-8")) - assert payload["event_type"] == "codeql-scan" + assert payload["event_type"] == "codeql-scan-v2" client = payload["client_payload"] assert client["target_repository"] == "ContextualWisdomLab/naruon" assert client["pr_number"] == "42" + assert client["pr_head"] == {"schema": "1", "ref": "feature", "sha": "b" * 40} + assert client["producer_source_sha"] == "c" * 40 assert client["required_run_id"] == "99" assert "required_job_id" not in client assert "required_language" not in client @@ -858,16 +971,8 @@ def test_codeql_coordinator_skips_dispatch_when_every_language_has_a_verdict( result, post_log, post_body = _run_coordinator( tmp_path, statuses=[ - { - "context": "codeql-dispatch/python", - "state": "success", - "creator": {"login": "opencode-agent[bot]"}, - }, - { - "context": "codeql-dispatch/actions", - "state": "failure", - "creator": {"login": "opencode-agent[bot]"}, - }, + _bound_status("python", "success", required_run_id="99"), + _bound_status("actions", "failure", required_run_id="99"), ], ) @@ -912,6 +1017,7 @@ def test_codeql_coordinator_dispatches_the_live_base_after_a_same_head_retarget( "state": "open", "head": {"sha": "b" * 40, "ref": "feature"}, "base": {"sha": live_base, "ref": "release"}, + "merge_commit_sha": "d" * 40, }, env_overrides={"PR_BASE_SHA": "a" * 40, "PR_BASE_REF": "main"}, ) @@ -923,7 +1029,8 @@ def test_codeql_coordinator_dispatches_the_live_base_after_a_same_head_retarget( client = json.loads(post_body.read_text(encoding="utf-8"))["client_payload"] assert client["pr_base_sha"] == live_base assert client["pr_base_ref"] == "release" - assert client["pr_head_sha"] == "b" * 40 + assert client["pr_head"] == {"schema": "1", "ref": "feature", "sha": "b" * 40} + assert client["producer_source_sha"] == "d" * 40 assert client["required_run_id"] == "99" @@ -1012,3 +1119,33 @@ def test_codeql_control_routing_keeps_pr_workflows_hosted() -> None: assert choices[0] == trusted assert json.loads(choices[1]) == {"group": "CWL central control", "labels": ["self-hosted", "linux", "x64"]} assert json.loads(choices[2]) == "ubuntu-24.04" + + +def test_codeql_pr_rejects_invalid_required_run_time(tmp_path: Path) -> None: + """Missing time must fail closed rather than scan all workflow history.""" + dispatch, verdict = _run_verdict_read(tmp_path, statuses=[], required_created_at="null") + assert dispatch.returncode != 0 + assert "validate required run creation time" in dispatch.stdout + assert verdict.returncode != 0 + + +def test_codeql_pr_scopes_dispatch_history_to_required_run_creation() -> None: + """Server-side history filtering retains pagination and exact identity checks.""" + script = _extract_run_block(WORKFLOW_PATH.read_text(), DISPATCH_STEP_NAME) + assert '-f created=">=${required_created_at}"' in script + assert '-f event=repository_dispatch' in script + assert '--paginate --slurp' in script + assert 'select(.display_title == $title or .name == $title)' in script + + +def test_codeql_pr_accepts_only_bound_organization_owned_noema_status(tmp_path: Path) -> None: + """The owned publisher cannot reuse an earlier producer's success receipt.""" + for stale in (False, True): + case = tmp_path / ("stale" if stale else "current") + case.mkdir() + status = _bound_status("python", "success", + producer_source_sha="d" * 40 if stale else _TEST_PRODUCER_SOURCE_SHA) + status["creator"] = {"login": "cwl-noema-review[bot]"} + dispatch, verdict = _run_verdict_read(case, statuses=[status]) + assert (dispatch.returncode == 0) != stale, dispatch.stdout + dispatch.stderr + assert (verdict.returncode == 0) != stale, verdict.stdout + verdict.stderr diff --git a/tests/test_codeql_scan_dispatch_workflow_contract.py b/tests/test_codeql_scan_dispatch_workflow_contract.py index b74600fc8c..290211e85d 100644 --- a/tests/test_codeql_scan_dispatch_workflow_contract.py +++ b/tests/test_codeql_scan_dispatch_workflow_contract.py @@ -1143,6 +1143,10 @@ def _run_settlement_step( "run_attempt": 1, "steps": [ {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + { + "name": "Verify GHAS base/head CodeQL configuration identity", + "conclusion": "success", + }, {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, ], }, @@ -1153,6 +1157,10 @@ def _run_settlement_step( "run_attempt": 1, "steps": [ {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + { + "name": "Verify GHAS base/head CodeQL configuration identity", + "conclusion": "success", + }, {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, ], }, @@ -1571,6 +1579,49 @@ def test_dispatch_settlement_rejects_missing_handler_gate_steps(tmp_path: Path) assert not post_log.exists() +def test_dispatch_settlement_rejects_failed_ghas_identity_after_clean_gate( + tmp_path: Path, +) -> None: + """Settlement cannot wake a required run after GHAS identity proof failed.""" + result, post_log = _run_settlement_step( + tmp_path, + handler_jobs=[ + { + "name": "CodeQL dispatch scan (python)", + "status": "completed", + "conclusion": "failure", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + { + "name": "Verify GHAS base/head CodeQL configuration identity", + "conclusion": "failure", + }, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + { + "name": "CodeQL dispatch scan (actions)", + "status": "completed", + "conclusion": "success", + "run_attempt": 1, + "steps": [ + {"name": "Enforce CodeQL Medium+ SARIF gate", "conclusion": "success"}, + { + "name": "Verify GHAS base/head CodeQL configuration identity", + "conclusion": "success", + }, + {"name": "Preserve CodeQL SARIF evidence", "conclusion": "success"}, + ], + }, + ], + ) + + assert result.returncode == 1 + assert "missing GHAS configuration identity proof for python" in result.stdout + assert not post_log.exists() + + def test_dispatch_settlement_rejects_unproven_matrix_subset(tmp_path: Path) -> None: """Every required shard needs current handler gate and artifact evidence.""" result, post_log = _run_settlement_step( @@ -1694,3 +1745,76 @@ def test_codeql_scan_checkout_cleans_reused_workspace_without_persisting_token() assert 'persist-credentials: false' in block assert 'clean: true' in block assert 'git remote add origin' not in block + + +@pytest.mark.parametrize("creator,accepted", [ + ("cwl-noema-review[bot]", True), ("attacker", False), + ("opencode-agent[bot]", False), +]) +def test_owned_codeql_status_token_checks_its_actual_creator( + tmp_path: Path, creator: str, accepted: bool, +) -> None: + """The owned credential cannot silently publish as a different principal.""" + publish = _extract_run_block(WORKFLOW_PATH.read_text(), "Publish CodeQL dispatch status") + function = publish[publish.index("post_status() {"):publish.index('if post_status')] + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + gh = fake_bin / "gh" + response = json.dumps({"creator": {"login": creator}}) + gh.write_text("#!/bin/bash\nprintf '%s\\n' '" + response + "'\n") + gh.chmod(0o755) + env = {**os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", + "FAKE_CREATOR": creator, "TARGET_REPOSITORY": "ContextualWisdomLab/naruon", + "HEAD_SHA": "b" * 40, "state": "success", + "receipt_context": "codeql-dispatch/python", "receipt_description": "verified", + "GITHUB_SERVER_URL": "https://github.com", "GITHUB_REPOSITORY": "ContextualWisdomLab/.github", + "GITHUB_RUN_ID": "100"} + result = subprocess.run(["bash"], input='set -euo pipefail\n' + function + + '\npost_status noema-status-token synthetic-owned-token\n', + env=env, text=True, capture_output=True) + assert (result.returncode == 0) == accepted, result.stdout + result.stderr + + +def test_owned_codeql_wake_preserves_exact_run_wide_settlement(tmp_path: Path) -> None: + """The owned token follows the existing full proof before a single mutation.""" + result, posts = _run_settlement_step(tmp_path, extra_env={ + "NOEMA_WAKE_TOKEN": "owned-token", "TARGET_APP_WAKE_TOKEN": "foreign-token", + "FAKE_DENIED_TOKEN": "foreign-token", "GITHUB_WAKE_TOKEN": "", + }) + assert result.returncode == 0, result.stdout + result.stderr + assert "using noema-settlement-token" in result.stdout + assert posts.read_text().splitlines() == [ + "repos/ContextualWisdomLab/naruon/actions/runs/42/rerun-failed-jobs", + ] + + +def test_owned_codeql_writers_are_separate_target_scoped_credentials() -> None: + """Read, status and wake tokens each retain one narrow permission purpose.""" + workflow = WORKFLOW_PATH.read_text() + for name,config,permission in ( + ("Noema CodeQL status token", "noema_analysis_config", "statuses"), + ("Noema CodeQL settlement token", "noema_settlement_config", "actions"), + ): + step = workflow.split(f" - name: Mint target-scoped {name}\n", 1)[1].split(" - name:", 1)[0] + assert f"repositories: ${{{{ steps.{config}.outputs.repository }}}}" in step + assert f"permission-{permission}: write" in step + assert "permission-security-events" not in step + assert "continue-on-error: true" in step + + +def test_owned_status_configuration_survives_failed_analysis_gate(tmp_path: Path) -> None: + """A failed gate can publish failure without minting an analysis reader.""" + workflow = WORKFLOW_PATH.read_text() + config = workflow.split(" id: noema_analysis_config\n", 1)[1].split(" - name:", 1)[0] + assert "if: always() && steps.live_metadata.outcome == 'success'" in config + reader = workflow.split(" id: noema_analysis_token\n", 1)[1].split(" - name:", 1)[0] + assert "if: steps.gate.outcome == 'success'" in reader + output = tmp_path / "outputs" + script = _extract_run_block(workflow, "Detect optional Noema analysis-read credential") + result = subprocess.run(["bash"], input=script, text=True, capture_output=True, + env={**os.environ, "TARGET_REPOSITORY": "ContextualWisdomLab/disksage", + "NOEMA_APP_CLIENT_ID": "synthetic-client", + "NOEMA_APP_PRIVATE_KEY": "synthetic-key", + "GITHUB_OUTPUT": str(output)}) + assert result.returncode == 0, result.stdout + result.stderr + assert output.read_text().splitlines() == ["repository=disksage", "available=true"] diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index b6dbb6b48a..e602f19268 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -585,3 +585,22 @@ def test_required_strix_uses_the_gateway_and_zdr_visibility_contract() -> None: "Provision contextual-orchestrator Strix sidecar" ) assert "STRIX_FALLBACK_MODELS: \"\"" in workflow + + +def test_sidecar_uses_lock_compatible_isolated_python() -> None: + """Every entry point provisions the wheel ABI before an isolated installation.""" + text = _read(SIDECAR) + guard = text.index('sys.version_info[:2] == (3, 12)') + venv = text.index('"$sidecar_python" -m venv "$ORCHESTRATOR_WORK/.venv"') + select = text.index('sidecar_python="$ORCHESTRATOR_WORK/.venv/bin/python"') + install = text.index('"$sidecar_python" -m pip install') + assert guard < venv < select < install + for path in (STRIX_WORKFLOW, NOEMA_WORKFLOW, OPENCODE_DISPATCH_WORKFLOW, + AUTOFIX_WORKFLOW, _ORG_REPO_ROOT / ".github/actions/orchestrator-free-sidecar/action.yml"): + workflow = _read(path) + setup = workflow.index("Set up lock-compatible sidecar Python") + call = workflow.index("contextual_orchestrator_review_sidecar.sh") + assert setup < call + assert 'python-version: "3.12"' in workflow[setup:call] + assert 'update-environment: false' in workflow[setup:call] + assert 'SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }}' in workflow[setup:call] diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index 6a1908e6f8..a0f691e2a2 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -93,12 +93,12 @@ def test_gate_job_is_byte_identical_across_the_five_workflows_apart_from_if(): workflow = _read(filename) block = _top_level_job_block(workflow, "changed-scope") normalized = "\n".join( - line for line in block.splitlines() if not line.strip().startswith("if:") + line for line in block.splitlines() if not line.strip().startswith(("if:", "runs-on:")) ) normalized_blocks.add(normalized) assert len(normalized_blocks) == 1, ( "changed-scope gate copies drifted; keep them byte-identical apart " - "from the single 'if:' line" + "from the event guard and separately tested runner allocation" ) @@ -132,9 +132,9 @@ def test_gate_jobs_use_supported_runner_allocation(): """Scope jobs preserve trusted-main routing and a supported hosted fallback.""" for filename in GATE_WORKFLOWS: block = _top_level_job_block(_read(filename), "changed-scope") - if filename == "opencode-review.yml": + if filename in ("opencode-review.yml", "strix.yml"): assert '"group":"CWL central control"' in block, filename - assert "github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main'" in block, filename + assert f"github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/{filename}@refs/heads/main'" in block, filename assert "fromJSON('[\"ubuntu-24.04\"]')" in block, filename elif filename == "noema-review.yml": assert "endsWith(github.workflow_ref, '@refs/heads/main')" in block, filename diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 51173c85ab..b9cd522819 100644 --- a/tests/test_noema_orchestrator_workflow_contract.py +++ b/tests/test_noema_orchestrator_workflow_contract.py @@ -214,7 +214,7 @@ def test_noema_review_credentials_and_llm_use_orchestrator_free() -> None: assert "needs.noema-review.result == 'failure'" in continuation_job assert " contents: write" in continuation_job assert " pull-requests: read" in continuation_job - assert "GH_TOKEN: ${{ github.token }}" in continuation_job + assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || github.token }}" in continuation_job assert "${TARGET_REPOSITORY}" in continuation_job assert '"$GITHUB_REPOSITORY"' in continuation_job assert "uses: actions/checkout" not in continuation_job @@ -231,8 +231,8 @@ def test_noema_review_credentials_and_llm_use_orchestrator_free() -> None: assert "secrets: inherit" not in workflow -def test_noema_continuation_dispatch_uses_only_live_same_repo_head_and_base(tmp_path: Path) -> None: - """Execute the privileged step against a fake API before allowing dispatch.""" +def test_noema_continuation_dispatch_uses_central_handler_and_live_identity(tmp_path: Path) -> None: + """Central continuation preserves target identity and rejects stale or fork heads.""" script = textwrap.dedent( workflow_step( workflow_text("noema-review.yml"), @@ -240,10 +240,11 @@ def test_noema_continuation_dispatch_uses_only_live_same_repo_head_and_base(tmp_ ).split(" run: |\n", 1)[1] ) calls = tmp_path / "dispatch.json" + endpoint = tmp_path / "endpoint.txt" fake_gh = tmp_path / "gh" fake_gh.write_text( '#!/bin/bash\nif [[ "$*" == *"/pulls/"* ]]; then printf "%s" "$LIVE_PR"; ' - 'else cat >"$DISPATCH_FILE"; fi\n', + 'else printf "%s" "$*" >"$ENDPOINT_FILE"; cat >"$DISPATCH_FILE"; exit "${POST_EXIT_CODE:-0}"; fi\n', encoding="utf-8", ) fake_gh.chmod(0o755) @@ -265,10 +266,11 @@ def test_noema_continuation_dispatch_uses_only_live_same_repo_head_and_base(tmp_ "PROVIDER_ATTEMPT_COUNT": "2", "TRANSPORT_HTTP_STATUS": "429", "DISPATCH_FILE": str(calls), + "ENDPOINT_FILE": str(endpoint), "LIVE_PR": json.dumps( { "state": "open", - "head": {"sha": head}, + "head": {"sha": head, "repo": {"full_name": "ContextualWisdomLab/demo"}}, "base": {"sha": base, "repo": {"full_name": "ContextualWisdomLab/demo"}}, } ), @@ -288,7 +290,22 @@ def run(values: dict[str, str]) -> subprocess.CompletedProcess[str]: "pr_head_sha": head, "transport_retry_attempt": 1, } + assert "repos/ContextualWisdomLab/.github/dispatches" in endpoint.read_text() calls.unlink() + central = {**env, "GITHUB_REPOSITORY": "ContextualWisdomLab/.github"} + assert run(central).returncode == 0 + assert calls.exists() + calls.unlink() + refused = run({**central, "POST_EXIT_CODE": "1"}) + assert refused.returncode != 0 + assert "Scheduled Noema transport continuation" not in refused.stdout + calls.unlink() + fork = json.loads(env["LIVE_PR"]) + fork["head"]["repo"]["full_name"] = "outside/demo" + assert run({**central, "LIVE_PR": json.dumps(fork)}).returncode == 0 + assert not calls.exists() + assert run({**env, "GITHUB_REPOSITORY": "ContextualWisdomLab/unrelated"}).returncode != 0 + assert not calls.exists() assert run({**env, "TARGET_REPOSITORY": "ContextualWisdomLab/other"}).returncode != 0 assert not calls.exists() changed_base = json.loads(env["LIVE_PR"]) diff --git a/tests/test_noema_preflight_capacity.py b/tests/test_noema_preflight_capacity.py new file mode 100644 index 0000000000..a6ed03db3c --- /dev/null +++ b/tests/test_noema_preflight_capacity.py @@ -0,0 +1,309 @@ +"""All-429 sidecar preflight reaches the existing bounded continuation. + +Reused from PR #2339; fixtures follow the producer preflight contract. +""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from scripts.ci import noema_preflight_capacity as capacity +from scripts.ci import noema_review_gate as gate + +HEAD = "e2393877" + "0" * 32 + + +def _route(index: int, **overrides: object) -> dict[str, object]: + """Return one rejected-429 route row in the launcher's sanitized schema.""" + row: dict[str, object] = { + "agent_id": f"openrouter-{index}", + "provider": "openrouter", + "model": f"vendor/model-{index}:free", + "attempts": 1, + "status": "rejected", + "error_type": "HTTPError", + "http_status": 429, + } + row.update(overrides) + return row + + +def _report(routes: list[dict[str, object]], **overrides: object) -> dict[str, object]: + """Return a failed preflight report shaped like the measured 5-candidate case.""" + report: dict[str, object] = { + "contract": "strix-plain-chat-preflight-v2", + "candidate_count": len(routes), + "probed_count": len(routes), + "ready_count": 0, + "deferred_count": 0, + "rejected_count": len(routes), + "skipped_count": 0, + "postponed_probed_count": 3, + "target_ready": 8, + "probe_budget": 12, + "account_skip_after_429": 2, + "escalations_used": 0, + "escalation_budget": 4, + "routes": routes, + } + report.update(overrides) + return report + + +def _all_429(count: int = 5) -> dict[str, object]: + """Return the measured all-429 failure shape.""" + return _report([_route(index) for index in range(count)]) + + +def _run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, payload: object | None, + *, attempt: str = "0", raw: str | None = None) -> dict[str, str]: + """Run the CLI against one preflight file and return its GitHub outputs.""" + report_path = tmp_path / "contextual-orchestrator-preflight.json" + if raw is not None: + report_path.write_text(raw, encoding="utf-8") + elif payload is not None: + report_path.write_text(json.dumps(payload), encoding="utf-8") + output_path = tmp_path / "github_output" + output_path.write_text("", encoding="utf-8") + monkeypatch.setenv("GITHUB_OUTPUT", str(output_path)) + monkeypatch.setenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", attempt) + assert capacity.main(["--preflight-report", str(report_path), "--expected-head", HEAD]) == 0 + outputs: dict[str, str] = {} + for line in output_path.read_text(encoding="utf-8").splitlines(): + key, _, value = line.partition("=") + outputs[key] = value + return outputs + + +@pytest.mark.parametrize("count", [4, 5]) +def test_measured_all_429_preflight_is_capacity_and_eligible(tmp_path, monkeypatch, count): + """Both measured failure shapes schedule the same bounded re-dispatch.""" + outputs = _run(tmp_path, monkeypatch, _all_429(count)) + expected_delay = gate.transport_redispatch_delay_seconds( + transport_retry_attempt=0, head_sha=HEAD + ) + assert outputs == { + "transport_capacity_unavailable": "true", + "transport_retry_eligible": "true", + "transport_http_status": "429", + "provider_attempt_count": str(count), + "transport_retry_delay_seconds": str(expected_delay), + "transport_retry_next_attempt": "1", + } + assert gate.TRANSPORT_REDISPATCH_JITTER_MIN_SECONDS <= expected_delay + assert expected_delay <= gate.TRANSPORT_REDISPATCH_JITTER_MAX_SECONDS + + +def test_second_attempt_advances_the_shared_counter(tmp_path, monkeypatch): + """The preflight path consumes the same NOEMA_TRANSPORT_RETRY_ATTEMPT counter.""" + outputs = _run(tmp_path, monkeypatch, _all_429(), attempt="1") + assert outputs["transport_retry_eligible"] == "true" + assert outputs["transport_retry_next_attempt"] == "2" + + +def test_exhausted_attempts_stay_capacity_but_not_eligible(tmp_path, monkeypatch, capsys): + """At the ADR-0031 bound the run fails closed with no further re-dispatch.""" + attempt = str(gate.MAX_TRANSPORT_REDISPATCH_ATTEMPTS) + outputs = _run(tmp_path, monkeypatch, _all_429(), attempt=attempt) + assert outputs["transport_capacity_unavailable"] == "true" + assert outputs["transport_retry_eligible"] == "false" + assert "transport_retry_delay_seconds" not in outputs + assert "transport_retry_next_attempt" not in outputs + assert "Review remains required" in capsys.readouterr().out + + +def test_in_cap_retry_after_is_honored_as_the_longest_stated_wait(tmp_path, monkeypatch): + """A provider Retry-After within the existing cap replaces the jitter.""" + routes = [_route(0, retry_after_s=5), _route(1, retry_after_s=40), _route(2)] + outputs = _run(tmp_path, monkeypatch, _report(routes)) + assert outputs["transport_retry_delay_seconds"] == "40" + assert outputs["transport_retry_eligible"] == "true" + + +@pytest.mark.parametrize("value", [0, gate.TRANSPORT_REDISPATCH_RETRY_AFTER_MAX_SECONDS + 1, "5", True]) +def test_out_of_cap_retry_after_falls_back_to_jitter(tmp_path, monkeypatch, value): + """An out-of-range or non-int Retry-After neither widens the cap nor kills eligibility.""" + outputs = _run(tmp_path, monkeypatch, _report([_route(0, retry_after_s=value), _route(1)])) + expected_delay = gate.transport_redispatch_delay_seconds( + transport_retry_attempt=0, head_sha=HEAD + ) + assert outputs["transport_retry_delay_seconds"] == str(expected_delay) + assert outputs["transport_retry_eligible"] == "true" + + +def test_deferred_429_rows_count_as_capacity(tmp_path, monkeypatch): + """A deferred 429 row is still a capacity answer.""" + routes = [_route(0, status="deferred"), _route(1)] + outputs = _run(tmp_path, monkeypatch, _report(routes, deferred_count=1)) + assert outputs["transport_capacity_unavailable"] == "true" + + +def test_nested_primary_attempt_must_also_be_all_429(tmp_path, monkeypatch): + """A fallback-stage report counts only when its nested primary stage is all-429 too.""" + good = _report([_route(0)], primary_attempt=_all_429(3)) + assert _run(tmp_path, monkeypatch, good)["transport_capacity_unavailable"] == "true" + bad_primary = _report([_route(1, http_status=404)]) + bad = _report([_route(0)], primary_attempt=bad_primary) + assert _run(tmp_path, monkeypatch, bad)["transport_capacity_unavailable"] == "false" + + +NOT_CAPACITY_REPORTS = { + "404_and_429_mix": _report([_route(0), _route(1, http_status=404)]), + "500_and_429_mix": _report([_route(0), _route(1, http_status=500)]), + "remote_disconnected_and_429_mix": _report( + [_route(0), {k: v for k, v in _route(1, error_type="RemoteDisconnected").items() + if k != "http_status"}] + ), + "escalation_budget_exhausted": _report( + [_route(0), {k: v for k, v in _route(1, error_type="escalation_budget_exhausted").items() + if k != "http_status"}] + ), + "string_429": _report([_route(0, http_status="429")]), + "bool_status": _report([_route(0, http_status=True)]), + "ready_route_present": _report([_route(0), _route(1, status="ready")], ready_count=1), + "ready_row_with_zero_count": _report([_route(0), _route(1, status="ready")]), + "ready_count_positive": _report([_route(0)], ready_count=1), + "ready_count_bool": _report([_route(0)], ready_count=False), + "zero_candidates": _report([], candidate_count=0, probed_count=0), + "probed_count_missing": {k: v for k, v in _all_429().items() if k != "probed_count"}, + "routes_count_mismatch": _report([_route(0)], probed_count=2), + "routes_not_list": {**_report([_route(0)]), "routes": {"0": _route(0)}}, + "route_not_dict": _report([_route(0), "429"], probed_count=2), + "wrong_contract": _report([_route(0)], contract="strix-plain-chat-preflight-v1"), + "primary_attempt_not_dict": _report([_route(0)], primary_attempt=["x"]), + "json_list": [_route(0)], +} + + +@pytest.mark.parametrize("name", sorted(NOT_CAPACITY_REPORTS)) +def test_anything_but_all_429_keeps_plain_failure(tmp_path, monkeypatch, name): + """Non-429 rejections and out-of-contract evidence are never capacity.""" + outputs = _run(tmp_path, monkeypatch, NOT_CAPACITY_REPORTS[name]) + assert outputs == { + "transport_capacity_unavailable": "false", + "transport_retry_eligible": "false", + } + + +@pytest.mark.parametrize("raw", ["", " \n", "{not json", "[" * 5000]) +def test_empty_or_malformed_report_keeps_plain_failure(tmp_path, monkeypatch, raw): + """The sidecar truncates the report at start; early failures leave it empty.""" + outputs = _run(tmp_path, monkeypatch, None, raw=raw) + assert outputs["transport_capacity_unavailable"] == "false" + assert outputs["transport_retry_eligible"] == "false" + + +def test_non_utf8_report_keeps_plain_failure(tmp_path, monkeypatch): + """Undecodable bytes are not capacity evidence.""" + (tmp_path / "contextual-orchestrator-preflight.json").write_bytes(b"\xff\xfe{") + outputs = _run(tmp_path, monkeypatch, None) + assert outputs["transport_capacity_unavailable"] == "false" + + +def test_missing_report_keeps_plain_failure(tmp_path, monkeypatch): + """A sidecar that failed before creating evidence is not capacity.""" + outputs = _run(tmp_path, monkeypatch, None) + assert outputs["transport_capacity_unavailable"] == "false" + + +def test_directory_report_path_keeps_plain_failure(tmp_path, monkeypatch): + """An unreadable report path is not capacity.""" + (tmp_path / "contextual-orchestrator-preflight.json").mkdir() + outputs = _run(tmp_path, monkeypatch, None) + assert outputs["transport_capacity_unavailable"] == "false" + + +def test_oversized_report_keeps_plain_failure(tmp_path, monkeypatch): + """The classifier reads a bounded prefix and rejects anything larger.""" + padded = _all_429() + padded["padding"] = "x" * capacity.MAX_PREFLIGHT_REPORT_BYTES + outputs = _run(tmp_path, monkeypatch, padded) + assert outputs["transport_capacity_unavailable"] == "false" + + +def test_noncanonical_head_emits_no_redispatch_outputs(tmp_path, monkeypatch, capsys): + """A malformed expected head cannot key a delay or a continuation.""" + report_path = tmp_path / "preflight.json" + report_path.write_text(json.dumps(_all_429()), encoding="utf-8") + output_path = tmp_path / "github_output" + output_path.write_text("", encoding="utf-8") + monkeypatch.setenv("GITHUB_OUTPUT", str(output_path)) + argv = ["--preflight-report", str(report_path), "--expected-head", HEAD.upper()] + assert capacity.main(argv) == 0 + assert output_path.read_text(encoding="utf-8") == "" + assert "canonical" in capsys.readouterr().out + + +def test_eligible_run_prints_a_capacity_notice(tmp_path, monkeypatch, capsys): + """The job log names the preflight capacity class and the scheduled attempt.""" + _run(tmp_path, monkeypatch, _all_429()) + out = capsys.readouterr().out + assert "::notice::" in out + assert "all-429" in out + assert f"attempt 1/{gate.MAX_TRANSPORT_REDISPATCH_ATTEMPTS}" in out + + +def test_workflow_invocation_needs_only_the_standard_library(tmp_path): + """The classify step runs on the runner's bare python3 after the sidecar failed. + + defusedxml is installed only by the later HWP reader step, which is skipped + once provisioning fails, so the script's import closure must be stdlib-only. + """ + script = Path(__file__).resolve().parents[1] / "scripts" / "ci" / "noema_preflight_capacity.py" + report_path = tmp_path / "contextual-orchestrator-preflight.json" + report_path.write_text(json.dumps(_all_429()), encoding="utf-8") + output_path = tmp_path / "github_output" + output_path.write_text("", encoding="utf-8") + wrapper = ( + "import runpy, sys\n" + "sys.modules['defusedxml'] = None\n" + "sys.argv = sys.argv[1:]\n" + "runpy.run_path(sys.argv[0], run_name='__main__')\n" + ) + env = { + "PATH": "/usr/bin:/bin", + "GITHUB_OUTPUT": str(output_path), + "NOEMA_TRANSPORT_RETRY_ATTEMPT": "0", + } + result = subprocess.run( + [sys.executable, "-c", wrapper, str(script), + "--preflight-report", str(report_path), "--expected-head", HEAD], + cwd=tmp_path, env=env, capture_output=True, text=True, check=False, + ) + assert result.returncode == 0, result.stderr + written = output_path.read_text(encoding="utf-8") + assert "transport_capacity_unavailable=true\n" in written + assert "transport_retry_eligible=true\n" in written + assert "transport_retry_next_attempt=1\n" in written + + +def test_preflight_reader_refuses_links_and_fifo_without_blocking(tmp_path): + import os + + path = tmp_path / 'report.json' + path.write_text(json.dumps(_all_429())) + link = tmp_path / 'link.json' + link.symlink_to(path) + assert capacity.load_preflight_report(link) is None + fifo = tmp_path / 'fifo.json' + os.mkfifo(fifo) + assert capacity.load_preflight_report(fifo) is None + hardlink = tmp_path / 'hardlink.json' + os.link(path, hardlink) + assert capacity.load_preflight_report(hardlink) is None + + +@pytest.mark.parametrize('attempt', ['garbage', '-1', 'true', '999']) +def test_invalid_retry_counter_exhausts_budget(tmp_path, monkeypatch, attempt): + report = tmp_path / 'report.json' + report.write_text(json.dumps(_all_429())) + monkeypatch.setenv('NOEMA_TRANSPORT_RETRY_ATTEMPT', attempt) + outputs = capacity.emit_preflight_capacity_outputs(report, expected_head=HEAD) + assert outputs['transport_capacity_unavailable'] == 'true' + assert outputs['transport_retry_eligible'] == 'false' + assert 'transport_retry_next_attempt' not in outputs diff --git a/tests/test_noema_two_phase_handoff.py b/tests/test_noema_two_phase_handoff.py index fd905fa343..0e2c5e2fe5 100644 --- a/tests/test_noema_two_phase_handoff.py +++ b/tests/test_noema_two_phase_handoff.py @@ -265,3 +265,17 @@ def raise_capacity(*_args: object, **_kwargs: object) -> None: assert "transport_retry_eligible=false" in written assert "transport_retry_delay_seconds=" not in written assert "automatic re-dispatch budget is exhausted" in capsys.readouterr().out + + +def test_sidecar_failure_outputs_reach_existing_continuation(): + """Startup stays failed while its capacity outputs reach the same-head job.""" + text = (ROOT / '.github/workflows/noema-review.yml').read_text() + classify = text.split(' - name: Classify sidecar provider-capacity failure', 1)[1].split(' - name:', 1)[0] + assert "if: failure() && steps.noema_sidecar.outcome == 'failure'" in classify + assert '--preflight-report' in classify + provision = text.split(' - name: Provision contextual-orchestrator review sidecar', 1)[1].split(' - name:', 1)[0] + assert 'continue-on-error' not in provision + assert 'rm -f "$GITHUB_WORKSPACE/strix_runs/contextual-orchestrator-preflight.json"' in provision + for name in ('transport_capacity_unavailable', 'transport_retry_eligible', + 'transport_retry_delay_seconds', 'transport_retry_next_attempt'): + assert f'steps.noema_prepare.outputs.{name} || steps.noema_sidecar_failure.outputs.{name}' in text diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index f9bc5888ea..1a428556ad 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -644,6 +644,36 @@ def opener(url: str, _token: str) -> object: assert [item.rule for item in result] == ["nginx_runtime_path"] +@pytest.mark.parametrize("name", ["deploy.sh", "deploy.dat", "deploy.txt"]) +def test_declared_prefix_does_not_admit_binary_marked_nginx_runtime(name: str) -> None: + """A stray non-UTF-8 byte cannot hide readable runtime commands.""" + + raw = b"#!/bin/sh\nnginx -c /etc/nginx/nginx.conf\n# \xff\n" + + def opener(url: str, _token: str) -> object: + if "/pulls/2197/files" in url: + return [{"filename": f"docs/delivery_interim_20260920/{name}", "status": "added"}] + if _declaration_url_fragment("main") in url: + return encoded_file("docs/delivery_interim_20260920/\n") + assert f"/contents/docs/delivery_interim_20260920/{name}" in url + return { + "type": "file", "encoding": "base64", "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + with pytest.raises(policy.PolicyError, match="not valid UTF-8"): + policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=2197, + head_sha="e" * 40, + event_action="opened", + token="token", + base_ref="main", + opener=opener, + ) + + @pytest.mark.parametrize( ("declaration_text", "message"), [ diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index e6ecbedf73..e724abc9ad 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,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 = "e691911e6f7c374247a0a86b57b74aafc88a3c60" +REVIEW_DISPATCH_BLOB_SHA = "135fca5f848ef7829b016460f1c90ee66eeb55f4" def _workflow_text(path: Path) -> str: diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 5c02e449dc..abe5a411c7 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -4896,7 +4896,17 @@ def fake_run(args, stdin=None): ] assert calls[2:] == [ ["gh", "api", "-X", "POST", "repos/owner/repo/dispatches", "--input", "-"], - ["gh", "api", "-X", "POST", "repos/owner/repo/actions/jobs/202/rerun"], + # OpenCode dispatch invalidates the run cache; Strix must recheck it + # before starting a fresh trusted-runtime scan. + [ + "gh", "api", "--method", "GET", "repos/owner/repo/actions/runs", + "--paginate", "--slurp", "-f", "status=queued", "-F", "per_page=100", + ], + [ + "gh", "api", "--method", "GET", "repos/owner/repo/actions/runs", + "--paginate", "--slurp", "-f", "status=in_progress", "-F", "per_page=100", + ], + ["gh", "api", "-X", "POST", "repos/owner/repo/dispatches", "--input", "-"], ] @@ -5466,6 +5476,11 @@ def fake_run_with_env(args, *, stdin=None, env=None): def test_dispatch_strix_evidence_rerun_defers_to_bounded_admission_budget(monkeypatch, tmp_path): """Rerunning an existing Strix job also respects the durable admission budget.""" + # Existing jobs now recover through fresh central dispatch, including its + # Actions control and active-run checks. Keep external calls mocked. + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda *_: None) + monkeypatch.setattr(sched, "active_review_run_refs", lambda *_a, **_k: ([], [])) + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_: []) pr = make_pr(baseRefOid="b" * 40, headRefOid="a" * 40) monkeypatch.setattr(sched, "matching_actions_job_id", lambda *_args: "202") @@ -5478,6 +5493,11 @@ def test_dispatch_strix_evidence_rerun_defers_to_bounded_admission_budget(monkey def test_dispatch_strix_evidence_rerun_rechecks_live_head(monkeypatch): """Rerunning an existing Strix job rechecks the exact live head first.""" + # Existing jobs now recover through fresh central dispatch, including its + # Actions control and active-run checks. Keep external calls mocked. + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda *_: None) + monkeypatch.setattr(sched, "active_review_run_refs", lambda *_a, **_k: ([], [])) + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_: []) pr = make_pr(baseRefOid="b" * 40, headRefOid="a" * 40) monkeypatch.setattr(sched, "matching_actions_job_id", lambda *_args: "202") monkeypatch.setattr(sched, "fetch_pr", lambda *_args: [make_pr(headRefOid="c" * 40)]) @@ -11141,3 +11161,25 @@ def behind_with(nodes): assert "checks are still queued or running" not in resumed.reason assert sched.has_in_flight_check_runs(behind_with([])) is False + + +def test_strix_failed_job_recovers_via_fresh_central_runtime(monkeypatch): + """An existing job must not pin recovery to its original broken runtime.""" + pr = make_pr(baseRefOid="b" * 40, headRefOid="a" * 40) + calls = [] + monkeypatch.setattr(sched, "matching_actions_job_id", lambda *_: "108529710783") + monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda *_: None) + monkeypatch.setattr(sched, "active_review_run_refs", lambda *_a, **_k: ([], [])) + monkeypatch.setattr(sched, "active_workflow_runs", lambda *_: []) + monkeypatch.setattr(sched, "review_dispatch_admitted", lambda *_: True) + monkeypatch.setattr(sched, "live_dispatch_head_matches", lambda *_: True) + monkeypatch.setattr(sched, "repository_dispatch_target", lambda _: "ContextualWisdomLab/.github") + monkeypatch.setattr(sched, "run_github_dispatch", lambda args, stdin: calls.append((args, json.loads(stdin)))) + monkeypatch.setattr(sched, "rerun_actions_job", lambda *_a, **_k: pytest.fail("old job runtime was reused")) + assert sched.dispatch_strix_evidence("owner/repo", "Strix Security Scan", pr, dry_run=False) == "dispatched" + assert len(calls) == 1 + args, payload = calls[0] + assert "repos/ContextualWisdomLab/.github/dispatches" in args + assert payload["event_type"] == "strix-scan" + assert payload["client_payload"]["pr_head_sha"] == pr["headRefOid"] + assert payload["client_payload"]["pr_base_sha"] == pr["baseRefOid"] diff --git a/tests/test_release_dependency_gate_workflow_contract.py b/tests/test_release_dependency_gate_workflow_contract.py index 84698cf7a5..7fad92bc85 100644 --- a/tests/test_release_dependency_gate_workflow_contract.py +++ b/tests/test_release_dependency_gate_workflow_contract.py @@ -130,7 +130,7 @@ def test_trusted_gate_is_materialized_from_this_repository_at_its_pinned_sha() - """The decision code is the base repository's, never the caller's tree.""" workflow = _workflow_text() assert "repository: ContextualWisdomLab/.github" in workflow - assert workflow.count("ref: d67a71750516dad8dbe5f76d4d7c9ff3b4184ea3") == 3 + assert workflow.count("ref: 64bb4e7d32667980223c70afda81ffac97209630") == 3 assert "path: trusted-gate" in workflow assert "persist-credentials: false" in workflow # The whole scripts/ci tree, because the trusted Strix gate, the diff --git a/tests/test_release_dependency_license_text_evidence.py b/tests/test_release_dependency_license_text_evidence.py index 8c15afc43b..b558a960b3 100644 --- a/tests/test_release_dependency_license_text_evidence.py +++ b/tests/test_release_dependency_license_text_evidence.py @@ -76,7 +76,7 @@ def test_a_recognized_text_that_contradicts_the_declaration_is_refused( tmp_path, license_expression="MIT", license_texts={"LICENSE": REVIEWED_TEXTS["atheris-3.1.0.txt"]}, - ) == [gate.LICENSE_TEXT_DISAGREEMENT] + ) == [gate.LICENSE_TEXT_DISAGREEMENT, policy.LICENSE_TEXT_MISSING] def test_a_denied_title_still_fails_as_a_disagreement(tmp_path: Path) -> None: @@ -137,7 +137,7 @@ def test_a_dual_licence_selection_is_checked_against_every_declared_identifier( } ], ) - assert _codes(capture) == [gate.LICENSE_TEXT_DISAGREEMENT] + assert _codes(capture) == [gate.LICENSE_TEXT_DISAGREEMENT, policy.LICENSE_TEXT_MISSING] @pytest.mark.parametrize("expression,body", [ diff --git a/tests/test_release_fixed_helper_identity.py b/tests/test_release_fixed_helper_identity.py index 8137a56737..2d4ec0675c 100644 --- a/tests/test_release_fixed_helper_identity.py +++ b/tests/test_release_fixed_helper_identity.py @@ -32,8 +32,8 @@ def test_literal_source_pin_and_sibling_scope(filename, destination): pin, tree = _pin_and_tree(script) if filename == "release-dependency-license-strix-gate.yml": assert (pin, tree) == ( - "aea63e1161cc2a9b63cf18c0c3ad6754fdef19a7", - "2048f968ea925a349651a3daae6064733e1667b5", + "64bb4e7d32667980223c70afda81ffac97209630", + "b15b746d2a1c38501c2ecd6f6c5962c27c9d11af", ) assert f"ref: {pin}" in checkout assert "repository: ContextualWisdomLab/.github" in checkout diff --git a/tests/test_required_review_runner_image_contract.py b/tests/test_required_review_runner_image_contract.py index 212ae41c61..ab7ede7aa4 100644 --- a/tests/test_required_review_runner_image_contract.py +++ b/tests/test_required_review_runner_image_contract.py @@ -27,8 +27,20 @@ def assert_explicit_supported_image(self, path: Path) -> None: self.assertEqual(runs_on, {"runs-on: ubuntu-24.04"}) def test_strix_uses_explicit_supported_image(self) -> None: - """Require every Strix job to use explicit Ubuntu 24.04.""" - self.assert_explicit_supported_image(STRIX) + """Route trusted metadata to control while preserving the scan image.""" + workflow = STRIX.read_text(encoding="utf-8") + for name in ("changed-scope", "admit-current-head", "cancel-superseded-pr-runs", "publish-manual-pr-evidence-status"): + block = re.split(r"\n [a-z][a-z-]*:\n", workflow.split(f"\n {name}:\n", 1)[1], maxsplit=1)[0] + self.assertIn('"group":"CWL central control"', block) + self.assertIn('"labels":["self-hosted","linux","x64","cwlab-control"]', block) + self.assertIn("github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main'", block) + self.assertIn("github.repository == 'ContextualWisdomLab/.github'", block) + self.assertIn("github.repository == 'ContextualWisdomLab/fast-mlsirm'", block) + self.assertIn("fromJSON('[\"ubuntu-24.04\"]')", block) + self.assertNotIn("actions/checkout", block) + scan = workflow.split("\n strix:\n", 1)[1].split("\n publish-manual-pr-evidence-status:\n", 1)[0] + self.assertIn("runs-on: ubuntu-24.04", scan) + self.assertNotIn("cwlab-control", scan) def test_opencode_review_uses_explicit_supported_image(self) -> None: """Keep metadata-only OpenCode admission on the trusted control pool.""" @@ -43,9 +55,12 @@ def test_opencode_review_uses_explicit_supported_image(self) -> None: def test_noema_review_uses_explicit_supported_image(self) -> None: """Keep trusted metadata jobs separate from the model review pool.""" workflow = NOEMA_REVIEW.read_text(encoding="utf-8") - self.assertEqual(workflow.count("endsWith(github.workflow_ref, '@refs/heads/main')"), 5) + self.assertEqual(workflow.count("github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main'"), 5) + self.assertNotIn("endsWith(github.workflow_ref", workflow) self.assertEqual(workflow.count('"group":"CWL MCP remediation"'), 1) self.assertEqual(workflow.count('"labels":["self-hosted","linux","x64"]'), 1) + for repository in ("cwl-telemetry", "naruon", "fast-mlsirm", "late-life-anxiety-reanalysis"): + self.assertEqual(workflow.count(f"github.repository == 'ContextualWisdomLab/{repository}'"), 5) self.assertEqual(workflow.count("github.repository == 'ContextualWisdomLab/contextual-orchestrator'"), 5) self.assertEqual(workflow.count("fromJSON('[\"ubuntu-24.04\"]')"), 5) self.assertNotIn("runs-on: ubuntu-24.04", workflow) diff --git a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py index 8f934b4fda..6ba6afe4c7 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -47,10 +47,22 @@ def test_hourly_review_repair_uses_explicit_supported_image(self) -> None: self.assertIn("labels: [self-hosted, linux, x64]", workflow) def test_codeql_pr_uses_explicit_supported_image(self) -> None: - """Require detect-languages, analyze-head, and the coordinator to pin Ubuntu 24.04.""" + """Require trusted-main control routing and Ubuntu fallback for all three jobs.""" workflow = CODEQL_PR.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) + selectors = [ + line.strip() for line in workflow.splitlines() + if line.strip().startswith("runs-on:") + ] + self.assertEqual(len(selectors), 3) + for selector in selectors: + self.assertIn( + "github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main'", + selector, + ) + self.assertIn('"group":"CWL central control"', selector) + self.assertIn('"labels":["self-hosted","linux","x64"]', selector) + self.assertIn("|| '\"ubuntu-24.04\"'", selector) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: """Require validation, scan, and attempt wake jobs in the dedicated group.""" diff --git a/tests/test_strix_rerun_job_selection.py b/tests/test_strix_rerun_job_selection.py index c1926b2ce3..6f96feb312 100644 --- a/tests/test_strix_rerun_job_selection.py +++ b/tests/test_strix_rerun_job_selection.py @@ -19,8 +19,8 @@ def _strix_job(name: str, job_id: int, conclusion: str) -> dict: } -def test_dispatch_strix_reruns_scan_job_not_sibling_publisher(monkeypatch) -> None: - """A skipped status-publisher sibling must never be selected as the Strix rerun target.""" +def test_strix_job_selection_excludes_sibling_publisher() -> None: + """A skipped status-publisher sibling must not count as the scan job.""" pr = { "number": 1055, "statusCheckRollup": { @@ -32,27 +32,4 @@ def test_dispatch_strix_reruns_scan_job_not_sibling_publisher(monkeypatch) -> No } }, } - reruns: list[tuple[str, str, str]] = [] - - def record_rerun(repo: str, job_id: str, *, dry_run: bool, action: str) -> None: - reruns.append((repo, job_id, action)) - - monkeypatch.setattr(sched, "rerun_actions_job", record_rerun) - monkeypatch.setattr(sched, "fetch_pr", lambda *_args: [pr]) - - assert ( - sched.dispatch_strix_evidence( - "ContextualWisdomLab/bandscope", - "Strix Security Scan", - pr, - dry_run=False, - ) - == "rerun" - ) - assert reruns == [ - ( - "ContextualWisdomLab/bandscope", - "99212031836", - "rerun-strix-evidence", - ) - ] + assert sched.matching_actions_job_id(pr, sched.is_strix_scan_check_run) == "99212031836" diff --git a/tests/test_strix_trusted_fixture_boundary.py b/tests/test_strix_trusted_fixture_boundary.py new file mode 100644 index 0000000000..8e0f158611 --- /dev/null +++ b/tests/test_strix_trusted_fixture_boundary.py @@ -0,0 +1,50 @@ +"""Regression contract for Strix trusted-runtime fixture isolation.""" + +from __future__ import annotations + +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +HARNESS_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "test_strix_quick_gate.sh" +CONSUMER_ROOT_MATERIALIZATION = ( + 'materialize_trusted_gate_fixture "$repo_root_dir/scripts/ci"' +) + + +def _consumer_root_materialization_owners(source: str) -> tuple[str, ...]: + """Return shell-function names that install trusted runtime in the consumer.""" + owners: list[str] = [] + current_function = "" + for raw_line in source.splitlines(): + stripped = raw_line.strip() + if stripped.endswith("() {"): + current_function = stripped.removesuffix("() {").strip() + if (CONSUMER_ROOT_MATERIALIZATION in raw_line + or ('cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh"' in raw_line + and current_function != "run_gate_case")): + owners.append(current_function) + return tuple(owners) + + +def test_specialized_strix_fixtures_keep_trusted_runtime_outside_consumer() -> None: + """Fail while any fixture can mask consumer-root binder resolution.""" + source = HARNESS_PATH.read_text(encoding="utf-8") + offenders = _consumer_root_materialization_owners(source) + + assert not offenders, ( + "trusted Strix gate/model/binder must be materialized outside " + "repo_root_dir; consumer-root materialization remains in: " + + ", ".join(offenders) + ) + + +def test_base_fixture_executes_trusted_runtime_when_consumer_source_is_retained() -> None: + """A source file under scan must never select the runtime being executed.""" + source = HARNESS_PATH.read_text(encoding="utf-8") + fixture = source.split("\nrun_gate_case() {", 1)[1].split( + "\nrun_gate_case_with_provider_signal_mode() {", 1)[0] + assert 'local gate_under_test="$trusted_script_dir/strix_quick_gate.sh"' in fixture + assert 'materialize_trusted_gate_fixture "$trusted_script_dir"' in fixture + assert 'STRIX_REPO_ROOT="$repo_root_dir" bash "$gate_under_test"' in fixture + assert 'bash "./scripts/ci/strix_quick_gate.sh"' not in fixture