diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..dbd43187c2 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +# Source validation only; labels do not attest runtime isolation. +self-hosted-runner: + labels: + - cwlab-ci-isolated + - cwlab-control diff --git a/.github/actions/noema-review/two_phase.py b/.github/actions/noema-review/two_phase.py index c850da81b9..d5da7b8270 100755 --- a/.github/actions/noema-review/two_phase.py +++ b/.github/actions/noema-review/two_phase.py @@ -156,6 +156,7 @@ def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> i return 0 expected_base = _canonical_base(pull_request) actor = _reviewer_actor() + author = gate.require_distinct_pr_author(pull_request, actor) if pull_request.get("isDraft"): print("PR is draft; Noema verdict preparation skipped.") return 0 @@ -190,6 +191,8 @@ def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> i "pull_request_number": number, "expected_head": expected, "expected_base": expected_base, + "reviewer_actor": actor, + "pull_request_author": author, "verdict": verdict, }, ) @@ -250,6 +253,8 @@ def publish_verdict(repo: str, number: int, expected_head: str, path: Path) -> i "pull_request_number", "expected_head", "expected_base", + "reviewer_actor", + "pull_request_author", "verdict", } if set(payload) != required_keys: @@ -277,6 +282,14 @@ def publish_verdict(repo: str, number: int, expected_head: str, path: Path) -> i print("Pull request base advanced after model review; stale prepared verdict was not published.") return 0 actor = _reviewer_actor(allow_refreshed_app=True) + author = gate.require_distinct_pr_author(current_pull_request, actor) + if payload["pull_request_author"] != author: + raise RuntimeError("Noema pull request author changed after verdict preparation") + reviewer_actor = payload["reviewer_actor"] + if not isinstance(reviewer_actor, str) or not reviewer_actor.strip(): + raise RuntimeError("Noema prepared reviewer actor is invalid") + if reviewer_actor.strip().casefold() != actor.strip().casefold(): + raise RuntimeError("Noema refreshed reviewer actor differs from verdict preparation") if current_pull_request.get("isDraft"): print("PR became draft after model review; prepared verdict was not published.") return 0 diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 71201af75d..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - target-branch: "main" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - - - package-ecosystem: "pip" - directory: "/" - target-branch: "main" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 diff --git a/.github/workflows/actions-queue-health.yml b/.github/workflows/actions-queue-health.yml index 2084765946..febf0679b2 100644 --- a/.github/workflows/actions-queue-health.yml +++ b/.github/workflows/actions-queue-health.yml @@ -15,7 +15,9 @@ permissions: jobs: collect: name: Collect exact-head queue evidence - runs-on: ubuntu-24.04 + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 30 permissions: contents: read diff --git a/.github/workflows/agent-mention-noema-dispatch.yml b/.github/workflows/agent-mention-noema-dispatch.yml index c9514a47a1..c2729b9819 100644 --- a/.github/workflows/agent-mention-noema-dispatch.yml +++ b/.github/workflows/agent-mention-noema-dispatch.yml @@ -28,7 +28,9 @@ permissions: jobs: validate-and-forward: if: github.repository == 'ContextualWisdomLab/.github' - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: actions: read diff --git a/.github/workflows/agent-mention-opencode-dispatch.yml b/.github/workflows/agent-mention-opencode-dispatch.yml index 3b21667832..221a5bdb74 100644 --- a/.github/workflows/agent-mention-opencode-dispatch.yml +++ b/.github/workflows/agent-mention-opencode-dispatch.yml @@ -28,7 +28,9 @@ permissions: jobs: validate-and-forward: if: github.repository == 'ContextualWisdomLab/.github' - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: actions: read diff --git a/.github/workflows/agent-mention-router-quality-ci.yml b/.github/workflows/agent-mention-router-quality-ci.yml index 6e95b322a3..57030437ef 100644 --- a/.github/workflows/agent-mention-router-quality-ci.yml +++ b/.github/workflows/agent-mention-router-quality-ci.yml @@ -46,7 +46,9 @@ env: jobs: quality: - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 20 steps: - name: Harden runner diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 9dd2772eca..9b2de6745c 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -64,10 +64,16 @@ on: - "scripts/ci/zdr_policy.py" - "scripts/ci/contextual_orchestrator_review_policy.py" - "scripts/ci/contextual_orchestrator_review_launcher.py" + - "scripts/ci/personal_review_route.py" - "scripts/ci/contextual_orchestrator_review_sidecar.sh" - "tests/test_zdr_policy.py" - "tests/test_contextual_orchestrator_review_policy.py" - "tests/test_contextual_orchestrator_review_launcher.py" + - "tests/test_review_auto_route.py" + - "tests/test_personal_review_route.py" + - "tests/test_tokyo_review_consumer.py" + - "tests/test_review_auto_cost_admission.py" + - "tests/test_review_ai_app_independence.py" - "tests/test_review_preflight_concurrency.py" - "tests/test_contextual_orchestrator_review_sidecar_contract.py" - "tests/test_sidecar_disk_free_guard.py" @@ -140,7 +146,9 @@ permissions: jobs: agent_review_runtime_quality: name: agent-review-runtime-quality - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 25 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -283,10 +291,16 @@ jobs: scripts/ci/zdr_policy.py|\ scripts/ci/contextual_orchestrator_review_policy.py|\ scripts/ci/contextual_orchestrator_review_launcher.py|\ + scripts/ci/personal_review_route.py|\ scripts/ci/contextual_orchestrator_review_sidecar.sh|\ tests/test_zdr_policy.py|\ tests/test_contextual_orchestrator_review_policy.py|\ tests/test_contextual_orchestrator_review_launcher.py|\ + tests/test_review_auto_route.py|\ + tests/test_personal_review_route.py|\ + tests/test_tokyo_review_consumer.py|\ + tests/test_review_auto_cost_admission.py|\ + tests/test_review_ai_app_independence.py|\ tests/test_review_preflight_concurrency.py|\ tests/test_contextual_orchestrator_review_sidecar_contract.py|\ tests/test_sidecar_disk_free_guard.py|\ @@ -473,6 +487,11 @@ jobs: tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ tests/test_contextual_orchestrator_review_launcher.py \ + tests/test_review_auto_route.py \ + tests/test_personal_review_route.py \ + tests/test_tokyo_review_consumer.py \ + tests/test_review_auto_cost_admission.py \ + tests/test_review_ai_app_independence.py \ tests/test_review_preflight_concurrency.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ tests/test_sidecar_disk_free_guard.py \ @@ -514,6 +533,11 @@ jobs: tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ tests/test_contextual_orchestrator_review_launcher.py \ + tests/test_review_auto_route.py \ + tests/test_personal_review_route.py \ + tests/test_tokyo_review_consumer.py \ + tests/test_review_auto_cost_admission.py \ + tests/test_review_ai_app_independence.py \ tests/test_review_preflight_concurrency.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ tests/test_sidecar_disk_free_guard.py \ diff --git a/.github/workflows/audit-central-ruleset.yml b/.github/workflows/audit-central-ruleset.yml index 2b72f21aab..e735ae1923 100644 --- a/.github/workflows/audit-central-ruleset.yml +++ b/.github/workflows/audit-central-ruleset.yml @@ -25,7 +25,9 @@ permissions: jobs: audit: name: Audit organization ruleset 18156473 - runs-on: ubuntu-latest + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} env: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} steps: diff --git a/.github/workflows/cloudflare-dns.yml b/.github/workflows/cloudflare-dns.yml index 991202f4e9..6b23e3b053 100644 --- a/.github/workflows/cloudflare-dns.yml +++ b/.github/workflows/cloudflare-dns.yml @@ -44,7 +44,9 @@ permissions: jobs: reconcile: name: Reconcile zones (${{ github.event.client_payload.mode || 'dry-run' }}) - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 356244f7fc..1be7ae0449 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -60,7 +60,9 @@ jobs: name: Detect CodeQL languages # Draft PRs get no runner; ready_for_review re-runs this on the same head. if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - 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"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -164,7 +166,9 @@ jobs: # dependency exactly; the only case where it's genuinely skipped is a # closed PR, where this job being implicitly skipped too is fine because # 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"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # Verdict reads have exceeded five minutes; retain the ten-minute control budget. timeout-minutes: 10 permissions: @@ -367,7 +371,9 @@ jobs: && github.event.pull_request.state != 'closed' && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' - 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"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/company-default-head-trust-gate.yml b/.github/workflows/company-default-head-trust-gate.yml new file mode 100644 index 0000000000..08745e92d0 --- /dev/null +++ b/.github/workflows/company-default-head-trust-gate.yml @@ -0,0 +1,64 @@ +name: Reusable company default-head trust gate + +on: + workflow_call: + inputs: + target_runner_label: + required: true + type: string + trusted_ref_sha: + required: false + type: string + default: "" + trusted_ref_ack: + required: false + type: string + default: "" + outputs: + runner_matrix: + value: ${{ jobs.validate.outputs.runner_matrix }} + +permissions: + contents: read + +jobs: + validate: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + timeout-minutes: 5 + outputs: + runner_matrix: ${{ steps.runner.outputs.runner_matrix }} + steps: + - id: runner + shell: bash + env: + TARGET: ${{ inputs.target_runner_label }} + run: | + case "$TARGET" in + all) matrix='["itxllmpgw01","itxllmpgw02","xtrmsales-llmgw1","xtrmsales-llmgw2"]' ;; + itxllmpgw01|itxllmpgw02|itxllmpgw03|itxllmpgw04|xtrmsales-llmgw1|xtrmsales-llmgw2|llm-gateway-dev|itx-llm-operation-1|itx-llm-operation-2) + matrix="[\"$TARGET\"]" + ;; + *) echo "unsupported target_runner_label=$TARGET" >&2; exit 2 ;; + esac + echo "runner_matrix=$matrix" >> "$GITHUB_OUTPUT" + - shell: bash + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} + REF_NAME: ${{ github.ref_name }} + SHA: ${{ github.sha }} + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + run: | + set -Eeuo pipefail + if [ "$REF_NAME" != "$DEFAULT_BRANCH" ]; then + echo "self-hosted failure diagnostics require the protected default branch" >&2 + exit 2 + fi + current_sha="$(gh api "repos/$REPOSITORY/commits/$DEFAULT_BRANCH" --jq .sha)" + if [ "$SHA" != "$current_sha" ]; then + echo "refusing stale default-branch run sha=$SHA current_sha=$current_sha" >&2 + exit 2 + fi + echo "trusted_ref_status=current_default_head sha=$SHA" diff --git a/.github/workflows/company-settlement-validate.yml b/.github/workflows/company-settlement-validate.yml new file mode 100644 index 0000000000..2cd0498ba3 --- /dev/null +++ b/.github/workflows/company-settlement-validate.yml @@ -0,0 +1,56 @@ +# Source-only fixed-pool candidate. Caller-context group admission remains separate. +name: Company settlement validate +on: + workflow_call: {} +permissions: + contents: read +jobs: + validate: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + - name: Install locked test dependencies + run: uv pip install --system -r requirements.txt -r requirements-dev.txt + - name: Start isolated PostgreSQL without a TCP listener + shell: bash + run: | + set -Eeuo pipefail + pg_bin="$(pg_config --bindir)" + test -x "$pg_bin/initdb" + test -x "$pg_bin/pg_ctl" + "$pg_bin/postgres" --version + data_dir="$RUNNER_TEMP/settlement-verify-postgres" + socket_dir="$RUNNER_TEMP/settlement-verify-socket" + mkdir -m 700 "$data_dir" "$socket_dir" + printf 'SETTLEMENT_VERIFY_PG_BIN=%s\n' "$pg_bin" >> "$GITHUB_ENV" + printf 'SETTLEMENT_VERIFY_PG_DATA=%s\n' "$data_dir" >> "$GITHUB_ENV" + "$pg_bin/initdb" -D "$data_dir" --no-locale --encoding=UTF8 \ + --auth-local=trust --auth-host=reject + "$pg_bin/pg_ctl" -D "$data_dir" \ + -o "-F -h '' -k $socket_dir" -l "$RUNNER_TEMP/settlement-verify-postgres.log" -w start + printf 'SETTLEMENT_VERIFY_TEST_DSN=host=%s dbname=postgres\n' "$socket_dir" >> "$GITHUB_ENV" + - name: Verify exact-head PgCat and lock regressions + env: + SETTLEMENT_VERIFY_REQUIRE_POSTGRES: "1" + run: >- + python -m pytest -q tests/test_issue119_pgcat_endpoint_resolver.py + tests/test_issue119_settlement_live_verify.py + tests/test_issue119_settlement_replica_routing.py + tests/test_issue119_settlement_review_regressions.py + tests/test_multi_instance_runtime_contract.py + - name: Stop isolated test database + if: always() + shell: bash + run: | + if test -f "${SETTLEMENT_VERIFY_PG_DATA:-}/postmaster.pid"; then + "$SETTLEMENT_VERIFY_PG_BIN/pg_ctl" -D "$SETTLEMENT_VERIFY_PG_DATA" -m fast -w stop + fi diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1bb83c2baf..fc63256794 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,6 +1,6 @@ # Reusable Dependency Review (workflow_call), consolidating the near- -# identical dependency-review.yml files argos, mightyETL, newsdom-api, -# scopeweave, and naruon each carried independently. See +# identical dependency-review.yml files argos, mightyETL, naruon, newsdom-api, +# and scopeweave each carried independently. See # docs/adr/0024-dependency-review-reusable-workflow-consolidation.md and # docs/doctoring/dependency-review-reusable-workflow-consolidation.md for the # per-repo field audit behind these inputs. @@ -9,34 +9,41 @@ # calling repo's own thin workflow file -- a workflow_call target cannot also # be the thing GitHub triggers directly on pull_request. # -# Dependency Review requires GitHub Dependency Graph (and, on private repos -# without GitHub Advanced Security, it is unavailable regardless of a repo's -# own settings). scopeweave's original workflow already detected this -# dynamically via the dependency-graph compare API instead of assuming from -# public/private repository status (mightyETL's original approach, which is -# wrong for a private repo that does have GHAS). This reusable workflow -# adopts the dynamic detection as the common, more-correct behavior for -# every caller, so no per-repo public/private input is needed. +# Dependency Review requires a successful Dependency Graph comparison for the +# exact pull-request base/head. Repository visibility is not a sufficient +# capability signal, and HTTP 403/404 are not safe availability signals: +# GitHub can use those statuses for authorization/policy denials as well as +# unavailable resources. For a pull_request, only exact immutable base/head +# object IDs plus a valid owner/name repository identity may reach transport, +# and only HTTP 200 authorizes running the pinned action. Every non-200 +# comparison fails closed. Non-pull_request triggers may skip because they do +# not carry the exact PR base/head identity. +# +# Reusable-workflow permissions can only be maintained or reduced through the +# call chain. Every thin caller therefore must declare at least `contents: +# read` and `pull-requests: read`; this workflow cannot elevate a caller token +# that omitted those scopes. # # Example caller (.github/workflows/dependency-review.yml in a product repo). -# Pin `uses:` to this file's exact commit SHA, not @main: an unpinned mutable -# ref would run an unreviewed central change against every PR check in the -# calling repo (Devin flagged this on the first four callers; fixed in all of -# them). If the calling repo's branch protection requires a status check -# literally named after the old standalone job, converting to `uses:` here -# will rename the published check to " / dependency-review" and -# silently break that required check -- update the branch protection's -# required-check name to match before or immediately after merging a caller. +# Pin `uses:` to the exact protected-main commit SHA carrying this workflow, +# never `@main`: a mutable central ref could run an unreviewed workflow change +# against the caller's PR checks. If branch protection requires the old +# standalone job name, note that reusable workflow adoption publishes the +# combined check name ` / dependency-review`; update the required +# check name to the exact published context without weakening the gate. # # name: Dependency Review # on: # pull_request: +# permissions: +# contents: read +# pull-requests: read # concurrency: # group: dependency-review-${{ github.event.pull_request.number || github.ref }} # cancel-in-progress: true # jobs: # dependency-review: -# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@ +# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@ # with: # fail_on_severity: high # allow_ghsas: "GHSA-69w3-r845-3855" @@ -86,7 +93,9 @@ permissions: jobs: dependency-review: - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] env: # Opts every JS action this job runs (checkout, dependency-review-action) # into the Node 24 actions runtime ahead of GitHub's default cutover, @@ -121,30 +130,43 @@ jobs: exit 0 fi + git_object_id='^[0-9a-f]{40}([0-9a-f]{24})?$' + repository_identity='^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' + if ! [[ "${BASE_SHA}" =~ $git_object_id ]] || ! [[ "${HEAD_SHA}" =~ $git_object_id ]]; then + echo "::error::Dependency review evidence unavailable: exact 40- or 64-character hexadecimal base and head revisions are required before any compare request. Named refs are not evidence. Verify the pull-request event SHAs, then rerun. Failing closed." + exit 1 + fi + if ! [[ "${REPOSITORY}" =~ $repository_identity ]]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + repository_owner="${REPOSITORY%%/*}" + repository_name="${REPOSITORY#*/}" + if [ "${repository_owner}" = "." ] || [ "${repository_owner}" = ".." ] || [ "${repository_name}" = "." ] || [ "${repository_name}" = ".." ]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Dot or parent-directory path components are not evidence. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + api_url="${GITHUB_API_URL:-https://api.github.com}" response_file="$(mktemp)" + curl_exit=0 status="$( curl -fsS -o "$response_file" -w '%{http_code}' \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \ - || true - )" + )" || curl_exit=$? - if [ "$status" = "200" ]; then + if [ "$curl_exit" -eq 0 ] && [ "$status" = "200" ]; then echo "available=true" >>"$GITHUB_OUTPUT" exit 0 fi - if [ "$status" = "403" ] || [ "$status" = "404" ]; then - echo "::warning::Dependency graph compare returned HTTP ${status} for ${REPOSITORY}; skipping the dependency-review hard gate (GitHub Dependency Graph, or GitHub Advanced Security on a private repository, is unavailable)." - echo "available=false" >>"$GITHUB_OUTPUT" - exit 0 + echo "::error::Dependency graph comparison failed with curl exit ${curl_exit} and HTTP ${status:-unavailable}. For a pull_request, transport failures and non-200 responses are ambiguous between feature availability and authorization/policy/transport failure, so Dependency Review fails closed instead of being skipped." + if [ -s "$response_file" ]; then + cat "$response_file" fi - - echo "::error::Dependency graph availability check failed with HTTP ${status}. This is not a 'graph unavailable' response (403/404) -- treating it as a genuine failure instead of silently skipping the security gate." - cat "$response_file" exit 1 - name: Dependency review @@ -155,9 +177,3 @@ jobs: fail-on-severity: ${{ inputs.fail_on_severity }} allow-ghsas: ${{ inputs.allow_ghsas }} comment-summary-in-pr: ${{ inputs.comment_summary_in_pr }} - - - name: Dependency graph unavailable note - if: steps.dependency_graph.outputs.available != 'true' && github.event_name == 'pull_request' - run: | - echo "Dependency Review requires GitHub Dependency Graph to be enabled for this repository (and, on private repositories, GitHub Advanced Security)." - echo "Other required dependency-vulnerability gates (OSV-Scanner, Scorecard) remain the blocking coverage until Dependency Graph is available here." diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index f86b614022..8097a8c1aa 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -45,7 +45,9 @@ permissions: jobs: deploy_pages: name: Deploy ${{ inputs.project_name }} - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] steps: - name: Checkout caller repo uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/exact-artifact-sbom-attestation.yml b/.github/workflows/exact-artifact-sbom-attestation.yml index 198392aa0c..1e280ecb4f 100644 --- a/.github/workflows/exact-artifact-sbom-attestation.yml +++ b/.github/workflows/exact-artifact-sbom-attestation.yml @@ -64,7 +64,9 @@ env: jobs: verify-evidence-artifact: name: Verify inert sealed evidence - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 15 permissions: actions: read @@ -180,7 +182,9 @@ jobs: attest-exact-artifacts: name: Attest exact wheel and sdist SBOMs needs: verify-evidence-artifact - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 20 permissions: actions: read @@ -372,13 +376,13 @@ jobs: EOF { printf '\n## Exact signed identity\n\n' - printf -- '- Source repository: `%s`\n' "$SOURCE_REPOSITORY" - printf -- '- Source SHA: `%s`\n' "$SOURCE_SHA" - printf -- '- Signer repository: `%s`\n' "$SIGNER_REPOSITORY" - printf -- '- Signer workflow: `%s`\n' "$signer_workflow" - printf -- '- Predicate type: `%s`\n' "$PREDICATE_TYPE" - printf -- '- Wheel: `%s`\n' "$WHEEL_FILENAME" - printf -- '- Source distribution: `%s`\n' "$SDIST_FILENAME" + printf -- "- Source repository: \`%s\`\n" "$SOURCE_REPOSITORY" + printf -- "- Source SHA: \`%s\`\n" "$SOURCE_SHA" + printf -- "- Signer repository: \`%s\`\n" "$SIGNER_REPOSITORY" + printf -- "- Signer workflow: \`%s\`\n" "$signer_workflow" + printf -- "- Predicate type: \`%s\`\n" "$PREDICATE_TYPE" + printf -- "- Wheel: \`%s\`\n" "$WHEEL_FILENAME" + printf -- "- Source distribution: \`%s\`\n" "$SDIST_FILENAME" cat <> offline-attestation-evidence/README.md ( cd offline-attestation-evidence - LC_ALL=C find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' \ - | LC_ALL=C sort \ + evidence_names="$(LC_ALL=C find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' | LC_ALL=C sort)" + printf '%s\n' "$evidence_names" \ | while IFS= read -r evidence_file; do + [ -n "$evidence_file" ] || continue sha256sum "$evidence_file" done > SHA256SUMS ) diff --git a/.github/workflows/exact-head-coverage-quality-gate.yml b/.github/workflows/exact-head-coverage-quality-gate.yml index 6b957571d2..f0670a4b50 100644 --- a/.github/workflows/exact-head-coverage-quality-gate.yml +++ b/.github/workflows/exact-head-coverage-quality-gate.yml @@ -37,7 +37,9 @@ permissions: jobs: quality-gate: - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: ${{ inputs.timeout_minutes }} steps: - name: Checkout exact source revision diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index e743a3e23d..6c81d859e2 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -41,7 +41,9 @@ 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' || 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"]') }} + runs-on: + group: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 outputs: admitted: ${{ steps.live_head.outputs.admitted }} @@ -70,7 +72,13 @@ jobs: exit 1 fi echo "repository=${TARGET_REPOSITORY#*/}" >>"$GITHUB_OUTPUT" - if [ -n "${METADATA_TOKEN:-}" ]; then + if [ "$TARGET_REPOSITORY" = "ContextualWisdomLab/k-csap-skills" ]; then + if [ -z "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] || [ -z "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then + echo "::error::k-csap-skills Noema metadata requires the native GitHub App client ID and private key; credential fallback is forbidden." + exit 1 + fi + echo "source=github-app" >>"$GITHUB_OUTPUT" + elif [ -n "${METADATA_TOKEN:-}" ]; then echo "source=pat" >>"$GITHUB_OUTPUT" elif [ -n "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] && [ -n "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then echo "source=github-app" >>"$GITHUB_OUTPUT" @@ -94,7 +102,7 @@ jobs: - name: Admit only the exact live Noema head id: live_head env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_metadata_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token)) || '' }} run: | set -euo pipefail echo "admitted=false" >>"$GITHUB_OUTPUT" @@ -137,7 +145,9 @@ 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' || 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"]') }} + runs-on: + group: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -193,7 +203,9 @@ 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' || 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"]') }} + runs-on: + group: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # 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 @@ -371,7 +383,9 @@ jobs: noema-review: name: noema-review needs: [admit-current-head, changed-scope] - 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"]') }} + runs-on: + group: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # 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 @@ -425,6 +439,8 @@ jobs: # diff. PR_NUMBER: ${{ (needs.changed-scope.outputs.code != 'false' && (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 || '' }} + NOEMA_TOOLING_MANIFEST_SHA256: ${{ vars.NOEMA_TOOLING_MANIFEST_SHA256 || '' }} + NOEMA_EXECUTION_ADMISSION_SHA256: ${{ vars.NOEMA_EXECUTION_ADMISSION_SHA256 || '' }} steps: - name: Skip events without pull request context if: env.PR_NUMBER == '' @@ -520,7 +536,13 @@ jobs: exit 1 fi echo "repository=${TARGET_REPOSITORY#*/}" >>"$GITHUB_OUTPUT" - if [ -n "${METADATA_TOKEN:-}" ]; then + if [ "$TARGET_REPOSITORY" = "ContextualWisdomLab/k-csap-skills" ]; then + if [ -z "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] || [ -z "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then + echo "::error::k-csap-skills Noema metadata requires the native GitHub App client ID and private key; credential fallback is forbidden." + exit 1 + fi + echo "source=github-app" >>"$GITHUB_OUTPUT" + elif [ -n "${METADATA_TOKEN:-}" ]; then echo "source=pat" >>"$GITHUB_OUTPUT" elif [ -n "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] && [ -n "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then echo "source=github-app" >>"$GITHUB_OUTPUT" @@ -544,7 +566,7 @@ jobs: - name: Reject a stale trigger before credential or model setup if: env.PR_NUMBER != '' env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_metadata_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token)) || '' }} run: | set -euo pipefail if [[ ! "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then @@ -635,6 +657,34 @@ jobs: done echo "Cancelled ${cancelled} superseded Noema run(s) after live-head validation." + - name: Admit independently pinned premerge tooling and execution + if: env.PR_NUMBER != '' + env: + GH_TOKEN: ${{ steps.noema_metadata_app_token.outputs.token || github.token }} + NOEMA_TOOLING_MANIFEST_SHA256: ${{ vars.NOEMA_TOOLING_MANIFEST_SHA256 || '' }} + NOEMA_EXECUTION_ADMISSION_SHA256: ${{ vars.NOEMA_EXECUTION_ADMISSION_SHA256 || '' }} + run: | + set -euo pipefail + # The existing custodian supplies original manifests, accepted namespace, + # source/dependency pins and phase JWT files. Missing evidence stays STOP. + python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/noema_bootstrap_producer.py" \ + --materialize --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" + python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/premerge_review_tooling.py" \ + --admit-production --tooling-root "$RUNNER_TEMP/noema-private-bootstrap/tooling" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" + - name: Select fail-closed Noema reviewer credential if: env.PR_NUMBER != '' id: noema_credential @@ -642,7 +692,7 @@ jobs: NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID || '' }} NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY || '' }} TOKEN_EXCHANGE_URL: ${{ vars.NOEMA_TOKEN_EXCHANGE_URL || vars.NOEMA_EXCHANGE_URL || '' }} - NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN }} + NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || '' }} run: | set -euo pipefail @@ -653,6 +703,15 @@ jobs: repository_name="${TARGET_REPOSITORY#*/}" echo "repository=$repository_name" >>"$GITHUB_OUTPUT" + if [ "$TARGET_REPOSITORY" = "ContextualWisdomLab/k-csap-skills" ]; then + if [ -z "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] || [ -z "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then + echo "::error::k-csap-skills Noema reviewer requires the native GitHub App client ID and private key; PAT/OIDC fallback is forbidden." + exit 1 + fi + echo "source=github-app" >>"$GITHUB_OUTPUT" + exit 0 + fi + if [ -n "${NOEMA_REVIEW_TOKEN:-}" ]; then echo "source=pat" >>"$GITHUB_OUTPUT" echo "::notice::Noema reviewer using the NOEMA_REVIEW_TOKEN secret fallback identity." @@ -683,14 +742,9 @@ jobs: private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} owner: ContextualWisdomLab repositories: ${{ steps.noema_credential.outputs.repository }} - permission-actions: read - permission-checks: read permission-contents: read permission-metadata: read - permission-pull-requests: write - permission-security-events: read - permission-statuses: read - permission-vulnerability-alerts: read + permission-pull-requests: read - name: Exchange Noema app token through OIDC if: env.PR_NUMBER != '' && steps.noema_credential.outputs.source == 'oidc' @@ -753,7 +807,7 @@ jobs: - name: Validate current pull request head if: env.PR_NUMBER != '' env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} run: | set -euo pipefail if ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then @@ -773,7 +827,7 @@ jobs: if: env.PR_NUMBER != '' id: target_visibility env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} run: | set -euo pipefail if [ -z "${GH_TOKEN:-}" ]; then @@ -826,9 +880,13 @@ jobs: if: env.PR_NUMBER != '' id: live_draft env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} run: | - set -uo pipefail + set -euo pipefail + if [ -z "${GH_TOKEN:-}" ]; then + echo "::error::Noema reviewer credential is missing for live draft check." + exit 1 + fi live_draft=false if pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" 2>/tmp/noema-live-draft-error)"; then if jq -e -s 'length == 1 and (.[0] | type == "object" and .draft == true)' <<<"$pull_request_json" >/dev/null 2>&1; then @@ -857,9 +915,71 @@ jobs: python-version: "3.12" update-environment: false + - name: Admit existing personal LiteLLM auto route + id: personal_route + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' + env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} + PRIVATE_TARGET: ${{ steps.target_visibility.outputs.require_zdr }} + run: | + set -euo pipefail + if [ -z "${LLM_GATEWAY_API_KEY:-}" ]; then + echo '::error::STOP: existing personal review Secret is unavailable.' + exit 1 + fi + python3 -m scripts.ci.personal_review_route \ + --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ + --private-target "$PRIVATE_TARGET" + echo 'selected=true' >>"$GITHUB_OUTPUT" + + - name: Initialize current Noema sidecar evidence + id: noema_evidence + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && steps.personal_route.outputs.selected != 'true' + run: | + set -euo pipefail + # Separate setup from the producer: unsafe paths fail before its outcome + # can admit an upload. Clear only these two files, before early startup. + python3 -I <<'PY' + import os + import stat + from pathlib import Path + + workspace_text = os.environ["GITHUB_WORKSPACE"] + workspace = Path(workspace_text) + if ( + not workspace.is_absolute() + or ".." in workspace.parts + or workspace.resolve(strict=True) != workspace + or not workspace.is_dir() + ): + raise SystemExit("Noema evidence workspace must be an unaliased directory") + evidence = workspace / "strix_runs" + if evidence.is_symlink() or (evidence.exists() and not evidence.is_dir()): + raise SystemExit("Noema evidence parent must be a non-symlink directory") + owned = ( + evidence / "contextual-orchestrator-preflight.json", + evidence / "contextual-orchestrator-sidecar.stderr.log", + ) + # Validate both before deleting either; reject dangling links, special + # files and hardlink aliases as well as nonregular evidence paths. + for path in owned: + try: + metadata = path.lstat() + except FileNotFoundError: + continue + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + raise SystemExit("Noema evidence must be regular, single-link files") + evidence.mkdir(mode=0o700, exist_ok=True) + for path in owned: + path.unlink(missing_ok=True) + PY + - name: Provision contextual-orchestrator review sidecar id: noema_sidecar - if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && steps.personal_route.outputs.selected != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -868,6 +988,7 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.require_zdr }} + CONTEXTUAL_ORCHESTRATOR_POOL: auto run: | set -euo pipefail test ! -L "$GITHUB_WORKSPACE/strix_runs" @@ -916,11 +1037,15 @@ jobs: if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' id: noema_prepare env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }} NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_token.outputs['installation-id'] }} NOEMA_TRANSPORT_RETRY_ATTEMPT: ${{ toJSON(github.event.client_payload.transport_retry_attempt) }} + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} + NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} run: | set -euo pipefail if [ -z "${PR_NUMBER:-}" ]; then @@ -932,18 +1057,34 @@ jobs: echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot prepare a verdict." exit 1 fi + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export NOEMA_LLM_API_URL="https://litellm.poinnetworks.net/v1/chat/completions" + export NOEMA_LLM_MODEL="auto" + export NOEMA_LLM_API_KEY="$LLM_GATEWAY_API_KEY" + unset NOEMA_LLM_VIA_ORCHESTRATOR + else if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then echo "::error::contextual-orchestrator review sidecar must be provisioned before Noema LLM review." exit 1 fi source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" - export NOEMA_LLM_MODEL="orchestrator/free" + export NOEMA_LLM_MODEL="orchestrator/auto" export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" export NOEMA_LLM_VIA_ORCHESTRATOR=1 - verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" - rm -f "$verdict_file" - python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --prepare-verdict-file "$verdict_file" + fi + verdict_file="${RUNNER_TEMP}/noema-private-bootstrap/verdict.json" + test ! -e "$verdict_file" + node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs" \ + --phase prepare --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" --verdict-file "$verdict_file" if [ -f "$verdict_file" ]; then echo "prepared=true" >>"$GITHUB_OUTPUT" else @@ -952,7 +1093,10 @@ jobs: fi - name: Upload contextual-orchestrator sidecar evidence - if: always() && env.PR_NUMBER != '' + # A live Draft never provisions the sidecar. Retain only current producer + # terminal diagnostics; absent/skipped outcomes cannot publish stale files. + # Non-Draft upload failures still fail the required review job. + if: always() && env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && (steps.noema_sidecar.outcome == 'success' || steps.noema_sidecar.outcome == 'failure' || steps.noema_sidecar.outcome == 'cancelled') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: noema-sidecar-evidence @@ -962,6 +1106,36 @@ jobs: if-no-files-found: ignore retention-days: 5 + - name: Refresh read-only Noema GitHub App metadata token after model + if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' && steps.noema_credential.outputs.source == 'github-app' + id: noema_publication_metadata_token + 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_credential.outputs.repository }} + permission-contents: read + permission-metadata: read + permission-pull-requests: read + + - name: Revalidate prepared scope before publisher credential refresh + if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' + env: + GH_TOKEN: ${{ steps.noema_publication_metadata_token.outputs.token || '' }} + run: | + set -euo pipefail + python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/noema_bootstrap_producer.py" \ + --revalidate-publish --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --verdict-file "$RUNNER_TEMP/noema-private-bootstrap/verdict.json" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" + - name: Refresh repository-scoped Noema GitHub App token for publication if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' && steps.noema_credential.outputs.source == 'github-app' id: noema_github_app_publication_token @@ -971,34 +1145,43 @@ jobs: private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} owner: ContextualWisdomLab repositories: ${{ steps.noema_credential.outputs.repository }} - permission-actions: read - permission-checks: read permission-contents: read permission-metadata: read - permission-pull-requests: write - permission-security-events: read - permission-statuses: read - permission-vulnerability-alerts: read + permission-pull-requests: read - name: Publish prepared Noema verdict on the exact live head if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' env: - GH_TOKEN: ${{ steps.noema_credential.outputs.source == 'pat' && secrets.NOEMA_REVIEW_TOKEN || steps.noema_credential.outputs.source == 'github-app' && steps.noema_github_app_publication_token.outputs.token || steps.noema_credential.outputs.source == 'oidc' && steps.noema_oidc_token.outputs.token || '' }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_publication_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (steps.noema_credential.outputs.source == 'pat' && secrets.NOEMA_REVIEW_TOKEN || steps.noema_credential.outputs.source == 'github-app' && steps.noema_github_app_publication_token.outputs.token || steps.noema_credential.outputs.source == 'oidc' && steps.noema_oidc_token.outputs.token || '')) || '' }} NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app-refresh' || steps.noema_credential.outputs.source == 'oidc' && 'noema-review-app-oidc' || '' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_publication_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_publication_token.outputs['app-slug']) || '' }} NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_publication_token.outputs['installation-id'] }} + NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} run: | set -euo pipefail if [ -z "${GH_TOKEN:-}" ]; then echo "::error::Noema publication has no credential for the explicitly selected reviewer source; refusing any GITHUB_TOKEN or author fallback." exit 1 fi - verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" + verdict_file="${RUNNER_TEMP}/noema-private-bootstrap/verdict.json" if [ ! -f "$verdict_file" ]; then echo "::error::Noema prepared-verdict output claimed success but its private envelope is missing." exit 1 fi - python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --publish-verdict-file "$verdict_file" + if [ "${{ steps.noema_credential.outputs.source }}" != 'github-app' ]; then + echo "::error::Two-phase Noema issuer requires the existing authenticated GitHub App route; no PAT/OIDC authority relabeling." + exit 1 + fi + node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs" \ + --phase publish --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" --verdict-file "$verdict_file" continue-noema-transport: needs: [admit-current-head, noema-review] @@ -1008,7 +1191,9 @@ 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' || 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"]') }} + runs-on: + group: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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.repository == 'ContextualWisdomLab/tokyo-travel-2026') && 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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 10 permissions: contents: write diff --git a/.github/workflows/opencode-review-coalesce-tick.yml b/.github/workflows/opencode-review-coalesce-tick.yml index 35f1da22c7..a9b657ba31 100644 --- a/.github/workflows/opencode-review-coalesce-tick.yml +++ b/.github/workflows/opencode-review-coalesce-tick.yml @@ -54,7 +54,9 @@ jobs: # gate back to step scope: that reintroduces multi-hour queue wait for # an inert echo under the org concurrent-job ceiling. if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 4 permissions: actions: write @@ -85,18 +87,18 @@ jobs: repos_file="${RUNNER_TEMP}/coalesce-repos.txt" : >"$repos_file" cursor="" - for page in 1 2 3 4 5 6 7 8 9 10; do + for _page in 1 2 3 4 5 6 7 8 9 10; do cursor_arg=() if [ -n "$cursor" ]; then cursor_arg=(-f "cursor=$cursor") fi - response="$(gh api graphql -f query=' - query($cursor: String) { - search(query: "org:ContextualWisdomLab is:pr is:open draft:false", type: ISSUE, first: 100, after: $cursor) { + response="$(gh api graphql -f query=" + query(\$cursor: String) { + search(query: \"org:ContextualWisdomLab is:pr is:open draft:false\", type: ISSUE, first: 100, after: \$cursor) { nodes { ... on PullRequest { repository { nameWithOwner } } } pageInfo { hasNextPage endCursor } } - }' "${cursor_arg[@]}" 2>/dev/null || echo '{}')" + }" "${cursor_arg[@]}" 2>/dev/null || echo '{}')" printf '%s' "$response" | jq -r '.data.search.nodes[]?.repository.nameWithOwner // empty' >>"$repos_file" has_next="$(printf '%s' "$response" | jq -r '.data.search.pageInfo.hasNextPage // false')" [ "$has_next" = "true" ] || break diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 09bbf8181a..443888bf1f 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -396,9 +396,11 @@ jobs: if: >- needs.validate-pr-metadata.result == 'success' && github.event_name == 'repository_dispatch' + # Pull-request merge-tree tests run only on the clean-per-job isolated pool. + # Keep metadata and review publication on their separate trusted jobs. runs-on: - group: CWL central OpenCode - labels: [self-hosted, linux, x64] + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 300 permissions: # The PR tree arrives through a same-run artifact. No repository-content, @@ -747,7 +749,7 @@ jobs: changed_basename="${changed_path##*/}" case "$changed_basename" in *.py|pyproject.toml|uv.lock|poetry.lock|Pipfile|Pipfile.lock|\ - setup.py|setup.cfg|tox.ini|pytest.ini|.pytest.ini|\ + setup.cfg|tox.ini|pytest.ini|.pytest.ini|\ requirements.lock|requirements*.txt|requirements*.in|\ environment.yml|environment.yaml) python_coverage_required=1 @@ -976,6 +978,9 @@ jobs: && rm -rf /tmp/base-python-requirements \ && rm -f /usr/local/libexec/install-base-python-locks.py COPY base-rust-dependencies /opt/base-rust-dependencies + # Crate archives keep modes such as 0640 through COPY, so make the base + # Rust dependency tree readable by the unprivileged coverage sandbox UID. + RUN chmod -R a+rX /opt/base-rust-dependencies DOCKERFILE # Debian's rustc lags the MSRV of pinned repositories (fast-mlsirm pins # 1.97.1; Debian ships 1.85), so a base-pinned repository gets that exact @@ -1116,9 +1121,11 @@ jobs: } append_command() { - printf '$ ' >>"$summary_file" - printf '%q ' "$@" >>"$summary_file" - printf '\n' >>"$summary_file" + { + printf '$ ' + printf '%q ' "$@" + printf '\n' + } >>"$summary_file" } emit_captured_log() { @@ -1406,13 +1413,13 @@ jobs: project_is_maturin_project "$project_dir" || return 0 run_and_capture "Offline PyO3/maturin extension build (${project_dir})" \ env CARGO_NET_OFFLINE=true CARGO_BUILD_JOBS=1 \ - bash -c 'set -eu - cd "$1" - dist_dir="$(mktemp -d)" - python3 -m maturin build --offline --release -o "$dist_dir" - python3 -m pip install --user --no-index --no-deps --force-reinstall "$dist_dir"/*.whl - python3 "$2" "$dist_dir"/*.whl . - rm -rf "$dist_dir"' bash "$project_dir" "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py" + bash -c "set -eu + cd \"\$1\" + dist_dir=\"\$(mktemp -d)\" + python3 -m maturin build --offline --release -o \"\$dist_dir\" + python3 -m pip install --user --no-index --no-deps --force-reinstall \"\$dist_dir\"/*.whl + python3 \"\$2\" \"\$dist_dir\"/*.whl . + rm -rf \"\$dist_dir\"" bash "$project_dir" "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py" } run_python_test_coverage() { @@ -1431,14 +1438,14 @@ jobs: done <<<"$configured_commands_json" else run_and_capture "Python coverage with missing-line report (${project_dir})" \ - bash -c 'cd "$1" && PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing' bash "$project_dir" + bash -c "cd \"\$1\" && PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing" bash "$project_dir" fi done < <(tracked_python_projects_with_tests) if [ "$measured_projects" -eq 0 ]; then if has_tracked_files '*.py'; then run_and_capture "Python coverage with missing-line report" \ - bash -c 'PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing' + bash -c "PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing" elif python3 -I -c 'import pytest_cov' >/dev/null 2>&1; then run_and_capture "Python pytest-cov coverage" python3 -m pytest --cov=. --cov-report=term-missing else @@ -1589,7 +1596,7 @@ jobs: if [ -f "${project_dir}/tests/test_docstrings.py" ]; then measured_projects=1 run_and_capture "Python docstring coverage (${project_dir})" \ - bash -c 'cd "$1" && PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m pytest tests/test_docstrings.py' bash "$project_dir" + bash -c "cd \"\$1\" && PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py" bash "$project_dir" fi done < <(tracked_python_projects_with_tests) [ "$measured_projects" -eq 1 ] @@ -1957,21 +1964,21 @@ jobs: if [ -n "$package_name" ] && jq -e '.workspaces // empty' package.json >/dev/null 2>&1; then run_and_capture "Tauri frontendDist build (${package_dir})" corepack npm run build --workspace "$package_name" else - run_and_capture "Tauri frontendDist build (${package_dir})" bash -c 'cd "$1" && corepack npm run build' bash "$package_dir" + run_and_capture "Tauri frontendDist build (${package_dir})" bash -c "cd \"\$1\" && corepack npm run build" bash "$package_dir" fi ;; pnpm) if [ -n "$package_name" ] && [ -f pnpm-workspace.yaml ]; then run_and_capture "Tauri frontendDist build (${package_dir})" corepack pnpm --filter "$package_name" run build else - run_and_capture "Tauri frontendDist build (${package_dir})" bash -c 'cd "$1" && corepack pnpm run build' bash "$package_dir" + run_and_capture "Tauri frontendDist build (${package_dir})" bash -c "cd \"\$1\" && corepack pnpm run build" bash "$package_dir" fi ;; yarn) if [ -n "$package_name" ] && jq -e '.workspaces // empty' package.json >/dev/null 2>&1; then run_and_capture "Tauri frontendDist build (${package_dir})" yarn workspace "$package_name" build else - run_and_capture "Tauri frontendDist build (${package_dir})" bash -c 'cd "$1" && yarn build' bash "$package_dir" + run_and_capture "Tauri frontendDist build (${package_dir})" bash -c "cd \"\$1\" && yarn build" bash "$package_dir" fi ;; esac @@ -2087,8 +2094,13 @@ jobs: # coverage command still runs and reports any uncovered GPU lines # exactly as before, so Rust repositories without GPU code are # unaffected and no gate is weakened. - if ls /usr/share/vulkan/icd.d/lvp_icd*.json >/dev/null 2>&1; then - lvp_icd="$(ls /usr/share/vulkan/icd.d/lvp_icd*.json | head -n1)" + local lvp_icd="" candidate_icd + for candidate_icd in /usr/share/vulkan/icd.d/lvp_icd*.json; do + [ -f "$candidate_icd" ] || continue + lvp_icd="$candidate_icd" + break + done + if [ -n "$lvp_icd" ]; then export VK_ICD_FILENAMES="$lvp_icd" export VK_DRIVER_FILES="$lvp_icd" export WGPU_BACKEND=vulkan @@ -2655,7 +2667,27 @@ jobs: python-version: "3.12" update-environment: false + - name: Admit existing personal LiteLLM auto route + id: personal_route + env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} + PRIVATE_TARGET: ${{ needs.validate-pr-metadata.outputs.is_private }} + run: | + set -euo pipefail + if [ -z "${LLM_GATEWAY_API_KEY:-}" ]; then + echo '::error::STOP: existing personal review Secret is unavailable.' + exit 1 + fi + python3 -m scripts.ci.personal_review_route \ + --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ + --private-target "$PRIVATE_TARGET" + echo 'selected=true' >>"$GITHUB_OUTPUT" + - name: Provision contextual-orchestrator review sidecar + if: steps.personal_route.outputs.selected != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -2664,6 +2696,7 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ needs.validate-pr-metadata.outputs.is_private }} + CONTEXTUAL_ORCHESTRATOR_POOL: auto run: | set -euo pipefail bash "$GITHUB_WORKSPACE/scripts/ci/contextual_orchestrator_review_sidecar.sh" @@ -3295,13 +3328,13 @@ jobs: language_signal="Match changed prose" fi - printf -- '- Preferred review language: `%s`\n' "$language_signal" + printf -- "- Preferred review language: \`%s\`\\n" "$language_signal" printf -- '- Rule: write human-readable review prose in the preferred language; keep file paths, identifiers, logs, quoted source, error text, and protocol literals unchanged.\n' - printf -- '- PR title: `%s`\n' "$(printf '%s' "$title" | tr '\r\n`' ' ' | cut -c 1-240)" + printf -- "- PR title: \`%s\`\\n" "$(printf '%s' "$title" | tr '\r\n`' ' ' | cut -c 1-240)" if [ -n "$body" ]; then - printf -- '- PR body excerpt: `%s`\n' "$(printf '%s' "$body" | tr '\r\n`' ' ' | cut -c 1-360)" + printf -- "- PR body excerpt: \`%s\`\\n" "$(printf '%s' "$body" | tr '\r\n`' ' ' | cut -c 1-360)" else - printf -- '- PR body excerpt: `[empty]`\n' + printf -- "- PR body excerpt: \`[empty]\`\\n" fi } @@ -3580,7 +3613,7 @@ jobs: shift if ! git -C "$OPENCODE_SOURCE_WORKDIR" diff "$@"; then - printf 'Unable to collect %s from `%s` to `%s`; continue review from available changed-file evidence and direct file inspection.\n' "$description" "$PR_MERGE_BASE" "$PR_HEAD_SHA" + printf "Unable to collect %s from \`%s\` to \`%s\`; continue review from available changed-file evidence and direct file inspection.\\n" "$description" "$PR_MERGE_BASE" "$PR_HEAD_SHA" fi } @@ -3590,13 +3623,13 @@ jobs: printf -- "- Base SHA: \`%s\`\n" "$PR_BASE_SHA" printf -- "- Head SHA: \`%s\`\n\n" "$PR_HEAD_SHA" if ! PR_MERGE_BASE="$(git -C "$OPENCODE_SOURCE_WORKDIR" merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")"; then - printf 'Merge-base discovery failed for `%s` and `%s`; falling back to base SHA for bounded diff evidence.\n\n' "$PR_BASE_SHA" "$PR_HEAD_SHA" + printf "Merge-base discovery failed for \`%s\` and \`%s\`; falling back to base SHA for bounded diff evidence.\\n\\n" "$PR_BASE_SHA" "$PR_HEAD_SHA" PR_MERGE_BASE="$PR_BASE_SHA" fi printf -- "- Merge base SHA: \`%s\`\n\n" "$PR_MERGE_BASE" printf '## Current-head authority order\n\n' printf 'Treat current-head sections in this file as authoritative for this run: Other unresolved review thread evidence, Failed GitHub Check evidence, Coverage execution evidence, Changed files, and Focused changed hunks.\n' - printf 'All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA `%s`.\n\n' "$PR_HEAD_SHA" + printf "All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA \`%s\`.\\n\\n" "$PR_HEAD_SHA" if ! git -C "$OPENCODE_SOURCE_WORKDIR" diff --name-only --find-renames "$PR_MERGE_BASE" "$PR_HEAD_SHA" | awk 'NF > 0 && $0 !~ /^\// && $0 !~ /(^|\/)\.\.($|\/)/ { print }' >"$OPENCODE_CHANGED_FILES_FILE"; then printf 'Changed-file discovery failed; downstream review must inspect the PR head directly.\n\n' @@ -3752,6 +3785,10 @@ jobs: - name: Prepare isolated OpenCode review workspace env: + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} + PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} + PRIVATE_TARGET: ${{ needs.validate-pr-metadata.outputs.is_private }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md @@ -4101,8 +4138,8 @@ jobs: jq -n '{ "$schema": "https://opencode.ai/config.json", - "model": "contextual-orchestrator/orchestrator/free", - "small_model": "contextual-orchestrator/orchestrator/free", + "model": "contextual-orchestrator/orchestrator/auto", + "small_model": "contextual-orchestrator/orchestrator/auto", "enabled_providers": ["contextual-orchestrator"], "lsp": false, "mcp": {}, @@ -4184,8 +4221,8 @@ jobs: gateway_config="${OPENCODE_REVIEW_WORKDIR}/opencode.gateway.jsonc" jq ' - .model = "contextual-orchestrator/orchestrator/free" | - .small_model = "contextual-orchestrator/orchestrator/free" | + .model = "contextual-orchestrator/orchestrator/auto" | + .small_model = "contextual-orchestrator/orchestrator/auto" | .enabled_providers = ["contextual-orchestrator"] | .provider = { "contextual-orchestrator": { @@ -4196,8 +4233,8 @@ jobs: "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "orchestrator/free": { - "name": "Orchestrator Free (ZDR-first zero-cost pool)", + "orchestrator/auto": { + "name": "Orchestrator Auto (cost-fenced dynamic selection)", "tool_call": true, "reasoning": true, "options": { @@ -4219,11 +4256,18 @@ jobs: ' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" >"$gateway_config" mv -- "$gateway_config" "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" - if ! grep -Fq 'contextual-orchestrator/orchestrator/free' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" \ + if ! grep -Fq 'contextual-orchestrator/orchestrator/auto' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" \ || ! grep -Fq 'CONTEXTUAL_ORCHESTRATOR_BASE_URL' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc"; then echo '::error::Generated isolated opencode.jsonc is missing the contextual-orchestrator gateway route.' exit 1 fi + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + python3 -m scripts.ci.personal_review_route \ + --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ + --private-target "$PRIVATE_TARGET" \ + --opencode-config "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" + fi printf 'Prepared isolated OpenCode review workspace: %s\n' "$OPENCODE_REVIEW_WORKDIR" - name: Run OpenCode PR Review model pool @@ -4231,10 +4275,12 @@ jobs: if: needs.coverage-evidence.result == 'success' continue-on-error: true env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free" + OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. @@ -4272,7 +4318,12 @@ jobs: RUN_ATTEMPT: ${{ github.run_attempt }} run: | set -euo pipefail - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export OPENCODE_MODEL_CANDIDATES="personal-litellm/auto" + else + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + fi set +e bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" pool_status=$? @@ -4607,27 +4658,27 @@ jobs: "$@" } - self_check_filter=' + self_check_filter=" def self_check: - (.name // "") as $n - | ["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation", "scan-pr-queue"] | index($n); + (.name // \"\") as \$n + | [\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"] | index(\$n); def latest_peer_checks: [ (.check_runs // [])[] | select(self_check | not) | . + { checkedAt: ( - if ((.started_at // "") != "") then .started_at - else (.completed_at // "") + if ((.started_at // \"\") != \"\") then .started_at + else (.completed_at // \"\") end ) } ] - | sort_by(.app.slug // "", .name // "", .checkedAt // "", .id // 0) - | group_by([.app.slug // "", .name // ""]) + | sort_by(.app.slug // \"\", .name // \"\", .checkedAt // \"\", .id // 0) + | group_by([.app.slug // \"\", .name // \"\"]) | map(last) | .[]; - ' + " check_runs_file="$(mktemp)" pending_checks_file="$(mktemp)" @@ -4830,6 +4881,8 @@ jobs: # extensions for image validation or package/GPU builds plus API and # publication overhead. env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} # The OpenCode app installation token is exchanged from api.opencode.ai @@ -4852,7 +4905,7 @@ jobs: COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - MODEL: contextual-orchestrator/orchestrator/free + MODEL: contextual-orchestrator/orchestrator/auto USE_GITHUB_TOKEN: "true" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -4886,6 +4939,10 @@ jobs: OPENCODE_EXPORT_TIMEOUT_SECONDS: "60" run: | set -euo pipefail + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export MODEL="personal-litellm/auto" + fi quoted_coverage="${COVERAGE_EVIDENCE_RESULT:-}" COVERAGE_EVIDENCE_RESULT="$(python3 scripts/ci/opencode_coverage_identity.py \ --repo "$GH_REPOSITORY" \ @@ -5274,7 +5331,7 @@ jobs: if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { printf '## OpenCode %s review body\n\n' "$event" - printf -- '- Head SHA: `%s`\n' "$HEAD_SHA" + printf -- "- Head SHA: \`%s\`\\n" "$HEAD_SHA" printf -- '- Workflow run: %s\n' "$RUN_ID" printf -- '- Workflow attempt: %s\n\n' "$RUN_ATTEMPT" printf '%s\n' "$body" @@ -5628,7 +5685,7 @@ jobs: printf '## Summary\n\n' printf '%s\n\n' "$summary" printf '## Adversarial validation\n\n' - printf '```json\n%s\n```\n\n' "$adversarial_evidence" + printf "\`\`\`json\\n%s\\n\`\`\`\\n\\n" "$adversarial_evidence" printf -- '- Result: REQUEST_CHANGES\n' printf -- '- Reason: %s\n\n' "$reason" printf -- "- Head SHA: \`%s\`\n" "$HEAD_SHA" @@ -6214,9 +6271,15 @@ jobs: printf 'Skipping publish-step failed-check OpenCode diagnosis for central review-process self-repair; using collected current-head failed-check logs/SARIF fallback so the publish step stays bounded.\n' >&2 return 1 fi - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then - return 1 + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + if [ -z "${LLM_GATEWAY_API_KEY:-}" ]; then + return 1 + fi + else + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then + return 1 + fi fi if ! python3 "$GITHUB_WORKSPACE/scripts/ci/assert_opencode_reasoning_effort.py" \ --config "$OPENCODE_REVIEW_WORKDIR/opencode.jsonc" \ @@ -6443,33 +6506,33 @@ jobs: case "$mode" in failed) - jq_filter=' + jq_filter=" [.[].check_runs[]?] - | sort_by((.started_at // .completed_at // .created_at // ""), (.id // 0)) - | group_by(.name // "") + | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0)) + | group_by(.name // \"\") | map(last) | .[]? - | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) - | select((.status // "") == "completed") - | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) - | select((.name // "") != "scan-pr-queue") - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review") | not) - | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) - ' + | select(((.name // \"\" | ascii_downcase) as \$n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index(\$n)) | not) + | select((.status // \"\") == \"completed\") + | select((.conclusion // \"\" | ascii_upcase) as \$c | [\"FAILURE\",\"TIMED_OUT\",\"ACTION_REQUIRED\",\"CANCELLED\",\"STARTUP_FAILURE\"] | index(\$c)) + | select((.name // \"\") != \"scan-pr-queue\") + | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and ((.name // \"\") | contains(\"\$\" + \"{{\"))) | not) + | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and (.name // \"\") == \"noema-review\") | not) + | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.conclusion // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end) + " ;; pending) - jq_filter=' + jq_filter=" [.[].check_runs[]?] - | sort_by((.started_at // .completed_at // .created_at // ""), (.id // 0)) - | group_by(.name // "") + | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0)) + | group_by(.name // \"\") | map(last) | .[]? - | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) - | select((.name // "") != "scan-pr-queue") - | select((.status // "") != "completed") - | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.status // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) - ' + | select(((.name // \"\" | ascii_downcase) as \$n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index(\$n)) | not) + | select((.name // \"\") != \"scan-pr-queue\") + | select((.status // \"\") != \"completed\") + | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.status // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end) + " ;; *) return 1 @@ -6527,10 +6590,10 @@ jobs: -f owner="$owner" \ -f name="$name" \ -F number="$PR_NUMBER" \ - -f query=' - query($owner:String!,$name:String!,$number:Int!) { - repository(owner:$owner,name:$name) { - pullRequest(number:$number) { + -f query=" + query(\$owner:String!,\$name:String!,\$number:Int!) { + repository(owner:\$owner,name:\$name) { + pullRequest(number:\$number) { statusCheckRollup { contexts(first: 100) { nodes { @@ -6555,7 +6618,7 @@ jobs: } } } - ' \ + " \ --jq ' (.data.repository.pullRequest.statusCheckRollup.contexts.nodes // []) | map( @@ -6680,7 +6743,7 @@ jobs: -f owner="$owner" \ -f name="$name" \ -F number="$PR_NUMBER" \ - -f query='query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id}}}' \ + -f query="query(\$owner:String!,\$name:String!,\$number:Int!){repository(owner:\$owner,name:\$name){pullRequest(number:\$number){id}}}" \ --jq '.data.repository.pullRequest.id // empty')"; then echo "GitHub Checks statusCheckRollup PR id lookup failed; falling back to current-head REST check-runs." >&2 pr_node_id="" @@ -7124,21 +7187,21 @@ jobs: -f "ref=refs/heads/${head_ref}" \ -f state=open \ -F per_page=100 \ - --jq ' + --jq " [ .[] | { number: (.number // 0), - rule: (.rule.id // .rule.name // "unknown"), - tool: (.tool.name // "code-scanning"), - severity: (.rule.security_severity_level // .rule.severity // "unknown"), - url: (.html_url // "") + rule: (.rule.id // .rule.name // \"unknown\"), + tool: (.tool.name // \"code-scanning\"), + severity: (.rule.security_severity_level // .rule.severity // \"unknown\"), + url: (.html_url // \"\") } - | select((.severity | ascii_downcase) as $s | ["medium","high","critical","warning","error"] | index($s)) + | select((.severity | ascii_downcase) as \$s | [\"medium\",\"high\",\"critical\",\"warning\",\"error\"] | index(\$s)) ] | .[] - | "- " + .tool + "/" + .rule + ": " + .severity + " alert #" + (.number | tostring) + (if .url != "" then " (" + .url + ")" else "" end) - ' >"$output_file" 2>"$lookup_error_file"; then + | \"- \" + .tool + \"/\" + .rule + \": \" + .severity + \" alert #\" + (.number | tostring) + (if .url != \"\" then \" (\" + .url + \")\" else \"\" end) + " >"$output_file" 2>"$lookup_error_file"; then sed 's/^/gh: /' "$lookup_error_file" >&2 || true rm -f "$lookup_error_file" return 1 @@ -7186,7 +7249,7 @@ jobs: printf 'OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.\n\n' printf '## Findings\n\n' printf '### 1. HIGH Current-head GitHub Checks - Fix failed required checks before approval\n' - printf -- '- Problem: Failed same-head checks remain for `%s`.\n' "$HEAD_SHA" + printf -- "- Problem: Failed same-head checks remain for \`%s\`.\\n" "$HEAD_SHA" printf -- '- Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.\n' printf -- '- Fix: Read and fix the failed check logs below, then rerun the current-head checks.\n' printf -- '- Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.\n\n' @@ -7242,11 +7305,11 @@ jobs: if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { printf '## OpenCode required check satisfied by existing same-head approval\n\n' - printf -- '- Result: `EXISTING_CURRENT_HEAD_APPROVAL`\n' - printf -- '- Head SHA: `%s`\n' "$HEAD_SHA" + printf -- "- Result: \`EXISTING_CURRENT_HEAD_APPROVAL\`\\n" + printf -- "- Head SHA: \`%s\`\\n" "$HEAD_SHA" printf -- '- Workflow run: %s\n' "$RUN_ID" printf -- '- Workflow attempt: %s\n' "$RUN_ATTEMPT" - printf -- '- Model-pool outcome: `%s`\n' "${OPENCODE_MODEL_POOL_OUTCOME:-unknown}" + printf -- "- Model-pool outcome: \`%s\`\\n" "${OPENCODE_MODEL_POOL_OUTCOME:-unknown}" printf -- '- Reason: a prior real-model OpenCode APPROVED review with passed structured adversarial probes already targets this exact head, and the fallback rechecked coverage, peer checks, code-scanning alerts, and unresolved review threads before accepting it.\n' printf -- '- Review state: unchanged; no duplicate APPROVE review was posted from model-output-unavailable evidence.\n\n' } >>"$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index bbe692b417..1cfbea07b7 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -43,7 +43,9 @@ jobs: # queue wait between two single-digit-second jobs. See # docs/doctoring/actions-capacity-root-cause-20260917.md for the # underlying measurement methodology. - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: read pull-requests: read @@ -308,7 +310,9 @@ jobs: # gap using the identical classifier. Fails OPEN: an unreadable, empty, # or truncated file list reviews everything. if: (github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft')) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -362,7 +366,9 @@ jobs: name: coverage-source-tree needs: [required-workflow-bootstrap] if: needs.required-workflow-bootstrap.outputs.admitted == 'true' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} steps: - run: >- echo "PR-head source and coverage execution are delegated to the @@ -382,7 +388,9 @@ jobs: # through coverage-source-tree, so an unadmitted head still skips it. needs: [required-workflow-bootstrap] if: needs.required-workflow-bootstrap.outputs.admitted == 'true' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} steps: - run: >- echo "This required-workflow job preserves the stable branch-protection @@ -401,7 +409,9 @@ jobs: # directly by this job's own `if:` below, not inherited through that edge. needs: [required-workflow-bootstrap, changed-scope] if: needs.required-workflow-bootstrap.outputs.admitted == 'true' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: read pull-requests: read @@ -521,6 +531,7 @@ jobs: PR_ACTION: ${{ github.event.action }} PR_DRAFT: ${{ github.event.pull_request.draft }} CHANGED_SCOPE_CODE: ${{ needs.changed-scope.outputs.code }} + WORKFLOW_SHA: ${{ github.workflow_sha }} run: | set -euo pipefail if [ "$PR_ACTION" = "closed" ]; then @@ -562,35 +573,40 @@ jobs: if [ "$PR_DRAFT" = "true" ]; then echo "Event draft snapshot is stale; checking the verdict for the live ready PR." fi - reviews="$(gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100")" - verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" ' - (add // []) - | [ - .[] - | select( - (.user.login // "" | ascii_downcase) as $user - | $user == "opencode-agent" or $user == "opencode-agent[bot]" - ) - | select((.commit_id // "" | ascii_downcase) == ($sha | ascii_downcase)) - | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED") - ] - | (last // {}) as $review - | ($review.body // "" | ascii_downcase) as $body - | if $review.state == "CHANGES_REQUESTED" then - "CHANGES_REQUESTED" - elif $review.state == "APPROVED" - and ($body | contains("deterministic current-head evidence") | not) - and ($body | contains("deterministic fallback approval") | not) - and ($body | contains("model-unavailable evidence fallback") | not) - and ($body | contains("did not emit a usable current-head control block") | not) - and ($body | contains("scope: `unsupported`") | not) - and ($body | contains("model-pool outcome: `unknown`") | not) - then - "APPROVED" - else - empty - end - ')" + reviews="$(gh api --paginate --slurp "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100")" + helper="$(mktemp)" + trap 'rm -f "$helper"' EXIT + gh api "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=${WORKFLOW_SHA}" \ + --jq .content | base64 --decode >"$helper" + verdict="$(python3 - "$helper" "$reviews" "$HEAD_SHA" <<'PY' + import importlib.machinery + import importlib.util + import json + import sys + + helper_path, reviews_json, head_sha = sys.argv[1:] + loader = importlib.machinery.SourceFileLoader("trusted_opencode_receipt_gate", helper_path) + spec = importlib.util.spec_from_loader(loader.name, loader) + if spec is None or spec.loader is None: + raise RuntimeError("trusted OpenCode receipt helper could not be loaded") + gate = importlib.util.module_from_spec(spec) + spec.loader.exec_module(gate) + pages = json.loads(reviews_json) + if not isinstance(pages, list) or not all( + isinstance(page, list) and all(isinstance(review, dict) for review in page) + for page in pages + ): + raise ValueError("formal review lookup returned malformed paginated JSON") + reviews = [review for page in pages for review in page + if str((review.get("user") or {}).get("login") or "").lower() + in {"opencode-agent", "opencode-agent[bot]"}] + receipt, reason = gate.evaluate_receipts(reviews, head_sha, is_draft=False) + if receipt is None: + print(f"No current-head formal OpenCode verdict: {reason}", file=sys.stderr) + raise SystemExit(0) + print(receipt["state"]) + PY + )" if [ -z "$verdict" ]; then echo "::error::No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. The dispatch workflow will rerun this failed job after publishing an authenticated exact-head verdict." exit 1 @@ -611,7 +627,9 @@ jobs: # head no longer matches the live one. The target job also revalidates the # live PR before dispatch and verdict admission. if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: actions: write contents: read diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml index fbe7efa434..fa1ef018ff 100644 --- a/.github/workflows/organization-commercial-readiness-loop.yml +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -16,7 +16,9 @@ jobs: if: >- github.repository == 'ContextualWisdomLab/.github' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) - runs-on: ubuntu-24.04 + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 25 permissions: contents: read diff --git a/.github/workflows/pr-auto-rebase.yml b/.github/workflows/pr-auto-rebase.yml index d25bb9c9f9..4992224926 100644 --- a/.github/workflows/pr-auto-rebase.yml +++ b/.github/workflows/pr-auto-rebase.yml @@ -68,7 +68,9 @@ permissions: jobs: auto-rebase: - runs-on: ubuntu-latest + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: read # checkout only; force-push uses the app or PR-write token below pull-requests: write # read PR queue and manage labels diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index a0cf3642d4..4528a3f014 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -22,7 +22,9 @@ permissions: jobs: autofix: - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] # No job-level timeout-minutes here, deliberately. This job's dominant # cost is `opencode run` (up to two invocations: the main autofix pass, # and a base-merge conflict-resolution pass) -- a job-level wall-clock diff --git a/.github/workflows/pr-review-fix-scheduler.yml b/.github/workflows/pr-review-fix-scheduler.yml index b6d3fff560..5543bbc2b9 100644 --- a/.github/workflows/pr-review-fix-scheduler.yml +++ b/.github/workflows/pr-review-fix-scheduler.yml @@ -92,7 +92,9 @@ jobs: # API reads and autofix dispatch only; no pull-request content is checked # out. The central main caller uses the idle control pool instead of the # saturated hosted queue; any other caller keeps hosted Ubuntu 24.04. - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/hourly-review-repair.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/hourly-review-repair.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/hourly-review-repair.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 35 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.github/workflows/python-security.yml b/.github/workflows/python-security.yml index 1788cfd40b..6e07d817c2 100644 --- a/.github/workflows/python-security.yml +++ b/.github/workflows/python-security.yml @@ -46,7 +46,9 @@ jobs: detect-python: name: Detect Python if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] outputs: has_python: ${{ steps.detect.outputs.has_python }} has_manifest: ${{ steps.detect.outputs.has_manifest }} @@ -77,7 +79,9 @@ jobs: name: Bandit (Python SAST) needs: detect-python if: github.event.action != 'closed' && needs.detect-python.outputs.has_python == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write @@ -204,7 +208,9 @@ jobs: name: pip-audit (Python dependency audit) needs: detect-python if: github.event.action != 'closed' && needs.detect-python.outputs.has_manifest == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read steps: diff --git a/.github/workflows/r-package-check.yml b/.github/workflows/r-package-check.yml index 221d66a838..0d23daac2a 100644 --- a/.github/workflows/r-package-check.yml +++ b/.github/workflows/r-package-check.yml @@ -45,8 +45,8 @@ on: description: >- JSON array of {os, r, http-user-agent?} objects for strategy.matrix.config. Default is a single ubuntu-latest/release - leg; override with a JSON array for a multi-OS/multi-R-version - matrix. + leg. Only ubuntu-latest and ubuntu-24.04 Linux/x64 image aliases + execute; other OS/architecture legs are explicitly STOP, not PASS. required: false type: string default: '[{"os": "ubuntu-latest", "r": "release"}]' @@ -91,14 +91,63 @@ permissions: contents: read jobs: + admit-platforms: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + outputs: + matrix: ${{ steps.admit.outputs.matrix }} + steps: + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.10' + - id: admit + env: + R_MATRIX: ${{ inputs.r_matrix }} + shell: bash + run: | + matrix=$(python - "$R_MATRIX" <<'PY' + """Bound the R matrix to supported Linux/x64 image aliases, never runner labels.""" + import json + import sys + + + def admit(raw): + """Reject unsupported matrix legs before any R job can be scheduled.""" + rows = json.loads(raw) + if not isinstance(rows, list) or not 1 <= len(rows) <= 64: + raise ValueError('Expected 1..64 matrix objects') + for row in rows: + if not isinstance(row, dict) or not isinstance(row.get('os'), str): + raise ValueError('Expected an OS string in each matrix object') + if not isinstance(row.get('r'), str) or not row['r'].strip(): + raise ValueError('Expected a nonempty R version') + if row['os'] in ('ubuntu-latest', 'ubuntu-24.04'): + continue + raise ValueError('STOP unsupported platform leg: ' + repr(row['os'])) + return rows + + + if __name__ == '__main__': + try: + print(json.dumps(admit(sys.argv[1]), separators=(',', ':'))) + except (ValueError, TypeError, IndexError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) + PY + ) + printf 'matrix=%s\n' "$matrix" >> "$GITHUB_OUTPUT" R-CMD-check: - runs-on: ${{ matrix.config.os }} + needs: admit-platforms + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] name: ${{ matrix.config.os }} (${{ matrix.config.r }}) strategy: fail-fast: false matrix: - config: ${{ fromJSON(inputs.r_matrix) }} + config: ${{ fromJSON(needs.admit-platforms.outputs.matrix) }} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-dependency-license-strix-gate.yml b/.github/workflows/release-dependency-license-strix-gate.yml index 68d48d15ef..8fbec4df37 100644 --- a/.github/workflows/release-dependency-license-strix-gate.yml +++ b/.github/workflows/release-dependency-license-strix-gate.yml @@ -162,7 +162,9 @@ env: jobs: prepare: name: Verify exact distributions and approve the licence closure - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 180 permissions: contents: read @@ -455,7 +457,9 @@ jobs: strix: name: Strix ${{ matrix.key }} needs: prepare - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 360 permissions: contents: read @@ -709,7 +713,9 @@ jobs: name: Strix overflow ${{ matrix.key }} needs: prepare if: ${{ needs.prepare.outputs.has_overflow == 'true' }} - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 360 permissions: contents: read @@ -726,7 +732,9 @@ jobs: ((needs.prepare.outputs.has_overflow == 'true' && needs.strix_overflow.result == 'success') || (needs.prepare.outputs.has_overflow == 'false' && needs.strix_overflow.result == 'skipped')) }} name: Collect every Strix binding and seal the complete verdict - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 180 permissions: contents: read diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index 2cba51bbc6..fd14ba112f 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -35,7 +35,9 @@ concurrency: jobs: validate: - runs-on: ubuntu-24.04 + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 20 steps: - name: Harden runner @@ -108,7 +110,9 @@ jobs: apply: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' needs: validate - runs-on: ubuntu-24.04 + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 45 environment: repository-metadata-maintenance steps: diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index f8ab04b865..2f128c1aea 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -50,7 +50,9 @@ jobs: # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read pull-requests: read diff --git a/.github/workflows/sbom-generation.yml b/.github/workflows/sbom-generation.yml index 588baefe1e..7d630dd78a 100644 --- a/.github/workflows/sbom-generation.yml +++ b/.github/workflows/sbom-generation.yml @@ -43,7 +43,9 @@ permissions: jobs: generate-sbom: - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: # write is needed for release-asset upload and dependency submission. contents: write diff --git a/.github/workflows/sbom-inventory-scheduler.yml b/.github/workflows/sbom-inventory-scheduler.yml index 8810c702fd..e60d66b7d0 100644 --- a/.github/workflows/sbom-inventory-scheduler.yml +++ b/.github/workflows/sbom-inventory-scheduler.yml @@ -30,7 +30,9 @@ permissions: jobs: aggregate-sbom-inventory: - runs-on: ubuntu-latest + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: write id-token: write diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml index 6b6a90aa36..0c2f118d1c 100644 --- a/.github/workflows/scheduled-security-scan.yml +++ b/.github/workflows/scheduled-security-scan.yml @@ -38,7 +38,9 @@ permissions: jobs: detect-languages: name: Detect CodeQL languages - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] outputs: matrix: ${{ steps.detect.outputs.matrix }} steps: @@ -72,7 +74,9 @@ jobs: codeql: name: CodeQL periodic (${{ matrix.language }}) needs: detect-languages - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: actions: read contents: read @@ -102,7 +106,9 @@ jobs: trivy-fs: name: Trivy filesystem (periodic) - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 6222b28b28..2064ab9b54 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -29,7 +29,9 @@ permissions: read-all jobs: analysis: name: Scorecard analysis - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: ["self-hosted", "linux", "x64", "cwlab-ci-isolated"] permissions: security-events: write id-token: write diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 948ae583d4..3698e30056 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -35,7 +35,9 @@ permissions: jobs: gitleaks: name: gitleaks (secret scan) - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index e04d7bf8f3..46cddd322e 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -71,7 +71,9 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 5 permissions: contents: read @@ -126,7 +128,9 @@ jobs: osv-scan: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.deps == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 25 permissions: actions: read @@ -339,6 +343,10 @@ jobs: echo "::warning::OSV SARIF upload to code scanning failed after the base/head comparison. The PR-introduced vulnerability reporter above remains the hard gate, so upload rate limits cannot hide OSV findings." - name: Upload OSV debug artifacts if: always() + # The reporter and SARIF upload above preserve the security verdict. + # Debug retention quota is observability only and must not turn a clean + # comparison into a failed security gate. + continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v6.0.0 with: name: osv-scan-debug @@ -352,7 +360,9 @@ jobs: dependency-review: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.deps == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read pull-requests: read @@ -387,6 +397,23 @@ jobs: set -euo pipefail api_url="${GITHUB_API_URL:-https://api.github.com}" + git_object_id='^[0-9a-f]{40}([0-9a-f]{24})?$' + repository_identity='^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' + if ! [[ "${BASE_SHA}" =~ $git_object_id ]] || ! [[ "${HEAD_SHA}" =~ $git_object_id ]]; then + echo "::error::Dependency review evidence unavailable: exact 40- or 64-character hexadecimal base and head revisions are required before any compare request. Named refs are not evidence. Verify the pull-request event SHAs, then rerun. Failing closed." + exit 1 + fi + if ! [[ "${REPOSITORY}" =~ $repository_identity ]]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + repository_owner="${REPOSITORY%%/*}" + repository_name="${REPOSITORY#*/}" + if [ "${repository_owner}" = "." ] || [ "${repository_owner}" = ".." ] || [ "${repository_name}" = "." ] || [ "${repository_name}" = ".." ]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Dot or parent-directory path components are not evidence. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + set +e status="$( curl -sS --connect-timeout 10 --max-time 30 \ @@ -436,7 +463,9 @@ jobs: gitleaks: name: gitleaks (secret scan) if: github.event.action != 'closed' && github.repository == 'ContextualWisdomLab/.github' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write @@ -526,7 +555,9 @@ jobs: trivy-fs: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write @@ -553,6 +584,10 @@ jobs: - name: Trivy filesystem scan uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 with: + # Runner groups can schedule onto different hosts. Reinstall the + # pinned binary instead of trusting a cache entry whose path or + # executable bit belongs to a previous runner. + cache: false scan-type: fs scan-ref: . scanners: vuln,secret,misconfig @@ -633,7 +668,9 @@ jobs: scorecard: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] # SOFT: posture findings are unrelated to the PR diff, so never block merge. continue-on-error: true permissions: diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index abdc8af49e..2b65a5cc64 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -130,7 +130,9 @@ 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')) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - 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"]') }} + runs-on: + group: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -190,7 +192,9 @@ jobs: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') ) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - 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"]') }} + runs-on: + group: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -340,7 +344,9 @@ 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: ${{ (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"]') }} + runs-on: + group: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # 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 @@ -462,7 +468,9 @@ jobs: # scans may take more than two hours per model (docs/product-goal-directive.md). # Inference has no wall-clock deadline; cancellation is reserved for an # explicit operator action or a superseded head. - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] # Least-privilege token scoped to this job (Scorecard alert #43): the scan # exchanges an OIDC token (id-token) and publishes same-repo status evidence # from the scan job only. @@ -839,18 +847,22 @@ jobs: - name: Gate Strix secrets id: gate env: - STRIX_MODEL: contextual-orchestrator/orchestrator/free + STRIX_MODEL: auto STRIX_MODEL_REQUESTED: ${{ github.event.client_payload.strix_llm || '' }} run: | requested_model="$(printf '%s' "$STRIX_MODEL_REQUESTED" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" case "$requested_model" in - ""|orchestrator/free|contextual-orchestrator/orchestrator/free) ;; + ""|auto|orchestrator/auto|contextual-orchestrator/orchestrator/auto) ;; *) - echo '::error::Strix model overrides are limited to contextual-orchestrator/orchestrator/free.' + echo '::error::Strix model overrides must select literal auto; free and direct-provider routes are not admitted.' exit 1 ;; esac strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [ "$strix_model" != "auto" ]; then + echo '::error::Strix primary model must remain literal auto.' + exit 1 + fi { echo "strix_model=$strix_model" echo 'enabled=true' @@ -876,7 +888,7 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.is_private }} - CONTEXTUAL_ORCHESTRATOR_POOL: free + CONTEXTUAL_ORCHESTRATOR_POOL: auto run: | set -euo pipefail report_parent="$GITHUB_WORKSPACE/strix_runs" @@ -1033,11 +1045,11 @@ jobs: strix_llm_file="$RUNNER_TEMP/strix_llm.txt" strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" case "$strix_model" in - orchestrator/free | contextual-orchestrator/orchestrator/free) - printf '%s' 'orchestrator/free' > "$strix_llm_file" + auto) + printf '%s' 'auto' > "$strix_llm_file" ;; *) - echo '::error::STRIX_LLM must select contextual-orchestrator/orchestrator/free.' + echo '::error::STRIX_LLM must remain literal auto; free fallback is not admitted.' exit 1 ;; esac @@ -1297,7 +1309,9 @@ 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: ${{ (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"]') }} + runs-on: + group: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # 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 @@ -1520,7 +1534,9 @@ jobs: && needs.strix.result == 'failure' && needs.strix.outputs.transport_capacity_unavailable == 'true' && needs.strix.outputs.transport_retry_eligible == 'true' - 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"]') }} + runs-on: + group: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((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('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 10 permissions: contents: write diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index ea78c30cc7..8a5e9db20c 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -4,6 +4,9 @@ on: pull_request: paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" + - ".github/workflows/company-default-head-trust-gate.yml" + - ".github/workflows/company-settlement-validate.yml" + - "tests/test_company_default_head_trust_gate.py" - "scripts/ci/materialize_base_python_requirements.py" - "scripts/ci/verify_release_maturin_tool_assets.py" - "tests/conftest.py" @@ -21,6 +24,9 @@ on: branches: [main] paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" + - ".github/workflows/company-default-head-trust-gate.yml" + - ".github/workflows/company-settlement-validate.yml" + - "tests/test_company_default_head_trust_gate.py" - "scripts/ci/materialize_base_python_requirements.py" - "scripts/ci/verify_release_maturin_tool_assets.py" - "tests/conftest.py" @@ -48,7 +54,9 @@ env: jobs: minimum-python-contract: name: Python 3.10 compatibility contract - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 10 steps: - name: Harden runner @@ -94,7 +102,9 @@ jobs: full-quality-gate: name: Python 3.14 full quality gate - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 20 steps: - name: Harden runner diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e8dfc577..3b2417a9e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +### Security scanner setup no longer inherits runner-local failures + +- Disable `trivy-action`'s binary cache on the heterogeneous `CWL CI isolated` + runner group. Exact run `37242578328`, job `112314054734`, restored a + `trivy-binary-v0.70.0-Linux-X64` cache entry but then failed before scanning + with `trivy: command not found`; installing the pinned binary per job removes + that runner-local path and executable-bit dependency. +- Treat the always-run OSV debug artifact upload as observability rather than a + security verdict. Exact job `112314054619` completed the base/head comparison, + reported no PR-introduced finding, and uploaded SARIF before GitHub artifact + quota exhaustion failed the debug upload. The scanner and SARIF gates remain + unchanged and fail closed; only that optional retention step is non-blocking. +- RED `cc42969ba8ed4abeb1819412c146af2a9dadae8f` adds both workflow contracts; + GREEN `26f993c0bbe88d50bf81473e31d179ad7b317657` passes 5,488 tests, 10 skips, + and 40 subtests. PR #2565 remains Draft/HOLD for fresh hosted evidence, + isolated-capacity canary, and qualifying independent approval. + +### Isolated CI routing gains a runner-group authority boundary + +- Reject the complete reusable R matrix when any requested leg is not one of + the admitted Linux/x64 image aliases. A required macOS, Windows, ARM, or + unknown leg can no longer disappear while the remaining Ubuntu leg reports + success; callers must request only executable legs or receive a failed + admission job. +- Serialize the caller-controlled R matrix OS label with GitHub `toJSON()` + before inserting it into the group-scoped runner selector. Quotes, + backslashes, newlines, and duplicate `group`/`labels` text remain one + data label and cannot replace the fixed `CWL CI isolated` authority + boundary. The executable workflow contract covers benign and hostile values. + The integrated Maturin verifier now also documents its archive extraction and + command entry point, restoring the repository's 100% public-doc gate. +- Scope every ordinary and non-main self-hosted fallback through the dedicated + `CWL CI isolated` runner group as well as the existing platform/isolation + labels. A durable regression rejects label-only isolated selectors, while + workflow-specific contracts preserve dynamic matrix and trusted-main routing. + The reusable R matrix selector now escapes its literal JSON braces for + GitHub's `format()` expression, so Linux, Windows, macOS and unsupported OS + labels render a valid group-scoped `runs-on` object instead of failing before + runner admission. + The change remains Draft/HOLD until operators provision the group, restrict + its repository access, demonstrate disposable cleanup, and return exact-head + canary and required-Check evidence. + ### Maturin download failures close every transport response - Refactor the bounded Maturin asset downloader so successful and rejected @@ -75,6 +118,22 @@ from silently returning to the vulnerable versions. Protected integration, immutable consumer-pin advancement, and fresh exact-head hosted security Checks remain required before release admission. +### Dependency Review requires a completed authenticated comparison + +- The reusable Dependency Review preflight now requires both curl exit zero + and HTTP 200 before publishing `available=true`. A regression reproduces a + partial transfer that prints `200` but exits 18, preserving the fail-closed + gate. The bundled Security Scan already enforced both signals and remains + unchanged. `.github#2565` now preserves the complete `.github#1725` owner + delta through published ordinary two-parent merge + `650bc6e35409d52d24fec890c2ab74063794e9b8` (tree + `9522f0b9c96247b3412e147bc6f89fd2595c0776`) while retaining its isolated + runner-group selectors. The merged-tree focused contract is 53/53 and the + warning-fatal full suite is 5,437 passed, 10 skipped, 40 subtests. Both PRs remain + Proposed until exact-head hosted Checks and qualifying independent review are + complete. The carried security decision is ADR-0034, the concurrent owned + CodeQL decision is ADR-0033, and a repository-wide executable contract now + rejects duplicate numeric ADR identities. ### Intel macOS native archives are bound to x86_64 bytes diff --git a/docs/adr/0034-dependency-review-fail-closed-permission-envelope.md b/docs/adr/0034-dependency-review-fail-closed-permission-envelope.md new file mode 100644 index 0000000000..75ec5f9cd2 --- /dev/null +++ b/docs/adr/0034-dependency-review-fail-closed-permission-envelope.md @@ -0,0 +1,72 @@ +# ADR-0034: Fail closed on ambiguous Dependency Review authority and preserve caller permissions + +- **Status:** Proposed +- **Date:** 2026-09-02 +- **Scope:** `.github/workflows/dependency-review.yml`, its thin product callers, and the reusable-workflow security contract +- **Supersedes:** ADR-0024 only where ADR-0024 treated HTTP 403/404 as confirmed Dependency Graph unavailability or showed callers without an explicit permission envelope + +## Problem + +The protected-main consolidation from #1724 exposed two independent security-contract defects. + +First, the reusable workflow classified Dependency Graph compare HTTP 403/404 as `available=false` and therefore skipped the blocking Dependency Review action. Those responses are not authoritative proof that the feature is unavailable: they can also be authorization or policy failures. Turning an ambiguous authorization-shaped response into success silently removes a security gate. + +Second, the migration examples and thin callers omitted the permission envelope that the original repository-local workflows carried. GitHub reusable workflows cannot elevate `GITHUB_TOKEN` permissions through the call chain. A called workflow may maintain or reduce permissions granted by the caller, but it cannot manufacture `pull-requests: read` when the caller did not grant it. The result is a workflow-level `startup_failure` before any job is created. + +## Constraints + +1. Dependency Review remains a distinct hard gate; OSV-Scanner, Scorecard, or another scanner cannot substitute for an ambiguous Dependency Review failure. +2. The called workflow needs only `contents: read` and `pull-requests: read`; no write permission is introduced. +3. Product callers remain thin and repository-owned. They keep repository-specific trigger, severity, allowlist, and `continue_on_error` policy. +4. Consumers pin the reusable workflow to an immutable protected-main commit after this proposal is merged. `@main`, branch URLs, and unmerged PR heads are not production authority. +5. Non-`pull_request` invocations may skip because they lack an exact PR base/head pair; pull requests fail closed unless the comparison endpoint returns HTTP 200. + +## Considered alternatives + +### Treat 403/404 as feature unavailable + +Rejected. The status code alone cannot distinguish a genuinely unavailable Dependency Graph from denied authorization/policy. A false negative here converts a required security control into a warning. + +### Infer support from repository visibility or GHAS assumptions + +Rejected. Visibility is not a capability proof and was already the weaker design ADR-0024 replaced. + +### Rely on the called workflow's `permissions:` block + +Rejected as insufficient. GitHub does not let a reusable workflow elevate permissions beyond the caller's grant. The called workflow still declares its least-privilege ceiling, but each thin caller must explicitly grant the same read scopes. + +### Grant broader token permissions globally + +Rejected. It increases blast radius and hides a caller-contract defect instead of repairing it. + +## Decision + +1. For `pull_request`, the Dependency Graph compare preflight sets `available=true` only when curl exits zero and the endpoint returns HTTP 200. Every transport failure or other HTTP status is emitted with an error and terminates the job nonzero. +2. Remove the pull-request "Dependency graph unavailable" success path. No alternate scanner is described as replacement authority. +3. Keep the called workflow at `contents: read` + `pull-requests: read` and require every thin caller to declare at least those same scopes explicitly. +4. Make the executable central contract fail when the canonical caller example omits either required scope. +5. Replace the mutable `@main` caller example with an immutable `` placeholder. After merge, consumers pin the resulting protected-main SHA. + +## Exact evidence + +- #1725 first RED: `cb07b8bb28ef9d3a147cc966a0c70654d132da1d`; first production repair: `31f60e532e135008cabd09fcddd46a53062b0ea0`. +- Permission-envelope RED: `ee0f1ce544965772775b590050e40476df4ea8f6`; it changes only the contract and requires the missing caller permission example. +- Permission-envelope production repair: `ca3bdbd210de988ccd31f7fb96d3a97adfdb9bff`. +- `newsdom-api#784@1623977e6c37c78cb1a94a7a48c48f6d02cac86c`: run `33622976911`, `startup_failure`, zero jobs, reusable workflow immutably resolved to `.github@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03`. +- `mightyETL#330@65efdf7b4064df5b9811c0403defb707e6efbc02`: run `33623035969`, `startup_failure`, zero jobs. +- Consumer permission repairs then produced materialized current-head runs: newsdom-api `9a798d5ac7b9b295a1accb2327fc76611352290f` run `33623818000`; mightyETL `4576f863ede9fca0673d6cce5ae8a4093246f5ab` run `33623854807`; scopeweave `db8b8ed6d36a6dc6cc1d07255a7a9a86bc88bf4f` run `33623761776`; Argos #557 `ee4c5dd326977407435b0f2425fdecebc34a810f` run `33623867278`. + +Hosted exact-current-head Checks and independent review remain required before this ADR may become Accepted. + +## Consequences and follow-up + +- A missing or denied Dependency Graph comparison is visible as a blocking failure instead of silent coverage loss. +- Caller permission omissions become an executable contract defect rather than an undocumented deployment prerequisite. +- The central workflow still cannot repair a consumer's omitted permissions by itself; each consumer must carry the explicit read-only envelope and later bump its immutable reusable-workflow pin to the protected-main SHA that contains this decision. +- #1643's canary run remains independent diagnostic evidence for the required Security Scan path; its run/check result is not transferred to this gate. #1725 nevertheless integrates the durable immutable-identity validation source delta into the canonical reusable Dependency Review owner and verifies that integrated behavior with its own tests and exact-head Checks. + +## References + +GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/actions/using-workflows/reusing-workflows + +GitHub. (n.d.). *Use GITHUB_TOKEN for authentication in workflows*. GitHub Docs. https://docs.github.com/actions/security-guides/automatic-token-authentication diff --git a/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md b/docs/adr/adr-0033-owned-codeql-status-and-settlement-authority.md similarity index 96% rename from docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md rename to docs/adr/adr-0033-owned-codeql-status-and-settlement-authority.md index 50c6392edc..70e554756e 100644 --- a/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md +++ b/docs/adr/adr-0033-owned-codeql-status-and-settlement-authority.md @@ -1,5 +1,5 @@ --- -title: "ADR-0032: Owned CodeQL status and settlement authority" +title: "ADR-0033: Owned CodeQL status and settlement authority" status: Proposed date: "2026-09-27" authors: "Codex" @@ -8,7 +8,7 @@ supersedes: "" superseded_by: "" --- -# ADR-0032: Owned CodeQL status and settlement authority +# ADR-0033: Owned CodeQL status and settlement authority ## Status diff --git a/docs/doctoring/all-self-hosted-runner-routing-20261003.md b/docs/doctoring/all-self-hosted-runner-routing-20261003.md new file mode 100644 index 0000000000..5eebc87357 --- /dev/null +++ b/docs/doctoring/all-self-hosted-runner-routing-20261003.md @@ -0,0 +1,48 @@ +# All-self-hosted workflow routing — staged rollout + +## Authority and scope + +On October 3, 2026, the user directed this SDP workstream: “self hosted runner로 모두 전환”. This change covers every runner declaration in the central `.github` repository (73 jobs) and SDP (4 jobs). It does not claim migration of unrelated ecosystem repositories or deployment of runner infrastructure. + +## Routing and isolation + +Existing restricted control, CodeQL and OpenCode groups stay intact. Existing exact-main workflow predicates and repository predicates stay intact; only their GitHub-hosted fallback changes. Ordinary jobs and non-main fallbacks require the dedicated `CWL CI isolated` runner group and all labels `self-hosted`, `linux`, `x64`, `cwlab-ci-isolated`. Both constraints are mandatory: GitHub routes a group selector through the group's repository access boundary and then matches every label inside that group. The label remains a provisioning contract, not a claim that a suitable runner already exists. Never add a privileged control/review runner to this group or apply the isolation label merely to release queued PR jobs. + +PR code must execute on disposable, job-isolated machines without production credentials, privileged Docker sockets, internal-network access or state shared with trusted control jobs. Rebuild the job environment after each job. Secret-bearing trusted jobs need separate disposable instances from untrusted PR jobs. Labels are mutable metadata and GitHub does not validate that the `linux` and `x64` labels match the runner machine; label-only routing therefore cannot prove the authority boundary. The dedicated group is the repository-access boundary, while the labels express the required platform and isolation capability inside it. Keep current token permissions, environment approvals, immutable artifact checks, exact-head review and required merge gates. This patch declares the required group but does not widen runner-group allowlists, register runners, alter secrets or bypass protection. + +R reusable jobs use Linux/x64-only admission: every requested leg must name `ubuntu-latest` or `ubuntu-24.04`. R versions and other fields remain unchanged for an admitted matrix. Any unsupported or malformed leg stops the complete matrix with STOP / nonzero, including mixed and unsupported-only matrices; no requested check is dropped to manufacture success. Admission and R execution both use the fixed `CWL CI isolated` group with `self-hosted`, `Linux`, `X64`, and `cwlab-ci-isolated` labels. Windows, macOS, ARM and unknown images are not platform coverage, and stopped legs have no queued R check. Linux instances still need compatible userspace and CI dependencies; labels do not install packages or attest the image. + +The first group-scoped R selector encoded its runner object with unescaped +literal JSON braces inside GitHub's `format()` expression. GitHub requires +literal braces in a format template to be doubled; the unescaped opening brace +was parsed as a replacement field and stopped the matrix before runner +admission. RED `f9211898d83f039cdf7ff82c8f93c72c9c10dceb` +renders the exact workflow template for Linux, Windows, macOS and an unsupported +OS and reproduces the failure. GREEN +`7fc1e2e8ad5091adff6c56b501e93d1d4ac2f211` doubles only the outer braces, +retains `{0}` and `{1}`, and passes all eight focused contracts both normally +and with `GITHUB_ACTIONS=true`. This proves selector serialization only; it does +not prove that the runner group, capacity, cleanup or operating-system images +exist. + +## Actual admission observation and rollout prerequisite + +The complete organization runner inventory read during preparation had 9 runners: 7 online Linux x64 and 2 offline. None carried `cwlab-ci-isolated`; no registered Windows or macOS runners were observed, and no verified `CWL CI isolated` group/capacity record was available. Existing groups contain privileged or workflow-restricted runners. Therefore this configuration is not runtime-ready and must not be enabled or merged before the authorized runner operator creates the dedicated group, limits its repository access, and provides disposable isolated capacity. The OpenCode group's sole runner `cwlab-s1-04` was offline; changing fallback routing does not repair it. + +Before promotion, the operator must return real runner identity/group/access records, prove that `CWL CI isolated` excludes privileged pools and admits only the intended repositories, demonstrate isolated cleanup and tool availability, and produce a successful canary job plus every exact-head required gate. Provisioning and runtime canaries are not completed by local YAML parsing or pytest. Preserve existing current-head executions; do not cancel or duplicate model reviews. Ship through normal reviewed protected PRs only, after prerequisites are satisfied. + +## Verification and rollback + +Permanent regression tests enumerate declarations, reject every GitHub-hosted fallback, require the dedicated group whenever `cwlab-ci-isolated` is selected, and keep PR-executing jobs outside privileged pools. Existing workflow-contract assertions are migrated rather than deleted. A structured YAML inventory verifies all 77 central/product job declarations; normal test suites and immutable source comparisons are separate evidence. Rollback is an ordinary reviewed revert of the migration PRs, not a protection change, group-access expansion or runner relabeling. + +## Primary sources (APA 7) + +GitHub. (n.d.). Choosing the runner for a job. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/choose-the-runner-for-a-job + +GitHub. (n.d.). *Expressions: format*. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#format + +GitHub. (n.d.). Self-hosted runners reference. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/reference/runners/self-hosted-runners + +GitHub. (n.d.). Managing access to self-hosted runners using groups. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access + +GitHub. (n.d.). Using self-hosted runners in a workflow. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/use-in-a-workflow diff --git a/docs/doctoring/dependency-review-fail-closed-permission-envelope.md b/docs/doctoring/dependency-review-fail-closed-permission-envelope.md new file mode 100644 index 0000000000..2f4dca0cc2 --- /dev/null +++ b/docs/doctoring/dependency-review-fail-closed-permission-envelope.md @@ -0,0 +1,114 @@ +# Dependency Review fail-closed authority and caller permission envelope + +## Incident boundary + +Protected `ContextualWisdomLab/.github` main introduced the central reusable Dependency Review workflow through #1724. Subsequent exact-head evidence exposed three owner-contract defects that are repaired together in #1725 because all belong to the canonical dependency-review admission boundary. + +### Defect A — ambiguous HTTP status normalized to success + +The initial reusable preflight treated Dependency Graph compare HTTP 403/404 as proof that the feature was unavailable, wrote `available=false`, and let the pull-request gate finish successfully. That inference is unsafe: an authorization/policy denial can present the same status shape. The repair keeps the exact base/head compare request but authorizes the action only on HTTP 200; every other pull-request response is blocking and reports its status. + +Test-first evidence: + +- RED `cb07b8bb28ef9d3a147cc966a0c70654d132da1d` makes 403/404-as-unavailable, the fallback note, and any non-explicit fail-closed response illegal. +- Production `31f60e532e135008cabd09fcddd46a53062b0ea0` removes the 403/404 success branch and fallback note, preserving the pinned action, inputs, and non-`pull_request` skip boundary. + +### Defect B — thin callers lost required `GITHUB_TOKEN` permissions + +The original repository-local workflows carried read permission envelopes, but the migration examples/thin callers did not preserve them uniformly. GitHub reusable workflows cannot elevate the caller token. Consequently the called workflow's own `permissions: contents: read, pull-requests: read` declaration is only a ceiling; it cannot grant `pull-requests: read` when the caller omitted it. + +Live immutable-pin evidence isolates this from mutable-ref resolution: + +| Consumer | Exact head | Run | Result before caller repair | +| --- | --- | --- | --- | +| `ContextualWisdomLab/newsdom-api#784` | `1623977e6c37c78cb1a94a7a48c48f6d02cac86c` | `33622976911` | `startup_failure`, zero jobs; referenced workflow resolved to `.github@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03` | +| `ContextualWisdomLab/mightyETL#330` | `65efdf7b4064df5b9811c0403defb707e6efbc02` | `33623035969` | `startup_failure`, zero jobs | + +The consumer-side repair explicitly restores: + +```yaml +permissions: + contents: read + pull-requests: read +``` + +Fresh heads then materialized Dependency Review runs instead of failing before job creation: + +- newsdom-api `9a798d5ac7b9b295a1accb2327fc76611352290f`, run `33623818000`; +- mightyETL `4576f863ede9fca0673d6cce5ae8a4093246f5ab`, run `33623854807`; +- scopeweave `db8b8ed6d36a6dc6cc1d07255a7a9a86bc88bf4f`, run `33623761776`; +- Argos #557 `ee4c5dd326977407435b0f2425fdecebc34a810f`, run `33623867278`. + +Central test-first repair for this second defect: + +- RED `ee0f1ce544965772775b590050e40476df4ea8f6` adds `test_example_caller_preserves_required_permission_envelope()` without changing production/example workflow text. Against its parent it fails because the canonical caller example has no `permissions:` block. +- GREEN production `ca3bdbd210de988ccd31f7fb96d3a97adfdb9bff` adds the least-privilege caller envelope, explains the reusable-workflow permission ceiling, and replaces the mutable `@main` example with ``. + +### Defect C — compare identity was trusted before transport + +The earlier bundled Security Scan accepted the event-provided repository/base/head strings without first proving they were exact immutable Git object identities and one legal `owner/name` repository identity. That left the preflight contract weaker than the evidence it claimed to authorize. + +The predecessor owner lane #1643 added fail-before-transport validation and a temporary A/B canary. Its exact-head canary run `33589436750`, job `100120235906`, executed on `2026-09-02` and produced decisive evidence for the same immutable pair: + +- repository: `ContextualWisdomLab/.github`; +- base: `bb14b014eee31e6abdb5d2fffbb805aa29420eac`; +- head: `a6a2759640e6aa1d1e1219e1cd7aacdeffef32c0`; +- anonymous compare: HTTP `404`, curl exit `0`; +- job-token compare with `contents: read` and `pull-requests: read`: HTTP `200`, curl exit `0`. + +This proves that anonymous status is not an availability authority and that the least-privilege job token can establish the exact comparison. The temporary canary itself is diagnostic-only and is not part of the publishable successor contract. + +#1725 carries the durable part of that predecessor delta into the canonical writer: + +- both the reusable workflow and bundled Security Scan reject non-exact base/head revisions before curl; +- repository identity must be exactly one non-dot `owner/name` pair; `ContextualWisdomLab/.github` remains legal; +- the compare request uses the job token and only HTTP 200 authorizes the pinned Dependency Review action; +- named refs, malformed identities, 403/404 and all other non-200 outcomes fail closed. + +### Defect D — HTTP text could hide a failed curl transfer + +The reusable preflight appended `|| true` to curl and then authorized solely on +the printed HTTP status. A partial body transfer can print `200` and still exit +nonzero, so the old branch could publish `available=true` for incomplete +evidence. The executable regression makes the fake transport print `200` and +exit `18`; the preflight must remain nonzero and must not publish availability. +The minimal repair captures curl's exit status and requires both exit zero and +HTTP 200. The bundled Security Scan already enforced both conditions and was +left unchanged. + +Test-first evidence: RED `fe9d193343d12dbd229815a18ce85b494b3acff9` adds only the executable transport +contract and fails on the inherited preflight; its ordinary child carries the +minimal production and documentation repair. + +## Security invariants + +1. Pull-request Dependency Review executes only after curl completes successfully and an exact base/head compare returns HTTP 200. +2. Base/head revisions must be exact 40- or 64-character lowercase hexadecimal Git object IDs before transport. +3. Repository identity must be exactly one non-dot `owner/name` pair before transport. +4. No anonymous response, HTTP 403/404, or other non-200 response is translated into a successful "unavailable" state. +5. OSV-Scanner, Scorecard, and the separate Security Scan path remain independent controls; they do not satisfy a failed Dependency Review gate. +6. The called workflow and each caller use only `contents: read` and `pull-requests: read` for this path. No write permission is introduced. +7. Product callers pin the central workflow to an immutable protected-main commit after merge. `@main`, PR heads, and branch URLs are not production authority. +8. A non-`pull_request` event may skip because it lacks the PR base/head identity required for the comparison. + +## Verification and merge boundary + +The focused owner contracts are: + +```bash +PYTHONPATH=. pytest -q \ + tests/test_dependency_review_reusable_workflow_contract.py \ + tests/test_dependency_review_bundled_scan_identity_contract.py +``` + +The repository's normal exact-current-head required Checks, full coverage evidence, security scans, and independent reviews remain authoritative. #1725 stays Proposed/Draft while those gates are non-terminal or any substantive finding is unresolved. Queue saturation does not authorize bypass of a startup, permission, provenance, review, or security defect. + +After #1725 reaches protected main through ordinary protection, each consumer must bump its immutable reusable-workflow pin to that protected-main SHA and regenerate exact-head Dependency Review evidence. No consumer should return to `@main`. + +## References + +GitHub. (n.d.-a). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/actions/using-workflows/reusing-workflows + +GitHub. (n.d.-b). *Use GITHUB_TOKEN for authentication in workflows*. GitHub Docs. https://docs.github.com/actions/security-guides/automatic-token-authentication + +GitHub. (n.d.-c). *REST API endpoints for the dependency graph*. GitHub Docs. https://docs.github.com/rest/dependency-graph diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b4882cee5a..25d6c727fd 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,25 @@ # Product and Technical Gap Baseline +## 2026-10-09 reusable R matrix conservation + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#2565@6e924f32a2dbaa0950709c6d8f1391fc1058ef43` printed `STOP` for unsupported R platform legs but returned exit 0 whenever at least one Ubuntu leg remained; the workflow then expanded only the filtered output, so a required macOS, Windows, ARM, or unknown check could disappear while the reusable workflow passed | RED changed the existing executable contract to require the mixed Ubuntu+macOS+Windows+ARM matrix to fail; exact-parent execution failed because the script returned 0 and emitted only Ubuntu. The inline workflow copy had identical behavior | **PRD:** requested required checks are conserved or the request fails; partial silent execution is forbidden. **TRD/RCA:** reject the entire matrix on the first unsupported leg and return the original rows only when every row is admitted; keep the owner script byte-equivalent to the workflow inline copy. **Context Map:** central `.github` owns reusable R admission; product callers consume the released contract. **Flow:** `caller matrix → validate every row → all admitted → fixed isolated Linux/x64 execution`, otherwise `STOP / nonzero`. DB/UI/ERD changes are N/A | **Proposed / focused RED→GREEN complete; full exact-tree verification, hosted exact-head Checks, isolated-capacity canary, and qualifying independent approval remain required** | + +## 2026-10-07 security scanner runner isolation + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#2565@3b35d3d427d17f71472d3bf52ba179338011c17a` restored the pinned Trivy binary cache on one self-hosted runner but could not execute `trivy`, while the completed OSV comparison was reclassified as failed only because optional debug-artifact storage quota was exhausted | Security run `37242578328`; Trivy job `112314054734` logged cache hit followed by `entrypoint.sh: line 88: trivy: command not found` (exit 127). OSV job `112314054619` found no PR-introduced issue and uploaded SARIF before `Upload OSV debug artifacts` failed with `Artifact storage quota has been hit` | **PRD:** a buyer-visible security verdict must represent scanner findings, not runner-local cache state or optional retention quota. **TRD/RCA:** disable `trivy-action` binary caching across the heterogeneous isolated runner group and make only OSV debug retention non-blocking; the base/head reporter and SARIF path remain fail-closed. **Context Map:** central `.github` owns security workflow execution; product repositories consume the released required-workflow contract. **Flow:** `exact head checkout → pinned scanner install → finding gate → SARIF → optional debug retention`. DB/UI/ERD changes are N/A. RED `cc42969b…` failed 2/2 contracts; GREEN `26f993c0…` passes focused tests normally and under `GITHUB_ACTIONS=true`, plus 5,488 passed, 10 skipped, 40 subtests repository-wide | **Proposed / source and contract repair published; fresh exact-head hosted security evidence, isolated-capacity canary, unresolved-thread zero, and qualifying independent approval required** | + +## 2026-10-03 isolated self-hosted runner authority boundary + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#2565@d6ba56d04e22476b2ea41bfe7bb26e2cbc98f98f` routed ordinary jobs and non-main fallbacks by the mutable `cwlab-ci-isolated` label alone, although the PR's own doctoring stated that a generic label does not attest separation; GitHub does not validate OS/architecture label truth, so a privileged or persistent runner with matching labels and repository access could receive untrusted work | RED contract `test_isolated_label_is_scoped_to_dedicated_runner_group` failed first at `.github/workflows/actions-queue-health.yml`; the reviewed tree contained 63 affected `runs-on` declarations across direct, conditional, CodeQL and R-matrix selectors | At the central `.github` owner, require both the dedicated `CWL CI isolated` runner group and existing labels for every isolated selector; preserve trusted-main groups and repository predicates; update exact workflow oracles and rollout doctoring; keep Draft/HOLD until the group, access policy, disposable capacity, cleanup proof, canary and exact-head Checks exist | **Proposed / local RED→GREEN complete; operator capacity, exact-head hosted Checks and qualifying independent approval required** | +| `.github#2565@cd49a60f1299fb8f7984de8d8ba223db673f7b84` wrapped the reusable R runner JSON in unescaped literal braces inside GitHub `format()`, so expression evaluation treated `"group"` as a replacement field and failed before any matrix job could request a runner; the prior test asserted that defective source literal instead of rendering it | RED `f9211898d83f039cdf7ff82c8f93c72c9c10dceb` added a behavior contract that renders and parses the exact template for Linux, Windows, macOS and an unsupported OS; focused result was 1 failed / 7 passed with `KeyError: '"group"'`. GREEN `7fc1e2e8ad5091adff6c56b501e93d1d4ac2f211` doubles only the outer JSON braces and passes 8/8 normally and with `GITHUB_ACTIONS=true` | Preserve `{0}`/`{1}` substitutions and the original matrix OS label, render one valid group-scoped selector for every supported/unsupported branch, and keep the PR Draft/HOLD until the independent runner-group prerequisites and hosted exact-head gates are satisfied | **Proposed / local RED→GREEN complete; no runner capacity or merge authority claimed** | +| `.github#2565@f0f7ed988fba5d94eda6513f35d171f23c4bd9a5` inserted caller-controlled `matrix.config.os` inside a quoted JSON string; a quote-bearing label could add duplicate `group` and `labels` members whose last values replaced the fixed isolation boundary | Exact-head RED updated the source contract to require an unquoted `{1}` JSON value plus `toJSON(matrix.config.os)`; before source repair the focused result was 2 failed / 6 passed, after repair 8/8 passed. The behavioral matrix includes benign Linux/Windows/macOS/unsupported labels, a duplicate-key injection string, and backslash/newline data. Full warning-fatal verification is 5,439 passed, 10 skipped, 40 subtests with 18,830/18,830 statements and 7,684/7,684 branches covered. Exact-head public-doc coverage initially exposed two inherited Maturin helper gaps; adding their public contracts restored 100%, and compileall/diff checks pass | **PRD:** untrusted reusable-workflow input must remain data and cannot select privileged capacity. **TRD/RCA:** serialize the OS label exactly once at the GitHub expression boundary, then parse one selector with fixed group and mandatory labels. **Context Map:** central `.github` owns runner admission; callers supply labels through the released workflow contract only. **Flow:** `r_matrix JSON → matrix.config.os → toJSON → format value slot → fromJSON → fixed runner group`. DB/UI/ERD changes are N/A. Require hosted exact-head security/quality Checks, isolated capacity canary, cleanup proof, and qualifying approval | **Proposed / local RED→GREEN and full verification complete; hosted/operator gates pending** | + ## 2026-10-01 Maturin response-lifecycle coverage closure | Gap | Exact evidence | Action | Status | @@ -84,6 +104,12 @@ |---|---|---|---| | CONTROL-GITHUB-API-HTTP-ERROR-CLOSE-01 | **Proposed — protected-main RED reproduced; source repair under hosted exact-head verification** | 보호된 `.github/main@37b10243cec3d160ecc9c1be75c71428b160a703`의 Python 3.14.7 `tests/test_github_api_url_boundary.py -W error`가 실제 CodeQL/Strix opener의 synthetic 302 여덟 경우에서 `ResourceWarning: Implicitly cleaning up `로 `8 failed, 26 passed`였다. 첫 repair의 warning-fatal full suite가 동일 defect를 Noema/Pingora/preflight/Pages/sandbox readiness에서 추가로 드러냈다. | Canonical owner는 중앙 `.github`이다. 각 caller가 기존 bounded status/telemetry와 fail-closed mapping을 보존한 뒤 file-like error response를 명시적으로 닫는다. `5161 passed, 10 skipped, 40 subtests passed`로 complete warning-fatal local tree가 GREEN이다. [RCA와 acceptance](doctoring/github-api-http-error-response-lifecycle.md)를 따라 exact-head hosted security, independent review, ordinary protected merge를 완료한 뒤 `.github#2040`과 review-transport stack이 새 protected head를 정상 병합해 downstream 증거를 재생성해야 한다. | +### 2026-10-03 Dependency Review transport-completion delta + +| Gap ID / 상태 | PRD / TRD / Context Map / evidence / next gate | +|---|---| +| `CONTROL-DEPENDENCY-REVIEW-TRANSPORT-EXIT-01` — **Complete owner carryover integrated into `.github#2565`; Proposed/HOLD pending exact-head hosted evidence and independent approval** | **PRD:** 구매자는 불완전한 dependency comparison이 보안 gate를 통과하지 않음을 보장받아야 한다. **TRD/RCA:** reusable preflight가 curl의 nonzero 종료를 `|| true`로 숨기고 출력된 HTTP `200`만 검사해 partial transfer에도 `available=true`를 발행할 수 있었다. **Context Map:** 중앙 `.github` Dependency Review admission owner가 thin callers 앞에서 exact base/head와 authenticated transport completion을 소유하며, bundled Security Scan은 같은 identity/transport invariant를 적용한다. **실행 흐름:** `exact identity validation → authenticated curl → curl exit == 0 AND HTTP 200 → available=true → pinned Dependency Review`; DB/UI/ERD 변화는 N/A다. **RED→GREEN:** #1725 RED `fe9d1933…`의 fake curl `HTTP 200 + exit 18` 회귀는 defect head에서 실패했다. #2565 published ordinary two-parent merge `650bc6e35409d52d24fec890c2ab74063794e9b8` (tree `9522f0b9c96247b3412e147bc6f89fd2595c0776`)가 #1725 exact `229027280e8bce6cc4f13e722b2b0c280a1ac17d`의 source/tests/ADR/doctoring/CHANGELOG/Gap delta를 runner-group stack에 보존했고, 결합 focused suite는 일반 및 `GITHUB_ACTIONS=true`에서 각각 53/53, warning-fatal 전체 suite는 5,437 passed, 10 skipped, 40 subtests다. Review에서 발견된 ADR-0025/0032 충돌은 security decision을 ADR-0034로, owned CodeQL decision을 ADR-0033으로 이동하고 numeric identity uniqueness 계약을 추가해 제거했다. **다음 gate:** fresh exact-head hosted security/review Checks, unresolved thread 0, qualifying independent approval, runner-group/capacity/cleanup canary, ordinary protected merge가 필요하다. | + ### 2026-09-19 exact-head incident delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | diff --git a/opencode.jsonc b/opencode.jsonc index 3b5f34e2a6..2663186084 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,11 +1,11 @@ { "$schema": "https://opencode.ai/config.json", // Default routed through the vendored contextual-orchestrator gateway - // (orchestrator/free: fail-closed zero-cost pool, ZDR-first). The gateway + // (orchestrator/auto: fail-closed zero-cost pool, ZDR-first). The gateway // must be provisioned before a review starts; there is no direct-provider // fallback for review content. - "model": "contextual-orchestrator/orchestrator/free", - "small_model": "contextual-orchestrator/orchestrator/free", + "model": "contextual-orchestrator/orchestrator/auto", + "small_model": "contextual-orchestrator/orchestrator/auto", "enabled_providers": ["contextual-orchestrator"], "lsp": false, "mcp": {}, @@ -290,7 +290,7 @@ // vendored contextual-orchestrator LLM gateway. It auto-discovers models // across Bytez/NVIDIA NIM (x2 keys)/OpenRouter/OpenAI from KV-registered // credentials and serves the fail-closed zero-cost pool under the virtual - // model id `orchestrator/free`, with ZDR-compliant (zero-data-retention) + // model id `orchestrator/auto`, with ZDR-compliant (zero-data-retention) // routes prioritized by scripts/ci/zdr_policy.py. Requires // CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN, // which scripts/ci/contextual_orchestrator_review_sidecar.sh provisions on @@ -306,8 +306,8 @@ "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "orchestrator/free": { - "name": "Orchestrator Free (ZDR-first zero-cost pool)", + "orchestrator/auto": { + "name": "Orchestrator Auto (cost-fenced dynamic selection)", "tool_call": true, "reasoning": true, "options": { diff --git a/scripts/ci/contextual_orchestrator_review_launcher.py b/scripts/ci/contextual_orchestrator_review_launcher.py index 8d5fe6c100..65911430c2 100644 --- a/scripts/ci/contextual_orchestrator_review_launcher.py +++ b/scripts/ci/contextual_orchestrator_review_launcher.py @@ -1219,6 +1219,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--zdr-endpoints", default=None, help="Optional OpenRouter /api/v1/endpoints/zdr JSON path") parser.add_argument("--require-zdr", action="store_true") parser.add_argument("--pool", choices=("free", "auto"), default="free") + parser.add_argument("--zero-cost-only", action="store_true", help="Exclude priced routes and priced fallback even for auto") args = parser.parse_args(argv) from contextual_orchestrator.credentials import get_credential @@ -1265,7 +1266,7 @@ def main(argv: list[str] | None = None) -> int: model_id = getattr(model, "model_id", "") if not is_general_chat_agent_model_id(model_id) or not _has_text_output(model): continue - if args.pool == "free" and _route_identity(model) not in free_route_identities: + if (args.pool == "free" or args.zero_cost_only) and _route_identity(model) not in free_route_identities: continue selected_models.append(model) if not selected_models: @@ -1310,8 +1311,10 @@ def main(argv: list[str] | None = None) -> int: account_cap=_catalog_account_cap(DEFAULT_ACCOUNT_CAP), zdr_endpoints=zdr_endpoints, require_zdr=args.require_zdr, - pool=args.pool, + pool="free" if args.zero_cost_only else args.pool, ) + result["report"]["requested_pool"] = f"orchestrator/{args.pool}" + result["report"]["zero_cost_only"] = args.zero_cost_only result["report"] = _with_discovery_counts( result["report"], normalized_rows, provider_account=provider_account ) @@ -1330,6 +1333,7 @@ def main(argv: list[str] | None = None) -> int: ) if ( args.pool == "auto" + and not args.zero_cost_only and admitted_free_rows and admitted_priced_rows and fallback_limit diff --git a/scripts/ci/contextual_orchestrator_review_policy.py b/scripts/ci/contextual_orchestrator_review_policy.py index 241f84bef7..2169dcc027 100644 --- a/scripts/ci/contextual_orchestrator_review_policy.py +++ b/scripts/ci/contextual_orchestrator_review_policy.py @@ -143,6 +143,29 @@ def _normalize_cost_evidence( ) +def normalize_input_modalities(value: object) -> list[str]: + """Preserve explicit input evidence as normalized catalog-tag tokens. + + Match the pinned contextual-orchestrator ``input:`` grammar. + Missing evidence remains empty, never guessed text. Malformed nonempty + evidence fails closed rather than silently dropping provider exclusions. + """ + if value is None: + return [] + if not isinstance(value, (list, tuple)): + raise PolicyError("input_modalities must be a list or tuple of tokens") + normalized: list[str] = [] + for item in value: + if not isinstance(item, str): + raise PolicyError("input_modalities contains a non-string token") + token = item.strip().casefold() + if not re.fullmatch(r"[a-z][a-z0-9_-]*", token): + raise PolicyError("input_modalities contains an invalid token") + if token not in normalized: + normalized.append(token) + return normalized + + def _bytez_non_token_price_evidence( *, is_free: bool, @@ -240,6 +263,7 @@ def parse_discovery_report(report: Mapping[str, Any]) -> list[dict[str, Any]]: "provider": provider, "model": model, "agent_id": str(candidate_id), + "input_modalities": normalize_input_modalities(row.get("input_modalities")), "is_free": is_free, "cost_evidence": cost_evidence, "prompt_price_per_1k": prompt_price, @@ -395,6 +419,9 @@ def build_zdr_prioritized_catalog( "review", f"cost:{evidence}", "zdr" if zdr else "non-zdr", + *(f"input:{value}" for value in normalize_input_modalities( + row.get("input_modalities") + )), ], "priority": -rank, "disabled": False, diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 46dd07c3e0..c9fa4ef81e 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -321,14 +321,12 @@ case "$orchestrator_pool" in free) pool_args=(--pool "$orchestrator_pool") ;; + auto) + # Auto resolves the gateway alias; the explicit fence excludes priced candidates and fallback. + pool_args=(--pool "$orchestrator_pool" --zero-cost-only) + ;; *) - # GitHub Actions Workflow usage of contextual-orchestrator is pinned to - # orchestrator/free: the org has not solved cost-safe free+ZDR routing - # well enough yet to justify a priced-inclusive "auto" pool in central CI, - # so "auto" is rejected here even though the launcher's own --pool flag - # (a general-purpose CLI also used outside GitHub Actions) still accepts - # it. - fail "CONTEXTUAL_ORCHESTRATOR_POOL must be free" + fail "CONTEXTUAL_ORCHESTRATOR_POOL must be free or auto" ;; esac diff --git a/scripts/ci/noema_bootstrap_producer.py b/scripts/ci/noema_bootstrap_producer.py new file mode 100644 index 0000000000..5a891b8ff4 --- /dev/null +++ b/scripts/ci/noema_bootstrap_producer.py @@ -0,0 +1,409 @@ +"""Preserve authenticated phase issuance bytes in explicit private custody. + +The request and revalidation collaborators belong to the existing trusted App +issuer/runtime owner. This module neither authenticates arbitrary dictionaries +nor admits native execution from source acceptance or token-action metadata. +""" +from __future__ import annotations + +import json +import os +import stat +import sys +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from scripts.ci.premerge_review_tooling import _unique_object + + +MAX_RESPONSE_BYTES = 2 * 1024 * 1024 + + +def materialize_tooling(source_root, target_root, raw_manifest, *, accepted_digest, + admission, private_parent): + """Copy exactly externally admitted inert source into a separate namespace. + + Supplied acceptance must be independently authenticated by the runtime owner. + This checks its contract and bytes; it never creates acceptance evidence. + The parent is held exclusively by that owner, not defended against same-UID + or privileged namespace mutation by directory permission bits alone. + """ + import hashlib + import sys + from scripts.ci.premerge_review_tooling import load_noema_tooling + parent, source, target = map(Path, (private_parent, source_root, target_root)) + if (not parent.is_absolute() or not source.is_absolute() or not target.is_absolute() + or target.parent != parent or source == target or parent.is_relative_to(source)): + raise ValueError('bootstrap namespace refused') + if any(path.is_symlink() for path in (parent, *parent.parents, source)): + raise ValueError('bootstrap namespace refused') + info = parent.lstat() + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or os.path.lexists(target)): + raise ValueError('bootstrap namespace refused') + if admission.get('namespace') != str(parent): + raise ValueError('bootstrap namespace differs from independent admission') + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', + 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('bootstrap execution evidence is not accepted') + _validate_runtime(admission, parent) + tooling = load_noema_tooling(source, raw_manifest, accepted_digest=accepted_digest, + interpreter=Path(sys.executable).resolve(), phase_environment=lambda _: {}) + inventory = json.loads(raw_manifest)['files'] + target.mkdir(mode=0o700) + for name, digest in inventory.items(): + raw = (source / name).read_bytes() + if hashlib.sha256(raw).hexdigest() != digest: + raise ValueError('bootstrap source changed during delivery') + destination = target / name + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + with os.fdopen(os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o400), 'wb') as stream: + stream.write(raw) + tooling._verify() + load_noema_tooling(target, raw_manifest, accepted_digest=accepted_digest, + interpreter=Path(sys.executable).resolve(), phase_environment=lambda _: {}) + return target + + +def _validate_runtime(admission, parent): + """Rebind explicit namespace, interpreter, dependencies and required admission.""" + import hashlib + parent = Path(parent) + if not parent.is_absolute() or any(path.is_symlink() for path in (parent, *parent.parents)): + raise ValueError('bootstrap namespace refused') + info = parent.lstat() + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or admission.get('namespace') != str(parent)): + raise ValueError('bootstrap namespace refused') + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', + 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('bootstrap execution evidence unavailable') + interpreter = admission.get('interpreter') + actual_interpreter = Path(sys.executable).resolve() + if (not isinstance(interpreter, dict) or set(interpreter) != {'path', 'sha256'} + or interpreter['path'] != str(actual_interpreter) + or hashlib.sha256(actual_interpreter.read_bytes()).hexdigest() != interpreter['sha256']): + raise ValueError('bootstrap interpreter differs from independent admission') + dependencies = admission.get('dependencies') + if not isinstance(dependencies, dict): + raise ValueError('bootstrap dependency inventory unavailable') + for name, digest in dependencies.items(): + path = Path(name) + if (not path.is_absolute() or path.is_symlink() or not path.is_file() + or hashlib.sha256(path.read_bytes()).hexdigest() != digest): + raise ValueError('bootstrap dependency differs from independent admission') + + +def _response(raw): + """Parse bounded original API bytes without reflecting credential payloads.""" + if not isinstance(raw, bytes) or not 0 < len(raw) <= MAX_RESPONSE_BYTES: + raise ValueError('issuer response size or type refused') + try: + value = json.loads(raw, object_pairs_hook=_unique_object) + except (UnicodeError, ValueError): + raise ValueError('issuer response schema refused') from None + if not isinstance(value, dict): + raise ValueError('issuer response schema refused') + return value + + +def run_bootstrap_review(source_root, raw_manifest, *, accepted_digest, admission, + private_parent, fetch_live, request, revalidate, + inference_key): + """Compose actual admitted tooling, original App response and two children. + + Trusted runtime callbacks are externally held authority; this function only + checks their supplied admission and orders preparation before the separately + minted write token. No supplier policy is inferred from source or fixtures. + """ + import hashlib + import subprocess + from scripts.ci.premerge_review_tooling import ( + load_noema_tooling, phase_environment_from_issuance, + require_prepared_envelope, require_review_scope, + ) + scope = admission['scope'] + parent = Path(private_parent) + source = Path(source_root) + target = materialize_tooling(source, parent / 'tooling', raw_manifest, + accepted_digest=accepted_digest, admission=admission, private_parent=parent) + if not isinstance(inference_key, str) or not inference_key.strip(): + raise ValueError('existing inference authority unavailable') + envs = {} + def fence(): + """Revalidate supplier, source, admitted scope and external authority.""" + revalidate() + if hashlib.sha256(raw_manifest).hexdigest() != accepted_digest: + raise ValueError('bootstrap source pin changed') + live = fetch_live() + require_review_scope(scope, live) + from scripts.ci.premerge_review_tooling import admit_tooling + inventory = json.loads(raw_manifest)['files'] + files = {name: (target / name).read_bytes() for name in inventory} + admit_tooling(raw_manifest, accepted_digest=accepted_digest, source_files=files) + def issue(phase): + """Store exact authenticated response privately and consume its grant.""" + fence() + path = parent / (phase + '.json') + produce_issuance(phase, scope=scope, installation_id=admission['installation_id'], + app_id=admission['app_id'], output=path, private_parent=parent, + request=request, revalidate=fence) + fence() + response = _response(path.read_bytes()) + return phase_environment_from_issuance(phase, response, + scope=scope, reviewer_actor=scope['reviewer'], + installation_id=admission['installation_id'], + inference_key=inference_key if phase == 'prepare' else None) + fence() + envs['prepare'] = issue('prepare') + tooling = load_noema_tooling(target, raw_manifest, + accepted_digest=accepted_digest, interpreter=Path(__import__('sys').executable), + phase_environment=lambda phase: envs[phase]) + verdict = parent / 'verdict.json' + if tooling.prepare_verdict(scope['repository'], scope['number'], scope['head'], verdict) != 0: + raise ValueError('model preparation failed') + require_prepared_envelope(verdict, scope) + fence() + # No write grant can be requested until preparation and live revalidation. + envs['publish'] = issue('publish') + fence() + require_prepared_envelope(verdict, scope) + return tooling.publish_verdict(scope['repository'], scope['number'], scope['head'], verdict) + + +def produce_issuance(phase, *, scope, installation_id, app_id, output, + private_parent, request, revalidate): + """Request exact phase grant and exclusively store the unchanged response. + + Authenticated request supplies original response bytes from the same App + transport. The caller must independently bind that collaborator, namespace, + and installations; structural checks alone do not establish issuer authority. + Publish revalidation must include the prepared envelope and live head/base. + No inference credential is consumed here or attached to an issuer request. + """ + if phase not in ('prepare', 'publish'): + raise ValueError('issuer phase refused') + if any(type(value) is not int or value < 1 for value in (installation_id, app_id)): + raise ValueError('issuer identity refused') + repository = scope['repository'] + # Validate repository grammar with the same canonical controller contract. + import re + if not isinstance(repository, str) or not re.fullmatch( + r'ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+', repository): + raise ValueError('issuer repository refused') + if scope['reviewer'] != 'cwl-noema-review[bot]': + raise ValueError('issuer actor refused') + parent, target = Path(private_parent), Path(output) + if not parent.is_absolute() or not target.is_absolute() or target.parent != parent: + raise ValueError('issuer output namespace refused') + for ancestor in (parent, *parent.parents): + if ancestor.is_symlink(): + raise ValueError('issuer output namespace refused') + info = parent.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise ValueError('issuer output namespace refused') + if os.path.lexists(target): + raise ValueError('issuer output already exists') + revalidate() + app = _response(request('GET', '/app', None)) + if type(app.get('id')) is not int or app.get('id') != app_id or app.get('slug') != 'cwl-noema-review': + raise ValueError('issuer App identity refused') + revalidate() + installation = _response(request('GET', '/repos/' + repository + '/installation', None)) + if (type(installation.get('id')) is not int or type(installation.get('app_id')) is not int + or installation.get('id') != installation_id or installation.get('app_id') != app_id): + raise ValueError('issuer installation refused') + revalidate() + permissions = {'contents': 'read', 'metadata': 'read', + 'pull_requests': 'read' if phase == 'prepare' else 'write'} + raw = request('POST', f'/app/installations/{installation_id}/access_tokens', + {'repositories': [repository.split('/', 1)[1]], 'permissions': permissions}) + response = _response(raw) + if response.get('permissions') != permissions: + raise ValueError('issuer phase grant refused') + repositories = response.get('repositories') + if (not isinstance(repositories, list) or len(repositories) != 1 + or not isinstance(repositories[0], dict) + or repositories[0].get('full_name') != repository): + raise ValueError('issuer response repository refused') + if not isinstance(response.get('token'), str) or not response['token'].strip(): + raise ValueError('issuer token missing') + revalidate() + current = parent.lstat() + if (current.st_dev, current.st_ino, current.st_mode, current.st_uid) != ( + info.st_dev, info.st_ino, info.st_mode, info.st_uid): + raise ValueError('issuer output custody changed') + fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + try: + with os.fdopen(fd, 'wb') as stream: + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + except BaseException: + # Retain incomplete private bytes for the existing custody owner; + # never overwrite/retry a secret-bearing output after ambiguous I/O. + raise + return target + + +def _private_bytes(path, parent): + """Read one bounded private single-link credential input without diagnostics.""" + path, parent = Path(path), Path(parent) + if not path.is_absolute() or path.parent != parent or parent.is_symlink(): + raise ValueError('private credential namespace refused') + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + try: + info = os.fstat(fd) + if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 + or info.st_uid != os.getuid() or info.st_mode & 0o077): + raise ValueError('private credential custody refused') + raw = os.read(fd, MAX_RESPONSE_BYTES + 1) + if not 0 < len(raw) <= MAX_RESPONSE_BYTES: + raise ValueError('private credential size refused') + return raw + finally: + os.close(fd) + + +def authenticated_transport(jwt_file, private_parent, *, opener=None): + """Use the existing externally supplied App JWT on fixed GitHub routes only. + + The runtime owner provides and refreshes this private credential; this helper + neither signs a JWT nor reads an App private key. Injected openers are trusted + test collaborators, not PR inputs. No redirect or ambient proxy is admitted. + """ + import re + import urllib.request + jwt = _private_bytes(jwt_file, private_parent).decode('ascii') + if not jwt or any(character.isspace() for character in jwt): + raise ValueError('existing App authentication unavailable') + class NoRedirect(urllib.request.HTTPRedirectHandler): + """Reject credential forwarding before any redirected request dispatch.""" + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Keep every response on the independently fixed issuer origin.""" + return None + if opener is None: + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), NoRedirect()).open + def request(method, route, payload): + """Return unchanged bounded body bytes from an exact authenticated request.""" + get = method == 'GET' and (route == '/app' or re.fullmatch( + r'/repos/ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/installation', route)) + post = method == 'POST' and re.fullmatch(r'/app/installations/[1-9][0-9]*/access_tokens', route) + if not (get or post) or (get and payload is not None): + raise ValueError('issuer endpoint refused') + body = json.dumps(payload, allow_nan=False).encode() if post else None + req = urllib.request.Request('https://api.github.com' + route, data=body, + method=method, headers={'Authorization': 'Bearer ' + jwt, + 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', + 'Content-Type': 'application/json'}) + with opener(req, timeout=60) as response: + headers = getattr(response, 'headers', {}) + length = headers.get('Content-Length') + raw = response.read(MAX_RESPONSE_BYTES + 1) + if (not isinstance(raw, bytes) or not 0 < len(raw) <= MAX_RESPONSE_BYTES + or (length is not None and (not str(length).isdigit() or int(length) != len(raw)))): + raise ValueError('issuer response framing refused') + return raw + return request + + +def main(argv=None): + """Consume independently pinned bootstrap inputs, refusing absent admission.""" + import argparse + import hashlib + import re + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_mutually_exclusive_group(required=True) + modes.add_argument('--materialize', action='store_true') + modes.add_argument('--issue-prepare', action='store_true') + modes.add_argument('--issue-publish', action='store_true') + modes.add_argument('--revalidate-publish', action='store_true') + modes.add_argument('--revalidate-prepare', action='store_true') + parser.add_argument('--private-parent', type=Path, required=True) + parser.add_argument('--source-root', type=Path, required=True) + parser.add_argument('--manifest', type=Path, required=True) + parser.add_argument('--accepted-digest', required=True) + parser.add_argument('--admission', type=Path, required=True) + parser.add_argument('--admission-digest', required=True) + parser.add_argument('--repo', required=True) + parser.add_argument('--pr-number', type=int, required=True) + parser.add_argument('--expected-head', required=True) + parser.add_argument('--app-jwt-file', type=Path) + parser.add_argument('--verdict-file', type=Path) + args = parser.parse_args(argv) + try: + if not all(re.fullmatch(r'[0-9a-f]{64}', value) for value in + (args.accepted_digest, args.admission_digest)): + raise ValueError('bootstrap independent pins unavailable') + raw = args.manifest.read_bytes() + admission_raw = args.admission.read_bytes() + if hashlib.sha256(admission_raw).hexdigest() != args.admission_digest: + raise ValueError('bootstrap admission pin refused') + admission = _response(admission_raw) + scope = admission['scope'] + if (scope['repository'], scope['number'], scope['head']) != ( + args.repo, args.pr_number, args.expected_head): + raise ValueError('bootstrap target refused') + if args.materialize: + materialize_tooling(args.source_root, args.private_parent / 'tooling', raw, + accepted_digest=args.accepted_digest, admission=admission, + private_parent=args.private_parent) + return 0 + from scripts.ci.premerge_review_tooling import ( + require_prepared_envelope, require_review_scope, load_noema_tooling, + ) + def fence(): + """Bind original pins, actual admitted tooling and fresh live metadata.""" + if (args.manifest.read_bytes() != raw + or args.admission.read_bytes() != admission_raw): + raise ValueError('bootstrap external pins changed') + tooling = args.private_parent / 'tooling' + _validate_runtime(admission, args.private_parent) + load_noema_tooling(tooling, raw, accepted_digest=args.accepted_digest, + interpreter=Path(sys.executable), phase_environment=lambda _: {}) + pr = json.loads(subprocess.run(['gh', 'api', f'repos/{args.repo}/pulls/{args.pr_number}'], + capture_output=True, text=True, check=True, timeout=60).stdout) + require_review_scope(scope, {'repository':pr['base']['repo']['full_name'], + 'number':pr['number'],'head':pr['head']['sha'],'base':pr['base']['sha'], + 'author':pr['user']['login'],'reviewer':scope['reviewer'], + 'state':str(pr['state']).upper(),'draft':pr['draft']}) + if (args.manifest.read_bytes() != raw + or args.admission.read_bytes() != admission_raw): + raise ValueError('bootstrap external pins changed during live read') + load_noema_tooling(tooling, raw, accepted_digest=args.accepted_digest, + interpreter=Path(sys.executable), phase_environment=lambda _: {}) + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', + 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('bootstrap execution evidence unavailable') + _validate_runtime(admission, args.private_parent) + if args.issue_publish or args.revalidate_publish: + if args.verdict_file != args.private_parent / 'verdict.json': + raise ValueError('bootstrap envelope namespace refused') + require_prepared_envelope(args.verdict_file, scope) + if args.revalidate_publish or args.revalidate_prepare: + fence() + return 0 + if args.app_jwt_file is None: + raise ValueError('authenticated issuer transport unavailable') + fence() + transport = authenticated_transport(args.app_jwt_file, args.private_parent) + phase = 'publish' if args.issue_publish else 'prepare' + produce_issuance(phase, scope=scope, installation_id=admission['installation_id'], + app_id=admission['app_id'], output=args.private_parent / (phase+'.json'), + private_parent=args.private_parent, request=transport, revalidate=fence) + return 0 + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): + print('Noema bootstrap refused: independent issuer or custody input unavailable', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/ci/noema_existing_app_issuer.cjs b/scripts/ci/noema_existing_app_issuer.cjs new file mode 100644 index 0000000000..9e43907388 --- /dev/null +++ b/scripts/ci/noema_existing_app_issuer.cjs @@ -0,0 +1,255 @@ +/* Existing-App phase issuer. Only trusted workflow steps supply Secret inputs. + * No key/JWT/token is projected into stdout, argv, artifacts or child env. + * Callbacks below are trusted fixture seams, never PR-provided code. + */ +'use strict'; +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const https = require('node:https'); +const {spawnSync} = require('node:child_process'); +const LIMIT = 2 * 1024 * 1024; + +function signApp(clientId, pem, now) { + if (typeof clientId !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(clientId) || + typeof pem !== 'string' || !Number.isSafeInteger(now)) throw new Error('signer refused'); + const key = crypto.createPrivateKey(pem); + if (key.asymmetricKeyType !== 'rsa' || key.asymmetricKeyDetails.modulusLength < 2048) + throw new Error('signer refused'); + const encode = value => Buffer.from(JSON.stringify(value)).toString('base64url'); + const body = encode({alg:'RS256',typ:'JWT'}) + '.' + encode({iat:now-60,exp:now+300,iss:clientId}); + return body + '.' + crypto.sign('RSA-SHA256',Buffer.from(body),key).toString('base64url'); +} + +function parse(raw, captureToken) { + if (!Buffer.isBuffer(raw) || !raw.length || raw.length > LIMIT) throw new Error('response refused'); + const text = new TextDecoder('utf-8',{fatal:true}).decode(raw); + const value = JSON.parse(text); + if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error('response refused'); + // Native JSON.parse validates syntax but silently keeps the last duplicate. + // Walk that validated grammar to reject duplicates at every object depth. + let cursor=0,duplicate=false,duplicateToken=false; + function whitespace(){while(/\s/.test(text[cursor]||'') && cursor128)throw new Error('response refused');whitespace(); + const c=text[cursor]; + if(c==='"'){string();return;} + if(c==='{' || c==='['){ + cursor++;whitespace();const keys=new Set(),end=c==='{'?'}':']'; + if(text[cursor]===end){cursor++;return;} + for(;;){ + if(c==='{'){ + const key=string();if(keys.has(key)){duplicate=true;if(depth===0 && key==='token')duplicateToken=true;}keys.add(key); + whitespace();cursor++; // colon: syntax was already validated above + } + walk(depth+1);whitespace();const separator=text[cursor++]; + if(separator===end)break;whitespace(); + } + return; + } + const m=text.slice(cursor).match(/^(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/); + if(!m)throw new Error('response refused');cursor+=m[0].length; + } + walk(0); + if(captureToken && !duplicateToken && typeof value.token==='string' && value.token.trim() && !/\s/.test(value.token)) + captureToken(value.token); + if(duplicate)throw new Error('response duplicate refused'); + return value; +} + +function transport(method, route, payload, credential) { + if (!['GET','POST','DELETE'].includes(method) || + !/^\/(app|repos\/ContextualWisdomLab\/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+\/installation|app\/installations\/[1-9][0-9]*\/access_tokens|installation\/token)$/.test(route)) + return Promise.reject(new Error('endpoint refused')); + return new Promise((resolve,reject) => { + const body = payload === null ? null : Buffer.from(JSON.stringify(payload)); + const req = https.request({hostname:'api.github.com',port:443,path:route,method, + headers:{Authorization:'Bearer '+credential,Accept:'application/vnd.github+json', + 'X-GitHub-Api-Version':'2022-11-28','User-Agent':'cwl-noema-issuer', + ...(body ? {'Content-Type':'application/json','Content-Length':body.length}: {})}}, res => { + const chunks=[];let size=0; + res.on('data',chunk=>{size+=chunk.length;if(size>LIMIT){res.destroy();reject(new Error('response refused'));}else chunks.push(chunk);}); + res.on('aborted',()=>reject(new Error('response refused'))); + res.on('error',()=>reject(new Error('response refused'))); + res.on('end',()=>{ + const length=res.headers['content-length']; + if (res.statusCode !== (method==='DELETE'?204:method==='POST'?201:200) || + !res.complete || (length!==undefined && (!/^[0-9]+$/.test(length) || Number(length)!==size))) + {const error=new Error('response refused');error.statusCode=res.statusCode;reject(error);} + else resolve(Buffer.concat(chunks)); + }); + }); + req.setTimeout(60000,()=>req.destroy(new Error('request refused'))); + req.on('error',()=>reject(new Error('request refused'))); + if(body)req.write(body);req.end(); + }); +} + +function privateParent(parent) { + if (!path.isAbsolute(parent) || path.resolve(parent)!==parent) throw new Error('custody refused'); + for(let p=parent;;p=path.dirname(p)){ + if(fs.lstatSync(p).isSymbolicLink())throw new Error('custody refused'); + if(path.dirname(p)===p)break; + } + const s=fs.lstatSync(parent); + if(!s.isDirectory() || s.uid!==process.getuid() || (s.mode&0o077))throw new Error('custody refused'); + return s; +} + +async function runPhase(options) { + const {phase,scope,appId,installationId,privateParent:parent,clientId,privateKey, + fence,controller,transport:request=transport,now=()=>Math.floor(Date.now()/1000)}=options; + if (!['prepare','publish'].includes(phase) || !Number.isSafeInteger(appId) || appId<1 || + !Number.isSafeInteger(installationId) || installationId<1 || + !/^ContextualWisdomLab\/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$/.test(scope.repository) || + scope.reviewer!=='cwl-noema-review[bot]')throw new Error('identity refused'); + const initial=privateParent(parent);const target=path.join(parent,phase+'.json'); + if(fs.existsSync(target) || fs.lstatSync(parent).ino!==initial.ino)throw new Error('delivery refused'); + const tokens=[];const expiries=new Map();let delivered=false;let primary;let result;let cleanupFailed=false; + async function issue(grant, readToken) { + // Short-lived App authentication is created inside each issuer phase, not + // supplied by an absent external JWT producer or retained across the model. + const jwt=signApp(clientId,privateKey,now()); + const app=parse(await request('GET','/app',null,jwt)); + if(app.id!==appId || app.slug!=='cwl-noema-review')throw new Error('identity refused'); + if(readToken)await fence(readToken,phase); + const install=parse(await request('GET','/repos/'+scope.repository+'/installation',null,jwt)); + if(install.id!==installationId || install.app_id!==appId)throw new Error('identity refused'); + if(readToken)await fence(readToken,phase); + const permissions={contents:'read',metadata:'read',pull_requests:grant}; + const raw=await request('POST','/app/installations/'+installationId+'/access_tokens', + {repositories:[scope.repository.split('/')[1]],permissions},jwt); + const response=parse(raw, token => tokens.push(token)); + // Capture a safely parsed token before checking grant/expiry/delivery so + // rejected issuance is still revocation-owned. Never use it for fallback. + if(!tokens.includes(response.token))throw new Error('token refused'); + if(Object.keys(response.permissions||{}).length!==3 || + Object.entries(permissions).some(([k,v])=>response.permissions[k]!==v) || + !Array.isArray(response.repositories) || response.repositories.length!==1 || + response.repositories[0].full_name!==scope.repository)throw new Error('grant refused'); + const expires=Date.parse(response.expires_at); + if(!Number.isFinite(expires) || expires<=now()*1000+60000)throw new Error('expiry refused'); + expiries.set(response.token,expires); + return {raw,token:response.token}; + } + try { + const read=await issue('read'); + await fence(read.token,phase); + let selected=read; + if(phase==='publish'){ + selected=await issue('write',read.token); + await fence(read.token,phase); + } + const current=privateParent(parent); + if(current.dev!==initial.dev || current.ino!==initial.ino || current.mode!==initial.mode) + throw new Error('custody refused'); + const fd=fs.openSync(target,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600); + delivered=true; + try{fs.writeFileSync(fd,selected.raw);fs.fsyncSync(fd);}finally{fs.closeSync(fd);} + const env={PATH:'/usr/bin:/bin',GH_TOKEN:read.token}; + // The controller's live metadata read uses the read grant; its publisher + // child receives the separately issued write grant from the original file. + result=await controller(target,env); + if(result!==0)throw new Error('controller refused'); + } catch(error){primary=error;} + finally { + for(const token of tokens){ + try{await request('DELETE','/installation/token',null,token);} + catch(error){ + if(!(error.statusCode===401 && expiries.has(token) && expiries.get(token)<=now()*1000))cleanupFailed=true; + } + } + if(delivered){try{fs.unlinkSync(target);}catch{cleanupFailed=true;}} + } + if(cleanupFailed)throw new Error('issuer cleanup unconfirmed'); + if(primary)throw primary; + return result; +} + +module.exports={signApp,runPhase,transport}; + +async function main(argv=process.argv.slice(2), environment=process.env) { + const values={}; + const names=['phase','private-parent','source-root','manifest','accepted-digest','admission', + 'admission-digest','repo','pr-number','expected-head','verdict-file']; + if(argv.length%2)throw new Error('arguments refused'); + for(let i=0;i!values[n]) || !['prepare','publish'].includes(values.phase) || + !/^[1-9][0-9]*$/.test(values['pr-number']) || !/^[a-f0-9]{40}$/.test(values['expected-head'])) + throw new Error('arguments refused'); + const phase=values.phase,parent=values['private-parent'];privateParent(parent); + const admissionRaw=fs.readFileSync(values.admission); + if(!/^[a-f0-9]{64}$/.test(values['admission-digest']) || + crypto.createHash('sha256').update(admissionRaw).digest('hex')!==values['admission-digest']) + throw new Error('admission refused'); + const admission=parse(admissionRaw),scope=admission.scope; + if(scope.repository!==values.repo || scope.number!==Number(values['pr-number']) || + scope.head!==values['expected-head'] || values['verdict-file']!==path.join(parent,'verdict.json')) + throw new Error('scope refused'); + const python=admission.interpreter?.path; + if(!python || !path.isAbsolute(python) || + !/^[a-f0-9]{64}$/.test(admission.interpreter.sha256) || + crypto.createHash('sha256').update(fs.readFileSync(python)).digest('hex')!==admission.interpreter.sha256) + throw new Error('interpreter refused'); + const pythonIdentity=fs.statSync(python); + const pythonRealpath=fs.realpathSync(python); + const pinnedNode=admission.dependencies?.[fs.realpathSync(process.execPath)]; + if(!pinnedNode || crypto.createHash('sha256').update(fs.readFileSync(process.execPath)).digest('hex')!==pinnedNode) + throw new Error('issuer interpreter refused'); + const root=path.resolve(__dirname,'../..'); + const common=['--private-parent',parent,'--source-root',values['source-root'], + '--manifest',values.manifest,'--accepted-digest',values['accepted-digest'], + '--admission',values.admission,'--admission-digest',values['admission-digest'], + '--repo',values.repo,'--pr-number',values['pr-number'],'--expected-head',values['expected-head']]; + function child(script,args,env){ + // Revalidate after every issuer wait, before either credential-bearing child. + // This is not atomic pathname execution; exclusive runtime custody remains required. + const current=fs.statSync(python); + if(!current.isFile() || fs.realpathSync(python)!==pythonRealpath || + ['dev','ino','mode','uid','gid'].some(key=>current[key]!==pythonIdentity[key]) || + crypto.createHash('sha256').update(fs.readFileSync(python)).digest('hex')!==admission.interpreter.sha256) + throw new Error('interpreter refused'); + const result=spawnSync(python,['-I','-B',path.join(root,'scripts/ci',script),...args], + {env,cwd:root,stdio:'ignore'}); + if(result.error || result.signal || result.status===null)throw new Error('child refused'); + return result.status; + } + const runtimePath=environment.PATH; + if(!runtimePath)throw new Error('runtime refused'); + const fence=async(token,requested)=>{ + const mode=requested==='publish'?'--revalidate-publish':'--revalidate-prepare'; + const args=[mode,...common]; + if(requested==='publish')args.push('--verdict-file',values['verdict-file']); + if(child('noema_bootstrap_producer.py',args,{PATH:runtimePath,GH_TOKEN:token})!==0) + throw new Error('scope refused'); + }; + const controller=async(file,metadataEnvironment)=>{ + // Do not inherit Secret/JWT, workflow write token or inference into publisher. + const env={PATH:runtimePath,GH_TOKEN:metadataEnvironment.GH_TOKEN}; + if(phase==='prepare'){ + if(!environment.LLM_GATEWAY_API_KEY)throw new Error('inference refused'); + env.LLM_GATEWAY_API_KEY=environment.LLM_GATEWAY_API_KEY; + } + const args=['--'+phase+'-production','--tooling-root',path.join(parent,'tooling'), + '--manifest',values.manifest,'--accepted-digest',values['accepted-digest'], + '--admission',values.admission,'--admission-digest',values['admission-digest'], + '--repo',values.repo,'--pr-number',values['pr-number'],'--expected-head',values['expected-head'], + '--verdict-file',values['verdict-file'],'--phase-issuance',file]; + return child('premerge_review_tooling.py',args,env); + }; + return runPhase({phase,scope,appId:admission.app_id,installationId:admission.installation_id, + privateParent:parent,clientId:environment.NOEMA_GITHUB_APP_CLIENT_ID, + privateKey:environment.NOEMA_GITHUB_APP_PRIVATE_KEY,fence,controller}); +} +module.exports.main=main; +if(require.main===module){ + main().then(status=>{process.exitCode=status;}).catch(()=>{ + // Fixed, credential-free disposition also covers unconfirmed revocation. + console.error('Noema existing-App issuer refused; issuance or cleanup unconfirmed');process.exitCode=1; + }); +} diff --git a/scripts/ci/noema_review_document.py b/scripts/ci/noema_review_document.py index 17d3ca603c..4594f5434b 100644 --- a/scripts/ci/noema_review_document.py +++ b/scripts/ci/noema_review_document.py @@ -158,7 +158,7 @@ def _extract_hwp_with_reviewed_reader(path: str, raw: bytes) -> str: handle.flush() try: completed = subprocess.run( - ["node", reader, source, handle.name], + [os.environ.get("NOEMA_NODE_EXECUTABLE") or "node", reader, source, handle.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index c08df174a3..156f5aafa3 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -357,6 +357,7 @@ def graphql(query: str, **fields: str | int) -> dict[str, Any]: number title body + author { login } isDraft state headRefOid @@ -526,6 +527,18 @@ def current_actor() -> str: return "" +def require_distinct_pr_author(pr: dict[str, Any], actor: str) -> str: + """Require a known PR author distinct from the active reviewer actor.""" + author = pr.get("author") + login = author.get("login") if isinstance(author, dict) else None + if not isinstance(login, str) or not login.strip(): + raise RuntimeError("Noema pull request author identity could not be verified") + normalized = login.strip().casefold() + if not actor or actor.strip().casefold() == normalized: + raise RuntimeError("Noema reviewer actor must be distinct from the pull request author") + return normalized + + def fetch_diff(repo: str, number: int) -> tuple[str, bool]: """Fetch the PR diff and truncate it to the bounded LLM prompt size.""" diff = run(["gh", "api", f"repos/{repo}/pulls/{number}", "-H", "Accept: application/vnd.github.v3.diff"]) @@ -1606,7 +1619,7 @@ def call_llm( """ api_url = os.environ.get("NOEMA_LLM_API_URL", "").strip() api_key = os.environ.get("NOEMA_LLM_API_KEY", "").strip() - model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "orchestrator/free" + model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "orchestrator/auto" if not api_url or not api_key: raise RuntimeError( "Noema LLM review unavailable: NOEMA_LLM_API_URL or NOEMA_LLM_API_KEY is not configured." @@ -1882,6 +1895,7 @@ def inspect_and_review(repo: str, number: int, expected_head: str) -> int: actor = current_actor() if not actor: raise RuntimeError("Noema reviewer identity could not be verified") + require_distinct_pr_author(pr, actor) if actor in PRIMARY_REVIEW_AUTHORS: raise RuntimeError( f"Current token actor {actor!r} is already a primary review actor; " @@ -1904,6 +1918,7 @@ def inspect_and_review(repo: str, number: int, expected_head: str) -> int: except RuntimeError: print("Pull request closed or its head changed during review; stale verdict was not published.") return 0 + require_distinct_pr_author(current_pr, actor) submit_review(repo, number, current_pr, actor, verdict) return 0 diff --git a/scripts/ci/noema_review_handoff.py b/scripts/ci/noema_review_handoff.py index a112e1e225..3f4a8a20a7 100644 --- a/scripts/ci/noema_review_handoff.py +++ b/scripts/ci/noema_review_handoff.py @@ -195,7 +195,7 @@ def dispatch_noema( *, runner: GhRunner = run_gh, ) -> None: - """Dispatch the target repository's default-branch Noema workflow.""" + """Dispatch the central handler while retaining the exact consumer identity.""" payload = { "event_type": "noema-review", "client_payload": { @@ -205,7 +205,7 @@ def dispatch_noema( }, } runner( - ["api", "-X", "POST", f"repos/{repo}/dispatches", "--input", "-"], + ["api", "-X", "POST", "repos/ContextualWisdomLab/.github/dispatches", "--input", "-"], json.dumps(payload), ) diff --git a/scripts/ci/opencode_repository_dispatch_targets.json b/scripts/ci/opencode_repository_dispatch_targets.json index dd82dd1fd0..db995c9bae 100644 --- a/scripts/ci/opencode_repository_dispatch_targets.json +++ b/scripts/ci/opencode_repository_dispatch_targets.json @@ -51,6 +51,7 @@ "ContextualWisdomLab/saju-caldav", "ContextualWisdomLab/scopeweave", "ContextualWisdomLab/semantic-data-portal", + "ContextualWisdomLab/tokyo-travel-2026", "ContextualWisdomLab/wardnet", "ContextualWisdomLab/xtrm-lead-pi-outbound", "ContextualWisdomLab/xtrmLLMBatchPython" diff --git a/scripts/ci/opencode_review_receipt_gate.py b/scripts/ci/opencode_review_receipt_gate.py index 4dcb24af88..5c86b1fc8e 100644 --- a/scripts/ci/opencode_review_receipt_gate.py +++ b/scripts/ci/opencode_review_receipt_gate.py @@ -149,6 +149,7 @@ def evaluate_receipts( if not SHA_RE.fullmatch(head_sha): return None, "receipt gate requires a 40-character head SHA" stale_hits = 0 + dismissed_publishers: set[str] = set() for review in reversed(list(reviews)): if not isinstance(review, Mapping): continue @@ -156,6 +157,17 @@ def evaluate_receipts( if commit and commit.lower() != head_sha.lower(): stale_hits += 1 continue + author = review_author(review) + if author in dismissed_publishers: + continue + if ( + author in FORMAL_AUTHORS + and str(review.get("state") or "").upper() == "DISMISSED" + and review_matches_head(review, head_sha) + and review.get("id") + ): + dismissed_publishers.add(author) + continue ok, reason = is_formal_receipt(review, head_sha, is_draft=is_draft) if ok: return review, reason @@ -166,6 +178,8 @@ def evaluate_receipts( if reason.startswith("stale"): stale_hits += 1 continue + if dismissed_publishers: + return None, "current-head OpenCode publisher review was dismissed" if stale_hits: return ( None, diff --git a/scripts/ci/personal_review_route.py b/scripts/ci/personal_review_route.py new file mode 100644 index 0000000000..b417285813 --- /dev/null +++ b/scripts/ci/personal_review_route.py @@ -0,0 +1,53 @@ +"""Trusted personal LiteLLM route; never infer cost/privacy from auto or HTTP200.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +BASE_URL = 'https://litellm.poinnetworks.net/v1' +MODEL = 'personal-litellm/auto' + + +def require_admission(*, zero_cost: bool, zero_retention: bool, private_target: bool) -> None: + """Reject missing supplier evidence; existing key possession is not policy.""" + if zero_cost is not True: + raise RuntimeError('STOP: personal auto zero-cost supplier is not attested') + if private_target and zero_retention is not True: + raise RuntimeError('STOP: private personal auto zero-retention supplier is not attested') + + +def configure_opencode(path: Path) -> None: + """Reset only provider/model routing, preserving read-only agent permissions.""" + source = path.read_text(encoding='utf-8') + # Generated workflow configs are JSON; tracked JSONC uses full-line comments. + data = json.loads('\n'.join(line for line in source.splitlines() + if not line.lstrip().startswith('//'))) + data.update(model=MODEL, small_model=MODEL, enabled_providers=['personal-litellm']) + data['provider'] = {'personal-litellm': { + 'npm': '@ai-sdk/openai-compatible', 'name': 'Personal LiteLLM auto', + 'options': {'baseURL': BASE_URL, 'apiKey': '{env:LLM_GATEWAY_API_KEY}'}, + 'models': {'auto': {'name': 'Auto', 'tool_call': True, + 'limit': {'context': 200000, 'output': 32768}}}, + }} + path.write_text(json.dumps(data, indent=2) + '\n', encoding='utf-8') + + +def main(argv: list[str] | None = None) -> int: + """Validate trusted nonsecret admission inputs and optionally reset config.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--zero-cost', choices=('true', 'false'), required=True) + parser.add_argument('--zero-retention', choices=('true', 'false'), required=True) + parser.add_argument('--private-target', choices=('true', 'false'), required=True) + parser.add_argument('--opencode-config', type=Path) + args = parser.parse_args(argv) + require_admission(zero_cost=args.zero_cost == 'true', + zero_retention=args.zero_retention == 'true', + private_target=args.private_target == 'true') + if args.opencode_config is not None: + configure_opencode(args.opencode_config) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 5a86bd24c8..b2990236f1 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -2249,28 +2249,78 @@ def has_current_head_approval(pr: dict[str, Any]) -> bool: return current_head_review_state(pr, "APPROVED") +def is_substantive_noema_app_approval(review: dict[str, Any], head: str) -> bool: + """Recognize only the known second-review App's actual publisher contract. + + API author and commit identity remain authoritative. Body markers alone + cannot authorize an arbitrary bot, a personal-token fallback or OpenCode. + """ + if ( + review_author_login(review) not in {"cwl-noema-review", "cwl-noema-review[bot]"} + or not is_bot_review_author(review) + or (review.get("commit") or {}).get("oid") != head + or not re.fullmatch(r"[0-9a-f]{40}", head) + ): + return False + body = str(review.get("body") or "") + footer_marker = "" + if body.count(footer_marker) != 1: + return False + evidence, footer = body.split(footer_marker, 1) + expected_footers = { + ( + f"\n- Result: APPROVE\n- Head SHA: `{head}`\n" + f"- Reviewer credential: `{source}`\n" + "- Actor: `cwl-noema-review[bot]`\n\n" + f"" + ) + for source in ("noema-review-github-app", "noema-review-github-app-refresh") + } + if footer not in expected_footers: + return False + return bool( + evidence.startswith("## Noema LLM review\n") + and re.search( + r"### Reviewed changed lines\n- `[^`\n]+:[1-9][0-9]* \((?:LEFT|RIGHT)\)`: [^\s][^\n]*", + evidence, + ) + and re.search( + r"### Adversarial validation\n- `[^`\n]+:[1-9][0-9]* \((?:LEFT|RIGHT)\)` " + r"falsified: [^\s][^\n]* — [^\s][^\n]*", + evidence, + ) + and re.search(r"(?m)^- Residual risk: [^\s][^\n]*$", evidence) + ) + + def has_independent_current_head_approval(pr: dict[str, Any]) -> bool: - """Return whether an eligible reviewer's latest exact-head policy state approves.""" - author = ((pr.get("author") or {}).get("login") or "").lower() + """Require a non-author human or trusted, substantive second-review App.""" + author = ((pr.get("author") or {}).get("login") or "").lower().removesuffix("[bot]") if not author: return False seen_reviewers: set[str] = set() for review in reversed((pr.get("reviews") or {}).get("nodes") or []): - reviewer = review_author_login(review) + reviewer = review_author_login(review).removesuffix("[bot]") state = (review.get("state") or "").upper() if ( not reviewer or reviewer == author or is_automated_opencode_review(review) or reviewer == "github-actions" - or is_bot_review_author(review) or not review_matches_current_head(review, pr) or state not in {"APPROVED", "CHANGES_REQUESTED", "DISMISSED"} or reviewer in seen_reviewers ): continue + # Even a later malformed approval must retire that actor's older one. seen_reviewers.add(reviewer) - if state == "APPROVED": + if state == "APPROVED" and ( + ( + not is_bot_review_author(review) + and reviewer != "cwl-noema-review" + ) + or is_substantive_noema_app_approval(review, str(pr.get("headRefOid") or "")) + ): return True return False diff --git a/scripts/ci/premerge_review_tooling.py b/scripts/ci/premerge_review_tooling.py new file mode 100644 index 0000000000..0961f15652 --- /dev/null +++ b/scripts/ci/premerge_review_tooling.py @@ -0,0 +1,510 @@ +"""Admit independently pinned reviewer source before loading any candidate code. + +This source-only boundary does not issue credentials or admit a runner. Its +caller must obtain accepted_digest independently of PR data and supply the +complete regular-file inventory from the selected immutable Git tree. Merely +hashing a caller's own manifest is not independent source acceptance. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import stat +import re +import tempfile +import subprocess +from pathlib import Path +from collections.abc import Mapping + + +def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]: + """Reject duplicate keys before any manifest interpretation.""" + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ValueError("manifest contains duplicate fields") + result[key] = value + return result + + +def _safe_path(value: object) -> bool: + """Accept canonical relative inventory paths without traversal or controls.""" + return ( + isinstance(value, str) + and bool(value) + and "\\" not in value + and not any(ord(character) < 32 for character in value) + and all(part not in {"", ".", ".."} for part in value.split("/")) + ) + + +def admit_tooling( + raw_manifest: bytes, *, accepted_digest: str, source_files: Mapping[str, bytes] +) -> None: + """Reject a manifest that does not match independently accepted bytes.""" + if not isinstance(raw_manifest, bytes) or not 0 < len(raw_manifest) <= 2 * 1024 * 1024: + raise ValueError("manifest size is outside the bounded contract") + if hashlib.sha256(raw_manifest).hexdigest() != accepted_digest: + raise ValueError("tooling does not match the accepted manifest digest") + try: + manifest = json.loads(raw_manifest, object_pairs_hook=_unique_object) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("manifest JSON is malformed") from exc + keys = {"schema_version", "repository", "commit", "tree", "entrypoint", "files"} + if not isinstance(manifest, dict) or set(manifest) != keys: + raise ValueError("manifest fields do not match the trusted schema") + if type(manifest["schema_version"]) is not int or manifest["schema_version"] != 1: + raise ValueError("manifest schema version is unsupported") + if manifest["repository"] != "ContextualWisdomLab/.github": + raise ValueError("manifest tooling repository is untrusted") + for field in ("commit", "tree"): + if not isinstance(manifest[field], str) or not re.fullmatch(r"[0-9a-f]{40}", manifest[field]): + raise ValueError("manifest requires immutable commit and tree identities") + inventory = manifest["files"] + if not isinstance(inventory, dict) or not inventory: + raise ValueError("manifest source inventory must be nonempty") + for name, digest in inventory.items(): + if not _safe_path(name) or not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest): + raise ValueError("manifest source inventory contains invalid paths or digests") + if not _safe_path(manifest["entrypoint"]) or manifest["entrypoint"] not in inventory: + raise ValueError("manifest entrypoint is not in the accepted source inventory") + actual = {name: hashlib.sha256(data).hexdigest() for name, data in source_files.items()} + if manifest["files"] != actual: + raise ValueError("tooling source inventory differs from the accepted manifest") + + +def require_review_scope(expected: Mapping[str, object], live: Mapping[str, object]) -> None: + """Refuse changed PR scope before model preparation or App publication.""" + fields = {"repository", "number", "head", "base", "author", "reviewer"} + if set(expected) != fields or set(live) != fields | {"state", "draft"}: + raise ValueError("review scope fields are incomplete or unexpected") + if type(live["number"]) is not int or type(expected["number"]) is not int or expected["number"] < 1: + raise ValueError("review scope PR number is invalid") + repository = expected["repository"] + if not isinstance(repository, str) or not re.fullmatch( + r"ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+", repository + ): + raise ValueError("review scope repository is outside the organization") + for field in ("head", "base"): + if not isinstance(expected[field], str) or not re.fullmatch(r"[0-9a-f]{40}", expected[field]): + raise ValueError("review scope requires immutable head and base") + for field in ("author", "reviewer"): + if not isinstance(expected[field], str) or not expected[field].strip(): + raise ValueError("review scope identity is missing") + if expected["author"].strip().casefold() == expected["reviewer"].strip().casefold(): + raise ValueError("review scope requires an independent reviewer") + if live["state"] != "OPEN" or live["draft"] is not False: + raise ValueError("review scope is closed or draft") + if any(expected[field] != live[field] for field in fields): + raise ValueError("review scope changed after admission") + + +def require_prepared_envelope(path: Path, scope: Mapping[str, object]) -> None: + """Validate compatible eight-field model output before publisher credentials. + + This is same-invocation runner-local custody, not cross-run authenticity. + The independently admitted tooling is responsible for substantive model + validation; no externally supplied envelope is accepted by run_review. + """ + try: + with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW), "rb") as stream: + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError("prepared envelope is not a private single-link regular file") + raw = stream.read(2 * 1024 * 1024 + 1) + if not 0 < len(raw) <= 2 * 1024 * 1024: + raise ValueError("prepared envelope exceeds its bounded size") + payload = json.loads(raw, object_pairs_hook=_unique_object) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("prepared envelope is unavailable or malformed") from exc + keys = {"schema_version", "repository", "pull_request_number", "expected_head", + "expected_base", "reviewer_actor", "pull_request_author", "verdict"} + if not isinstance(payload, dict) or set(payload) != keys: + raise ValueError("prepared envelope does not match compatible producer schema") + if type(payload["schema_version"]) is not int or payload["schema_version"] != 1: + raise ValueError("prepared envelope schema version is invalid") + bound = {"repository": "repository", "number": "pull_request_number", + "head": "expected_head", "base": "expected_base", + "author": "pull_request_author", "reviewer": "reviewer_actor"} + if any(payload[key] != scope[field] for field, key in bound.items()): + raise ValueError("prepared envelope identity differs from admitted scope") + verdict = payload["verdict"] + if not isinstance(verdict, dict) or verdict.get("decision") not in {"approve", "request_changes", "comment"}: + raise ValueError("prepared envelope is missing a model verdict") + + +def require_phase_environment(phase: str, environment: Mapping[str, str]) -> None: + """Reject loader overrides and inference capability in publisher children. + + Metadata alone cannot distinguish read/write GitHub tokens; exact token + scope must still be verified by the trusted runtime admission adapter. + Never include values in refusal diagnostics. + """ + forbidden = {"PYTHONPATH", "PYTHONHOME", "LD_PRELOAD", "LD_LIBRARY_PATH", + "DYLD_INSERT_LIBRARIES", "BASH_ENV", "ENV"} + if any(key in environment for key in forbidden): + raise ValueError("phase environment contains executable-loader overrides") + if phase == "publish": + if any(key in environment for key in ("NOEMA_LLM_API_KEY", "LLM_GATEWAY_API_KEY")): + raise ValueError("phase environment publisher contains inference capability") + elif phase == "prepare": + if environment.get("NOEMA_LLM_MODEL") != "auto": + raise ValueError("phase environment must request literal auto") + else: + raise ValueError("phase environment has an unsupported phase") + + +def accepted_prepare_transport(context): + """Validate only independently pinned workflow/document transport capabilities. + + The enclosing execution admission digest binds these fields, not ambient + environment values. Runtime admission still owns their namespace and supplier. + """ + if context is None: + return {} + if not isinstance(context, dict) or not context or set(context) - { + 'GITHUB_OUTPUT', 'NOEMA_TRANSPORT_RETRY_ATTEMPT', + 'NOEMA_HWP_MCP_SOURCE', 'NOEMA_NODE_EXECUTABLE', 'node_sha256', 'hwp_files', + }: + raise ValueError('prepare transport has unsupported fields') + for field in ('GITHUB_OUTPUT', 'NOEMA_HWP_MCP_SOURCE'): + if field in context: + value = context[field] + if (not isinstance(value, str) or value != value.strip() + or any(ord(character) < 32 or ord(character) == 127 for character in value)): + raise ValueError('prepare transport path differs from consumer identity') + environment = {} + if 'GITHUB_OUTPUT' in context or 'NOEMA_TRANSPORT_RETRY_ATTEMPT' in context: + attempt = context.get('NOEMA_TRANSPORT_RETRY_ATTEMPT') + if not isinstance(attempt, str) or not re.fullmatch(r'[0-2]', attempt): + raise ValueError('prepare retry context is outside the existing bound') + output = context.get('GITHUB_OUTPUT') + if not isinstance(output, str) or not Path(output).is_absolute(): + raise ValueError('prepare output requires an accepted absolute path') + info = Path(output).lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError('prepare output is not private regular custody') + environment.update(GITHUB_OUTPUT=output, NOEMA_TRANSPORT_RETRY_ATTEMPT=attempt) + document_keys = {'NOEMA_HWP_MCP_SOURCE', 'NOEMA_NODE_EXECUTABLE', 'node_sha256', 'hwp_files'} + if document_keys & set(context): + if not document_keys <= set(context): + raise ValueError('prepare document transport is incomplete') + source, node = context['NOEMA_HWP_MCP_SOURCE'], context['NOEMA_NODE_EXECUTABLE'] + if not all(isinstance(value, str) and Path(value).is_absolute() for value in (source, node)): + raise ValueError('prepare document supplier paths are not absolute') + root, executable = Path(source), Path(node) + if root.is_symlink() or not root.is_dir() or executable.is_symlink(): + raise ValueError('prepare document supplier is not regular custody') + info = executable.stat() + if not stat.S_ISREG(info.st_mode) or not info.st_mode & 0o111: + raise ValueError('prepare Node supplier is not executable') + digest = context['node_sha256'] + if not isinstance(digest, str) or not re.fullmatch(r'[0-9a-f]{64}', digest): + raise ValueError('prepare Node digest is missing') + if hashlib.sha256(executable.read_bytes()).hexdigest() != digest: + raise ValueError('prepare Node supplier bytes changed') + inventory = context['hwp_files'] + if not isinstance(inventory, dict) or not inventory or any( + not _safe_path(name) or not isinstance(value, str) or not re.fullmatch(r'[0-9a-f]{64}', value) + for name, value in inventory.items() + ): + raise ValueError('prepare HWP inventory is malformed') + actual = {} + for path in root.rglob('*'): + if path.is_symlink() or not (path.is_dir() or path.is_file()): + raise ValueError('prepare HWP supplier has unsupported custody') + if path.is_file(): + actual[path.relative_to(root).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest() + if actual != inventory: + raise ValueError('prepare HWP supplier bytes changed') + environment.update(NOEMA_HWP_MCP_SOURCE=source, NOEMA_NODE_EXECUTABLE=node) + return environment + + +def phase_environment_from_issuance( + phase, response, *, scope, reviewer_actor, installation_id, inference_key=None, + prepare_transport=None, +): + """Consume a trusted installation-token issuance response without logging it. + + The response must come directly from the existing authenticated App issuer, + not PR data or an artifact. The controller does not mint a token, read a + secret, or establish issuer authenticity by validating this dictionary. + """ + expected_permissions = {"contents": "read", "metadata": "read", + "pull_requests": "read" if phase == "prepare" else "write"} + if phase not in {"prepare", "publish"} or not isinstance(response, dict): + raise ValueError("credential issuance has unsupported phase or shape") + if response.get("permissions") != expected_permissions: + raise ValueError("credential issuance permissions do not match the phase") + repositories = response.get("repositories") + if not isinstance(repositories, list) or len(repositories) != 1: + raise ValueError("credential issuance must be single-repository scoped") + if not isinstance(repositories[0], dict) or repositories[0].get("full_name") != scope["repository"]: + raise ValueError("credential issuance repository does not match the review") + token = response.get("token") + if not isinstance(token, str) or not token.strip(): + raise ValueError("credential issuance returned no usable token") + if reviewer_actor != scope["reviewer"] or not re.fullmatch(r"[a-z0-9][a-z0-9-]*\[bot\]", reviewer_actor): + raise ValueError("credential issuance reviewer actor is not the admitted independent App") + if type(installation_id) is not int or installation_id < 1: + raise ValueError("credential issuance installation identity is invalid") + environment = {"PATH": "/usr/bin:/bin", "GH_TOKEN": token, + "NOEMA_REVIEW_ACTOR": reviewer_actor, + "NOEMA_REVIEW_INSTALLATION_ID": str(installation_id), + "NOEMA_REVIEW_TOKEN_SOURCE": "noema-review-github-app" if phase == "prepare" else "noema-review-github-app-refresh"} + if phase == "prepare": + if not isinstance(inference_key, str) or not inference_key.strip(): + raise ValueError("credential issuance preparation requires existing inference authority") + environment.update(NOEMA_LLM_MODEL="auto", NOEMA_LLM_API_KEY=inference_key, + NOEMA_LLM_API_URL="https://litellm.poinnetworks.net/v1/chat/completions") + environment.update(accepted_prepare_transport(prepare_transport)) + return environment + + +class _NoemaTooling: + """Execute one compatible producer/publisher from a sealed tooling namespace. + + The trusted caller owns the namespace exclusively through both phases and + supplies phase-scoped environments, including separately attested read or + write authority. This adapter does not authenticate an external envelope. + Interpreter and installed third-party dependencies need independent runtime + admission; the manifest binds repository files only. + """ + + def __init__(self, root, raw, accepted_digest, interpreter, phase_environment, child_runner): + """Retain explicit controller dependencies without loading candidate code.""" + self.root = Path(root) + self.raw = raw + self.accepted_digest = accepted_digest + self.interpreter = Path(interpreter) + self.phase_environment = phase_environment + self.child_runner = child_runner + + def _verify(self): + """Re-read complete regular source files before each child invocation.""" + if not self.root.is_absolute() or self.root.is_symlink() or not self.root.is_dir(): + raise ValueError("source inventory root is not an absolute owned directory") + files = {} + for path in self.root.rglob("*"): + if path.is_symlink(): + raise ValueError("source inventory contains a symbolic link") + if path.is_file(): + files[path.relative_to(self.root).as_posix()] = path.read_bytes() + elif not path.is_dir(): + raise ValueError("source inventory contains an unsupported file kind") + admit_tooling(self.raw, accepted_digest=self.accepted_digest, source_files=files) + manifest = json.loads(self.raw) + if manifest["entrypoint"] != ".github/actions/noema-review/two_phase.py": + raise ValueError("source inventory entrypoint is not the compatible Noema tooling") + return self.root / manifest["entrypoint"] + + def _run(self, phase, repo, number, head, path): + """Run a fresh isolated-import child with explicit phase environment only.""" + entrypoint = self._verify() + if not self.interpreter.is_absolute() or not self.interpreter.is_file(): + raise ValueError("trusted interpreter must be an absolute executable file") + environment = self.phase_environment(phase) + require_phase_environment(phase, environment) + # Credential acquisition can yield to a trusted issuer; bind source + # again afterwards so those credentials never reach stale tooling. + entrypoint = self._verify() + command = [str(self.interpreter), "-I", "-B", str(entrypoint), + "--repo", repo, "--pr-number", str(number), "--expected-head", head, + f"--{phase}-verdict-file", str(path)] + result = self.child_runner(command, cwd=self.root, env=environment, check=False) + return result.returncode + + def prepare_verdict(self, repo, number, head, path): + """Consume PR bytes as data through the admitted model producer.""" + return self._run("prepare", repo, number, head, path) + + def publish_verdict(self, repo, number, head, path): + """Publish only through the same compatible trusted tooling source.""" + return self._run("publish", repo, number, head, path) + + +def load_noema_tooling( + root, raw_manifest, *, accepted_digest, interpreter, phase_environment, + child_runner=subprocess.run, +): + """Bind the real two-phase CLI without importing caller-selected modules.""" + tooling = _NoemaTooling(root, raw_manifest, accepted_digest, interpreter, + phase_environment, child_runner) + tooling._verify() + return tooling + + +def run_review( + raw_manifest, *, accepted_digest, source_files, scope, fetch_live, + require_execution_admission, load_tooling, refresh_publisher, private_parent, +): + """Admit source and current scope before invoking trusted runtime collaborators. + + Callbacks are trusted controller dependencies, never PR payload. Their + authority, isolation, supplier policy and source binding require separate + operational acceptance; these checks do not manufacture such authority. + """ + admit_tooling(raw_manifest, accepted_digest=accepted_digest, source_files=source_files) + require_review_scope(scope, fetch_live()) + require_execution_admission() + tooling = load_tooling() + with tempfile.TemporaryDirectory(prefix="premerge-review-", dir=private_parent) as directory: + envelope = Path(directory) / "verdict.json" + result = tooling.prepare_verdict(scope["repository"], scope["number"], scope["head"], envelope) + if result != 0 or not envelope.is_file(): + raise RuntimeError("model preparation did not produce a verdict") + require_prepared_envelope(envelope, scope) + require_review_scope(scope, fetch_live()) + require_execution_admission() + refresh_publisher() + require_review_scope(scope, fetch_live()) + require_prepared_envelope(envelope, scope) + return tooling.publish_verdict(scope["repository"], scope["number"], scope["head"], envelope) + + +def production_main(argv=None): + """Validate independently pinned custody inputs and live scope before setup. + + Files must be supplied by the existing custody owner outside PR data. This + validates supplied evidence, not its authority: independent acceptance of + the digest is an explicit operational prerequisite, never generated here. + """ + import argparse + import sys + + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_mutually_exclusive_group(required=True) + modes.add_argument('--admit-production', action='store_true') + modes.add_argument('--run-production', action='store_true') + modes.add_argument('--prepare-production', action='store_true') + modes.add_argument('--publish-production', action='store_true') + parser.add_argument('--phase-issuance', type=Path) + parser.add_argument('--verdict-file', type=Path) + parser.add_argument('--prepare-issuance', type=Path) + parser.add_argument('--publish-issuance', type=Path) + parser.add_argument('--private-parent', type=Path) + parser.add_argument('--tooling-root', type=Path, required=True) + parser.add_argument('--manifest', type=Path, required=True) + parser.add_argument('--accepted-digest', required=True) + parser.add_argument('--admission', type=Path, required=True) + parser.add_argument('--admission-digest', required=True) + parser.add_argument('--repo', required=True) + parser.add_argument('--pr-number', type=int, required=True) + parser.add_argument('--expected-head', required=True) + args = parser.parse_args(argv) + try: + if not all(re.fullmatch(r'[0-9a-f]{64}', value) for value in + (args.accepted_digest, args.admission_digest)): + raise ValueError('independent admission pins are missing or malformed') + raw = args.manifest.read_bytes() + tooling = _NoemaTooling(args.tooling_root, raw, args.accepted_digest, + Path(sys.executable), None, None) + tooling._verify() + admission_raw = args.admission.read_bytes() + if hashlib.sha256(admission_raw).hexdigest() != args.admission_digest: + raise ValueError('execution admission differs from independent pin') + admission = json.loads(admission_raw, object_pairs_hook=_unique_object) + scope = admission['scope'] + if scope['repository'] != args.repo or scope['number'] != args.pr_number or scope['head'] != args.expected_head: + raise ValueError('execution admission target differs from request') + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('execution admission required evidence is not accepted') + fetched = subprocess.run(['gh', 'api', f'repos/{args.repo}/pulls/{args.pr_number}'], + check=True, capture_output=True, text=True, timeout=60) + pr = json.loads(fetched.stdout) + live = {'repository': pr['base']['repo']['full_name'], 'number': pr['number'], + 'head': pr['head']['sha'], 'base': pr['base']['sha'], + 'author': pr['user']['login'], 'reviewer': scope['reviewer'], + 'state': str(pr['state']).upper(), 'draft': pr['draft']} + require_review_scope(scope, live) + # Live metadata retrieval may yield while custody is revoked. Refuse + # stale admission before either phase obtains credentials or executes. + if args.admission.read_bytes() != admission_raw or args.manifest.read_bytes() != raw: + raise ValueError('independent custody changed during live scope retrieval') + if args.prepare_production or args.publish_production: + phase = 'prepare' if args.prepare_production else 'publish' + if args.phase_issuance is None or args.verdict_file is None: + raise ValueError('phase issuer output and private verdict custody required') + info = args.phase_issuance.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError('issuer output custody is not private') + response = json.loads(args.phase_issuance.read_bytes(), object_pairs_hook=_unique_object) + environment = phase_environment_from_issuance( + phase, response, scope=scope, reviewer_actor=scope['reviewer'], + installation_id=admission['installation_id'], + inference_key=os.environ.get('LLM_GATEWAY_API_KEY') if phase == 'prepare' else None, + prepare_transport=admission.get('prepare_transport') if phase == 'prepare' else None) + phase_tooling = load_noema_tooling(args.tooling_root, raw, + accepted_digest=args.accepted_digest, interpreter=Path(sys.executable), + phase_environment=lambda requested: environment if requested == phase else {}) + if phase == 'publish': + require_prepared_envelope(args.verdict_file, scope) + status = getattr(phase_tooling, phase + '_verdict')( + args.repo, args.pr_number, args.expected_head, args.verdict_file) + if phase == 'prepare' and status == 0 and args.verdict_file.exists(): + require_prepared_envelope(args.verdict_file, scope) + return status + if args.run_production: + if not args.prepare_issuance or not args.publish_issuance or not args.private_parent: + raise ValueError('phase issuance and private custody are required') + environments = {} + + def phase_environment(phase): + """Expose only the admitted credential for the selected phase.""" + return environments[phase] + + def read_issuance(path, phase): + """Consume private issuer output without logging credential bytes.""" + info = path.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError('issuer output is not private regular custody') + response = json.loads(path.read_bytes(), object_pairs_hook=_unique_object) + return phase_environment_from_issuance( + phase, response, scope=scope, reviewer_actor=scope['reviewer'], + installation_id=admission['installation_id'], + inference_key=os.environ.get('LLM_GATEWAY_API_KEY') if phase == 'prepare' else None, + prepare_transport=admission.get('prepare_transport') if phase == 'prepare' else None, + ) + + def revalidate_admission(): + """Refuse evidence or source drift at every controller boundary.""" + if args.admission.read_bytes() != admission_raw or args.manifest.read_bytes() != raw: + raise ValueError('independent custody changed after admission') + tooling._verify() + + def fetch_live(): + """Re-fetch current scope before each admitted phase.""" + current = subprocess.run(['gh', 'api', f'repos/{args.repo}/pulls/{args.pr_number}'], + check=True, capture_output=True, text=True, timeout=60) + document = json.loads(current.stdout) + return {'repository': document['base']['repo']['full_name'], 'number': document['number'], + 'head': document['head']['sha'], 'base': document['base']['sha'], + 'author': document['user']['login'], 'reviewer': scope['reviewer'], + 'state': str(document['state']).upper(), 'draft': document['draft']} + + def refresh_publisher(): + """Consume separately issued publication authority only after model completion.""" + environments['publish'] = read_issuance(args.publish_issuance, 'publish') + + environments['prepare'] = read_issuance(args.prepare_issuance, 'prepare') + files = {name: (args.tooling_root / name).read_bytes() for name in json.loads(raw)['files']} + return run_review(raw, accepted_digest=args.accepted_digest, source_files=files, + scope=scope, fetch_live=fetch_live, + require_execution_admission=revalidate_admission, + load_tooling=lambda: load_noema_tooling(args.tooling_root, raw, + accepted_digest=args.accepted_digest, interpreter=Path(sys.executable), + phase_environment=phase_environment), + refresh_publisher=refresh_publisher, private_parent=args.private_parent) + return 0 + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): + # No credential values, response bodies or exception payloads in logs. + print('premerge admission refused: independent admission pins or custody/scope evidence unavailable', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(production_main()) diff --git a/scripts/ci/r_isolated_matrix.py b/scripts/ci/r_isolated_matrix.py new file mode 100644 index 0000000000..02f44f5574 --- /dev/null +++ b/scripts/ci/r_isolated_matrix.py @@ -0,0 +1,27 @@ +"""Bound the R matrix to supported Linux/x64 image aliases, never runner labels.""" +import json +import sys + + +def admit(raw): + """Reject unsupported matrix legs before any R job can be scheduled.""" + rows = json.loads(raw) + if not isinstance(rows, list) or not 1 <= len(rows) <= 64: + raise ValueError('Expected 1..64 matrix objects') + for row in rows: + if not isinstance(row, dict) or not isinstance(row.get('os'), str): + raise ValueError('Expected an OS string in each matrix object') + if not isinstance(row.get('r'), str) or not row['r'].strip(): + raise ValueError('Expected a nonempty R version') + if row['os'] in ('ubuntu-latest', 'ubuntu-24.04'): + continue + raise ValueError('STOP unsupported platform leg: ' + repr(row['os'])) + return rows + + +if __name__ == '__main__': + try: + print(json.dumps(admit(sys.argv[1]), separators=(',', ':'))) + except (ValueError, TypeError, IndexError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) diff --git a/scripts/ci/strix_model_utils.sh b/scripts/ci/strix_model_utils.sh index 9f20eae670..16b32de179 100755 --- a/scripts/ci/strix_model_utils.sh +++ b/scripts/ci/strix_model_utils.sh @@ -77,6 +77,16 @@ normalize_model() { return 0 fi + # Literal auto is a gateway request, never a guessed Vertex/direct model. + if [ "$model" = "auto" ]; then + if [ "${DEFAULT_PROVIDER:-}" != "contextual_orchestrator" ]; then + echo "ERROR: literal auto requires the explicit contextual_orchestrator provider." >&2 + return 2 + fi + printf '%s\n' 'auto' + return 0 + fi + if is_vertex_resource_path "$model"; then local provider provider="$(sanitize_provider_name "${DEFAULT_PROVIDER:-}")" || { diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index cb97fd8eb8..5f1568e824 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -379,7 +379,7 @@ is_vertex_model() { is_contextual_orchestrator_model() { case "$1" in - orchestrator/free | contextual-orchestrator/orchestrator/free) + auto | orchestrator/free | contextual-orchestrator/orchestrator/free) return 0 ;; *) @@ -2651,8 +2651,8 @@ child_model_for_api_base() { # LiteLLM requires an explicit provider prefix even when the gateway is an # OpenAI-compatible local endpoint. Strip only the connector-facing alias so - # the selected orchestrator/free virtual pool reaches contextual-orchestrator - # unchanged. + # literal auto or a standalone legacy virtual pool reaches the gateway + # unchanged; the openai/ connector prefix is not part of the model ID. if is_contextual_orchestrator_model "$model" && is_contextual_orchestrator_api_base "$llm_api_base_value"; then local contextual_orchestrator_model diff --git a/scripts/ci/strix_report_scope.py b/scripts/ci/strix_report_scope.py index 158a570ea2..08fab1bef2 100644 --- a/scripts/ci/strix_report_scope.py +++ b/scripts/ci/strix_report_scope.py @@ -10,7 +10,15 @@ # The PR scan target is a private directory holding only the changed files, so # a path under it (as Strix reports it) is bound to this invocation's scope. -SCOPE_PATH_RE = re.compile(r"/workspace/strix-pr-scope\.[A-Za-z0-9]+/([A-Za-z0-9_./-]+)") +PATH_TOKEN_END = ( + r"(?=$|[\s`\"'<>,;!?)]" + r"|:(?=[0-9]+(?::[0-9]+)?(?:$|[\s`\"'<>,;!?)]|\.(?![\w./-])))" + r"|\.(?![\w./-]))" +) +SCOPE_PATH_RE = re.compile( + r"(? bool: @@ -22,8 +30,34 @@ def _names_scoped_ancestor(report: str, changed_paths: list[str]) -> bool: return False +def _token(text: str) -> re.Pattern[str]: + """Match a path token only at whole-segment boundaries.""" + return re.compile(rf"(? re.Pattern[str]: + """Match a filename token without accepting suffixes or child paths.""" + return re.compile(rf"(? bool: + """Match exact file paths or a named directory plus its exact filename.""" + if _file_token(path).search(report) is not None: + return True + directory, _, name = path.rpartition("/") + if not directory or _file_token(name).search(report) is None: + return False + if _token(directory).search(report) is not None: + return True + parts = directory.split("/") + return any( + _token("/".join(parts[:depth])).search(report) + for depth in range(2, len(parts) + 1) + ) + + def validate(output: Path, changed_paths: list[str]) -> None: - """Validate that a completed Strix report covers changed source scope.""" + """Raise ValueError unless one completed, unlinked report names changed scope.""" if not output.is_dir() or output.is_symlink(): raise ValueError("scan output directory is missing") @@ -44,7 +78,7 @@ def validate(output: Path, changed_paths: list[str]) -> None: if metadata.get("status") != "completed" or results.get("scan_completed") is not True or results.get("success") is not True: raise ValueError("scan report is incomplete") report = report_path.read_text(encoding="utf-8") - if not any(path in report for path in changed_paths) and not _names_scoped_ancestor(report, changed_paths): + if not any(names_changed_path(report, path) for path in changed_paths) and not _names_scoped_ancestor(report, changed_paths): raise ValueError("scan report does not identify a changed source file") diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index 0b5a4a54d4..d1f192ea6f 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -180,9 +180,9 @@ assert_file_contains "$full_gate_test" "assert_strix_workflow_pr_trigger_hardene assert_file_contains "$workflow_file" "Provision contextual-orchestrator Strix sidecar" "Strix workflow provisions the trusted contextual-orchestrator gateway" assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR" "Strix workflow binds target visibility to the gateway ZDR policy" active_strix_models="$(sed -n -E 's/^[[:space:]]*STRIX_MODEL:[[:space:]]*([^#[:space:]]+)[[:space:]]*$/\1/p' "$workflow_file")" -[ "$active_strix_models" = "contextual-orchestrator/orchestrator/free" ] || record_failure "Strix must define exactly one active zero-cost free default model" -assert_file_not_contains "$workflow_file" "STRIX_MODEL: contextual-orchestrator/orchestrator/auto" "Strix must not retain the paid-inclusive auto default route" -assert_file_contains "$decision_record" "2026-08-30 amendment: Strix uses \`orchestrator/free\`" "The binding ADR amendment records the owner's explicit free-only override" +[ "$active_strix_models" = "auto" ] || record_failure "Strix must define exactly one literal auto default model" +assert_file_not_contains "$workflow_file" "STRIX_MODEL: contextual-orchestrator/orchestrator/free" "Strix must not silently restore a free default route" +assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_POOL: auto" "Strix sidecar pool matches the literal auto request" assert_file_contains "$decision_record" "Zero Data Retention (ZDR)-compliant routes remain mandatory for private targets" "The binding ADR preserves private-target privacy" assert_file_contains "$workflow_file" "provider_mode=contextual_orchestrator" "Strix workflow selects the contextual-orchestrator provider mode" assert_file_contains "$workflow_file" "STRIX_FALLBACK_MODELS: \"\"" "Strix delegates provider discovery and failover to the gateway" diff --git a/scripts/ci/verify_release_maturin_tool_assets.py b/scripts/ci/verify_release_maturin_tool_assets.py index a59507ac83..ed5afc90c3 100644 --- a/scripts/ci/verify_release_maturin_tool_assets.py +++ b/scripts/ci/verify_release_maturin_tool_assets.py @@ -120,6 +120,7 @@ def _download(filename: str) -> bytes: def _binary(raw: bytes, filename: str) -> bytes: + """Extract one bounded Maturin executable from its release archive.""" if filename.endswith(".zip"): with zipfile.ZipFile(io.BytesIO(raw)) as archive: members = archive.infolist() @@ -168,6 +169,7 @@ def verify_assets(evidence: dict, reader: str, fetch=_download) -> None: def main() -> None: + """Verify recorded Maturin assets from a local root or official release.""" parser = argparse.ArgumentParser() parser.add_argument("--asset-root", type=Path) args = parser.parse_args() diff --git a/tests/fixture_github_reads.py b/tests/fixture_github_reads.py new file mode 100644 index 0000000000..1b44abda54 --- /dev/null +++ b/tests/fixture_github_reads.py @@ -0,0 +1,36 @@ +"""Explicit offline fixture-only GitHub CLI; all writes/endpoints denied.""" +import json +import os +from pathlib import Path +import sys + +fixture = json.loads(Path(os.environ["FIXTURE_FILE"]).read_text()) +args = sys.argv[1:] +with Path(fixture["calls"]).open("a") as handle: + handle.write(json.dumps(args) + "\n") +if any(value in args for value in ("POST", "PATCH", "PUT", "DELETE", "--method", "-X")): + raise SystemExit("fixture forbids mutations") +if not args or args[0] != "api": + raise SystemExit("fixture only permits api reads") +endpoints = [value for value in args[1:] if value.startswith("repos/")] +if len(endpoints) != 1: + raise SystemExit("fixture requires one endpoint") +endpoint = endpoints[0] +if endpoint == "repos/ContextualWisdomLab/example/pulls/1437": + print(json.dumps(fixture["pull"])) +elif endpoint == "repos/ContextualWisdomLab/fast-mlsirm/pulls/2018": + print(json.dumps(fixture["pull"])) +elif endpoint.startswith("repos/ContextualWisdomLab/fast-mlsirm/pulls/2018/reviews") or endpoint.startswith("repos/ContextualWisdomLab/example/pulls/1437/reviews"): + if fixture["lookup_fail"]: + print("fixture read failure", file=sys.stderr) + raise SystemExit(1) + if fixture["malformed"]: + print("not-json") + else: + print(json.dumps([fixture["reviews"]] if "--slurp" in args else fixture["reviews"])) +elif endpoint == "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=6a37e4cdfbd8bf6f3a60645a0ad7c13e1b9c1ae3": + if args[-2:] != ["--jq", ".content"]: + raise SystemExit("fixture expects pinned content field") + print(fixture["helper_base64"]) +else: + raise SystemExit("fixture rejects endpoint outside exact bounded contract") diff --git a/tests/fixtures/opencode_shell_literal_operands.json b/tests/fixtures/opencode_shell_literal_operands.json new file mode 100644 index 0000000000..5185ec4e88 --- /dev/null +++ b/tests/fixtures/opencode_shell_literal_operands.json @@ -0,0 +1,177 @@ +[ + { + "before": "'cd \"$1\" && corepack npm run build'", + "after": "\"cd \\\"\\$1\\\" && corepack npm run build\"", + "expected": "cd \"$1\" && corepack npm run build", + "literal_sha256": "5b75beac7d183f624302beeede5ef80ebdae5220c68d4e62e8fef76b1f7d95ae", + "count": 1 + }, + { + "before": "'cd \"$1\" && corepack pnpm run build'", + "after": "\"cd \\\"\\$1\\\" && corepack pnpm run build\"", + "expected": "cd \"$1\" && corepack pnpm run build", + "literal_sha256": "e322a3754c05bac3697ef5148b8d91fc821f772bbb825a65ef216789652671a9", + "count": 1 + }, + { + "before": "'cd \"$1\" && yarn build'", + "after": "\"cd \\\"\\$1\\\" && yarn build\"", + "expected": "cd \"$1\" && yarn build", + "literal_sha256": "7a32a0647a33e794473ad69fecd80c09b0ff0c9e07f48bbe4ac9fdc5985b0e57", + "count": 1 + }, + { + "before": "'set -eu\n cd \"$1\"\n dist_dir=\"$(mktemp -d)\"\n python3 -m maturin build --offline --release -o \"$dist_dir\"\n python3 -m pip install --user --no-index --no-deps --force-reinstall \"$dist_dir\"/*.whl\n python3 \"$2\" \"$dist_dir\"/*.whl .\n rm -rf \"$dist_dir\"'", + "after": "\"set -eu\n cd \\\"\\$1\\\"\n dist_dir=\\\"\\$(mktemp -d)\\\"\n python3 -m maturin build --offline --release -o \\\"\\$dist_dir\\\"\n python3 -m pip install --user --no-index --no-deps --force-reinstall \\\"\\$dist_dir\\\"/*.whl\n python3 \\\"\\$2\\\" \\\"\\$dist_dir\\\"/*.whl .\n rm -rf \\\"\\$dist_dir\\\"\"", + "expected": "set -eu\n cd \"$1\"\n dist_dir=\"$(mktemp -d)\"\n python3 -m maturin build --offline --release -o \"$dist_dir\"\n python3 -m pip install --user --no-index --no-deps --force-reinstall \"$dist_dir\"/*.whl\n python3 \"$2\" \"$dist_dir\"/*.whl .\n rm -rf \"$dist_dir\"", + "literal_sha256": "4bba65aefcb59292fa58d0c47228faa9de663c1be54f4fdcfc30e37e234ad82e", + "count": 1 + }, + { + "before": "'cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing'", + "after": "\"cd \\\"\\$1\\\" && PYTHONPATH=\\\"\\$([ -d src ] && printf src:. || printf .)\\\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing\"", + "expected": "cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing", + "literal_sha256": "8328238f7e8a9efdf50a7d5c1f7bf2e3e9d407b93736b459cc49a720d0dbe253", + "count": 1 + }, + { + "before": "'PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing'", + "after": "\"PYTHONPATH=\\\"\\$([ -d src ] && printf src:. || printf .)\\\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing\"", + "expected": "PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing", + "literal_sha256": "5b8f66d840e67514ded7204289d8d6544f8d9e9b51783bcc5e1f5e99ba45c103", + "count": 1 + }, + { + "before": "'cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py'", + "after": "\"cd \\\"\\$1\\\" && PYTHONPATH=\\\"\\$([ -d src ] && printf src:. || printf .)\\\" python3 -m pytest tests/test_docstrings.py\"", + "expected": "cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py", + "literal_sha256": "f6e4b346d6c2285ecff7172605110fedcd34b0e7119695918a3c683adc9edb83", + "count": 1 + }, + { + "before": "'- Preferred review language: `%s`\\n'", + "after": "\"- Preferred review language: \\`%s\\`\\\\n\"", + "expected": "- Preferred review language: `%s`\\n", + "literal_sha256": "a55f9a0314aca4f2919688735698b948e4eb3c6fb5b9a764343626111edff099", + "count": 1 + }, + { + "before": "'- PR title: `%s`\\n'", + "after": "\"- PR title: \\`%s\\`\\\\n\"", + "expected": "- PR title: `%s`\\n", + "literal_sha256": "990a7ab75683f7fc6ebb36792ac611d816bc67aa456f96ba5f40e33641c6c6d4", + "count": 1 + }, + { + "before": "'- PR body excerpt: `%s`\\n'", + "after": "\"- PR body excerpt: \\`%s\\`\\\\n\"", + "expected": "- PR body excerpt: `%s`\\n", + "literal_sha256": "068807903f94d5f4944b5dd5afc54a62face32538f66d1b09d177b986bebb1da", + "count": 1 + }, + { + "before": "'- PR body excerpt: `[empty]`\\n'", + "after": "\"- PR body excerpt: \\`[empty]\\`\\\\n\"", + "expected": "- PR body excerpt: `[empty]`\\n", + "literal_sha256": "3daa3390c09d66fccc34f340c3142b8b1f2de29a23c30e8caa73b62e1617ddab", + "count": 1 + }, + { + "before": "'Unable to collect %s from `%s` to `%s`; continue review from available changed-file evidence and direct file inspection.\\n'", + "after": "\"Unable to collect %s from \\`%s\\` to \\`%s\\`; continue review from available changed-file evidence and direct file inspection.\\\\n\"", + "expected": "Unable to collect %s from `%s` to `%s`; continue review from available changed-file evidence and direct file inspection.\\n", + "literal_sha256": "31eccf9ccdc69d89d9033eac2d0c10beaa644ecdab06f53305d0904a267de44a", + "count": 1 + }, + { + "before": "'Merge-base discovery failed for `%s` and `%s`; falling back to base SHA for bounded diff evidence.\\n\\n'", + "after": "\"Merge-base discovery failed for \\`%s\\` and \\`%s\\`; falling back to base SHA for bounded diff evidence.\\\\n\\\\n\"", + "expected": "Merge-base discovery failed for `%s` and `%s`; falling back to base SHA for bounded diff evidence.\\n\\n", + "literal_sha256": "3520ee4d10ac9a7f302555dabdc7f8370a0c821ffda3694ffd3aab3daf135c5d", + "count": 1 + }, + { + "before": "'All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA `%s`.\\n\\n'", + "after": "\"All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA \\`%s\\`.\\\\n\\\\n\"", + "expected": "All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA `%s`.\\n\\n", + "literal_sha256": "a62108037c8187ed5baa230d394c788909c36cbb2489fbd292c8c6d7f74eca39", + "count": 1 + }, + { + "before": "'\n def self_check:\n (.name // \"\") as $n\n | [\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"] | index($n);\n def latest_peer_checks:\n [\n (.check_runs // [])[]\n | select(self_check | not)\n | . + {\n checkedAt: (\n if ((.started_at // \"\") != \"\") then .started_at\n else (.completed_at // \"\")\n end\n )\n }\n ]\n | sort_by(.app.slug // \"\", .name // \"\", .checkedAt // \"\", .id // 0)\n | group_by([.app.slug // \"\", .name // \"\"])\n | map(last)\n | .[];\n '", + "after": "\"\n def self_check:\n (.name // \\\"\\\") as \\$n\n | [\\\"opencode-review\\\", \\\"coverage-evidence\\\", \\\"coverage-source-tree\\\", \\\"required-workflow-bootstrap\\\", \\\"metadata-only gate evaluation\\\", \\\"scan-pr-queue\\\"] | index(\\$n);\n def latest_peer_checks:\n [\n (.check_runs // [])[]\n | select(self_check | not)\n | . + {\n checkedAt: (\n if ((.started_at // \\\"\\\") != \\\"\\\") then .started_at\n else (.completed_at // \\\"\\\")\n end\n )\n }\n ]\n | sort_by(.app.slug // \\\"\\\", .name // \\\"\\\", .checkedAt // \\\"\\\", .id // 0)\n | group_by([.app.slug // \\\"\\\", .name // \\\"\\\"])\n | map(last)\n | .[];\n \"", + "expected": "\n def self_check:\n (.name // \"\") as $n\n | [\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"] | index($n);\n def latest_peer_checks:\n [\n (.check_runs // [])[]\n | select(self_check | not)\n | . + {\n checkedAt: (\n if ((.started_at // \"\") != \"\") then .started_at\n else (.completed_at // \"\")\n end\n )\n }\n ]\n | sort_by(.app.slug // \"\", .name // \"\", .checkedAt // \"\", .id // 0)\n | group_by([.app.slug // \"\", .name // \"\"])\n | map(last)\n | .[];\n ", + "literal_sha256": "aee9423e1e806ca41f083a4ab8ae311c6c423c67a4750a05f6aceff961c29321", + "count": 1 + }, + { + "before": "'\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.status // \"\") == \"completed\")\n | select((.conclusion // \"\" | ascii_upcase) as $c | [\"FAILURE\",\"TIMED_OUT\",\"ACTION_REQUIRED\",\"CANCELLED\",\"STARTUP_FAILURE\"] | index($c))\n | select((.name // \"\") != \"scan-pr-queue\")\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and ((.name // \"\") | contains(\"$\" + \"{{\"))) | not)\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and (.name // \"\") == \"noema-review\") | not)\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.conclusion // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n '", + "after": "\"\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \\\"\\\"), (.id // 0))\n | group_by(.name // \\\"\\\")\n | map(last)\n | .[]?\n | select(((.name // \\\"\\\" | ascii_downcase) as \\$n | [\\\"opencode-review\\\",\\\"coverage-evidence\\\",\\\"metadata-only gate evaluation\\\"] | index(\\$n)) | not)\n | select((.status // \\\"\\\") == \\\"completed\\\")\n | select((.conclusion // \\\"\\\" | ascii_upcase) as \\$c | [\\\"FAILURE\\\",\\\"TIMED_OUT\\\",\\\"ACTION_REQUIRED\\\",\\\"CANCELLED\\\",\\\"STARTUP_FAILURE\\\"] | index(\\$c))\n | select((.name // \\\"\\\") != \\\"scan-pr-queue\\\")\n | select(((.conclusion // \\\"\\\" | ascii_downcase) == \\\"cancelled\\\" and ((.name // \\\"\\\") | contains(\\\"\\$\\\" + \\\"{{\\\"))) | not)\n | select(((.conclusion // \\\"\\\" | ascii_downcase) == \\\"cancelled\\\" and (.name // \\\"\\\") == \\\"noema-review\\\") | not)\n | \\\"- \\\" + (if (.name // \\\"\\\") == \\\"strix\\\" then \\\"Strix Security Scan/strix\\\" else ((.name // \\\"check\\\") + \\\" check run\\\") end) + \\\": \\\" + (.conclusion // \\\"unknown\\\") + (if (.details_url // .html_url // \\\"\\\") != \\\"\\\" then \\\" (\\\" + (.details_url // .html_url) + \\\")\\\" else \\\"\\\" end)\n \"", + "expected": "\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.status // \"\") == \"completed\")\n | select((.conclusion // \"\" | ascii_upcase) as $c | [\"FAILURE\",\"TIMED_OUT\",\"ACTION_REQUIRED\",\"CANCELLED\",\"STARTUP_FAILURE\"] | index($c))\n | select((.name // \"\") != \"scan-pr-queue\")\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and ((.name // \"\") | contains(\"$\" + \"{{\"))) | not)\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and (.name // \"\") == \"noema-review\") | not)\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.conclusion // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n ", + "literal_sha256": "6c4a46c21e26f6edff27c80ec673a1587677626d749ac929899db7d8c6c13613", + "count": 1 + }, + { + "before": "'\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.name // \"\") != \"scan-pr-queue\")\n | select((.status // \"\") != \"completed\")\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.status // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n '", + "after": "\"\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \\\"\\\"), (.id // 0))\n | group_by(.name // \\\"\\\")\n | map(last)\n | .[]?\n | select(((.name // \\\"\\\" | ascii_downcase) as \\$n | [\\\"opencode-review\\\",\\\"coverage-evidence\\\",\\\"metadata-only gate evaluation\\\"] | index(\\$n)) | not)\n | select((.name // \\\"\\\") != \\\"scan-pr-queue\\\")\n | select((.status // \\\"\\\") != \\\"completed\\\")\n | \\\"- \\\" + (if (.name // \\\"\\\") == \\\"strix\\\" then \\\"Strix Security Scan/strix\\\" else ((.name // \\\"check\\\") + \\\" check run\\\") end) + \\\": \\\" + (.status // \\\"unknown\\\") + (if (.details_url // .html_url // \\\"\\\") != \\\"\\\" then \\\" (\\\" + (.details_url // .html_url) + \\\")\\\" else \\\"\\\" end)\n \"", + "expected": "\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.name // \"\") != \"scan-pr-queue\")\n | select((.status // \"\") != \"completed\")\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.status // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n ", + "literal_sha256": "3c98f8ca91cdb37939293cd06808463e69bad6e1c015cafa05d07fb2d5a036ce", + "count": 1 + }, + { + "before": "'\n query($owner:String!,$name:String!,$number:Int!) {\n repository(owner:$owner,name:$name) {\n pullRequest(number:$number) {\n statusCheckRollup {\n contexts(first: 100) {\n nodes {\n __typename\n ... on CheckRun {\n name\n status\n conclusion\n completedAt\n detailsUrl\n checkSuite {\n workflowRun {\n workflow {\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n '", + "after": "\"\n query(\\$owner:String!,\\$name:String!,\\$number:Int!) {\n repository(owner:\\$owner,name:\\$name) {\n pullRequest(number:\\$number) {\n statusCheckRollup {\n contexts(first: 100) {\n nodes {\n __typename\n ... on CheckRun {\n name\n status\n conclusion\n completedAt\n detailsUrl\n checkSuite {\n workflowRun {\n workflow {\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n \"", + "expected": "\n query($owner:String!,$name:String!,$number:Int!) {\n repository(owner:$owner,name:$name) {\n pullRequest(number:$number) {\n statusCheckRollup {\n contexts(first: 100) {\n nodes {\n __typename\n ... on CheckRun {\n name\n status\n conclusion\n completedAt\n detailsUrl\n checkSuite {\n workflowRun {\n workflow {\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n ", + "literal_sha256": "a1f6ebee2275c7fb64e9c80219b8102ded1c712d19d40c867ee72f16f79839aa", + "count": 1 + }, + { + "before": "'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id}}}'", + "after": "\"query(\\$owner:String!,\\$name:String!,\\$number:Int!){repository(owner:\\$owner,name:\\$name){pullRequest(number:\\$number){id}}}\"", + "expected": "query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id}}}", + "literal_sha256": "286a6960231d8dfb8ff7781de00eea84606a9da93dba3bacf96062e541f8ef77", + "count": 1 + }, + { + "before": "'\n [\n .[]\n | {\n number: (.number // 0),\n rule: (.rule.id // .rule.name // \"unknown\"),\n tool: (.tool.name // \"code-scanning\"),\n severity: (.rule.security_severity_level // .rule.severity // \"unknown\"),\n url: (.html_url // \"\")\n }\n | select((.severity | ascii_downcase) as $s | [\"medium\",\"high\",\"critical\",\"warning\",\"error\"] | index($s))\n ]\n | .[]\n | \"- \" + .tool + \"/\" + .rule + \": \" + .severity + \" alert #\" + (.number | tostring) + (if .url != \"\" then \" (\" + .url + \")\" else \"\" end)\n '", + "after": "\"\n [\n .[]\n | {\n number: (.number // 0),\n rule: (.rule.id // .rule.name // \\\"unknown\\\"),\n tool: (.tool.name // \\\"code-scanning\\\"),\n severity: (.rule.security_severity_level // .rule.severity // \\\"unknown\\\"),\n url: (.html_url // \\\"\\\")\n }\n | select((.severity | ascii_downcase) as \\$s | [\\\"medium\\\",\\\"high\\\",\\\"critical\\\",\\\"warning\\\",\\\"error\\\"] | index(\\$s))\n ]\n | .[]\n | \\\"- \\\" + .tool + \\\"/\\\" + .rule + \\\": \\\" + .severity + \\\" alert #\\\" + (.number | tostring) + (if .url != \\\"\\\" then \\\" (\\\" + .url + \\\")\\\" else \\\"\\\" end)\n \"", + "expected": "\n [\n .[]\n | {\n number: (.number // 0),\n rule: (.rule.id // .rule.name // \"unknown\"),\n tool: (.tool.name // \"code-scanning\"),\n severity: (.rule.security_severity_level // .rule.severity // \"unknown\"),\n url: (.html_url // \"\")\n }\n | select((.severity | ascii_downcase) as $s | [\"medium\",\"high\",\"critical\",\"warning\",\"error\"] | index($s))\n ]\n | .[]\n | \"- \" + .tool + \"/\" + .rule + \": \" + .severity + \" alert #\" + (.number | tostring) + (if .url != \"\" then \" (\" + .url + \")\" else \"\" end)\n ", + "literal_sha256": "0f75e1d5894b2b73171b4eb30786e20af5178dda107f415b3c80de0af1f69fe2", + "count": 1 + }, + { + "before": "'- Problem: Failed same-head checks remain for `%s`.\\n'", + "after": "\"- Problem: Failed same-head checks remain for \\`%s\\`.\\\\n\"", + "expected": "- Problem: Failed same-head checks remain for `%s`.\\n", + "literal_sha256": "f65b717a34c174fb8de45a0c7a004762a1e300a8010b3c7d0a3f816512bd5ccd", + "count": 1 + }, + { + "before": "'- Result: `EXISTING_CURRENT_HEAD_APPROVAL`\\n'", + "after": "\"- Result: \\`EXISTING_CURRENT_HEAD_APPROVAL\\`\\\\n\"", + "expected": "- Result: `EXISTING_CURRENT_HEAD_APPROVAL`\\n", + "literal_sha256": "8423de58f35ef3649d71f174e789b7fdde0c794d6c471f79a13eb5e9f18c828c", + "count": 1 + }, + { + "before": "'- Head SHA: `%s`\\n'", + "after": "\"- Head SHA: \\`%s\\`\\\\n\"", + "expected": "- Head SHA: `%s`\\n", + "literal_sha256": "840f8302400a82c3ba04fc0ed8e5b4d230fe277cb276c0a043d61a0a025a5b65", + "count": 2 + }, + { + "before": "'- Model-pool outcome: `%s`\\n'", + "after": "\"- Model-pool outcome: \\`%s\\`\\\\n\"", + "expected": "- Model-pool outcome: `%s`\\n", + "literal_sha256": "5b8ec084e8d6b976374a8332b42978c39f0222d6f12183d3683b001d44d83899", + "count": 1 + }, + { + "before": "'```json\\n%s\\n```\\n\\n'", + "after": "\"\\`\\`\\`json\\\\n%s\\\\n\\`\\`\\`\\\\n\\\\n\"", + "expected": "```json\\n%s\\n```\\n\\n", + "literal_sha256": "1fd318239f7f84df9676717e141bd7ca8ef672b4c585e01f942ce3aa15349fee", + "count": 1 + } +] \ No newline at end of file diff --git a/tests/test_actions_queue_health_contract.py b/tests/test_actions_queue_health_contract.py index 4b2a49a58f..4958d18376 100644 --- a/tests/test_actions_queue_health_contract.py +++ b/tests/test_actions_queue_health_contract.py @@ -15,7 +15,15 @@ def test_queue_health_workflow_is_scheduled_read_only_and_pinned() -> None: assert "workflow_dispatch:" not in workflow assert "cancel-in-progress: false" in workflow assert "timeout-minutes: 30" in workflow - assert "runs-on: ubuntu-24.04" in workflow + assert "github.repository == 'ContextualWisdomLab/.github'" in workflow + assert "github.ref == 'refs/heads/main'" in workflow + assert "endsWith(github.workflow_ref, '@refs/heads/main')" in workflow + assert '"group":"CWL MCP remediation"' in workflow + assert '"labels":["self-hosted","linux","x64"]' in workflow + assert '"group":"CWL CI isolated"' in workflow + assert '"labels":["self-hosted","linux","x64","cwlab-ci-isolated"]' in workflow + assert "runs-on: ubuntu-24.04" not in workflow + assert "runs-on: ubuntu-latest" not in workflow assert "actions: read" in workflow assert "pull-requests: read" not in workflow assert "contents: write" not in workflow diff --git a/tests/test_adr_identifier_uniqueness.py b/tests/test_adr_identifier_uniqueness.py new file mode 100644 index 0000000000..c82662b79a --- /dev/null +++ b/tests/test_adr_identifier_uniqueness.py @@ -0,0 +1,30 @@ +"""Contracts for unambiguous architecture decision record identifiers.""" + +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +ADR_DIRECTORY = ROOT / "docs" / "adr" +ADR_FILE_ID = re.compile(r"(?:^|[-_])(?P\d{4})(?:[-_]|$)") + + +def test_adr_file_identifiers_are_unique() -> None: + """Each numeric ADR identifier must name exactly one decision record.""" + records_by_identifier: dict[str, list[str]] = defaultdict(list) + + for path in sorted(ADR_DIRECTORY.glob("*.md")): + match = ADR_FILE_ID.search(path.stem) + if match is not None: + records_by_identifier[match.group("identifier")].append(path.name) + + duplicates = { + identifier: records + for identifier, records in records_by_identifier.items() + if len(records) > 1 + } + + assert duplicates == {}, f"duplicate ADR identifiers: {duplicates}" diff --git a/tests/test_agent_mention_downstream_idempotency.py b/tests/test_agent_mention_downstream_idempotency.py index 28c9084a9b..2be8080c7f 100644 --- a/tests/test_agent_mention_downstream_idempotency.py +++ b/tests/test_agent_mention_downstream_idempotency.py @@ -135,10 +135,10 @@ def test_forwarders_restrict_self_hosted_admission_to_trusted_main() -> None: """Branch workflows cannot select the main-only privileged runner group.""" for path in (NOEMA_WORKFLOW, OPENCODE_WORKFLOW): text = path.read_text(encoding="utf-8") - selector = next(line for line in text.splitlines() if "runs-on:" in line) + selector = next(line for line in text.splitlines() if line.startswith(" group:")) assert "github.repository == 'ContextualWisdomLab/.github'" in selector assert "endsWith(github.workflow_ref, '@refs/heads/main')" in selector assert '"group":"CWL MCP remediation"' in selector assert '"labels":["self-hosted","linux","x64"]' in selector - assert "|| fromJSON('[\"ubuntu-24.04\"]')" in selector + assert '|| fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')' in selector assert "actions/checkout@" not in text diff --git a/tests/test_all_self_hosted_runner_contract.py b/tests/test_all_self_hosted_runner_contract.py new file mode 100644 index 0000000000..2833ddb09f --- /dev/null +++ b/tests/test_all_self_hosted_runner_contract.py @@ -0,0 +1,183 @@ +"""Require self-hosted CI without admitting PR code to privileged runner pools. + +The isolated label is an operator provisioning requirement, not evidence that an +isolated machine exists. Existing trusted control and review selectors retain +their main-ref predicates; their non-main fallback must never be GitHub-hosted. +""" +from pathlib import Path +import json +import re +import pytest +import yaml + + +STATIC_RUNNER_GROUP_LABELS = { + 'CWL central control': set(), + 'CWL central CodeQL': set(), + 'CWL central OpenCode': set(), + 'CWL CI isolated': {'cwlab-ci-isolated'}, + 'CWL law CI': {'law-ai-agent-ci'}, +} + + +def required_static_labels(group: object) -> set[str]: + """Return the exact additional labels admitted for a static runner group.""" + assert group in STATIC_RUNNER_GROUP_LABELS, ('unsupported static group', group) + return {'self-hosted', 'linux', 'x64'} | STATIC_RUNNER_GROUP_LABELS[group] + + +def test_every_runner_requires_self_hosted_and_no_hosted_fallback() -> None: + """Inspect every declaration, including expression and matrix selectors.""" + declarations = [] + for path in sorted(Path('.github/workflows').glob('*.yml')): + lines = path.read_text().splitlines() + for index, line in enumerate(lines): + if re.match(r'^ runs-on:', line): + block = line + if line.strip() == 'runs-on:': + block += '\n' + '\n'.join(lines[index + 1:index + 3]) + declarations.append((path, block)) + assert declarations + for path, block in declarations: + assert 'self-hosted' in block, (path, block) + assert not re.search(r'fromJSON\(\'\["ubuntu-|\|\| \'"ubuntu-', block), (path, block) + assert not re.search(r'runs-on: (?:ubuntu-|windows-|macos-)', block), (path, block) + + +def test_nonprivileged_runners_require_isolated_label() -> None: + """Check actual YAML selectors, including static labels and both extensions.""" + paths = sorted(set(Path('.github/workflows').glob('*.yml')) | + set(Path('.github/workflows').glob('*.yaml'))) + observed = 0 + groups = set(STATIC_RUNNER_GROUP_LABELS) + for path in paths: + for job_id, job in yaml.safe_load(path.read_text())['jobs'].items(): + if 'runs-on' not in job: + continue + observed += 1 + runner = job['runs-on'] + if isinstance(runner, dict): + group = runner.get('group') + if isinstance(group, str) and group.startswith('${{'): + # Dynamic predicates remain covered by the exact fallback, + # typed-pair and R-matrix contracts, not literal evaluation. + assert any(name in group for name in groups), (path, job_id, group) + continue + labels = runner.get('labels') + assert isinstance(labels, list) and all(isinstance(label, str) for label in labels), ( + path, job_id, 'static labels must be a string list', labels, + ) + required = required_static_labels(group) + assert required.issubset(label.lower() for label in labels), ( + path, job_id, 'missing required static labels', required.difference(labels), + ) + else: + assert isinstance(runner, str) and 'cwlab-ci-isolated' in runner, (path, job_id, runner) + assert observed, 'runner selector coverage is empty' + + +def test_static_runner_group_policy_is_explicit_and_fail_closed() -> None: + """Bind dedicated groups to their isolation labels and reject unknown groups.""" + assert required_static_labels('CWL law CI') == { + 'self-hosted', 'linux', 'x64', 'law-ai-agent-ci', + } + with pytest.raises(AssertionError, match='unsupported static group'): + required_static_labels('CWL ungoverned CI') + + +def test_isolated_fallback_never_selects_privileged_group() -> None: + """Inspect scalar and typed runner fallbacks, never an empty declaration set.""" + observed = 0 + expected = { + 'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated'], + } + paths = sorted(set(Path('.github/workflows').glob('*.yml')) | + set(Path('.github/workflows').glob('*.yaml'))) + for path in paths: + for job_id, job in yaml.safe_load(path.read_text())['jobs'].items(): + runner = job.get('runs-on') + fields = list(runner.values()) if isinstance(runner, dict) else [runner] + expressions = [field for field in fields if isinstance(field, str) and + field.startswith('${{') and '||' in field and + 'matrix.config.os' not in field] + if not expressions: + continue + observed += 1 + for expression in expressions: + fallback = expression.rsplit('||', 1)[1] + match = re.fullmatch( + r"\s*(?:fromJSON\(\s*)?'(\{[^']*\})'\s*\)*\s*" + r"(?:\.group|\.labels)?\s*\}\}", fallback, + ) + assert match is not None, (path, job_id, 'unsupported fallback', fallback) + assert json.loads(match.group(1)) == expected, ( + path, job_id, 'unsafe fallback', fallback, + ) + assert observed == 29, ('conditional runner fallback coverage', observed) + + +def test_isolated_label_is_scoped_to_dedicated_runner_group() -> None: + """A mutable label cannot replace the isolated runner access boundary.""" + for path in sorted(Path('.github/workflows').glob('*.yml')): + lines = path.read_text().splitlines() + for index, line in enumerate(lines): + if not re.match(r'^ runs-on:', line): + continue + block = line + if line.strip() == 'runs-on:': + block += '\n' + '\n'.join(lines[index + 1:index + 3]) + if 'cwlab-ci-isolated' in block: + assert 'CWL CI isolated' in block, (path, block) + + +TRUSTED_MAIN_UNION_SELECTORS = { + '.github/workflows/actions-queue-health.yml': {'collect'}, + '.github/workflows/audit-central-ruleset.yml': {'audit'}, + '.github/workflows/organization-commercial-readiness-loop.yml': {'coordinate'}, + '.github/workflows/pr-auto-rebase.yml': {'auto-rebase'}, + '.github/workflows/repository-metadata-reconcile.yml': {'validate', 'apply'}, + '.github/workflows/sbom-inventory-scheduler.yml': {'aggregate-sbom-inventory'}, +} + + +def test_trusted_main_union_selectors_preserve_control_and_isolated_fallback() -> None: + """Resolve main/head routing conflicts without restoring hosted fallback.""" + trusted_main = '{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}' + isolated_fallback = ( + '{"group":"CWL CI isolated",' + '"labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}' + ) + for workflow_path, job_ids in TRUSTED_MAIN_UNION_SELECTORS.items(): + workflow = yaml.safe_load(Path(workflow_path).read_text()) + for job_id in job_ids: + mapping = workflow['jobs'][job_id]['runs-on'] + assert isinstance(mapping, dict), (workflow_path, job_id, mapping) + assert set(mapping) == {'group', 'labels'} + group = mapping['group'] + labels = mapping['labels'] + assert group.endswith(').group }}') + assert labels.endswith(').labels }}') + assert group.removesuffix(').group }}') == labels.removesuffix(').labels }}') + runner = '${{ ' + group[5:].removesuffix(').group }}') + ' }}' + expected = ( + "${{ github.repository == 'ContextualWisdomLab/.github' && " + "github.ref == 'refs/heads/main' && " + "endsWith(github.workflow_ref, '@refs/heads/main') && " + f"fromJSON('{trusted_main}') || fromJSON('{isolated_fallback}') }}" + "}" + ) + assert runner == expected, (workflow_path, job_id, runner) + condition = runner[4:].split(' && fromJSON(', 1)[0] + for repository in ('ContextualWisdomLab/.github', 'ContextualWisdomLab/example'): + for ref in ('refs/heads/main', 'refs/pull/2565/merge', 'refs/heads/candidate'): + for workflow_ref in ('main', 'candidate'): + terms = { + "github.repository == 'ContextualWisdomLab/.github'": repository == 'ContextualWisdomLab/.github', + "github.ref == 'refs/heads/main'": ref == 'refs/heads/main', + "endsWith(github.workflow_ref, '@refs/heads/main')": workflow_ref == 'main', + } + admitted = all(terms[term] for term in condition.split(' && ')) + wanted = repository == 'ContextualWisdomLab/.github' and ref == 'refs/heads/main' and workflow_ref == 'main' + assert admitted == wanted, (workflow_path, job_id, repository, ref, workflow_ref) + assert 'ubuntu-24.04' not in runner + assert 'ubuntu-latest' not in runner diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..8d95cdb0b1 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -262,7 +262,14 @@ def test_null_latest_analysis_is_not_coverage() -> None: ] -def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: +def test_audit_codeql_coverage_defaults_now_to_current_time(monkeypatch) -> None: + class FixedClock(datetime): + @classmethod + def now(cls, tz=None): + assert tz is timezone.utc + return NOW + + monkeypatch.setattr(audit, "datetime", FixedClock) repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] assert audit.audit_codeql_coverage(repositories) == [] @@ -299,7 +306,14 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: assert "FAIL: 1 repositories have no CodeQL coverage" in captured.err -def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: +def test_main_pass_path_reports_from_file_arg(tmp_path, capsys, monkeypatch) -> None: + class FixedClock(datetime): + @classmethod + def now(cls, tz=None): + assert tz is timezone.utc + return NOW + + monkeypatch.setattr(audit, "datetime", FixedClock) payload_path = tmp_path / "repositories.json" payload_path.write_text( json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), diff --git a/tests/test_codeql_pr_workflow_contract.py b/tests/test_codeql_pr_workflow_contract.py index 82569e89c7..23cf31660f 100644 --- a/tests/test_codeql_pr_workflow_contract.py +++ b/tests/test_codeql_pr_workflow_contract.py @@ -1150,7 +1150,7 @@ def test_codeql_moved_head_requires_forward_ancestry(tmp_path: Path) -> None: def test_codeql_control_routing_keeps_pr_workflows_hosted() -> None: """Only the trusted main revision may request the restricted control group.""" workflow = WORKFLOW_PATH.read_text(encoding="utf-8") - selectors = [line.strip() for line in workflow.splitlines() if line.strip().startswith("runs-on:")] + selectors = [line.strip() for line in workflow.splitlines() if line.startswith(" group:")] trusted = "ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main" assert len(selectors) == 3 for selector in selectors: @@ -1158,7 +1158,10 @@ def test_codeql_control_routing_keeps_pr_workflows_hosted() -> None: choices = re.findall(r"'([^']*)'", selector) 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" + assert json.loads(choices[2]) == { + "group": "CWL CI isolated", + "labels": ["self-hosted", "linux", "x64", "cwlab-ci-isolated"], + } def test_codeql_pr_rejects_invalid_required_run_time(tmp_path: Path) -> None: diff --git a/tests/test_company_default_head_trust_gate.py b/tests/test_company_default_head_trust_gate.py new file mode 100644 index 0000000000..b54e88aa61 --- /dev/null +++ b/tests/test_company_default_head_trust_gate.py @@ -0,0 +1,131 @@ +"""Source-only trust gate: real YAML Bash, inert API, no company operation.""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / '.github/workflows/company-default-head-trust-gate.yml' +CURRENT = 'a' * 40 + + +def workflow(): + assert WORKFLOW.is_file(), 'Missing central company trust-gate callee' + return yaml.safe_load(WORKFLOW.read_text()) + + +def test_exact_callee_contract(): + data = workflow() + trigger = data.get('on', data.get(True)) + assert set(trigger) == {'workflow_call'} + call = trigger['workflow_call'] + assert set(call['inputs']) == {'target_runner_label', 'trusted_ref_sha', 'trusted_ref_ack'} + assert call['inputs']['target_runner_label'] == {'required': True, 'type': 'string'} + assert call.get('secrets', {}) == {} + assert call['outputs']['runner_matrix']['value'] == '${{ jobs.validate.outputs.runner_matrix }}' + assert data['permissions'] == {'contents': 'read'} + assert set(data['jobs']) == {'validate'} + job = data['jobs']['validate'] + assert job['runs-on'] == {'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'Linux', 'X64', 'cwlab-ci-isolated']} + assert job['timeout-minutes'] == 5 + assert job['outputs']['runner_matrix'] == '${{ steps.runner.outputs.runner_matrix }}' + assert len(job['steps']) == 2 + assert all('uses' not in step and step['shell'] == 'bash' for step in job['steps']) + body = '\n'.join(step['run'] for step in job['steps']) + assert 'inputs.trusted_ref_' not in body + assert 'secrets.' not in body + assert job['steps'][1]['env']['GH_TOKEN'] == '${{ github.token }}' + + +@pytest.mark.parametrize('case,ref,target,api_exit,sha,expected,calls', [ + ('current', 'main', 'all', 0, CURRENT, 0, 1), + ('stale', 'main', 'itxllmpgw01', 0, 'b' * 40, 2, 1), + ('nondefault', 'feature', 'itxllmpgw01', 0, CURRENT, 2, 0), + ('invalid-target', 'main', 'injected; echo unsafe', 0, CURRENT, 2, 0), + ('api-failure', 'main', 'itxllmpgw01', 23, CURRENT, 23, 1), +]) +def test_actual_callee_bash(tmp_path, case, ref, target, api_exit, sha, expected, calls): + job = workflow()['jobs']['validate'] + stub = tmp_path / 'gh' + stub.write_text('#!' + sys.executable + '\n' + 'import json, os, sys\n' + 'with open(os.environ["CALLS"], "a") as f:\n' + ' f.write(json.dumps(sys.argv[1:]) + "\\n")\n' + 'if int(os.environ["API_EXIT"]): sys.exit(int(os.environ["API_EXIT"]))\n' + 'print(os.environ["CURRENT_SHA"])\n') + stub.chmod(0o700) + output, record = tmp_path / 'output', tmp_path / 'calls' + env = {'PATH': str(tmp_path) + os.pathsep + os.defpath, + 'TARGET': target, 'DEFAULT_BRANCH': 'main', 'REF_NAME': ref, + 'SHA': sha, 'REPOSITORY': 'fixture/company-ops', 'GH_TOKEN': 'synthetic-only', + 'GITHUB_OUTPUT': str(output), 'CALLS': str(record), + 'CURRENT_SHA': CURRENT, 'API_EXIT': str(api_exit)} + bash = shutil.which('bash') + assert bash, 'Bash is required; missing tool is not a skipped PASS' + results = [] + for step in job['steps']: + result = subprocess.run([bash, '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', step['run']], + env=env, cwd=tmp_path, capture_output=True, text=True, timeout=10) + results.append(result) + if result.returncode: + break + assert results[-1].returncode == expected, (case, results[-1].stderr) + actual_calls = record.read_text().splitlines() if record.exists() else [] + assert len(actual_calls) == calls + for line in actual_calls: + assert json.loads(line) == ['api', 'repos/fixture/company-ops/commits/main', '--jq', '.sha'] + if case == 'invalid-target': + assert not output.exists() + else: + value = output.read_text().strip().removeprefix('runner_matrix=') + assert json.loads(value) == (['itxllmpgw01', 'itxllmpgw02', 'xtrmsales-llmgw1', + 'xtrmsales-llmgw2'] if target == 'all' else [target]) + if case == 'current': + assert 'trusted_ref_status=current_default_head' in results[-1].stdout + else: + assert 'trusted_ref_status=current_default_head' not in results[-1].stdout + + +@pytest.mark.parametrize('result', ['success', 'failure', 'cancelled', 'skipped']) +def test_always_downstream_requires_overall_success(result): + # Source caller fixture/truth table, not GitHub scheduling or operating execution. + data = workflow() + fixture = yaml.safe_load('''jobs: + trust: + uses: ContextualWisdomLab/.github/.github/workflows/company-default-head-trust-gate.yml@ + operate: + needs: trust + if: ${{ always() && needs.trust.result == 'success' }} + matrix_data: ${{ needs.trust.outputs.runner_matrix }} +''') + assert data['jobs']['validate']['outputs']['runner_matrix'] + job = fixture['jobs']['operate'] + assert job['needs'] == 'trust' + assert job['if'] == "${{ always() && needs.trust.result == 'success' }}" + produced_matrix = '["itxllmpgw01"]' # Can exist even on stale/nondefault failure. + assert produced_matrix + # Exercise the supported fixture predicate with supplied result as data. + predicate = job['if'].removeprefix('${{ ').removesuffix(' }}') + assert predicate == "always() && needs.trust.result == 'success'" + shell = predicate.replace('always()', 'true').replace( + "needs.trust.result == 'success'", '[ "$TRUST_RESULT" = success ]') + bash = shutil.which('bash') + assert bash + child = subprocess.run([bash, '--noprofile', '--norc', '-c', + 'if ' + shell + '; then printf admitted; fi'], + env={'TRUST_RESULT': result}, capture_output=True, + text=True, timeout=5) + assert child.returncode == 0 + assert child.stdout == ('admitted' if result == 'success' else '') + # Removing the success gate admits all failed/skipped fixtures: oracle sensitivity. + unsafe = subprocess.run([bash, '--noprofile', '--norc', '-c', + 'if true; then printf admitted; fi'], + env={'TRUST_RESULT': result}, capture_output=True, + text=True, timeout=5) + assert unsafe.stdout == 'admitted' diff --git a/tests/test_contextual_orchestrator_review_launcher.py b/tests/test_contextual_orchestrator_review_launcher.py index 50d484da9c..ae7745c239 100644 --- a/tests/test_contextual_orchestrator_review_launcher.py +++ b/tests/test_contextual_orchestrator_review_launcher.py @@ -6,6 +6,7 @@ import logging import runpy import sys +import threading import time from pathlib import Path from types import ModuleType, SimpleNamespace @@ -473,30 +474,59 @@ def reject_primary(agents: list[object], **settings: object) -> object: def test_concurrent_preflight_waits_for_an_outstanding_route() -> None: - """Exhausting the iterator waits for its live probe instead of failing early.""" + """Hold the probe until the scheduler reaches its blocking receive.""" agent = SimpleNamespace( id="slow-ready", provider_name="bytez", model="test/free", priority=0 ) + release = threading.Event() + original_queue = launcher.queue.Queue + original_thread = launcher.threading.Thread + workers: list[threading.Thread] = [] + blocking_get_calls: list[bool] = [] - class SlowClient: - """Delay long enough for the scheduler to enter its blocking receive.""" + class RecordingQueue(original_queue): + """Release the held probe only on an actual blocking receive.""" + + def get(self, block: bool = True, timeout: float | None = None) -> object: + if block: + blocking_get_calls.append(True) + release.set() + return super().get(block=block, timeout=timeout) + + def recording_thread(*args: object, **kwargs: object) -> threading.Thread: + worker = original_thread(*args, **kwargs) + workers.append(worker) + return worker + + class HeldClient: + """Wait for the receive handshake, not a scheduling-dependent sleep.""" def proxy_send_once( self, selected_agent: object, endpoint: str, payload: dict[str, object] ) -> dict[str, object]: - time.sleep(0.02) + assert release.wait(timeout=5), "scheduler did not enter blocking receive" return { "choices": [ {"finish_reason": "stop", "message": {"content": "OK"}} ] } - viable_agents, preflight_report = launcher._preflight_review_agents_concurrently( - [agent], client=SlowClient() - ) - - assert viable_agents == [agent] - assert preflight_report["ready_count"] == 1 + monkeypatch = pytest.MonkeyPatch() + try: + monkeypatch.setattr(launcher.queue, "Queue", RecordingQueue) + monkeypatch.setattr(launcher.threading, "Thread", recording_thread) + viable_agents, preflight_report = launcher._preflight_review_agents_concurrently( + [agent], client=HeldClient() + ) + assert blocking_get_calls == [True] + assert viable_agents == [agent] + assert preflight_report["ready_count"] == 1 + finally: + release.set() + for worker in workers: + worker.join(timeout=5) + monkeypatch.undo() + assert all(not worker.is_alive() for worker in workers) def test_postponed_concurrent_probe_cannot_spend_an_escalation( diff --git a/tests/test_contextual_orchestrator_review_policy.py b/tests/test_contextual_orchestrator_review_policy.py index e13e94107e..b38449c9cf 100644 --- a/tests/test_contextual_orchestrator_review_policy.py +++ b/tests/test_contextual_orchestrator_review_policy.py @@ -237,6 +237,52 @@ def test_route_key_prefixes_provider() -> None: assert policy._route_key("nvidia_nim", "/leda/nemotron") == "nvidia_nim/leda/nemotron" +def test_discovery_modalities_survive_into_catalog_tags() -> None: + """Explicit input evidence survives parse and the actual catalog consumer.""" + report = _report() + report["models"][0]["input_modalities"] = [" TEXT ", "Image", "text"] + rows = policy.parse_discovery_report(report) + assert rows[0].get("input_modalities") == ["text", "image"] + result = policy.build_zdr_prioritized_catalog(rows, zdr_endpoints=ZDR_FEED) + agent = next(item for item in result["agents"] if item["model"] == rows[0]["model"]) + assert [tag for tag in agent["tags"] if tag.startswith("input:")] == [ + "input:text", "input:image" + ] + assert "cost:free" in agent["tags"] and "zdr" in agent["tags"] + + +@pytest.mark.parametrize("value", ["text", {"text": True}, [False], ["text", 1], + [""], ["image extra"], ["../image"], ["追加"]]) +def test_catalog_rejects_malformed_input_evidence(value: object) -> None: + """Direct normalized-row consumers cannot inject or lose modality tags.""" + rows = policy.parse_discovery_report(_report()) + rows[0]["input_modalities"] = value + with pytest.raises(policy.PolicyError, match="input_modalities"): + policy.build_zdr_prioritized_catalog(rows, zdr_endpoints=ZDR_FEED) + + +@pytest.mark.parametrize("value, expected", [ + (None, []), ([], []), + ((" TEXT ", "image", "text", "audio_2", "video-1"), + ["text", "image", "audio_2", "video-1"]), +]) +def test_modality_normalization_preserves_only_explicit_evidence( + value: object, expected: list[str] +) -> None: + """Absent modalities are not guessed; tokens retain order without duplicates.""" + assert policy.normalize_input_modalities(value) == expected + + +@pytest.mark.parametrize("value", ["text", {"text": True}, [False], ["text", 1], + [""], ["image extra"], ["../image"], ["追加"]]) +def test_discovery_rejects_malformed_input_evidence(value: object) -> None: + """Malformed discovery evidence is rejected before catalog construction.""" + report = _report() + report["models"][0]["input_modalities"] = value + with pytest.raises(policy.PolicyError, match="input_modalities"): + policy.parse_discovery_report(report) + + def test_parse_discovery_report_normalizes_rows() -> None: """Base URL, credential, and auth scheme fall back to the policy table.""" rows = policy.parse_discovery_report(_report()) diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index 16b85c5c78..48e597fc83 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -41,6 +41,7 @@ ) GATEWAY_MODEL = "contextual-orchestrator/orchestrator/free" +AUTO_REVIEW_MODEL = "contextual-orchestrator/orchestrator/auto" ORCH_PIN_SHA = "01bf92a3ec67a0e1f9b68978eb16b60301e985fd" @@ -93,47 +94,23 @@ def test_sidecar_feeds_discovery_and_policy_artifacts_to_the_launcher() -> None: assert "https://openrouter.ai/api/v1/endpoints/zdr" in text -def test_sidecar_pins_the_pool_to_free_for_github_actions() -> None: - """GitHub Actions Workflow usage of contextual-orchestrator is pinned free. - - ``auto`` was removed from the sidecar's own accepted - ``CONTEXTUAL_ORCHESTRATOR_POOL`` values: the org has not solved cost-safe - free+ZDR routing well enough yet to justify a priced-inclusive pool in - central CI. The launcher's own ``--pool`` flag (a general-purpose CLI - also used outside GitHub Actions, asserted separately above) still - accepts ``auto`` -- only this repo's GitHub-Actions-facing sidecar script - is narrowed. - """ +def test_sidecar_accepts_auto_with_explicit_zero_cost_ceiling() -> None: + """Auto alias changes selection, not authorization for paid fallback.""" text = _read(SIDECAR) - assert 'orchestrator_pool="${CONTEXTUAL_ORCHESTRATOR_POOL:-free}"' in text - assert 'case "$orchestrator_pool" in\n free)' in text - assert "fail \"CONTEXTUAL_ORCHESTRATOR_POOL must be free\"" in text - assert "free|auto" not in text - assert "must be free or auto" not in text - - pool_case = text.split('orchestrator_pool="${CONTEXTUAL_ORCHESTRATOR_POOL:-free}"', 1)[ - 1 - ].split("esac", 1)[0] - for candidate, should_fail in (("free", False), ("auto", True), ("", False), ("bogus", True)): + prefix = 'orchestrator_pool="${CONTEXTUAL_ORCHESTRATOR_POOL:-free}"' + pool_case = text[text.index(prefix):].split("esac", 1)[0] + "esac" + for candidate, expected in (("free", 0), ("auto", 0), ("", 0), ("bogus", 7)): result = subprocess.run( - [ - "bash", - "-c", - 'fail() { echo "FAIL: $*"; exit 7; }\n' - f'CONTEXTUAL_ORCHESTRATOR_POOL="{candidate}"\n' - 'orchestrator_pool="${CONTEXTUAL_ORCHESTRATOR_POOL:-free}"\n' - + pool_case - + "esac\necho \"pool_args=${pool_args[*]}\"", - ], - capture_output=True, - text=True, + ["bash", "-c", 'fail() { exit 7; }; ' + pool_case + + '\nprintf "%s\\n" "${pool_args[@]}"'], + env={"CONTEXTUAL_ORCHESTRATOR_POOL": candidate}, + capture_output=True, text=True, timeout=5, ) - if should_fail: - assert result.returncode == 7, (candidate, result.stdout, result.stderr) - assert "must be free" in result.stdout - else: - assert result.returncode == 0, (candidate, result.stdout, result.stderr) - assert "pool_args=--pool free" in result.stdout + assert result.returncode == expected, (candidate, result.stdout, result.stderr) + if candidate == "auto": + assert result.stdout.splitlines() == ["--pool", "auto", "--zero-cost-only"] + elif expected == 0: + assert result.stdout.splitlines() == ["--pool", "free"] def test_sidecar_exports_gateway_env_for_review_steps() -> None: @@ -389,10 +366,10 @@ def test_launcher_sets_a_bounded_review_request_body_limit() -> None: def test_strix_gateway_uses_provider_neutral_reasoning_effort() -> None: - """Gateway free-pool scans must not force unsupported provider controls.""" + """Gateway auto-pool scans must not force unsupported provider controls.""" text = _read(STRIX_WORKFLOW) assert "STRIX_REASONING_EFFORT: none" in text - assert "CONTEXTUAL_ORCHESTRATOR_POOL: free" in text + assert "CONTEXTUAL_ORCHESTRATOR_POOL: auto" in text def test_sidecar_probes_the_pinned_server_body_limit_at_http_boundary() -> None: @@ -439,14 +416,14 @@ def test_autofix_workflow_provisions_sidecar_with_all_five_secrets() -> None: def test_opencode_config_defaults_to_the_contextual_gateway() -> None: - """OpenCode's default review route is orchestrator/free through the gateway.""" + """OpenCode's default review route is orchestrator/auto through the gateway.""" config = _read(OPENCODE_CONFIG) - assert f'"model": "{GATEWAY_MODEL}"' in config - assert f'"small_model": "{GATEWAY_MODEL}"' in config + assert f'"model": "{AUTO_REVIEW_MODEL}"' in config + assert f'"small_model": "{AUTO_REVIEW_MODEL}"' in config assert '"enabled_providers": ["contextual-orchestrator"' in config assert '"baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}/v1"' in config assert '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"' in config - assert '"orchestrator/free": {' in config + assert '"orchestrator/auto": {' in config def test_sidecar_trap_keeps_the_gateway_alive_after_provisioning() -> None: @@ -532,7 +509,7 @@ def test_noema_review_workflow_provisions_sidecar_with_all_five_secrets() -> Non assert "contextual_orchestrator_review_sidecar.sh" in workflow for secret in FIVE_SECRETS: assert f"{secret}: ${{{{ secrets.{secret} }}}}" in workflow - assert 'export NOEMA_LLM_MODEL="orchestrator/free"' in workflow + assert 'export NOEMA_LLM_MODEL="orchestrator/auto"' in workflow assert "NOEMA_LLM_VIA_ORCHESTRATOR=1" in workflow assert "${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" in workflow assert "${CONTEXTUAL_ORCHESTRATOR_TOKEN}" in workflow @@ -540,7 +517,7 @@ def test_noema_review_workflow_provisions_sidecar_with_all_five_secrets() -> Non assert "nvidia/nemotron-3-ultra-550b-a55b" not in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow assert "secrets: inherit" not in workflow - assert "NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN }}" in workflow + assert "NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || '' }}" in workflow def test_noema_private_targets_require_zdr_only_sidecar_routing() -> None: @@ -565,10 +542,10 @@ def test_required_opencode_dispatch_uses_the_gateway_for_model_pool_and_diagnosi assert workflow.index("Validate pull request head repository trust") < workflow.index( "Provision contextual-orchestrator review sidecar" ) - assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free"' in workflow - assert 'MODEL: contextual-orchestrator/orchestrator/free' in workflow + assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto"' in workflow + assert 'MODEL: contextual-orchestrator/orchestrator/auto' in workflow assert '.enabled_providers = ["contextual-orchestrator"]' in workflow - assert '.model = "contextual-orchestrator/orchestrator/free"' in workflow + assert '.model = "contextual-orchestrator/orchestrator/auto"' in workflow assert 'CONTEXTUAL_ORCHESTRATOR_TOKEN:-' in workflow assert 'STRIX_GITHUB_MODELS_TOKEN:-' not in workflow assert 'MODEL: github-models/' not in workflow @@ -579,7 +556,7 @@ def test_required_strix_uses_the_gateway_and_zdr_visibility_contract() -> None: workflow = _read(STRIX_WORKFLOW) assert "Provision contextual-orchestrator Strix sidecar" in workflow assert "CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR" in workflow - assert 'STRIX_MODEL: contextual-orchestrator/orchestrator/free' in workflow + assert 'STRIX_MODEL: auto' in workflow assert "provider_mode=contextual_orchestrator" in workflow assert "STRIX_LLM_DEFAULT_PROVIDER: contextual_orchestrator" in workflow assert workflow.index("Resolve target repository visibility") < workflow.index( @@ -647,7 +624,7 @@ def test_sidecar_rebuilds_a_damaged_persistent_venv(tmp_path) -> None: re-running ``venv`` over a damaged ``pip`` package leaves it unimportable. """ text = _read(SIDECAR) - line = next(l for l in text.splitlines() if '-m venv --clear "$ORCHESTRATOR_WORK/.venv"' in l) + line = next(candidate for candidate in text.splitlines() if '-m venv --clear "$ORCHESTRATOR_WORK/.venv"' in candidate) work = tmp_path / "work" site = work / ".venv" / "lib" / f"python{sys.version_info[0]}.{sys.version_info[1]}" / "site-packages" env = {"PATH": os.environ["PATH"], "ORCHESTRATOR_WORK": str(work), diff --git a/tests/test_dependency_review_bundled_scan_identity_contract.py b/tests/test_dependency_review_bundled_scan_identity_contract.py new file mode 100644 index 0000000000..cee8d44374 --- /dev/null +++ b/tests/test_dependency_review_bundled_scan_identity_contract.py @@ -0,0 +1,46 @@ +"""Regression contract for the bundled Security Scan Dependency Review preflight.""" + +from __future__ import annotations + +from pathlib import Path + + +_WORKFLOW = Path(".github/workflows/security-scan.yml") + + +def _workflow_text() -> str: + """Return the bundled Security Scan workflow as UTF-8 text.""" + return _WORKFLOW.read_text(encoding="utf-8") + + +def test_bundled_scan_requires_exact_git_object_ids_before_transport() -> None: + """Named or malformed base/head revisions must fail before compare transport.""" + workflow = _workflow_text() + assert "git_object_id='^[0-9a-f]{40}([0-9a-f]{24})?$'" in workflow + assert 'if ! [[ "${BASE_SHA}" =~ $git_object_id ]]' in workflow + assert '! [[ "${HEAD_SHA}" =~ $git_object_id ]]' in workflow + assert "exact 40- or 64-character hexadecimal base and head revisions" in workflow + assert "Named refs are not evidence" in workflow + + +def test_bundled_scan_requires_one_non_dot_owner_name_identity() -> None: + """Repository identity validation keeps .github legal but rejects path sentinels.""" + workflow = _workflow_text() + assert "repository_identity='^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'" in workflow + assert 'if ! [[ "${REPOSITORY}" =~ $repository_identity ]]' in workflow + assert 'repository_owner="${REPOSITORY%%/*}"' in workflow + assert 'repository_name="${REPOSITORY#*/}"' in workflow + assert '[ "${repository_owner}" = "." ]' in workflow + assert '[ "${repository_owner}" = ".." ]' in workflow + assert '[ "${repository_name}" = "." ]' in workflow + assert '[ "${repository_name}" = ".." ]' in workflow + + +def test_bundled_scan_uses_job_token_and_fails_closed_on_non_200() -> None: + """Only an authenticated successful exact comparison may admit Dependency Review.""" + workflow = _workflow_text() + assert "GH_TOKEN: ${{ github.token }}" in workflow + assert '-H "Authorization: Bearer ${GH_TOKEN}"' in workflow + assert 'if [ "$curl_status" -ne 0 ] || [ "$http_status" != "200" ]; then' in workflow + assert 'echo "supported=true" >>"$GITHUB_OUTPUT"' in workflow + assert "actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294" in workflow diff --git a/tests/test_dependency_review_merge_evidence_contract.py b/tests/test_dependency_review_merge_evidence_contract.py new file mode 100644 index 0000000000..a0a8e97888 --- /dev/null +++ b/tests/test_dependency_review_merge_evidence_contract.py @@ -0,0 +1,111 @@ +"""Contracts for published dependency-review carryover evidence.""" + +from __future__ import annotations + +from pathlib import Path +import re +import subprocess + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +CHANGELOG_PATH = REPOSITORY_ROOT / "CHANGELOG.md" +GAP_BASELINE_PATH = REPOSITORY_ROOT / "docs/product-technical-gap-baseline.md" +PUBLISHED_SECURITY_PARENT = "229027280e8bce6cc4f13e722b2b0c280a1ac17d" +PUBLISHED_RUNNER_PARENT = "cd2c3f9748e6bc50efd8bdfee9a385a74ec6e3ba" +MERGE_EVIDENCE_PATTERN = re.compile( + r"ordinary (?:two-parent )?merge\s+`([0-9a-f]{40})`" +) + + +def _dependency_review_evidence(document_text: str, record_marker: str) -> str: + """Return the bounded dependency-review evidence paragraph or table row.""" + if record_marker.startswith("Dependency Review"): + section_start = document_text.index(f"### {record_marker}") + section_end = document_text.find("\n### ", section_start + 4) + evidence_section = document_text[ + section_start : section_end if section_end >= 0 else None + ] + assert "#2565" in evidence_section + assert "ordinary" in evidence_section + assert "merge" in evidence_section + return evidence_section + + evidence_blocks = [ + block + for block in document_text.split("\n\n") + if record_marker in block + and "#2565" in block + and "ordinary" in block + and "merge" in block + ] + assert len(evidence_blocks) == 1, ( + "#2565 must have one ordinary-merge evidence record" + ) + return evidence_blocks[0] + + +def _published_merge_sha(evidence_text: str) -> str: + """Extract the full published merge identifier from one evidence record.""" + match = MERGE_EVIDENCE_PATTERN.search(evidence_text) + assert match is not None, "#2565 evidence must name a full ordinary-merge SHA" + return match.group(1) + + +def _git_output(*arguments: str) -> subprocess.CompletedProcess[str]: + """Run one read-only Git evidence query from the repository root.""" + return subprocess.run( + ["git", *arguments], + cwd=REPOSITORY_ROOT, + check=False, + capture_output=True, + text=True, + ) + + +def test_dependency_review_receipts_bind_published_two_parent_merge() -> None: + """Both owner receipts must name the same reachable two-parent merge.""" + changelog = CHANGELOG_PATH.read_text(encoding="utf-8") + baseline = GAP_BASELINE_PATH.read_text(encoding="utf-8") + changelog_evidence = _dependency_review_evidence( + changelog, "Dependency Review requires a completed authenticated comparison" + ) + baseline_evidence = _dependency_review_evidence( + baseline, "CONTROL-DEPENDENCY-REVIEW-TRANSPORT-EXIT-01" + ) + + changelog_sha = _published_merge_sha(changelog_evidence) + baseline_sha = _published_merge_sha(baseline_evidence) + assert changelog_sha == baseline_sha + + resolvable = _git_output("cat-file", "-e", f"{changelog_sha}^{{commit}}") + assert resolvable.returncode == 0, ( + f"#2565 evidence {changelog_sha} is not a published commit" + ) + + ancestor = _git_output("merge-base", "--is-ancestor", changelog_sha, "HEAD") + assert ancestor.returncode == 0, ( + f"#2565 evidence {changelog_sha} is not in current HEAD ancestry" + ) + + parents = _git_output("show", "-s", "--format=%P", changelog_sha) + assert parents.returncode == 0 + assert parents.stdout.strip().split() == [ + PUBLISHED_RUNNER_PARENT, + PUBLISHED_SECURITY_PARENT, + ] + + +def test_dependency_review_receipts_bind_current_exact_tree_counts() -> None: + """Both owner receipts must retain the exact merged-tree test counts.""" + changelog_evidence = _dependency_review_evidence( + CHANGELOG_PATH.read_text(encoding="utf-8"), + "Dependency Review requires a completed authenticated comparison", + ) + baseline_evidence = _dependency_review_evidence( + GAP_BASELINE_PATH.read_text(encoding="utf-8"), + "CONTROL-DEPENDENCY-REVIEW-TRANSPORT-EXIT-01", + ) + + for evidence_text in (changelog_evidence, baseline_evidence): + assert "53/53" in evidence_text + assert "5,437 passed, 10 skipped, 40 subtests" in evidence_text diff --git a/tests/test_dependency_review_reusable_workflow_contract.py b/tests/test_dependency_review_reusable_workflow_contract.py index cadefcb8ac..93ac57f200 100644 --- a/tests/test_dependency_review_reusable_workflow_contract.py +++ b/tests/test_dependency_review_reusable_workflow_contract.py @@ -1,6 +1,6 @@ """Contract for the reusable Dependency Review workflow. -Replaces argos's, mightyETL's, newsdom-api's, and scopeweave's +Replaces Argos's, mightyETL's, naruon's, newsdom-api's, and scopeweave's independently hand-written ``dependency-review.yml`` files with one reusable ``workflow_call`` workflow, ``.github/workflows/dependency-review.yml``, plus a thin caller left in each product repository. See @@ -11,6 +11,9 @@ from __future__ import annotations +import os +import subprocess +import textwrap from pathlib import Path _WORKFLOW = Path(".github/workflows/dependency-review.yml") @@ -24,8 +27,75 @@ def _workflow_text() -> str: return _WORKFLOW.read_text(encoding="utf-8") +def _availability_probe_script() -> str: + """Extract the dependency-graph preflight shell body for executable tests.""" + workflow = _workflow_text() + step = " - name: Check dependency graph availability\n" + start = workflow.index(step) + end = workflow.index("\n - name:", start + len(step)) + block = workflow[start:end] + run_marker = " run: |\n" + run_start = block.index(run_marker) + len(run_marker) + script = textwrap.dedent(block[run_start:]) + return script.replace('${{ github.event_name }}', "pull_request") + + +def _run_availability_probe( + tmp_path: Path, + repository: str, + *, + base_sha: str = "a" * 40, + head_sha: str = "b" * 40, + http_status: str = "200", + curl_exit: int = 0, +) -> tuple[subprocess.CompletedProcess[str], Path, Path]: + """Execute the real preflight shell against a marker-only fake curl.""" + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + curl_marker = tmp_path / "curl-called" + fake_curl = fake_bin / "curl" + fake_curl.write_text( + "#!/usr/bin/env bash\n" + "set -euo pipefail\n" + "authorized=false\n" + "for arg in \"$@\"; do\n" + " if [[ \"$arg\" == Authorization:* ]]; then authorized=true; fi\n" + "done\n" + "printf '%s\\n' \"$authorized\" >>\"${CURL_MARKER}\"\n" + "printf '%s' \"${HTTP_STATUS:-200}\"\n" + "exit \"${CURL_EXIT:-0}\"\n", + encoding="utf-8", + ) + fake_curl.chmod(0o755) + output = tmp_path / "github-output" + env = os.environ.copy() + env.update( + { + "PATH": f"{fake_bin}:{env.get('PATH', '')}", + "GH_TOKEN": "test-token", + "BASE_SHA": base_sha, + "HEAD_SHA": head_sha, + "REPOSITORY": repository, + "GITHUB_API_URL": "https://api.github.invalid", + "GITHUB_OUTPUT": str(output), + "CURL_MARKER": str(curl_marker), + "HTTP_STATUS": http_status, + "CURL_EXIT": str(curl_exit), + } + ) + result = subprocess.run( + ["bash", "-c", _availability_probe_script()], + cwd=Path.cwd(), + env=env, + text=True, + capture_output=True, + check=False, + ) + return result, curl_marker, output + + def test_declares_workflow_call_with_four_inputs_and_recorded_defaults() -> None: - """Every genuinely-varying field found while auditing the five originals is an input.""" + """Every genuinely varying field found while auditing the five originals is an input.""" workflow = _workflow_text() assert "on:\n workflow_call:\n inputs:" in workflow for name in ( @@ -42,33 +112,35 @@ def test_declares_workflow_call_with_four_inputs_and_recorded_defaults() -> None assert 'default: "on-failure"' in workflow -def test_step_order_is_harden_then_checkout_then_preflight_then_gated_steps() -> None: - """harden-runner -> checkout -> dependency-graph preflight -> conditional gate/note.""" +def test_step_order_is_harden_then_checkout_then_preflight_then_dependency_review() -> None: + """Runner hardening, checkout, capability proof, then the gated action stay ordered.""" workflow = _workflow_text() order = [ "Harden the runner", "actions/checkout@", "Check dependency graph availability", "Dependency review", - "Dependency graph unavailable note", ] positions = [workflow.index(marker) for marker in order] assert positions == sorted(positions), "steps are out of order" -def test_dependency_review_and_note_steps_are_mutually_exclusive_on_availability() -> None: - """The gate and the fallback note must never both run.""" +def test_dependency_review_runs_only_after_a_confirmed_successful_comparison() -> None: + """The action must execute only after the compare endpoint returned HTTP 200.""" workflow = _workflow_text() assert ( "if: steps.dependency_graph.outputs.available == 'true'\n" " continue-on-error: ${{ inputs.continue_on_error }}" in workflow ) - assert "if: steps.dependency_graph.outputs.available != 'true'" in workflow + assert ( + 'if [ "$curl_exit" -eq 0 ] && [ "$status" = "200" ]; then' in workflow + ) + assert 'echo "available=true" >>"$GITHUB_OUTPUT"' in workflow def test_inputs_are_forwarded_to_the_dependency_review_action() -> None: - """fail_on_severity, allow_ghsas, and comment_summary_in_pr must reach the action untouched.""" + """Every caller-varying action input must reach the pinned action untouched.""" workflow = _workflow_text() assert "fail-on-severity: ${{ inputs.fail_on_severity }}" in workflow assert "allow-ghsas: ${{ inputs.allow_ghsas }}" in workflow @@ -76,67 +148,142 @@ def test_inputs_are_forwarded_to_the_dependency_review_action() -> None: def test_harden_runner_audits_egress() -> None: - """naruon's harden-runner step applies uniformly, not only to that one caller.""" + """naruon's harden-runner control applies uniformly in the reusable owner.""" workflow = _workflow_text() assert "step-security/harden-runner@" in workflow assert "egress-policy: audit" in workflow def test_action_pins_are_current_and_uniform() -> None: - """checkout and dependency-review-action share one current pin, not per-caller drift.""" + """Checkout and Dependency Review use one immutable current pin.""" workflow = _workflow_text() assert f"actions/checkout@{_CHECKOUT_PIN}" in workflow - assert ( - f"actions/dependency-review-action@{_DEPENDENCY_REVIEW_PIN}" in workflow - ) + assert f"actions/dependency-review-action@{_DEPENDENCY_REVIEW_PIN}" in workflow def test_uniform_fields_are_hardcoded_not_parameterized() -> None: - """Fields byte-identical across all four originals stay static, not inputs.""" + """Uniform least-privilege and checkout controls stay static.""" workflow = _workflow_text() assert "permissions:\n contents: read\n pull-requests: read" in workflow assert "persist-credentials: false" in workflow +def test_example_caller_preserves_required_permission_envelope() -> None: + """Thin callers must explicitly pass the reusable job's read permission ceiling.""" + workflow = _workflow_text() + assert ( + "# permissions:\n" + "# contents: read\n" + "# pull-requests: read\n" + "# concurrency:" + in workflow + ) + + +def test_example_caller_requires_immutable_protected_main_pin() -> None: + """The canonical example must never teach consumers to execute a mutable owner ref.""" + workflow = _workflow_text() + assert "@" in workflow + assert "uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@main" not in workflow + + def test_forces_node24_runtime_for_js_actions() -> None: - """newsdom-api's Node24 opt-in applies uniformly, not only to that one caller.""" + """newsdom-api's Node24 opt-in applies uniformly, not only to one caller.""" workflow = _workflow_text() assert "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true" in workflow def test_availability_check_uses_the_dependency_graph_compare_api() -> None: - """The preflight must query the real capability, not infer from repository visibility.""" + """The preflight must query the real capability, not infer from visibility.""" workflow = _workflow_text() assert "dependency-graph/compare" in workflow assert "github.event.repository.private" not in workflow + assert '-H "Authorization: Bearer ${GH_TOKEN}"' in workflow -def test_availability_check_distinguishes_unavailable_from_genuine_failure() -> None: - """403/404 means 'unavailable, skip gracefully'; any other status must hard-fail - the job instead of silently treating a real error the same as unavailability.""" +def test_pull_request_http_403_and_404_are_not_normalized_to_unavailable() -> None: + """Authorization-shaped HTTP responses are ambiguous and must remain blocking.""" workflow = _workflow_text() - assert 'if [ "$status" = "403" ] || [ "$status" = "404" ]' in workflow - assert "available=false" in workflow - assert "::error::Dependency graph availability check failed with HTTP" in workflow + assert 'if [ "$status" = "403" ] || [ "$status" = "404" ]' not in workflow + assert "skipping the dependency-review hard gate" not in workflow + assert "Dependency graph unavailable note" not in workflow + assert "::error::Dependency graph comparison failed with curl exit" in workflow assert "exit 1" in workflow def test_availability_check_only_runs_the_gate_for_pull_request_events() -> None: - """A non-pull_request trigger (e.g. workflow_dispatch) must skip the gate, not error, - since base/head SHAs only exist on a pull_request event.""" + """A non-pull_request trigger may skip because it has no PR base/head identity.""" workflow = _workflow_text() assert '"${{ github.event_name }}" != "pull_request"' in workflow +def test_preflight_rejects_named_revisions_before_transport(tmp_path: Path) -> None: + """Named or malformed revisions never reach the dependency-graph endpoint.""" + for index, (base_sha, head_sha) in enumerate( + (("main", "b" * 40), ("a" * 40, "develop"), ("a" * 39, "b" * 40)) + ): + case_dir = tmp_path / f"revision-{index}" + case_dir.mkdir() + result, curl_marker, _output = _run_availability_probe( + case_dir, + "ContextualWisdomLab/Orgmetra", + base_sha=base_sha, + head_sha=head_sha, + ) + assert result.returncode != 0, (base_sha, head_sha) + assert not curl_marker.exists(), (base_sha, head_sha) + assert "exact 40- or 64-character hexadecimal" in result.stdout.lower() + + +def test_preflight_rejects_malformed_repository_before_transport(tmp_path: Path) -> None: + """Only one non-dot owner/name repository identity may reach transport.""" + repositories = ( + "ContextualWisdomLab", + "ContextualWisdomLab/Orgmetra/extra", + "/Orgmetra", + "../.github", + "ContextualWisdomLab/..", + "ContextualWisdomLab/.", + "./.github", + ) + for index, repository in enumerate(repositories): + case_dir = tmp_path / f"repository-{index}" + case_dir.mkdir() + result, curl_marker, _output = _run_availability_probe(case_dir, repository) + assert result.returncode != 0, repository + assert not curl_marker.exists(), repository + assert "repository identity" in result.stdout.lower(), repository + + +def test_preflight_accepts_dotgithub_and_uses_job_token(tmp_path: Path) -> None: + """The legitimate .github repository reaches exactly one authenticated compare.""" + result, curl_marker, output = _run_availability_probe( + tmp_path, "ContextualWisdomLab/.github" + ) + assert result.returncode == 0, result.stdout + result.stderr + assert curl_marker.read_text(encoding="utf-8") == "true\n" + assert output.read_text(encoding="utf-8") == "available=true\n" + + +def test_preflight_rejects_http_200_when_curl_transport_fails(tmp_path: Path) -> None: + """Partial transport failure cannot authorize an incomplete HTTP 200 compare.""" + result, curl_marker, output = _run_availability_probe( + tmp_path, + "ContextualWisdomLab/.github", + http_status="200", + curl_exit=18, + ) + + assert result.returncode != 0, result.stdout + result.stderr + assert curl_marker.read_text(encoding="utf-8") == "true\n" + assert not output.exists() or "available=true" not in output.read_text( + encoding="utf-8" + ) + + def test_dependency_review_comment_summary_defaults_to_on_failure() -> None: - """scopeweave's PR-comment-on-failure UX applies uniformly by default, overridable per caller. - - naruon explicitly overrides it to "never" -- see - test_declares_workflow_call_with_four_inputs_and_recorded_defaults for - the default assertion and test_inputs_are_forwarded_to_the_dependency_review_action - for the forwarding assertion; this test just pins the specific default - value chosen (scopeweave's original, not naruon's or some other value). - """ + """The shared UX defaults to on-failure while remaining caller-overridable.""" workflow = _workflow_text() - assert 'comment_summary_in_pr:\n' in workflow + assert "comment_summary_in_pr:" in workflow assert 'default: "on-failure"' in workflow + assert "comment-summary-in-pr: ${{ inputs.comment_summary_in_pr }}" in workflow diff --git a/tests/test_docs_only_pr_runner_admission.py b/tests/test_docs_only_pr_runner_admission.py index a0f691e2a2..f5ece19be5 100644 --- a/tests/test_docs_only_pr_runner_admission.py +++ b/tests/test_docs_only_pr_runner_admission.py @@ -18,7 +18,7 @@ See also `tests/test_required_security_runner_image_contract.py` and `tests/test_required_review_runner_image_contract.py`, which pin the -`runs-on: ubuntu-24.04` counts these gate jobs add. +`CWL CI isolated` group and isolation-label counts these gate jobs add. """ from __future__ import annotations @@ -93,7 +93,8 @@ 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:", "runs-on:")) + line for line in block.splitlines() + if not line.strip().startswith(("if:", "runs-on:", "group:", "labels:")) ) normalized_blocks.add(normalized) assert len(normalized_blocks) == 1, ( @@ -135,12 +136,13 @@ def test_gate_jobs_use_supported_runner_allocation(): if filename in ("opencode-review.yml", "strix.yml"): assert '"group":"CWL central control"' 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 + assert 'fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')' in block, filename elif filename == "noema-review.yml": assert "endsWith(github.workflow_ref, '@refs/heads/main')" in block, filename - assert "fromJSON('[\"ubuntu-24.04\"]')" in block, filename + assert 'fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')' in block, filename else: - assert "runs-on: ubuntu-24.04" in block, filename + assert "group: CWL CI isolated" in block, filename + assert "labels: [self-hosted, linux, x64, cwlab-ci-isolated]" in block, filename assert "runs-on: ubuntu-latest" not in block, filename @@ -250,7 +252,8 @@ def test_sast_semgrep_folds_the_gate_into_its_single_consumer_at_step_level(): assert "changed-scope:" not in workflow assert "needs: changed-scope" not in workflow assert "needs.changed-scope" not in workflow - assert workflow.count("runs-on: ubuntu-24.04") == 1 + assert workflow.count("group: CWL CI isolated") == 1 + assert workflow.count("labels: [self-hosted, linux, x64, cwlab-ci-isolated]") == 1 semgrep = _top_level_job_block(workflow, "semgrep") assert not re.search(r"(?m)^ needs:", semgrep) diff --git a/tests/test_exact_artifact_sbom_attestation_contract.py b/tests/test_exact_artifact_sbom_attestation_contract.py index 0f9613a682..e745cd6dd4 100644 --- a/tests/test_exact_artifact_sbom_attestation_contract.py +++ b/tests/test_exact_artifact_sbom_attestation_contract.py @@ -245,10 +245,11 @@ def test_workflow_attests_each_exact_distribution_and_exports_offline_evidence() def test_quality_workflow_pins_supported_runner_images() -> None: - """Keep exact supply-chain evidence on an explicit runner image.""" + """Keep exact supply-chain evidence on isolated self-hosted Linux.""" workflow = _required_text(QUALITY_WORKFLOW, "attestation quality workflow") assert "ubuntu-latest" not in workflow - assert workflow.count("runs-on: ubuntu-24.04") == 1 + assert workflow.count("group: CWL CI isolated") == 1 + assert workflow.count("labels: [self-hosted, linux, x64, cwlab-ci-isolated]") == 1 def test_doctoring_records_claim_boundary_recovery_and_primary_sources() -> None: diff --git a/tests/test_kcsap_noema_app_identity.py b/tests/test_kcsap_noema_app_identity.py new file mode 100644 index 0000000000..18537ee4d5 --- /dev/null +++ b/tests/test_kcsap_noema_app_identity.py @@ -0,0 +1,111 @@ +"""중앙 Noema의 k-csap native App 경로를 실제 shell로 검증한다.""" + +import os +import re +import shutil +import subprocess +import tempfile +import textwrap +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github/workflows/noema-review.yml" +TARGET = "ContextualWisdomLab/k-csap-skills" +METADATA = "Select native Noema credential for metadata reads" +REVIEWER = "Select fail-closed Noema reviewer credential" + + +def job_text(job): + source = WORKFLOW.read_text() + return re.split(r"\n {2}(?=\S)", source.split(f"\n {job}:\n", 1)[1], maxsplit=1)[0] + + +def step_text(job, name): + body = job_text(job).split(f" - name: {name}\n", 1)[1] + return body.split("\n - name:", 1)[0] + + +def run_selection(job, name, repository=TARGET, client="fixture-client", key="fixture-key", pat="fixture-pat", oidc="https://fixture.invalid/exchange"): + script = textwrap.dedent(step_text(job, name).split(" run: |\n", 1)[1]) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "output" + result = subprocess.run( + [shutil.which("bash") or "/bin/bash"], input=script, + text=True, capture_output=True, timeout=10, check=False, + env={"PATH": os.defpath, "GITHUB_OUTPUT": str(output), + "TARGET_REPOSITORY": repository, "PR_NUMBER": "269", + "EXPECTED_HEAD_SHA": "a" * 40, + "METADATA_TOKEN": pat, "NOEMA_REVIEW_TOKEN": pat, + "NOEMA_GITHUB_APP_CLIENT_ID": client, + "NOEMA_GITHUB_APP_PRIVATE_KEY": key, "TOKEN_EXCHANGE_URL": oidc}, + ) + values = dict(line.split("=", 1) for line in output.read_text().splitlines()) if output.exists() else {} + # 비밀값 대신 합성 placeholder만 사용하며 선택 단계의 누출도 거부한다. + for value in (client, key, pat, oidc): + if value: + assert value not in result.stdout + result.stderr + return result, values + + +class KcsapNoemaAppIdentity(unittest.TestCase): + def test_metadata_requires_app_even_when_pat_and_oidc_exist(self): + for job in ("admit-current-head", "noema-review"): + for client, key in (("fixture-client", "fixture-key"), ("", "fixture-key"), ("fixture-client", ""), ("", "")): + with self.subTest(job=job, client_present=bool(client), key_present=bool(key)): + result, values = run_selection(job, METADATA, client=client, key=key) + if client and key: + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(values.get("source"), "github-app") + self.assertEqual(values.get("repository"), "k-csap-skills") + else: + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("source", values) + self.assertIn("::error::", result.stdout) + + def test_kcsap_token_wiring_cannot_reenter_legacy_fallback(self): + metadata_legacy = "secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token" + reviewer_legacy = "secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token" + publication_legacy = "steps.noema_credential.outputs.source == 'pat' && secrets.NOEMA_REVIEW_TOKEN || steps.noema_credential.outputs.source == 'github-app' && steps.noema_github_app_publication_token.outputs.token || steps.noema_credential.outputs.source == 'oidc' && steps.noema_oidc_token.outputs.token || ''" + cases = [ + ("admit-current-head", "Admit only the exact live Noema head", "noema_metadata_app_token", metadata_legacy), + ("noema-review", "Reject a stale trigger before credential or model setup", "noema_metadata_app_token", metadata_legacy), + ("noema-review", "Validate current pull request head", "noema_github_app_token", reviewer_legacy), + ("noema-review", "Resolve Noema target repository visibility", "noema_github_app_token", reviewer_legacy), + ("noema-review", "Check live pull request draft state before sidecar provisioning", "noema_github_app_token", reviewer_legacy), + ("noema-review", "Prepare Noema model verdict", "noema_github_app_token", reviewer_legacy), + ("noema-review", "Publish prepared Noema verdict on the exact live head", "noema_github_app_publication_token", publication_legacy), + ] + for job, name, app_step, legacy in cases: + with self.subTest(step=name): + expression = re.search(r"GH_TOKEN: \$\{\{ (.*?) \}\}", step_text(job, name)).group(1) + expected = f"env.TARGET_REPOSITORY == '{TARGET}' && steps.{app_step}.outputs.token || (env.TARGET_REPOSITORY != '{TARGET}' && ({legacy})) || ''" + self.assertEqual(expression, expected) + + def test_other_repositories_preserve_legacy_selection(self): + for repository in ("ContextualWisdomLab/example", TARGET + "-other"): + for job in ("admit-current-head", "noema-review"): + for client, key, pat, expected in (("fixture-client", "fixture-key", "fixture-pat", "pat"), ("fixture-client", "fixture-key", "", "github-app"), ("", "", "", "workflow")): + with self.subTest(repository=repository, job=job, source=expected): + result, values = run_selection(job, METADATA, repository=repository, client=client, key=key, pat=pat) + self.assertEqual(result.returncode, 0) + self.assertEqual(values.get("source"), expected) + for client, key, pat, oidc, expected in (("fixture-client", "fixture-key", "fixture-pat", "", "pat"), ("fixture-client", "fixture-key", "", "", "github-app"), ("", "", "", "https://fixture.invalid/exchange", "oidc")): + with self.subTest(repository=repository, source=expected): + result, values = run_selection("noema-review", REVIEWER, repository=repository, client=client, key=key, pat=pat, oidc=oidc) + self.assertEqual(result.returncode, 0) + self.assertEqual(values.get("source"), expected) + + def test_reviewer_requires_native_app_without_pat_or_oidc_fallback(self): + for client, key in (("fixture-client", "fixture-key"), ("", "fixture-key"), ("fixture-client", ""), ("", "")): + for pat, oidc in (("fixture-pat", "https://fixture.invalid/exchange"), ("", "https://fixture.invalid/exchange"), ("", "")): + with self.subTest(client_present=bool(client), key_present=bool(key), pat_present=bool(pat), oidc_present=bool(oidc)): + result, values = run_selection("noema-review", REVIEWER, client=client, key=key, pat=pat, oidc=oidc) + if client and key: + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(values.get("source"), "github-app") + self.assertEqual(values.get("repository"), "k-csap-skills") + else: + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("source", values) + self.assertIn("::error::", result.stdout) diff --git a/tests/test_noema_author_distinct.py b/tests/test_noema_author_distinct.py new file mode 100644 index 0000000000..58185986fa --- /dev/null +++ b/tests/test_noema_author_distinct.py @@ -0,0 +1,57 @@ +"""Reject author-owned credentials in the legacy direct Noema entry point.""" +from __future__ import annotations + +import pytest + +from scripts.ci import noema_review_gate as gate + +HEAD = "a" * 40 + + +def _bind(monkeypatch, author): + """Replace network/model boundaries with labelled synthetic witnesses.""" + pr = {"state": "OPEN", "headRefOid": HEAD, "baseRefOid": "b" * 40, + "isDraft": False, "author": {"login": author} if author else None} + monkeypatch.setattr(gate, "fetch_pr", lambda *_: pr) + monkeypatch.setattr(gate, "current_actor", lambda: "cwl-noema-review[bot]") + monkeypatch.setattr(gate, "existing_noema_review", lambda *_: False) + monkeypatch.setattr(gate, "fetch_diff", lambda *_: ("diff", False)) + monkeypatch.setattr(gate, "fetch_changed_files", lambda *_: [("src/a.py", "MODIFIED")]) + monkeypatch.setattr(gate, "build_review_context", lambda *_: "context") + return pr + + +@pytest.mark.parametrize("author", (None, "cwl-noema-review[bot]", "CWL-Noema-Review[bot]")) +def test_direct_entry_rejects_unknown_or_self_author(monkeypatch, author): + """Neither unknown author nor case variation may permit self-review.""" + _bind(monkeypatch, author) + monkeypatch.setattr(gate, "call_llm", lambda *_: pytest.fail("model reached before author admission")) + with pytest.raises(RuntimeError, match="author"): + gate.inspect_and_review("ContextualWisdomLab/example", 7, HEAD) + + +def test_direct_entry_rechecks_author_before_publication(monkeypatch): + """The live author must be revalidated after model work.""" + pr = _bind(monkeypatch, "seonghobae") + def model(*_): + pr["author"] = {"login": "cwl-noema-review[bot]"} + return {"decision": "approve"} + monkeypatch.setattr(gate, "call_llm", model) + monkeypatch.setattr( + gate, + "submit_review", + lambda *_: pytest.fail("author-owned review was published"), + ) + with pytest.raises(RuntimeError, match="author"): + gate.inspect_and_review("ContextualWisdomLab/example", 7, HEAD) + + +def test_direct_entry_permits_independent_actor(monkeypatch): + """A distinct App can publish its own substantive verdict.""" + _bind(monkeypatch, "seonghobae") + monkeypatch.setattr(gate, "call_llm", lambda *_: {"decision": "approve"}) + submitted = [] + monkeypatch.setattr(gate, "submit_review", lambda *args: submitted.append(args)) + assert gate.inspect_and_review("ContextualWisdomLab/example", 7, HEAD) == 0 + assert len(submitted) == 1 + assert submitted[0][3] == "cwl-noema-review[bot]" diff --git a/tests/test_noema_bootstrap_final_missing.py b/tests/test_noema_bootstrap_final_missing.py new file mode 100644 index 0000000000..f191c9655d --- /dev/null +++ b/tests/test_noema_bootstrap_final_missing.py @@ -0,0 +1,77 @@ +"""New exact remaining branch controls; trusted fault seams are explicit.""" +import hashlib +import json +import runpy +import sys +from pathlib import Path +from types import SimpleNamespace +import pytest +from scripts.ci import noema_bootstrap_producer as producer +from scripts.ci import premerge_review_tooling as tooling +from tests.test_noema_issuer_producer_controls import bootstrap_case +from tests.test_noema_bootstrap_missing_quality import cli_inputs + + +def test_source_root_symlink_refuses_before_copy(tmp_path): + """Reach materializer alias denial with valid independent fixture pins.""" + parent,source,entry,raw,admission=bootstrap_case(tmp_path) + alias=tmp_path/'alias';alias.symlink_to(source) + with pytest.raises(ValueError,match='namespace'): + producer.materialize_tooling(alias,parent/'tooling',raw,accepted_digest=hashlib.sha256(raw).hexdigest(),admission=admission,private_parent=parent) + + +def test_source_changes_after_verified_load_refuse_copy(tmp_path,monkeypatch): + """Change actual source bytes in the trusted loader wait before delivery.""" + parent,source,entry,raw,admission=bootstrap_case(tmp_path) + original=tooling.load_noema_tooling + def load(*args,**kwargs): + """Return the real admitted loader, then mutate the fixture supplier.""" + result=original(*args,**kwargs);entry.write_bytes(b'CHANGED');return result + monkeypatch.setattr(tooling,'load_noema_tooling',load) + with pytest.raises(ValueError,match='changed during delivery'): + producer.materialize_tooling(source,parent/'tooling',raw,accepted_digest=hashlib.sha256(raw).hexdigest(),admission=admission,private_parent=parent) + + +def test_missing_inference_authority_refuses_composition(tmp_path): + """Actual missing-key branch refuses before any issuer or child callback.""" + parent,source,entry,raw,admission=bootstrap_case(tmp_path);admission['scope']={} + with pytest.raises(ValueError,match='inference authority'): + producer.run_bootstrap_review(source,raw,accepted_digest=hashlib.sha256(raw).hexdigest(),admission=admission,private_parent=parent,fetch_live=lambda:None,request=lambda *a:None,revalidate=lambda:None,inference_key=None) + + +def test_digest_provider_fault_is_denied_before_issuer(tmp_path,monkeypatch): + """Explicit trusted digest fault tests the otherwise redundant immutable pin guard.""" + parent,source,entry,raw,admission=bootstrap_case(tmp_path);admission['scope']={} + def revalidate(): + """Inject only digest-provider failure after real initial source admission.""" + monkeypatch.setattr(hashlib,'sha256',lambda *a:SimpleNamespace(hexdigest=lambda:'0'*64)) + with pytest.raises(ValueError,match='source pin changed'): + producer.run_bootstrap_review(source,raw,accepted_digest=hashlib.sha256(raw).hexdigest(),admission=admission,private_parent=parent,fetch_live=lambda:None,request=lambda *a:None,revalidate=revalidate,inference_key='FIXTURE_INFERENCE') + + +@pytest.mark.parametrize('mutation',['pre_fence_file','post_read_memory']) +def test_remaining_cli_wait_fences(tmp_path,monkeypatch,mutation): + """Reach first file drift and defensive post-read memory revocation gates.""" + args,parent,mf,af,pr=cli_inputs(tmp_path);args+=['--app-jwt-file',str(parent/'jwt')] + original=producer._response;held=[] + def response(raw): + """Parse actual bytes, then hold the returned trusted admission object.""" + value=original(raw);held.append(value) + if mutation=='pre_fence_file':mf.write_bytes(b'{}') + return value + def live(*a,**kw): + """Return otherwise valid live scope; revoke only in-memory admission.""" + if mutation=='post_read_memory':held[0]['native_custody']=False + return SimpleNamespace(stdout=json.dumps(pr)) + monkeypatch.setattr(producer,'_response',response);monkeypatch.setattr(producer.subprocess,'run',live) + assert producer.main(args)==1 + + +def test_actual_isolated_entry_missing_pins_refuses_without_network(tmp_path,monkeypatch): + """Exercise root bootstrap and __main__ in-process for coverage transport.""" + helper=Path(producer.__file__);root=str(helper.resolve().parents[2]) + monkeypatch.setattr(sys,'path',[x for x in sys.path if x!=root]) + args=['--materialize','--private-parent',str(tmp_path),'--source-root',str(tmp_path/'source'),'--manifest',str(tmp_path/'absent'),'--accepted-digest','','--admission',str(tmp_path/'absent2'),'--admission-digest','','--repo','ContextualWisdomLab/example','--pr-number','7','--expected-head','a'*40] + monkeypatch.setattr(sys,'argv',[str(helper),*args]) + with pytest.raises(SystemExit) as error:runpy.run_path(str(helper),run_name='__main__') + assert error.value.code==1 diff --git a/tests/test_noema_bootstrap_missing_quality.py b/tests/test_noema_bootstrap_missing_quality.py new file mode 100644 index 0000000000..110da0225e --- /dev/null +++ b/tests/test_noema_bootstrap_missing_quality.py @@ -0,0 +1,124 @@ +"""New missing bootstrap boundary controls from retained exact55d7 coverage.""" +import copy +import json +import os +from pathlib import Path +from types import SimpleNamespace + +import pytest +from scripts.ci import noema_bootstrap_producer as producer +from tests.test_noema_issuer_producer_controls import bootstrap_case, issuer_case + + +@pytest.mark.parametrize('raw', [b'', b'\xff', b'{', b'[]']) +def test_missing_response_shape_controls(raw): + """Reject absent, invalid UTF8, malformed and nonobject original responses.""" + with pytest.raises(ValueError): + producer._response(raw) + + +@pytest.mark.parametrize('mutation', ['phase', 'identity', 'repository', 'actor', 'symlink_parent', 'parent_mode', 'custody_drift', 'fsync_error']) +def test_missing_issuer_refusals_preserve_output_boundary(tmp_path, monkeypatch, mutation): + """Reach distinct missing producer denials using only fixture authority.""" + parent,scope,app,installation,response,calls,request=issuer_case(tmp_path) + phase='prepare';app_id=11 + if mutation=='phase':phase='other' + elif mutation=='identity':app_id=0 + elif mutation=='repository':scope['repository']='Other/example' + elif mutation=='actor':scope['reviewer']='other[bot]' + elif mutation=='symlink_parent': + alias=tmp_path/'alias';alias.symlink_to(parent);parent=alias + elif mutation=='parent_mode':parent.chmod(0o755) + def fence(): + """Change private mode only after actual issuance to reach custody check.""" + if mutation=='custody_drift' and len(calls)==3:parent.chmod(0o750) + if mutation=='fsync_error': + def fail(fd): + """Inject final ordinary IO failure after actual private file writing.""" + raise OSError('FIXTURE_IO') + monkeypatch.setattr(producer.os,'fsync',fail) + with pytest.raises((ValueError,OSError)): + producer.produce_issuance(phase,scope=scope,installation_id=7,app_id=app_id, + output=parent/'prepare.json',private_parent=parent,request=request,revalidate=fence) + assert (parent/'prepare.json').exists()==(mutation=='fsync_error') + + +@pytest.mark.parametrize('mutation', ['relative', 'namespace', 'missing_evidence', 'dependencies']) +def test_missing_runtime_admission_controls(tmp_path, mutation): + """Exercise otherwise admitted runtime evidence with one invalid boundary.""" + parent,source,entry,raw,admission=bootstrap_case(tmp_path) + if mutation=='relative':parent=Path('relative') + elif mutation=='namespace':admission['namespace']='foreign' + elif mutation=='missing_evidence':admission['native_custody']=False + else:admission['dependencies']=None + with pytest.raises(ValueError):producer._validate_runtime(admission,parent) + + +@pytest.mark.parametrize('mutation', ['relative','hardlink','empty','whitespace']) +def test_missing_private_input_controls(tmp_path, mutation): + """Private transport never reads an unowned or unusable fixture credential.""" + parent=tmp_path/'private';parent.mkdir(mode=0o700) + jwt=parent/'jwt';jwt.write_bytes(b'FIXTURE_JWT');jwt.chmod(0o600) + if mutation=='relative':jwt=Path('relative') + elif mutation=='hardlink':os.link(jwt,parent/'alias') + elif mutation=='empty':jwt.write_bytes(b'') + else:jwt.write_bytes(b'FIXTURE WHITESPACE') + with pytest.raises((ValueError,OSError)):producer.authenticated_transport(jwt,parent,opener=lambda *a,**kw:None) + + +@pytest.mark.parametrize('mutation',['endpoint','payload','framing','redirect']) +def test_missing_transport_controls(tmp_path, mutation): + """Inspect fixed transport denials with no network or provider activity.""" + import urllib.request + parent=tmp_path/'private';parent.mkdir(mode=0o700) + jwt=parent/'jwt';jwt.write_bytes(b'FIXTURE_JWT');jwt.chmod(0o600) + class Response: + """Return a syntactically valid body with contrary declared length.""" + headers={'Content-Length':'100'} + def __enter__(self):return self + def __exit__(self,*args):return False + def read(self,size):return b'{}' + transport=producer.authenticated_transport(jwt,parent,opener=lambda *a,**kw:Response()) + if mutation=='redirect': + # Recover the actual generated handler class without making any request. + with pytest.MonkeyPatch.context() as mp: + held=[] + def build(*handlers): + """Retain actual default handlers at the trusted construction seam.""" + held.extend(handlers);return SimpleNamespace(open=lambda *a:None) + mp.setattr(urllib.request,'build_opener',build) + producer.authenticated_transport(jwt,parent) + assert held[1].redirect_request(None,None,302,'',{},'https://foreign.invalid') is None + else: + with pytest.raises(ValueError):transport('GET','/foreign' if mutation=='endpoint' else '/app',{} if mutation=='payload' else None) + + +def cli_inputs(tmp_path): + """Build independent fixture pins for only newly missing CLI conditions.""" + import hashlib + parent,source,entry,raw,admission=bootstrap_case(tmp_path) + scope={'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40,'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + admission.update(scope=scope,app_id=11,installation_id=7) + producer.materialize_tooling(source,parent/'tooling',raw,accepted_digest=hashlib.sha256(raw).hexdigest(),admission=admission,private_parent=parent) + mf,af=tmp_path/'manifest',tmp_path/'admission';mf.write_bytes(raw);af.write_text(json.dumps(admission)) + args=['--issue-prepare','--private-parent',str(parent),'--source-root',str(source),'--manifest',str(mf),'--accepted-digest',hashlib.sha256(raw).hexdigest(),'--admission',str(af),'--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(),'--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head']] + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}},'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + return args,parent,mf,af,pr + + +@pytest.mark.parametrize('mutation',['pin','admission_digest','target','no_jwt','verdict_namespace','pre_live_drift']) +def test_missing_cli_refusals(tmp_path,monkeypatch,mutation): + """CLI rejects new malformed pins/scope/custody before transport acquisition.""" + args,parent,mf,af,pr=cli_inputs(tmp_path) + if mutation=='pin':args[args.index('--accepted-digest')+1]='' + elif mutation=='admission_digest':args[args.index('--admission-digest')+1]='0'*64 + elif mutation=='target':args[args.index('--repo')+1]='ContextualWisdomLab/other' + elif mutation=='verdict_namespace':args[0]='--revalidate-publish';args+=['--verdict-file',str(parent/'foreign')] + elif mutation=='pre_live_drift': + args+=['--app-jwt-file',str(parent/'jwt')] + def pre_read(admission,path): + """Alter the pinned file after initial parsing, before the next fence.""" + mf.write_bytes(b'{}') + monkeypatch.setattr(producer,'_validate_runtime',pre_read) + monkeypatch.setattr(producer.subprocess,'run',lambda *a,**kw:SimpleNamespace(stdout=json.dumps(pr))) + assert producer.main(args)==1 diff --git a/tests/test_noema_child_environment_composition.py b/tests/test_noema_child_environment_composition.py new file mode 100644 index 0000000000..3fc0d21c4e --- /dev/null +++ b/tests/test_noema_child_environment_composition.py @@ -0,0 +1,145 @@ +"""Actual controller child composition with inert GitHub/model/HWP suppliers.""" +import hashlib +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest +from scripts.ci import premerge_review_tooling as caller + +ROOT = Path(__file__).resolve().parents[1] + + +def composition(tmp_path, monkeypatch, scenario, attempt="1"): + """Copy unchanged consumers and supply independently pinned synthetic context.""" + root = tmp_path / "tooling" + entry = root / ".github/actions/noema-review/two_phase.py" + entry.parent.mkdir(parents=True) + shutil.copyfile(ROOT / ".github/actions/noema-review/two_phase.py", entry) + for source in (ROOT / "scripts/ci").glob("*.py"): + target = root / "scripts/ci" / source.name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + shutil.copyfile(ROOT / "scripts/ci/noema_hwp_mcp_reader.mjs", root / "scripts/ci/noema_hwp_mcp_reader.mjs") + files = {p.relative_to(root).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest() + for p in root.rglob("*") if p.is_file()} + manifest = {"schema_version": 1, "repository": "ContextualWisdomLab/.github", + "commit": "c" * 40, "tree": "d" * 40, + "entrypoint": ".github/actions/noema-review/two_phase.py", "files": files} + scope = {"repository": "ContextualWisdomLab/example", "number": 7, + "head": "a" * 40, "base": "b" * 40, + "author": "writer", "reviewer": "cwl-noema-review[bot]"} + reader = tmp_path / "reader" + (reader / "dist/core").mkdir(parents=True) + (reader / "dist/tools").mkdir() + (reader / "node_modules/@rhwp/core").mkdir(parents=True) + (reader / "package.json").write_text(json.dumps({"name": "hwp-mcp", "version": "0.3.0", "type": "module"})) + (reader / "node_modules/@rhwp/core/package.json").write_text(json.dumps({"name": "@rhwp/core", "version": "0.7.7"})) + (reader / "dist/core/document.js").write_text("export async function openDocument(p) { return p; } export function closeDocument(d) {}\n") + (reader / "dist/tools/read.js").write_text("import {readFileSync} from 'node:fs'; export async function readHwp({file_path}) { return readFileSync(file_path, 'utf8'); }\n") + node = Path(shutil.which("node")).resolve() + assert node.name == "node" + output = tmp_path / "step-output" + output.touch(mode=0o600) + context = {"GITHUB_OUTPUT": str(output), "NOEMA_TRANSPORT_RETRY_ATTEMPT": attempt, + "NOEMA_HWP_MCP_SOURCE": str(reader), "NOEMA_NODE_EXECUTABLE": str(node), + "node_sha256": hashlib.sha256(node.read_bytes()).hexdigest(), + "hwp_files": {p.relative_to(reader).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest() + for p in reader.rglob("*") if p.is_file()}} + admission = {"scope": scope, "installation_id": 7, "prepare_transport": context, + **{k: True for k in ("isolated_clean_job", "native_custody", "supplier_cost", + "supplier_retention", "issuer_phase_separation")}} + paths = {k: tmp_path / k for k in ("manifest", "admission", "issuer", "verdict")} + paths["manifest"].write_text(json.dumps(manifest)) + paths["admission"].write_text(json.dumps(admission)) + paths["issuer"].write_text(json.dumps({"token": "SYNTHETIC_READ", "permissions": { + "contents": "read", "metadata": "read", "pull_requests": "read"}, + "repositories": [{"full_name": scope["repository"]}]})) + paths["issuer"].chmod(0o600) + pr = {"base": {"sha": scope["base"], "repo": {"full_name": scope["repository"]}}, + "head": {"sha": scope["head"]}, "number": 7, "user": {"login": "writer"}, + "state": "open", "draft": False} + real_run = subprocess.run + children = [] + driver = '''import runpy,sys,socket,os +from pathlib import Path +socket.socket.connect=lambda *a,**k: (_ for _ in ()).throw(AssertionError('external network forbidden')) +ns=runpy.run_path(sys.argv[1],run_name='fixture_two_phase') +g=ns['gate'] +g.fetch_pr=lambda *a: {'headRefOid':'a'*40,'baseRefOid':'b'*40,'state':'OPEN','isDraft':False} +g.require_expected_head=lambda *a: None +g.current_actor=lambda: 'cwl-noema-review[bot]' +g.require_distinct_pr_author=lambda *a: 'writer' +g.existing_noema_review=lambda *a: False +g.fetch_diff=lambda *a: ('fixture',False) +g.fetch_changed_files=lambda *a: [('fixture.hwpx','modified')] +def context(*a): + if sys.argv[2]=='hwp': + from scripts.ci.noema_review_document import extract_review_document + text=extract_review_document('fixture.hwpx',b'SYNTHETIC_DOCUMENT_TEXT') + assert text=='SYNTHETIC_DOCUMENT_TEXT' + return 'fixture' +g.build_review_context=context +def model(*a): + if sys.argv[2]=='retry': + raise g.NoemaTransportError('FIXTURE_CAPACITY_ONLY',capacity_unavailable=True,http_status=429,retry_after_seconds=60) + return {'decision':'approve','summary':'FIXTURE_ONLY'} +g.call_llm=model +try: sys.exit(ns['main'](sys.argv[3:])) +except g.NoemaTransportError: sys.exit(1) +''' + + def boundary(command, **kwargs): + """Stub metadata only; execute real child consumers without provider access.""" + if command[0] == "gh": + return subprocess.CompletedProcess(command, 0, stdout=json.dumps(pr)) + children.append(dict(kwargs["env"])) + actual = command[:3] + ["-c", driver, command[3], scenario] + command[4:] + result = real_run(actual, **kwargs, capture_output=True, text=True, timeout=30) + children[-1]["_fixture_stderr"] = result.stderr + children[-1]["_fixture_stdout"] = result.stdout + return result + + monkeypatch.setattr(caller.subprocess, "run", boundary) + # load_noema_tooling's definition-time runner remains untouched; route only + # its subprocess adapter to a real isolated child with inert external sinks. + real_load = caller.load_noema_tooling + monkeypatch.setattr(caller, "load_noema_tooling", + lambda *a, **kw: real_load(*a, **kw, child_runner=boundary)) + monkeypatch.setenv("LLM_GATEWAY_API_KEY", "SYNTHETIC_INFERENCE") + monkeypatch.setenv("GITHUB_OUTPUT", str(tmp_path / "ambient-output-not-accepted")) + monkeypatch.setenv("NOEMA_TRANSPORT_RETRY_ATTEMPT", "0") + monkeypatch.setenv("NODE_OPTIONS", "AMBIENT_LOADER_SENTINEL") + args = ["--prepare-production", "--tooling-root", str(root), + "--manifest", str(paths["manifest"]), "--accepted-digest", hashlib.sha256(paths["manifest"].read_bytes()).hexdigest(), + "--admission", str(paths["admission"]), "--admission-digest", hashlib.sha256(paths["admission"].read_bytes()).hexdigest(), + "--repo", scope["repository"], "--pr-number", "7", "--expected-head", scope["head"], + "--phase-issuance", str(paths["issuer"]), "--verdict-file", str(paths["verdict"])] + return args, paths, output, children, context + + +@pytest.mark.parametrize("attempt,eligible,next_attempt", [("1", "true", "2"), ("2", "false", None)]) +def test_actual_child_preserves_capacity_output_and_bounded_retry(tmp_path, monkeypatch, attempt, eligible, next_attempt): + """Real unchanged two_phase writes the accepted sink without resetting its bound.""" + args, paths, output, children, _ = composition(tmp_path, monkeypatch, "retry", attempt) + assert caller.production_main(args) == 1 + values = dict(line.split("=", 1) for line in output.read_text().splitlines()) + assert values.get("transport_capacity_unavailable") == "true", children[0]["_fixture_stderr"] + assert values["transport_retry_eligible"] == eligible + assert values.get("transport_retry_next_attempt") == next_attempt + assert children[0]["NOEMA_TRANSPORT_RETRY_ATTEMPT"] == attempt + assert "NODE_OPTIONS" not in children[0] + assert not paths["verdict"].exists() + + +def test_actual_child_hwp_consumer_receives_trusted_reader_and_node(tmp_path, monkeypatch): + """Real document consumer and JS wrapper execute with synthetic package bodies.""" + args, paths, _, children, context = composition(tmp_path, monkeypatch, "hwp") + assert caller.production_main(args) == 0, children[0]["_fixture_stderr"] + assert paths["verdict"].exists() + assert children[0]["NOEMA_HWP_MCP_SOURCE"] == context["NOEMA_HWP_MCP_SOURCE"] + assert children[0]["NOEMA_NODE_EXECUTABLE"] == context["NOEMA_NODE_EXECUTABLE"] + assert children[0]["PATH"] == "/usr/bin:/bin" + assert "NODE_OPTIONS" not in children[0] diff --git a/tests/test_noema_current_node_byte_drift.py b/tests/test_noema_current_node_byte_drift.py new file mode 100644 index 0000000000..607fa6fc1d --- /dev/null +++ b/tests/test_noema_current_node_byte_drift.py @@ -0,0 +1,15 @@ +"""New actual executable-byte replacement control, not a forged digest replay.""" +from pathlib import Path + +import pytest +from scripts.ci import premerge_review_tooling as caller +from tests.test_noema_current_quality_delta import context_fixture + + +def test_actual_node_bytes_change_after_pin_is_rejected(tmp_path): + """Keep the real original digest and mutate only independently pinned bytes.""" + context = context_fixture(tmp_path) + node = Path(context["NOEMA_NODE_EXECUTABLE"]) + node.write_bytes(b"distinct executable after independent pin") + with pytest.raises(ValueError, match="Node supplier bytes changed"): + caller.accepted_prepare_transport(context) diff --git a/tests/test_noema_current_quality_delta.py b/tests/test_noema_current_quality_delta.py new file mode 100644 index 0000000000..4c011a8569 --- /dev/null +++ b/tests/test_noema_current_quality_delta.py @@ -0,0 +1,115 @@ +"""New current-source coverage controls; no predecessor cohorts replayed.""" +import hashlib +import json +import os +import runpy +import sys +from pathlib import Path + +import pytest +from scripts.ci import premerge_review_tooling as caller +from scripts.ci import noema_review_document as document +from tests.test_noema_production_cli_coverage import setup_caller + + +def context_fixture(tmp_path): + """Create a minimal pinned regular Node and complete reader namespace.""" + output = tmp_path / "output" + output.touch(mode=0o600) + reader = tmp_path / "reader" + reader.mkdir() + (reader / "body").write_bytes(b"fixture") + node = tmp_path / "node" + node.write_bytes(b"fixture executable identity") + node.chmod(0o700) + return {"GITHUB_OUTPUT": str(output), "NOEMA_TRANSPORT_RETRY_ATTEMPT": "0", + "NOEMA_HWP_MCP_SOURCE": str(reader), "NOEMA_NODE_EXECUTABLE": str(node), + "node_sha256": hashlib.sha256(node.read_bytes()).hexdigest(), + "hwp_files": {"body": hashlib.sha256(b"fixture").hexdigest()}} + + +@pytest.mark.parametrize("case", ["none", "output_only", "document_only", "integer_path", "output_relative", "output_hardlink", "empty", "list", "incomplete", "relative_node", "root_symlink", "node_symlink", "nonexecutable", "bad_digest", "empty_inventory", "unsafe_name", "bad_inventory_digest", "supplier_symlink", "supplier_fifo", "root_file", "node_directory", "both", "internal_control"]) +def test_new_prepare_transport_shape_and_capability_cases(tmp_path, case): + """Exercise newly introduced helper branches with explicit native file kinds.""" + context = context_fixture(tmp_path) + node, reader = Path(context["NOEMA_NODE_EXECUTABLE"]), Path(context["NOEMA_HWP_MCP_SOURCE"]) + if case == "none": + assert caller.accepted_prepare_transport(None) == {} + return + if case == "output_only": + context = {k: context[k] for k in ("GITHUB_OUTPUT", "NOEMA_TRANSPORT_RETRY_ATTEMPT")} + elif case == "document_only": + context = {k: v for k, v in context.items() if k not in ("GITHUB_OUTPUT", "NOEMA_TRANSPORT_RETRY_ATTEMPT")} + elif case == "integer_path": context["GITHUB_OUTPUT"] = 3 + elif case == "output_relative": context["GITHUB_OUTPUT"] = "output" + elif case == "output_hardlink": os.link(context["GITHUB_OUTPUT"], tmp_path / "second") + elif case == "empty": context = {} + elif case == "list": context = [] + elif case == "incomplete": context.pop("node_sha256") + elif case == "relative_node": context["NOEMA_NODE_EXECUTABLE"] = "node" + elif case == "root_symlink": + link = tmp_path / "reader-link"; link.symlink_to(reader); context["NOEMA_HWP_MCP_SOURCE"] = str(link) + elif case == "node_symlink": + link = tmp_path / "node-link"; link.symlink_to(node); context["NOEMA_NODE_EXECUTABLE"] = str(link) + elif case == "nonexecutable": node.chmod(0o600) + elif case == "bad_digest": context["node_sha256"] = "bad" + elif case == "empty_inventory": context["hwp_files"] = {} + elif case == "unsafe_name": context["hwp_files"] = {"../escape": "a" * 64} + elif case == "bad_inventory_digest": context["hwp_files"] = {"body": 4} + elif case == "supplier_symlink": (reader / "link").symlink_to(reader / "body") + elif case == "supplier_fifo": os.mkfifo(reader / "fifo") + elif case == "root_file": context["NOEMA_HWP_MCP_SOURCE"] = str(node) + elif case == "node_directory": context["NOEMA_NODE_EXECUTABLE"] = str(reader) + elif case == "internal_control": context["GITHUB_OUTPUT"] = str(tmp_path / "a\x01b") + if case in ("both", "output_only", "document_only"): + result = caller.accepted_prepare_transport(context) + assert result == {k: v for k, v in context.items() if k not in ("hwp_files", "node_sha256")} + else: + with pytest.raises(ValueError): caller.accepted_prepare_transport(context) + + +@pytest.mark.parametrize("target", ["manifest", "unchanged"]) +def test_new_live_read_manifest_identity_and_success(tmp_path, monkeypatch, target): + """Revalidate manifest rather than admission after a live metadata wait.""" + args, paths, _, events = setup_caller(tmp_path, monkeypatch, "prepare") + original = caller.subprocess.run + + def metadata(*args, **kwargs): + """Complete metadata before independently changing the manifest bytes.""" + response = original(*args, **kwargs) + if target == "manifest": paths["manifest"].write_text("{}") + return response + + monkeypatch.setattr(caller.subprocess, "run", metadata) + assert caller.production_main(args) == (1 if target == "manifest" else 0) + assert events == ([] if target == "manifest" else ["prepare"]) + + +def test_new_run_mode_admits_output_transport(tmp_path, monkeypatch): + """The combined controller's issuance closure validates the new context too.""" + args, paths, _, events = setup_caller(tmp_path, monkeypatch, "run") + data = json.loads(paths["admission"].read_text()) + output = tmp_path / "accepted-output"; output.touch(mode=0o600) + data["prepare_transport"] = {"GITHUB_OUTPUT": str(output), "NOEMA_TRANSPORT_RETRY_ATTEMPT": "2"} + paths["admission"].write_text(json.dumps(data)) + args[args.index("--admission-digest") + 1] = hashlib.sha256(paths["admission"].read_bytes()).hexdigest() + assert caller.production_main(args) == 0 + assert events == ["prepare", "publish"] + + +def test_new_document_cli_explicit_executable_inprocess(tmp_path, monkeypatch): + """Execute actual document CLI and Node with only synthetic supplier functions.""" + import shutil + from tests.test_noema_child_environment_composition import composition + _, _, _, _, context = composition(tmp_path, monkeypatch, "hwp") + # Restore subprocess to the native adapter: composition installs a fixture + # controller seam, not a replacement for the actual Node document reader. + monkeypatch.undo() + monkeypatch.setenv("NOEMA_HWP_MCP_SOURCE", context["NOEMA_HWP_MCP_SOURCE"]) + monkeypatch.setenv("NOEMA_NODE_EXECUTABLE", context["NOEMA_NODE_EXECUTABLE"]) + monkeypatch.delenv("NODE_OPTIONS", raising=False) + path = tmp_path / "probe.hwpx"; path.write_bytes(b"NEW_DOCUMENT_EXECUTABLE_BOUNDARY") + monkeypatch.setattr(sys, "argv", [document.__file__, str(path)]) + with pytest.raises(SystemExit) as stopped: + runpy.run_path(document.__file__, run_name="__main__") + assert stopped.value.code == 0 diff --git a/tests/test_noema_current_quality_final_edges.py b/tests/test_noema_current_quality_final_edges.py new file mode 100644 index 0000000000..1719a50d87 --- /dev/null +++ b/tests/test_noema_current_quality_final_edges.py @@ -0,0 +1,48 @@ +"""New unmeasured current executable/error boundaries, not old cohort replay.""" +import subprocess +from pathlib import Path + +import pytest +from scripts.ci import noema_review_document as document +from scripts.ci import premerge_review_tooling as caller +from tests.test_noema_current_quality_delta import context_fixture + + +@pytest.mark.parametrize("case", ["missing_retry", "null_inventory", "extra_file"]) +def test_new_missing_context_and_inventory_topology(tmp_path, case): + """Each otherwise-valid accepted context reaches one unmeasured denial.""" + context = context_fixture(tmp_path) + if case == "missing_retry": + context.pop("NOEMA_TRANSPORT_RETRY_ATTEMPT") + elif case == "null_inventory": + context["hwp_files"] = None + else: + (Path(context["NOEMA_HWP_MCP_SOURCE"]) / "unlisted").write_bytes(b"unlisted supplier") + with pytest.raises(ValueError): + caller.accepted_prepare_transport(context) + + +@pytest.mark.parametrize("failure", ["timeout", "oserror"]) +def test_new_explicit_node_document_launch_errors(tmp_path, monkeypatch, failure): + """The actual consumer honors explicit Node on both native launch exceptions.""" + reader = tmp_path / "reader" + reader.mkdir() + node = tmp_path / "explicit-node" + monkeypatch.setenv("NOEMA_HWP_MCP_SOURCE", str(reader)) + monkeypatch.setenv("NOEMA_NODE_EXECUTABLE", str(node)) + observed = [] + + def launch(command, **kwargs): + """Record actual consumer argv and raise an inert subprocess boundary error.""" + observed.append(command) + assert command[0] == str(node) + assert command[2] == str(reader) + assert Path(command[3]).read_bytes() == b"NEW_HWP_BOUNDARY" + if failure == "timeout": + raise subprocess.TimeoutExpired(command, kwargs["timeout"]) + raise OSError("FIXTURE_EXECUTABLE_UNAVAILABLE") + + monkeypatch.setattr(document.subprocess, "run", launch) + with pytest.raises(document.DocumentReadError, match="timed out|could not start"): + document.extract_review_document("new.hwpx", b"NEW_HWP_BOUNDARY") + assert len(observed) == 1 diff --git a/tests/test_noema_current_quality_remaining.py b/tests/test_noema_current_quality_remaining.py new file mode 100644 index 0000000000..12319df764 --- /dev/null +++ b/tests/test_noema_current_quality_remaining.py @@ -0,0 +1,44 @@ +"""Unexecuted current-helper edge cases identified by retained arc reconciliation.""" +import hashlib +import json +from pathlib import Path + +import pytest +from scripts.ci import premerge_review_tooling as caller +from tests.test_noema_current_quality_delta import context_fixture +from tests.test_noema_production_cli_coverage import setup_caller + + +@pytest.mark.parametrize("case", ["boolean_retry", "inventory_list", "bad_hash_length", "nested_reader"]) +def test_new_current_helper_remaining_shapes(tmp_path, case): + """Reject distinct malformed shapes and accept an exact nested inventory.""" + context = context_fixture(tmp_path) + if case == "boolean_retry": + context["NOEMA_TRANSPORT_RETRY_ATTEMPT"] = True + elif case == "inventory_list": + context["hwp_files"] = ["body"] + elif case == "bad_hash_length": + context["hwp_files"] = {"body": "a" * 63} + else: + root = Path(context["NOEMA_HWP_MCP_SOURCE"]) + (root / "nested").mkdir() + (root / "nested/body").write_bytes(b"nested accepted") + context["hwp_files"]["nested/body"] = hashlib.sha256(b"nested accepted").hexdigest() + result = caller.accepted_prepare_transport(context) + assert result["NOEMA_HWP_MCP_SOURCE"] == str(root) + return + with pytest.raises(ValueError): + caller.accepted_prepare_transport(context) + + +def test_new_admission_only_does_not_consume_prepare_context(tmp_path, monkeypatch): + """Preflight with pinned extra context is not phase execution or supplier admission.""" + args, paths, _, events = setup_caller(tmp_path, monkeypatch, "admit") + data = json.loads(paths["admission"].read_text()) + data["prepare_transport"] = {"GITHUB_OUTPUT": str(tmp_path / "missing-output"), + "NOEMA_TRANSPORT_RETRY_ATTEMPT": "0"} + paths["admission"].write_text(json.dumps(data)) + args[args.index("--admission-digest") + 1] = hashlib.sha256(paths["admission"].read_bytes()).hexdigest() + assert caller.production_main(args) == 0 + assert events == [] + assert not (tmp_path / "missing-output").exists() diff --git a/tests/test_noema_draft_admission_before_sidecar.py b/tests/test_noema_draft_admission_before_sidecar.py index 715f2be495..5a7e72866e 100644 --- a/tests/test_noema_draft_admission_before_sidecar.py +++ b/tests/test_noema_draft_admission_before_sidecar.py @@ -19,17 +19,29 @@ import textwrap from pathlib import Path +import pytest +import yaml + from tests.test_required_workflow_queue_contract import workflow_step, workflow_text DRAFT_STEP = "Check live pull request draft state before sidecar provisioning" DRAFT_GATE = "steps.live_draft.outputs.live_draft != 'true'" +EXPECTED_UPLOAD_CONDITION = ( + "always() && env.PR_NUMBER != '' && " + DRAFT_GATE + + " && (steps.noema_sidecar.outcome == 'success'" + + " || steps.noema_sidecar.outcome == 'failure'" + + " || steps.noema_sidecar.outcome == 'cancelled')" +) REVIEWER_TOKEN = ( - "GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token" - " || steps.noema_oidc_token.outputs.token }}" + "GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && " + "steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != " + "'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || " + "steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }}" ) GATED_MODEL_STEPS = ( "Provision pinned Node.js for Noema document review", "Set up lock-compatible sidecar Python", + "Admit existing personal LiteLLM auto route", "Provision contextual-orchestrator review sidecar", "Provision local reviewed HWP document reader", "Prepare Noema model verdict", @@ -46,6 +58,114 @@ def _step_index(job: str, name: str) -> int: return job.index(f" - name: {name}\n") +def _parsed_noema_job(workflow: str) -> dict: + """Read the actual YAML job, retaining GitHub's literal ``on`` key.""" + return yaml.load(workflow, Loader=yaml.BaseLoader)["jobs"]["noema-review"] + + +def _sidecar_upload_is_fail_closed(workflow: str) -> bool: + """Bind the exact gate to one case-normalized action, never a display-name decoy.""" + uploads = [ + step for step in _parsed_noema_job(workflow)["steps"] + if str(step.get("uses", "")).casefold().startswith("actions/upload-artifact@") + ] + if len(uploads) != 1: + return False + upload = uploads[0] + return ( + upload.get("name") == "Upload contextual-orchestrator sidecar evidence" + and upload.get("if") == EXPECTED_UPLOAD_CONDITION + and upload.get("with", {}).get("name") == "noema-sidecar-evidence" + and "continue-on-error" not in upload + ) + + +def _noema_review_job_is_fail_closed(workflow: str) -> bool: + """The review job itself must propagate upload and producer failures.""" + return "continue-on-error" not in _parsed_noema_job(workflow) + + +def _upload_admitted(workflow: str, pr: str, draft: str, outcome: str) -> bool: + """Evaluate the actual condition's bounded grammar; unknown expressions fail.""" + uploads = [ + step for step in _parsed_noema_job(workflow)["steps"] + if str(step.get("uses", "")).casefold().startswith("actions/upload-artifact@") + ] + assert len(uploads) == 1 + terms = { + "always()": True, + "env.PR_NUMBER != ''": bool(pr), + DRAFT_GATE: draft != "true", + "steps.noema_sidecar.outcome == 'success'": outcome == "success", + "steps.noema_sidecar.outcome == 'failure'": outcome == "failure", + "steps.noema_sidecar.outcome == 'cancelled'": outcome == "cancelled", + } + values = [] + for part in uploads[0]["if"].split(" && "): + if part.startswith("(") and part.endswith(")"): + alternatives = part[1:-1].split(" || ") + assert all(term in terms for term in alternatives), part + values.append(any(terms[term] for term in alternatives)) + else: + assert part in terms, part + values.append(terms[part]) + return all(values) + + +@pytest.mark.parametrize("pr", ["", "2565"]) +@pytest.mark.parametrize("draft", ["true", "false", ""]) +@pytest.mark.parametrize("outcome", ["", "skipped", "unknown", "success", "failure", "cancelled"]) +def test_upload_admission_requires_current_terminal_producer(pr: str, draft: str, outcome: str) -> None: + """Absent/skipped producers cannot publish stale files; terminal diagnostics survive.""" + admitted = _upload_admitted(workflow_text("noema-review.yml"), pr, draft, outcome) + assert admitted == ( + bool(pr) and draft != "true" and outcome in {"success", "failure", "cancelled"} + ), (pr, draft, outcome) + + +def test_upload_action_identity_oracle_rejects_decoys_and_failure_bypasses() -> None: + """Keep the owner's unique casefold oracle and quoted/spaced failure controls.""" + workflow = workflow_text("noema-review.yml") + assert _sidecar_upload_is_fail_closed(workflow) + assert _noema_review_job_is_fail_closed(workflow) + assert not _sidecar_upload_is_fail_closed( + workflow.replace(EXPECTED_UPLOAD_CONDITION, EXPECTED_UPLOAD_CONDITION + " || true", 1) + ) + marker = " - name: Upload contextual-orchestrator sidecar evidence\n" + for bypass in ("continue-on-error: true", '"continue-on-error": true', "continue-on-error : true"): + assert not _sidecar_upload_is_fail_closed( + workflow.replace(marker, marker + " " + bypass + "\n", 1) + ), bypass + assert not _noema_review_job_is_fail_closed( + workflow.replace(" noema-review:\n", " noema-review:\n " + bypass + "\n", 1) + ), bypass + run_decoy = marker + f" if: {EXPECTED_UPLOAD_CONDITION}\n run: 'true'\n\n" + assert _sidecar_upload_is_fail_closed(workflow.replace(marker, run_decoy + marker, 1)) + assert not _sidecar_upload_is_fail_closed( + workflow.replace(marker, run_decoy + marker + " continue-on-error: true\n", 1) + ) + action = next( + step["uses"] for step in _parsed_noema_job(workflow)["steps"] + if str(step.get("uses", "")).casefold().startswith("actions/upload-artifact@") + ) + mixed_case = workflow.replace( + f"uses: {action}", f"uses: Actions/Upload-Artifact@{action.split('@', 1)[1]}", 1 + ) + assert mixed_case != workflow + assert _sidecar_upload_is_fail_closed(mixed_case) + action_decoy = ( + marker + f" if: {EXPECTED_UPLOAD_CONDITION}\n uses: {action}\n" + + " with:\n name: noema-sidecar-evidence\n path: CHANGELOG.md\n\n" + ) + assert not _sidecar_upload_is_fail_closed(mixed_case.replace(marker, action_decoy + marker, 1)) + assert not _sidecar_upload_is_fail_closed(mixed_case.replace( + marker, action_decoy + marker + " continue-on-error: true\n", 1 + )) + assert _noema_review_job_is_fail_closed(workflow.replace( + " continue-noema-transport:\n", " continue-noema-transport:\n continue-on-error: true\n", 1 + )) + + def test_live_draft_check_runs_after_head_validation_and_before_sidecar() -> None: """The draft decision sits between live-head validation and model provisioning.""" job = _noema_job() @@ -73,7 +193,8 @@ def test_live_draft_step_reads_the_live_pull_request_with_the_reviewer_token() - assert 'gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in step assert 'gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in validate assert "jq -e -s" in step - assert "set -e" not in step + assert "set -uo pipefail" not in step + assert "set -euo pipefail" in step # Ruleset-launched runs never see ready_for_review, so neither this check # nor any trigger-level filter may trust the event's draft snapshot. assert "github.event.pull_request.draft" not in workflow @@ -84,7 +205,25 @@ def test_model_heavy_steps_are_gated_on_the_live_draft_output() -> None: workflow = workflow_text("noema-review.yml") for name in GATED_MODEL_STEPS: step = workflow_step(workflow, name) - assert f"if: env.PR_NUMBER != '' && {DRAFT_GATE}\n" in step, name + expected = f"env.PR_NUMBER != '' && {DRAFT_GATE}" + if name == "Provision contextual-orchestrator review sidecar": + expected += " && steps.personal_route.outputs.selected != 'true'" + condition = next(line.strip()[4:] for line in step.splitlines() + if line.strip().startswith("if: ")) + assert condition == expected, name + # Evaluate every supported conjunction, so draft=true denies both routes; + # additional OR terms or omitted guards fail the exact contract above. + for pr in ("", "2565"): + for draft in ("true", "false", ""): + for personal in ("true", "false", ""): + terms = {"env.PR_NUMBER != ''": bool(pr), + DRAFT_GATE: draft != "true", + "steps.personal_route.outputs.selected != 'true'": personal != "true"} + admitted = all(terms[term] for term in condition.split(" && ")) + wanted = bool(pr) and draft != "true" + if name == "Provision contextual-orchestrator review sidecar": + wanted = wanted and personal != "true" + assert admitted == wanted, (name, pr, draft, personal) def test_downstream_publication_treats_unset_prepare_outputs_as_skipped() -> None: @@ -99,9 +238,8 @@ def test_downstream_publication_treats_unset_prepare_outputs_as_skipped() -> Non assert "needs.noema-review.result == 'failure'" in continuation assert "needs.noema-review.outputs.transport_capacity_unavailable == 'true'" in continuation assert "needs.noema-review.outputs.transport_retry_eligible == 'true'" in continuation - assert "if: always() && env.PR_NUMBER != ''" in workflow_step( - workflow, "Upload contextual-orchestrator sidecar evidence" - ) + assert _noema_review_job_is_fail_closed(workflow) + assert _sidecar_upload_is_fail_closed(workflow) def test_trigger_types_are_unchanged_by_the_runtime_draft_check() -> None: @@ -113,12 +251,13 @@ def test_trigger_types_are_unchanged_by_the_runtime_draft_check() -> None: ) -def _run_draft_step(tmp_path: Path, gh_body: str) -> tuple[subprocess.CompletedProcess[str], dict[str, str]]: +def _run_draft_step(tmp_path: Path, gh_body: str, gh_token: str = "synthetic-token") -> tuple[subprocess.CompletedProcess[str], dict[str, str]]: """Execute the draft step's bash with a fake ``gh`` and return its outputs.""" bash_executable = shutil.which("bash") or "/bin/bash" script = textwrap.dedent( workflow_step(workflow_text("noema-review.yml"), DRAFT_STEP).split(" run: |\n", 1)[1] ) + script = script.replace("/tmp/noema-live-draft-error", str(tmp_path / "live-draft-error")) fake_gh = tmp_path / "gh" fake_gh.write_text(f"#!/usr/bin/env bash\n{gh_body}\n", encoding="utf-8") fake_gh.chmod(0o755) @@ -131,7 +270,7 @@ def _run_draft_step(tmp_path: Path, gh_body: str) -> tuple[subprocess.CompletedP "PATH": f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}", "TARGET_REPOSITORY": "ContextualWisdomLab/example", "PR_NUMBER": "7", - "GH_TOKEN": "synthetic-token", + "GH_TOKEN": gh_token, "GITHUB_OUTPUT": str(output), }, capture_output=True, @@ -150,6 +289,16 @@ def _live_pr(draft: object) -> str: return f"printf '%s' '{payload}'" +def test_live_draft_missing_token_fails_closed_before_lookup(tmp_path: Path) -> None: + """Missing reviewer authority cannot proceed to a live lookup or model setup.""" + marker = tmp_path / "lookup-reached" + result, outputs = _run_draft_step(tmp_path, f"touch '{marker}'", gh_token="") + assert result.returncode != 0 + assert outputs == {} + assert not marker.exists() + assert "::error::" in result.stdout + + def test_live_draft_pr_skips_model_review_with_a_clear_notice(tmp_path: Path) -> None: """A live draft sets live_draft=true and explains the skip; the step succeeds.""" result, outputs = _run_draft_step(tmp_path, _live_pr(True)) diff --git a/tests/test_noema_existing_app_composition.py b/tests/test_noema_existing_app_composition.py new file mode 100644 index 0000000000..8de60f7dc8 --- /dev/null +++ b/tests/test_noema_existing_app_composition.py @@ -0,0 +1,384 @@ +"""Source-only existing-App issuer controls; keys and HTTP are synthetic.""" +import json +import shutil +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +HELPER = ROOT / 'scripts/ci/noema_existing_app_issuer.cjs' + + +def test_workflow_uses_existing_secret_for_both_short_lived_issuer_phases(): + """No missing JWT-file supplier or token-action output masquerades as issuer.""" + text=(ROOT/'.github/workflows/noema-review.yml').read_text() + prepare=text.split(' - name: Prepare Noema model verdict\n',1)[1].split(' - name:',1)[0] + publish=text.split(' - name: Publish prepared Noema verdict on the exact live head\n',1)[1].split('\n continue-noema-transport:',1)[0] + for phase,block in (('prepare',prepare),('publish',publish)): + assert 'node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs"' in block + assert '--phase '+phase in block + assert 'NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }}' in block + assert 'NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }}' in block + assert '--app-jwt-file' not in block + assert '--phase-issuance' not in block + assert 'python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/premerge_review_tooling.py"' not in block + assert text.index(' - name: Revalidate prepared scope before publisher credential refresh') < text.index(' - name: Refresh repository-scoped Noema GitHub App token for publication') + + +def test_existing_app_signing_original_body_controller_and_cleanup(tmp_path): + """Real RSA signing and private delivery feed the controller, then revoke.""" + assert HELPER.exists(), 'existing Secret to original issuer response composition is missing' + import hashlib + import sys + from tests.test_noema_issuer_producer_controls import bootstrap_case + private, source, entry, _, admission = bootstrap_case(tmp_path) + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + entry.write_text('import os,sys\nfrom pathlib import Path\n' + 'assert os.environ["GH_TOKEN"]=="FIXTURE_WRITE"\n' + 'assert "NOEMA_GITHUB_APP_PRIVATE_KEY" not in os.environ\n' + 'assert "NOEMA_LLM_API_KEY" not in os.environ\n' + 'p=Path(sys.argv[sys.argv.index("--publish-verdict-file")+1]);p.unlink()\n') + manifest = {'schema_version':1,'repository':'ContextualWisdomLab/.github','commit':'c'*40, + 'tree':'d'*40,'entrypoint':entry.relative_to(source).as_posix(), + 'files':{entry.relative_to(source).as_posix():hashlib.sha256(entry.read_bytes()).hexdigest()}} + admission.update(scope=scope,installation_id=7,app_id=11) + tooling=private/'tooling';(tooling/entry.relative_to(source)).parent.mkdir(parents=True) + (tooling/entry.relative_to(source)).write_bytes(entry.read_bytes()) + mf,af=tmp_path/'manifest',tmp_path/'admission';mf.write_text(json.dumps(manifest));af.write_text(json.dumps(admission)) + envelope={'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'],'reviewer_actor':scope['reviewer'], + 'pull_request_author':'writer','verdict':{'decision':'approve'}} + verdict=private/'verdict.json';verdict.write_text(json.dumps(envelope));verdict.chmod(0o600) + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}},'head':{'sha':scope['head']}, + 'number':7,'user':{'login':'writer'},'state':'open','draft':False} + gh=tmp_path/'gh';gh.write_text('#!/bin/sh\n[ "$GH_TOKEN" = FIXTURE_READ_0 ] || exit 77\n'+"printf '%s' '"+json.dumps(pr)+"'\n");gh.chmod(0o755) + config={'python':sys.executable,'controller':str(ROOT/'scripts/ci/premerge_review_tooling.py'), + 'path':str(tmp_path)+':/usr/bin:/bin','args':['--publish-production','--tooling-root',str(tooling), + '--manifest',str(mf),'--accepted-digest',hashlib.sha256(mf.read_bytes()).hexdigest(), + '--admission',str(af),'--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'],'--verdict-file',str(verdict)]} + configpath=tmp_path/'config.json';configpath.write_text(json.dumps(config)) + script = tmp_path/'composition.cjs' + script.write_text(r''' +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const issuer = require(process.argv[2]); +(async () => { + const {privateKey, publicKey} = crypto.generateKeyPairSync('rsa', {modulusLength:2048}); + const jwt = issuer.signApp('Iv1.fixture', privateKey.export({type:'pkcs8',format:'pem'}), 2000000000); + const pieces = jwt.split('.'); + const claims = JSON.parse(Buffer.from(pieces[1], 'base64url')); + assert.equal(claims.iss, 'Iv1.fixture'); + assert.equal(claims.iat, 1999999940); assert.equal(claims.exp, 2000000300); + assert(crypto.verify('RSA-SHA256', Buffer.from(pieces.slice(0,2).join('.')), publicKey, Buffer.from(pieces[2],'base64url'))); + const parent=path.join(process.argv[3],'private'); + const events=[];let lastBody;const rawTokens=[]; + const scope={repository:'ContextualWisdomLab/example',number:7,head:'a'.repeat(40),base:'b'.repeat(40),author:'writer',reviewer:'cwl-noema-review[bot]'}; + const transport=async(method,route,payload,credential)=>{ + events.push([method,route]);assert(credential); + if(route==='/app')return Buffer.from('{"id":11,"slug":"cwl-noema-review"}'); + if(route.endsWith('/installation'))return Buffer.from('{"id":7,"app_id":11}'); + if(method==='DELETE')return Buffer.alloc(0); + const token=payload.permissions.pull_requests==='write'?'FIXTURE_WRITE':'FIXTURE_READ_'+rawTokens.length; + rawTokens.push(token); + const raw=Buffer.from(JSON.stringify({token,expires_at:'2033-05-18T04:33:20Z',permissions:payload.permissions,repositories:[{full_name:scope.repository}]})); + lastBody=raw;return raw; + }; + let fenced=false; + const cfg=JSON.parse(fs.readFileSync(process.argv[4])); + const controller=async(file,env)=>{ + assert(fenced);assert.deepEqual(fs.readFileSync(file),lastBody); + assert.equal(fs.statSync(file).mode&0o777,0o600);assert.equal(fs.statSync(file).nlink,1); + assert(!('NOEMA_GITHUB_APP_PRIVATE_KEY' in env)); + assert(!('LLM_GATEWAY_API_KEY' in env));assert(!('NOEMA_LLM_API_KEY' in env)); + const result=require('node:child_process').spawnSync(cfg.python,['-I','-B',cfg.controller,...cfg.args,'--phase-issuance',file], + {env:{PATH:cfg.path,GH_TOKEN:env.GH_TOKEN},encoding:'utf8',timeout:15000}); + assert.equal(result.status,0,'real Python controller and inert publisher child refused');return result.status; + }; + await issuer.runPhase({phase:'publish',scope,appId:11,installationId:7,privateParent:parent, + clientId:'Iv1.fixture',privateKey:privateKey.export({type:'pkcs8',format:'pem'}),now:()=>2000000000, + transport,fence:async(token,phase)=>{assert(token.startsWith('FIXTURE_READ'));assert.equal(phase,'publish');fenced=true;}, + controller}); + assert.equal(events.filter(e=>e[0]==='DELETE').length,2); + assert(!fs.existsSync(path.join(parent,'publish.json'))); + assert.equal(rawTokens.length,2); + console.log('SYNTHETIC_COMPOSITION_PASS'); +})().catch(()=>{console.error('SYNTHETIC_COMPOSITION_FAIL');process.exitCode=1}); +''') + result = subprocess.run([shutil.which('node'),str(script),str(HELPER),str(tmp_path),str(configpath)], + env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=20) + assert result.returncode == 0 and result.stdout.strip() == 'SYNTHETIC_COMPOSITION_PASS', result.stderr + + +def test_real_issuer_main_fences_then_runs_prepare_and_publish_without_external_jwt(tmp_path): + """Execute both real CLIs with only synthetic HTTPS and inert phase children.""" + import hashlib + import sys + from tests.test_noema_issuer_producer_controls import bootstrap_case + private, source, entry, _, admission=bootstrap_case(tmp_path) + scope={'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + envelope={'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'],'reviewer_actor':scope['reviewer'], + 'pull_request_author':'writer','verdict':{'decision':'approve'}} + entry.write_text('import os,sys\nfrom pathlib import Path\n' + 'a=sys.argv\n' + 'assert "NOEMA_GITHUB_APP_PRIVATE_KEY" not in os.environ\n' + 'if "--prepare-verdict-file" in a:\n' + ' assert os.environ["GH_TOKEN"]=="MAIN_READ_0"\n' + ' assert os.environ["NOEMA_LLM_MODEL"]=="auto"\n' + ' p=Path(a[a.index("--prepare-verdict-file")+1]);p.write_text('+repr(json.dumps(envelope))+');p.chmod(0o600)\n' + 'else:\n' + ' assert os.environ["GH_TOKEN"]=="MAIN_WRITE_2"\n' + ' assert "LLM_GATEWAY_API_KEY" not in os.environ and "NOEMA_LLM_API_KEY" not in os.environ\n' + ' p=Path(a[a.index("--publish-verdict-file")+1]);Path(str(p)+".marker").write_text("INERT_NATIVE_PUBLISH");p.unlink()\n') + mf,af=tmp_path/'manifest',tmp_path/'admission' + manifest={'schema_version':1,'repository':'ContextualWisdomLab/.github','commit':'c'*40,'tree':'d'*40, + 'entrypoint':entry.relative_to(source).as_posix(), + 'files':{entry.relative_to(source).as_posix():hashlib.sha256(entry.read_bytes()).hexdigest()}} + node=Path(shutil.which('node')).resolve() + admission.update(scope=scope,installation_id=7,app_id=11) + admission['dependencies'][str(node)]=hashlib.sha256(node.read_bytes()).hexdigest() + mf.write_text(json.dumps(manifest));af.write_text(json.dumps(admission)) + from scripts.ci import noema_bootstrap_producer as producer + producer.materialize_tooling(source,private/'tooling',mf.read_bytes(),accepted_digest=hashlib.sha256(mf.read_bytes()).hexdigest(), + admission=admission,private_parent=private) + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}},'head':{'sha':scope['head']}, + 'number':7,'user':{'login':'writer'},'state':'open','draft':False} + gh=tmp_path/'gh';gh.write_text('#!/bin/sh\ncase "$GH_TOKEN" in MAIN_READ_0|MAIN_READ_1) ;; *) exit 77;; esac\n'+"printf '%s' '"+json.dumps(pr)+"'\n");gh.chmod(0o755) + common=['--private-parent',str(private),'--source-root',str(source),'--manifest',str(mf), + '--accepted-digest',hashlib.sha256(mf.read_bytes()).hexdigest(),'--admission',str(af), + '--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(),'--repo',scope['repository'], + '--pr-number','7','--expected-head',scope['head'],'--verdict-file',str(private/'verdict.json')] + cfg=tmp_path/'cfg.json';cfg.write_text(json.dumps({'common':common,'path':str(tmp_path)+':/usr/bin:/bin','parent':str(private)})) + script=tmp_path/'main.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),fs=require('node:fs'),crypto=require('node:crypto'),https=require('node:https'); +const {EventEmitter}=require('node:events');const helper=require(process.argv[2]); +const config=JSON.parse(fs.readFileSync(process.argv[3]));const events=[];let issued=0; +https.request=(options,callback)=>{ + const req=new EventEmitter();let body='';req.setTimeout=()=>{};req.write=chunk=>{body+=chunk};req.destroy=()=>{}; + req.end=()=>{ + events.push([options.method,options.path]); + assert.equal(options.hostname,'api.github.com'); + assert(!('NOEMA_GITHUB_APP_PRIVATE_KEY' in options)); + let raw;const res=new EventEmitter();res.statusCode=options.method==='DELETE'?204:options.method==='POST'?201:200;res.complete=true; + if(options.path==='/app')raw=Buffer.from('{"id":11,"slug":"cwl-noema-review"}'); + else if(options.path.endsWith('/installation'))raw=Buffer.from('{"id":7,"app_id":11}'); + else if(options.method==='DELETE')raw=Buffer.alloc(0); + else {const p=JSON.parse(body);if(p.permissions.pull_requests==='write')assert(fs.existsSync(config.parent+'/verdict.json')); + raw=Buffer.from(JSON.stringify({token:(p.permissions.pull_requests==='write'?'MAIN_WRITE_':'MAIN_READ_')+(issued++), + expires_at:new Date(Date.now()+3600000).toISOString(),permissions:p.permissions,repositories:[{full_name:'ContextualWisdomLab/example'}]}));} + res.headers={'content-length':String(raw.length)};callback(res); + queueMicrotask(()=>{res.emit('data',raw);res.emit('end')}); + };return req; +}; +(async()=>{ + const pair=crypto.generateKeyPairSync('rsa',{modulusLength:2048}); + const env={PATH:config.path,NOEMA_GITHUB_APP_CLIENT_ID:'Iv1.fixture',NOEMA_GITHUB_APP_PRIVATE_KEY:pair.privateKey.export({type:'pkcs8',format:'pem'}),LLM_GATEWAY_API_KEY:'SYNTHETIC_EXISTING_INFERENCE'}; + assert.equal(await helper.main(['--phase','prepare',...config.common],env),0,'real prepare fence or controller missing'); + assert.equal(await helper.main(['--phase','publish',...config.common],env),0,'real publisher composition missing'); + assert.equal(issued,3);assert.equal(events.filter(x=>x[0]==='DELETE').length,3); + assert.equal(fs.readFileSync(config.parent+'/verdict.json.marker','utf8'),'INERT_NATIVE_PUBLISH'); + console.log('MAIN_COMPOSITION_PASS'); +})().catch(()=>{console.error('MAIN_COMPOSITION_FAIL');process.exitCode=1}); +''') + result=subprocess.run([str(node),str(script),str(HELPER),str(cfg)],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=25) + assert result.returncode==0 and result.stdout.strip()=='MAIN_COMPOSITION_PASS','real prepare fence or controller missing: '+result.stderr + + +def test_issuer_parses_duplicate_responses_fail_closed_and_cleans_token(tmp_path): + """Duplicate permission keys cannot rewrite an original grant into acceptance.""" + script=tmp_path/'duplicate.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),fs=require('node:fs'),crypto=require('node:crypto');const issuer=require(process.argv[2]); +(async()=>{ + const parent=process.argv[3]+'/private';fs.mkdirSync(parent,{mode:0o700});let deletes=0,controller=0; + const key=crypto.generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}); + await assert.rejects(issuer.runPhase({phase:'prepare',scope:{repository:'ContextualWisdomLab/example',reviewer:'cwl-noema-review[bot]'},appId:11,installationId:7, + privateParent:parent,clientId:'Iv1.fixture',privateKey:key,now:()=>2000000000, + transport:async(method,route)=>{ + if(method==='DELETE'){deletes++;return Buffer.alloc(0);} + if(route==='/app')return Buffer.from('{"id":11,"slug":"cwl-noema-review"}'); + if(route.endsWith('/installation'))return Buffer.from('{"id":7,"app_id":11}'); + return Buffer.from('{"token":"DUPLICATE_FIXTURE","expires_at":"2033-05-18T04:33:20Z","permissions":{"contents":"write","contents":"read","metadata":"read","pull_requests":"read"},"repositories":[{"full_name":"ContextualWisdomLab/example"}]}'); + },fence:async()=>{},controller:async()=>{controller++;return 0;}})); + assert.equal(controller,0);assert.equal(deletes,1,'safely parsed unique token lost revocation custody'); + console.log('DUPLICATE_REJECTED'); +})().catch(()=>{console.error('DUPLICATE_FAIL');process.exitCode=1}); +''') + result=subprocess.run([shutil.which('node'),str(script),str(HELPER),str(tmp_path)],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=15) + assert result.returncode==0 and result.stdout.strip()=='DUPLICATE_REJECTED', 'duplicate original grant accepted or cleanup lost' + + +def test_fixed_origin_http_response_framing_and_missing_signer_refuse(tmp_path): + """Characterize bounded original-body framing with synthetic HTTP events.""" + script=tmp_path/'framing.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),https=require('node:https'),{EventEmitter}=require('node:events'); +const issuer=require(process.argv[2]); +(async()=>{ + for(const mutation of ['complete','short','redirect','oversize','incomplete']){ + https.request=(opts,callback)=>{ + assert.equal(opts.hostname,'api.github.com');assert.equal(opts.port,443); + const req=new EventEmitter();req.setTimeout=()=>{};req.write=()=>{};req.end=()=>{ + const res=new EventEmitter();res.statusCode=mutation==='redirect'?302:200; + res.complete=mutation!=='incomplete';res.destroy=()=>{}; + const raw=mutation==='oversize'?Buffer.alloc(2*1024*1024+1):Buffer.from('{"ok":true}'); + res.headers={'content-length':String(raw.length+(mutation==='short'?3:0))};callback(res); + queueMicrotask(()=>{res.emit('data',raw);res.emit('end')}); + };return req; + }; + if(mutation==='complete')assert.deepEqual(await issuer.transport('GET','/app',null,'FIXTURE_JWT'),Buffer.from('{"ok":true}')); + else await assert.rejects(issuer.transport('GET','/app',null,'FIXTURE_JWT')); + } + assert.throws(()=>issuer.signApp('Iv1.fixture','',2000000000)); + await assert.rejects(issuer.transport('GET','/foreign',null,'FIXTURE_JWT')); + console.log('HTTP_CONTROLS_PASS'); +})().catch(()=>{console.error('HTTP_CONTROLS_FAIL');process.exitCode=1}); +''') + result=subprocess.run([shutil.which('node'),str(script),str(HELPER)],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=15) + assert result.returncode==0 and result.stdout.strip()=='HTTP_CONTROLS_PASS',result.stderr + + +def test_write_scope_revocation_during_app_wait_never_posts_write(tmp_path): + """A scope change while publisher authentication waits must block write POST.""" + script=tmp_path/'wait.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),fs=require('node:fs'),crypto=require('node:crypto');const issuer=require(process.argv[2]); +(async()=>{ + const parent=process.argv[3]+'/private';fs.mkdirSync(parent,{mode:0o700});let appReads=0,writes=0,revoked=false,deletes=0; + const key=crypto.generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}); + await assert.rejects(issuer.runPhase({phase:'publish',scope:{repository:'ContextualWisdomLab/example',reviewer:'cwl-noema-review[bot]'},appId:11,installationId:7, + privateParent:parent,clientId:'Iv1.fixture',privateKey:key,now:()=>2000000000, + transport:async(method,route,payload)=>{ + if(method==='DELETE'){deletes++;return Buffer.alloc(0);} + if(route==='/app'){if(++appReads===2)revoked=true;return Buffer.from('{"id":11,"slug":"cwl-noema-review"}');} + if(route.endsWith('/installation'))return Buffer.from('{"id":7,"app_id":11}'); + if(payload.permissions.pull_requests==='write')writes++; + return Buffer.from(JSON.stringify({token:'WAIT_FIXTURE',expires_at:'2033-05-18T04:33:20Z',permissions:payload.permissions,repositories:[{full_name:'ContextualWisdomLab/example'}]})); + },fence:async()=>{if(revoked)throw Error('SCOPE_REVOKED');},controller:async()=>0})); + assert.equal(writes,0,'scope revoked during App authentication reached write issuance');assert.equal(deletes,1); + console.log('WAIT_REVOKED'); +})().catch(()=>{console.error('WAIT_FAIL');process.exitCode=1}); +''') + result=subprocess.run([shutil.which('node'),str(script),str(HELPER),str(tmp_path)],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=15) + assert result.returncode==0 and result.stdout.strip()=='WAIT_REVOKED','scope revoked during App authentication reached write issuance' + + +def test_prepublication_action_never_mints_unused_write_capability(): + """Only the original-response issuer may mint the actual publisher grant.""" + text=(ROOT/'.github/workflows/noema-review.yml').read_text() + refresh=text.split(' - name: Refresh repository-scoped Noema GitHub App token for publication\n',1)[1].split(' - name:',1)[0] + assert 'permission-pull-requests: write' not in refresh, 'unused action write token minted before original-response issuer' + assert 'permission-pull-requests: read' in refresh + + +def test_issuer_main_refuses_unpinned_python_before_executing_it(tmp_path): + """Accepted admission must bind executable bytes before any subprocess spawn.""" + import hashlib + import sys + from tests.test_noema_issuer_producer_controls import bootstrap_case + private,source,entry,raw,admission=bootstrap_case(tmp_path) + marker=tmp_path/'executed' + fake=tmp_path/'fake-python';fake.write_text('#!/bin/sh\nprintf RUN > '+str(marker)+'\nexit 0\n');fake.chmod(0o755) + admission['interpreter']={'path':str(fake),'sha256':'0'*64} + admission.update(scope={'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'}, + app_id=11,installation_id=7) + node=Path(shutil.which('node')).resolve();admission['dependencies'][str(node)]=hashlib.sha256(node.read_bytes()).hexdigest() + mf,af=tmp_path/'manifest',tmp_path/'admission';mf.write_bytes(raw);af.write_text(json.dumps(admission)) + args=['--phase','prepare','--private-parent',str(private),'--source-root',str(source), + '--manifest',str(mf),'--accepted-digest',hashlib.sha256(raw).hexdigest(), + '--admission',str(af),'--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(), + '--repo','ContextualWisdomLab/example','--pr-number','7','--expected-head','a'*40, + '--verdict-file',str(private/'verdict.json')] + cfg=tmp_path/'config.json';cfg.write_text(json.dumps(args)) + script=tmp_path/'python-pin.cjs' + script.write_text(r''' +const fs=require('node:fs'),crypto=require('node:crypto'),https=require('node:https'),{EventEmitter}=require('node:events'); +const issuer=require(process.argv[2]); +https.request=(opts,callback)=>{const req=new EventEmitter();req.setTimeout=()=>{};req.write=()=>{};req.end=()=>{ + const raw=Buffer.from(opts.path==='/app'?'{"id":11,"slug":"cwl-noema-review"}':opts.path.endsWith('/installation')?'{"id":7,"app_id":11}':opts.method==='DELETE'?'':JSON.stringify({token:'INTERPRETER_FIXTURE',expires_at:new Date(Date.now()+3600000).toISOString(),permissions:{contents:'read',metadata:'read',pull_requests:'read'},repositories:[{full_name:'ContextualWisdomLab/example'}]})); + const res=new EventEmitter();res.statusCode=opts.method==='DELETE'?204:opts.method==='POST'?201:200;res.complete=true;res.headers={'content-length':String(raw.length)};callback(res);queueMicrotask(()=>{res.emit('data',raw);res.emit('end')});};return req;}; +const key=crypto.generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}); +issuer.main(JSON.parse(fs.readFileSync(process.argv[3])),{PATH:'/usr/bin:/bin',NOEMA_GITHUB_APP_CLIENT_ID:'Iv1.fixture',NOEMA_GITHUB_APP_PRIVATE_KEY:key}).then(()=>{process.exitCode=2}).catch(()=>{process.exitCode=1}); +''') + result=subprocess.run([str(node),str(script),str(HELPER),str(cfg)],env={'PATH':'/usr/bin:/bin'}, + capture_output=True,text=True,timeout=15) + assert result.returncode != 0 + assert not marker.exists(), 'unadmitted executable ran before interpreter byte check' + + +def test_multi_hour_preparation_settles_expired_read_grant_without_blocking_result(tmp_path): + """Known expiry after the model is not an unconfirmed live credential leak.""" + script=tmp_path/'long-model.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),fs=require('node:fs'),crypto=require('node:crypto');const issuer=require(process.argv[2]); +(async()=>{ + const parent=process.argv[3]+'/private';fs.mkdirSync(parent,{mode:0o700});let now=2000000000,deletes=0; + const key=crypto.generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}); + const outcome=await issuer.runPhase({phase:'prepare',scope:{repository:'ContextualWisdomLab/example',reviewer:'cwl-noema-review[bot]'},appId:11,installationId:7, + privateParent:parent,clientId:'Iv1.fixture',privateKey:key,now:()=>now, + transport:async(method,route,payload)=>{ + if(method==='DELETE'){deletes++;const error=Error('FIXTURE_EXPIRED');error.statusCode=401;throw error;} + if(route==='/app')return Buffer.from('{"id":11,"slug":"cwl-noema-review"}'); + if(route.endsWith('/installation'))return Buffer.from('{"id":7,"app_id":11}'); + return Buffer.from(JSON.stringify({token:'LONG_MODEL_READ_ONLY',expires_at:new Date((now+3600)*1000).toISOString(),permissions:payload.permissions,repositories:[{full_name:'ContextualWisdomLab/example'}]})); + },fence:async()=>{},controller:async()=>{now+=10800;return 0;}}); + assert.equal(outcome,0);assert.equal(deletes,1);assert(!fs.existsSync(parent+'/prepare.json')); + console.log('LONG_MODEL_PASS'); +})().catch(()=>{console.error('LONG_MODEL_FAIL');process.exitCode=1}); +''') + result=subprocess.run([shutil.which('node'),str(script),str(HELPER),str(tmp_path)],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=15) + assert result.returncode==0 and result.stdout.strip()=='LONG_MODEL_PASS','expired readonly cleanup blocks completed multi-hour model' + + +def test_prepare_controller_does_not_impose_a_model_deadline(): + """The new wrapper must preserve the existing unlimited inference contract.""" + source=HELPER.read_text() + assert "timeout:phase==='prepare'?" not in source, 'issuer adds a fixed model deadline' + + +def test_rejected_grants_and_downstream_errors_keep_revocation_custody(tmp_path): + """Exercise new issuer negatives without touching completed composition cases.""" + script=tmp_path/'controls.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'); +const issuer=require(process.argv[2]); +(async()=>{ + const key=crypto.generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}); + const scope={repository:'ContextualWisdomLab/example',reviewer:'cwl-noema-review[bot]'}; + for(const mutation of ['bad_signer','stale_scope','expiry','permission','wrong_app','wrong_install','wrong_repo','downstream','cleanup','delivery']){ + const parent=path.join(process.argv[3],mutation);fs.mkdirSync(parent,{mode:0o700}); + let posts=0,deletes=0,controllers=0; + const originalWrite=fs.writeFileSync; + const request=async(method,route,payload)=>{ + if(method==='DELETE'){deletes++;if(mutation==='cleanup')throw Error('DENIED');return Buffer.alloc(0);} + if(route==='/app')return Buffer.from(JSON.stringify({id:mutation==='wrong_app'?12:11,slug:'cwl-noema-review'})); + if(route.endsWith('/installation'))return Buffer.from(JSON.stringify({id:mutation==='wrong_install'?8:7,app_id:11})); + posts++; + return Buffer.from(JSON.stringify({token:'FIXTURE_CONTROL',expires_at:mutation==='expiry'?'2000-01-01T00:00:00Z':'2033-05-18T04:33:20Z', + permissions:mutation==='permission'?{...payload.permissions,contents:'write'}:payload.permissions, + repositories:[{full_name:mutation==='wrong_repo'?'ContextualWisdomLab/other':scope.repository}]})); + }; + if(mutation==='delivery')fs.writeFileSync=(...args)=>{if(typeof args[0]==='number'){originalWrite(args[0],Buffer.from('PARTIAL'));throw Error('IO');}return originalWrite(...args);}; + try{ + await assert.rejects(issuer.runPhase({phase:'publish',scope,appId:11,installationId:7,privateParent:parent, + clientId:'Iv1.fixture',privateKey:mutation==='bad_signer'?'BAD_SYNTHETIC_KEY':key,now:()=>2000000000,transport:request, + fence:async()=>{if(mutation==='stale_scope')throw Error('STALE');}, + controller:async()=>{controllers++;throw Error('DOWNSTREAM');}})); + if(['bad_signer','wrong_app','wrong_install'].includes(mutation)){assert.equal(posts,0);assert.equal(deletes,0);} + else if(mutation==='stale_scope'){assert.equal(posts,1);assert.equal(deletes,1);} + else if(['expiry','permission','wrong_repo'].includes(mutation)){assert.equal(posts,1);assert.equal(deletes,1);assert.equal(controllers,0);} + else {assert.equal(posts,2);assert.equal(deletes,2);} + assert(!fs.existsSync(path.join(parent,'publish.json')),'partial delivery survived cleanup'); + }finally{fs.writeFileSync=originalWrite;} + } + console.log('ISSUER_CONTROLS_PASS'); +})().catch(()=>{console.error('ISSUER_CONTROLS_FAIL');process.exitCode=1}); +''') + result=subprocess.run([shutil.which('node'),str(script),str(HELPER),str(tmp_path)],env={'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=20) + assert result.returncode==0 and result.stdout.strip()=='ISSUER_CONTROLS_PASS','partial delivery survived cleanup or missing issuer denial: '+result.stderr diff --git a/tests/test_noema_issuer_producer_connection.py b/tests/test_noema_issuer_producer_connection.py new file mode 100644 index 0000000000..e827da8b47 --- /dev/null +++ b/tests/test_noema_issuer_producer_connection.py @@ -0,0 +1,313 @@ +"""New issuer producer composition; all authority here is synthetic fixture data.""" +import json +import re +from pathlib import Path + +from scripts.ci import premerge_review_tooling as caller + +ROOT = Path(__file__).resolve().parents[1] + + +def test_actual_prepare_request_is_consumable_without_permission_relabeling(): + """Execute the controller with the response to the real workflow request.""" + workflow = (ROOT / '.github/workflows/noema-review.yml').read_text() + step = workflow.split(' - name: Mint repository-scoped Noema GitHub App token\n', 1)[1] + step = step.split(' - name:', 1)[0] + permissions = {name.replace('-', '_'): value for name, value in + re.findall(r' permission-([a-z-]+): (read|write)', step)} + # A recording App API returns its actual requested grant; the controller + # receives those original bytes, not labels synthesized from action outputs. + raw_response = json.dumps({'token': 'SYNTHETIC_PREPARE_ONLY', + 'permissions': permissions, + 'repositories': [{'full_name': 'ContextualWisdomLab/example'}]}).encode() + scope = {'repository': 'ContextualWisdomLab/example', 'reviewer': 'cwl-noema-review[bot]'} + observed = None + try: + observed = caller.phase_environment_from_issuance( + 'prepare', json.loads(raw_response), scope=scope, + reviewer_actor=scope['reviewer'], installation_id=7, + inference_key='SYNTHETIC_INFERENCE_ONLY') + except ValueError: + pass + assert observed is not None, 'actual prepare request grants broader authority than controller permits' + assert observed['GH_TOKEN'] == 'SYNTHETIC_PREPARE_ONLY' + assert observed['NOEMA_LLM_MODEL'] == 'auto' + + +def test_original_authenticated_response_reaches_controller(tmp_path): + """Missing producer cannot be replaced by action-output permission metadata.""" + import importlib.util + helper = ROOT / 'scripts/ci/noema_bootstrap_producer.py' + scope = {'repository': 'ContextualWisdomLab/example', 'reviewer': 'cwl-noema-review[bot]'} + raw = b'{"token":"SYNTHETIC_ORIGINAL","permissions":{"contents":"read","metadata":"read","pull_requests":"read"},"repositories":[{"full_name":"ContextualWisdomLab/example"}]}' + calls = [] + + def request(method, route, payload): + """Return authenticated API fixture bytes without rewriting the grant.""" + calls.append((method, route, payload)) + if route == '/app': + return b'{"id":11,"slug":"cwl-noema-review"}' + if route.endswith('/installation'): + return b'{"id":7,"app_id":11}' + return raw + + private = tmp_path / 'private' + private.mkdir(mode=0o700) + output = private / 'prepare.json' + if helper.exists(): + spec = importlib.util.spec_from_file_location('issuer_fixture_producer', helper) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.produce_issuance('prepare', scope=scope, installation_id=7, + app_id=11, output=output, private_parent=private, + request=request, revalidate=lambda: None) + assert output.exists() and len(calls) == 3, 'authenticated issuer response producer is missing' + assert output.read_bytes() == raw + observed = caller.phase_environment_from_issuance( + 'prepare', json.loads(output.read_bytes()), scope=scope, + reviewer_actor=scope['reviewer'], installation_id=7, + inference_key='SYNTHETIC_INFERENCE_ONLY') + assert observed['GH_TOKEN'] == 'SYNTHETIC_ORIGINAL' + + +def test_missing_bootstrap_materialization_cannot_run_original_phase_child(tmp_path): + """Execute a newly supplied child only after complete external pin admission.""" + import hashlib + import importlib.util + import sys + helper = ROOT / 'scripts/ci/noema_bootstrap_producer.py' + spec = importlib.util.spec_from_file_location('bootstrap_fixture_producer', helper) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + source = tmp_path / 'supplier' + entry = source / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + entry.write_text('print("INERT_ACCEPTED_CHILD")\n') + files = {entry.relative_to(source).as_posix(): hashlib.sha256(entry.read_bytes()).hexdigest()} + manifest = {'schema_version': 1, 'repository': 'ContextualWisdomLab/.github', + 'commit': 'c' * 40, 'tree': 'd' * 40, + 'entrypoint': entry.relative_to(source).as_posix(), 'files': files} + raw = json.dumps(manifest).encode() + private = tmp_path / 'private' + private.mkdir(mode=0o700) + target = private / 'tooling' + admission = {'namespace': str(private), + 'interpreter': {'path': str(Path(sys.executable).resolve()), + 'sha256': hashlib.sha256(Path(sys.executable).read_bytes()).hexdigest()}, + 'dependencies': {}, + **{key: True for key in ('isolated_clean_job', 'native_custody', + 'supplier_cost', 'supplier_retention', 'issuer_phase_separation')}} + if hasattr(module, 'materialize_tooling'): + module.materialize_tooling(source, target, raw, + accepted_digest=hashlib.sha256(raw).hexdigest(), + admission=admission, private_parent=private) + assert (target / entry.relative_to(source)).exists(), 'immutable bootstrap producer is missing' + import subprocess + result = subprocess.run([sys.executable, '-I', '-B', str(target / entry.relative_to(source))], + capture_output=True, text=True, timeout=10) + assert result.returncode == 0 and result.stdout == 'INERT_ACCEPTED_CHILD\n' + + +def test_issuer_write_grant_rejected_before_private_delivery(tmp_path): + """Original broad response cannot be relabeled as preparation read authority.""" + import pytest + from scripts.ci.noema_bootstrap_producer import produce_issuance + private = tmp_path / 'private' + private.mkdir(mode=0o700) + calls = [] + def request(method, route, payload): + """Return otherwise valid fixture identities and a contrary original grant.""" + calls.append(route) + if route == '/app': + return b'{"id":11,"slug":"cwl-noema-review"}' + if route.endswith('/installation'): + return b'{"id":7,"app_id":11}' + return b'{"token":"SYNTHETIC_ONLY","permissions":{"contents":"read","metadata":"read","pull_requests":"write"},"repositories":[{"full_name":"ContextualWisdomLab/example"}]}' + output = private / 'prepare.json' + with pytest.raises(ValueError, match='phase grant'): + produce_issuance('prepare', scope={'repository':'ContextualWisdomLab/example', + 'reviewer':'cwl-noema-review[bot]'}, installation_id=7, app_id=11, + output=output, private_parent=private, request=request, revalidate=lambda: None) + assert not output.exists() and len(calls) == 3 + + +def test_revocation_during_issuer_identity_wait_prevents_token_request(tmp_path): + """The post-wait fence denies token POST rather than only later output.""" + import pytest + from scripts.ci.noema_bootstrap_producer import produce_issuance + private = tmp_path / 'private' + private.mkdir(mode=0o700) + calls = [] + def request(method, route, payload): + """Record the only reached synthetic identity read.""" + calls.append(route) + return b'{"id":11,"slug":"cwl-noema-review"}' + def revalidate(): + """Revoke once the identity read has returned.""" + if calls: + raise ValueError('FIXTURE_REVOKED') + with pytest.raises(ValueError, match='FIXTURE_REVOKED'): + produce_issuance('prepare', scope={'repository':'ContextualWisdomLab/example', + 'reviewer':'cwl-noema-review[bot]'}, installation_id=7, app_id=11, + output=private/'prepare.json', private_parent=private, + request=request, revalidate=revalidate) + assert calls == ['/app'] and not (private/'prepare.json').exists() + + +def composition_fixture(tmp_path, mutation=None): + """Native inert children consume real producer files with separated grants.""" + import importlib.util + import hashlib + import sys + from scripts.ci import noema_bootstrap_producer as producer + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + source = tmp_path/'supplier' + entry = source/'.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + envelope = {'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'], + 'reviewer_actor':scope['reviewer'],'pull_request_author':'writer', + 'verdict':{'decision':'approve'}} + entry.write_text('import os,sys,json\nfrom pathlib import Path\n' + 'a=sys.argv\n' + 'if "--prepare-verdict-file" in a:\n' + ' assert os.environ["GH_TOKEN"]=="SYNTHETIC_READ"\n' + ' assert os.environ["NOEMA_LLM_MODEL"]=="auto"\n' + ' p=Path(a[a.index("--prepare-verdict-file")+1]);p.write_text('+repr(json.dumps(envelope))+');p.chmod(0o600)\n' + 'else:\n' + ' assert os.environ["GH_TOKEN"]=="SYNTHETIC_WRITE"\n' + ' assert "NOEMA_LLM_API_KEY" not in os.environ\n' + ' p=Path(a[a.index("--publish-verdict-file")+1]);Path(str(p)+".marker").write_text("PUBLISHED_FIXTURE");p.unlink()\n') + manifest = {'schema_version':1,'repository':'ContextualWisdomLab/.github','commit':'c'*40, + 'tree':'d'*40,'entrypoint':entry.relative_to(source).as_posix(), + 'files':{entry.relative_to(source).as_posix():hashlib.sha256(entry.read_bytes()).hexdigest()}} + raw=json.dumps(manifest).encode() + private=tmp_path/'private';private.mkdir(mode=0o700) + interpreter=Path(sys.executable).resolve() + admission={'namespace':str(private),'scope':scope,'installation_id':7,'app_id':11, + 'interpreter':{'path':str(interpreter),'sha256':hashlib.sha256(interpreter.read_bytes()).hexdigest()}, + 'dependencies':{},**{k:True for k in ('isolated_clean_job','native_custody', + 'supplier_cost','supplier_retention','issuer_phase_separation')}} + events=[] + def live(): + """Return exact synthetic live scope, never a real GitHub response.""" + return {**scope,'state':'OPEN','draft':False} + def request(method,route,payload): + """Assert original requested grant at the actual issuer call.""" + if route=='/app':return b'{"id":11,"slug":"cwl-noema-review"}' + if route.endswith('/installation'):return b'{"id":7,"app_id":11}' + mode=payload['permissions']['pull_requests'];events.append(mode) + if mode=='write':assert (private/'prepare.json').exists() + return json.dumps({'token':'SYNTHETIC_READ' if mode=='read' else 'SYNTHETIC_WRITE', + 'permissions':payload['permissions'],'repositories':[{'full_name':scope['repository']}]}).encode() + if mutation == 'prepare_failure': + entry.write_text('raise SystemExit(23)\n') + manifest['files'][entry.relative_to(source).as_posix()] = hashlib.sha256(entry.read_bytes()).hexdigest() + raw = json.dumps(manifest).encode() + if mutation == 'missing_cost': + admission['supplier_cost'] = False + def execute(): + """Execute unchanged real composition with this case's synthetic inputs.""" + return producer.run_bootstrap_review(source,raw,accepted_digest=hashlib.sha256(raw).hexdigest(), + admission=admission,private_parent=private,fetch_live=live,request=request, + revalidate=lambda:None,inference_key='SYNTHETIC_INFERENCE') + return execute, events, private + + +def test_actual_producer_controller_composition_issues_publish_only_after_prepare(tmp_path): + """Retain original composition oracle without replaying its completed GREEN.""" + execute, events, _ = composition_fixture(tmp_path) + assert execute() == 0 and events == ['read', 'write'] + + +def test_new_prepare_failure_never_requests_publisher(tmp_path): + """A real failed child blocks independent publication issuance.""" + import pytest + execute, events, private = composition_fixture(tmp_path, 'prepare_failure') + with pytest.raises(ValueError, match='preparation failed'): + execute() + assert events == ['read'] and not (private/'publish.json').exists() + + +def test_new_missing_runtime_cost_refuses_before_issuer(tmp_path): + """Missing actual acceptance stays false rather than derived from manifest.""" + import pytest + execute, events, private = composition_fixture(tmp_path, 'missing_cost') + with pytest.raises(ValueError, match='execution evidence'): + execute() + assert events == [] and not (private/'prepare.json').exists() + + +def test_producer_cli_missing_independent_inputs_denies_before_issuer(tmp_path): + """Actual CLI must refuse missing pins instead of successful no-op execution.""" + import subprocess + import sys + result = subprocess.run([sys.executable, '-B', '-m', + 'scripts.ci.noema_bootstrap_producer', '--materialize', + '--private-parent', str(tmp_path/'private'), '--source-root', str(tmp_path/'absent'), + '--manifest', str(tmp_path/'missing'), '--accepted-digest', '', + '--admission', str(tmp_path/'missing-admission'), '--admission-digest', '', + '--repo', 'ContextualWisdomLab/example', '--pr-number', '7', + '--expected-head', 'a'*40], env={'PATH':'/usr/bin:/bin'}, + capture_output=True, text=True, timeout=10) + assert result.returncode != 0, 'missing producer CLI accepts unavailable inputs as no-op' + assert 'Traceback' not in result.stderr, 'producer refusal leaks an uncaught adapter exception' + assert not (tmp_path/'private').exists() + + +def test_pinned_authenticated_transport_dispatches_original_phase_request(tmp_path): + """Actual fixed-origin adapter must preserve response bytes, not metadata.""" + import pytest + from scripts.ci import noema_bootstrap_producer as producer + private = tmp_path/'private';private.mkdir(mode=0o700) + jwt = private/'jwt';jwt.write_bytes(b'SYNTHETIC_JWT_ONLY');jwt.chmod(0o600) + calls=[] + class Response: + """Minimal trusted transport fixture, never a live HTTP response.""" + def __enter__(self):return self + def __exit__(self,*args):return False + def read(self,size):return b'{"original":"FIXTURE"}' + def opener(request,timeout): + """Inspect actual serialized fixed-origin request.""" + calls.append(request) + return Response() + transport = None + if hasattr(producer,'authenticated_transport'): + transport=producer.authenticated_transport(jwt, private, opener=opener) + assert transport is not None, 'actual issuer transport connection is missing' + raw=transport('POST','/app/installations/7/access_tokens', + {'permissions':{'pull_requests':'read'},'repositories':['example']}) + assert raw==b'{"original":"FIXTURE"}' + assert calls[0].full_url=='https://api.github.com/app/installations/7/access_tokens' + assert json.loads(calls[0].data)['permissions']=={'pull_requests':'read'} + + +def test_workflow_invokes_real_bootstrap_and_phase_producer_before_controller(): + """A module sitting unused on disk cannot supply any issuance output.""" + workflow=(ROOT/'.github/workflows/noema-review.yml').read_text() + issuer=(ROOT/'scripts/ci/noema_existing_app_issuer.cjs').read_text() + assert workflow.count('scripts/ci/noema_bootstrap_producer.py') == 2 + assert workflow.index('--materialize') < workflow.index('--phase prepare') < workflow.index('--revalidate-publish') < workflow.index('--phase publish') + assert workflow.count('node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs"') == 2 + assert "child('premerge_review_tooling.py',args,env)" in issuer + assert "'--'+phase+'-production'" in issuer + assert "'--phase-issuance',file" in issuer + assert "await fence(read.token,phase);" in issuer + assert issuer.index("await fence(read.token,phase);") < issuer.index("selected=await issue('write',read.token);") + + +def test_actual_publish_request_has_only_controller_write_permissions(): + """Publication grant is independently refreshed without unrelated privileges.""" + workflow=(ROOT/'.github/workflows/noema-review.yml').read_text() + step=workflow.split(' - name: Refresh repository-scoped Noema GitHub App token for publication\n',1)[1] + step=step.split(' - name:',1)[0] + permissions={name.replace('-','_'):value for name,value in + re.findall(r' permission-([a-z-]+): (read|write)',step)} + assert permissions=={'contents':'read','metadata':'read','pull_requests':'read'}, 'metadata action requests excess permissions' + issuer=(ROOT/'scripts/ci/noema_existing_app_issuer.cjs').read_text() + assert "const permissions={contents:'read',metadata:'read',pull_requests:grant};" in issuer + assert "selected=await issue('write',read.token);" in issuer + assert "{repositories:[scope.repository.split('/')[1]],permissions},jwt" in issuer + assert "Object.keys(response.permissions||{}).length!==3" in issuer + assert "Object.entries(permissions).some(([k,v])=>response.permissions[k]!==v)" in issuer diff --git a/tests/test_noema_issuer_producer_controls.py b/tests/test_noema_issuer_producer_controls.py new file mode 100644 index 0000000000..1fbe758403 --- /dev/null +++ b/tests/test_noema_issuer_producer_controls.py @@ -0,0 +1,283 @@ +"""Additional new fail-closed issuer boundaries using fixture-only authority.""" +import hashlib +import json +import os +from pathlib import Path + +import pytest +from scripts.ci import noema_bootstrap_producer as producer + + +def issuer_case(tmp_path): + """Return private native paths and a recording, original-byte issuer.""" + private = tmp_path / 'private' + private.mkdir(mode=0o700) + scope = {'repository': 'ContextualWisdomLab/example', 'reviewer': 'cwl-noema-review[bot]'} + app = {'id': 11, 'slug': 'cwl-noema-review'} + installation = {'id': 7, 'app_id': 11} + response = {'token': 'FIXTURE_ONLY_NEVER_LIVE', + 'permissions': {'contents': 'read', 'metadata': 'read', 'pull_requests': 'read'}, + 'repositories': [{'full_name': scope['repository']}]} + calls = [] + + def request(method, route, payload): + """Project exact fixture responses rather than inventing action metadata.""" + calls.append((method, route, payload)) + value = app if route == '/app' else installation if route.endswith('/installation') else response + return json.dumps(value).encode() + + return private, scope, app, installation, response, calls, request + + +@pytest.mark.parametrize('mutation', ['wrong_app', 'wrong_actor', 'wrong_installation', + 'wrong_repository', 'metadata_only', 'output_existing', 'output_symlink', + 'output_hardlink', 'output_external', 'output_same_jwt']) +def test_new_issuer_identity_and_delivery_controls(tmp_path, mutation): + """Each distinct malformed boundary rejects without overwriting a private file.""" + private, scope, app, installation, response, calls, request = issuer_case(tmp_path) + output = private / 'prepare.json' + sentinel = private / 'sentinel' + sentinel.write_bytes(b'KEEP_ORIGINAL') + sentinel.chmod(0o600) + if mutation == 'wrong_app': + app['id'] = 12 + elif mutation == 'wrong_actor': + app['slug'] = 'other-reviewer' + elif mutation == 'wrong_installation': + installation['id'] = 8 + elif mutation == 'wrong_repository': + response['repositories'][0]['full_name'] = 'ContextualWisdomLab/other' + elif mutation == 'metadata_only': + response.pop('token') + elif mutation == 'output_existing': + output.write_bytes(b'KEEP_EXISTING') + elif mutation == 'output_symlink': + output.symlink_to(sentinel) + elif mutation == 'output_hardlink': + os.link(sentinel, output) + elif mutation == 'output_external': + output = tmp_path / 'foreign.json' + elif mutation == 'output_same_jwt': + output = sentinel + with pytest.raises(ValueError): + producer.produce_issuance('prepare', scope=scope, installation_id=7, app_id=11, + output=output, private_parent=private, request=request, revalidate=lambda: None) + assert sentinel.read_bytes() == b'KEEP_ORIGINAL' + if mutation not in ('output_existing', 'output_symlink', 'output_hardlink', 'output_same_jwt'): + assert not output.exists() + if mutation.startswith('output_'): + assert calls == [] + + +def test_new_issuer_boolean_app_identity_cannot_match_integer(tmp_path): + """Exact integer admission must not permit Python's true-equals-one alias.""" + private, scope, app, installation, response, calls, request = issuer_case(tmp_path) + app['id'] = True + installation['app_id'] = 1 + with pytest.raises(ValueError, match='App identity'): + producer.produce_issuance('prepare', scope=scope, installation_id=7, app_id=1, + output=private/'prepare.json', private_parent=private, + request=request, revalidate=lambda: None) + assert [row[1] for row in calls] == ['/app'] + + +def bootstrap_case(tmp_path): + """Build exact stdlib-only tooling and explicit synthetic runtime pins.""" + import sys + private = tmp_path / 'private' + private.mkdir(mode=0o700) + source = tmp_path / 'supplier' + entry = source / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + entry.write_bytes(b'# INERT ORIGINAL SUPPLIER\n') + manifest = {'schema_version': 1, 'repository': 'ContextualWisdomLab/.github', + 'commit': 'c'*40, 'tree': 'd'*40, + 'entrypoint': entry.relative_to(source).as_posix(), + 'files': {entry.relative_to(source).as_posix(): hashlib.sha256(entry.read_bytes()).hexdigest()}} + interpreter = Path(sys.executable).resolve() + admission = {'namespace':str(private), 'dependencies':{}, + 'interpreter':{'path':str(interpreter), 'sha256':hashlib.sha256(interpreter.read_bytes()).hexdigest()}, + **{field:True for field in ('isolated_clean_job','native_custody','supplier_cost', + 'supplier_retention','issuer_phase_separation')}} + raw = json.dumps(manifest).encode() + return private, source, entry, raw, admission + + +@pytest.mark.parametrize('mutation', ['stale_pin','source_drift','missing_custody', + 'wrong_interpreter','bad_dependency','foreign_namespace','same_source_target','target_symlink']) +def test_new_bootstrap_supply_and_runtime_controls(tmp_path, mutation): + """Invalid source/namespace/runtime evidence refuses before any phase dispatch.""" + private, source, entry, raw, admission = bootstrap_case(tmp_path) + digest = hashlib.sha256(raw).hexdigest() + target = private/'tooling' + if mutation == 'stale_pin': digest = '0'*64 + elif mutation == 'source_drift': entry.write_bytes(b'# MODIFIED AFTER INDEPENDENT PIN\n') + elif mutation == 'missing_custody': admission['native_custody'] = False + elif mutation == 'wrong_interpreter': admission['interpreter']['sha256'] = '0'*64 + elif mutation == 'bad_dependency': admission['dependencies'][str(entry)] = '0'*64 + elif mutation == 'foreign_namespace': admission['namespace'] = str(tmp_path) + elif mutation == 'same_source_target': target = source + elif mutation == 'target_symlink': target.symlink_to(source) + with pytest.raises(ValueError): + producer.materialize_tooling(source,target,raw,accepted_digest=digest, + admission=admission,private_parent=private) + assert entry.read_bytes().startswith(b'#') + if mutation not in ('same_source_target','target_symlink'): + assert not target.exists() + + +def test_dependency_revocation_during_live_wait_blocks_issuance(tmp_path, monkeypatch): + """A changed runtime supplier cannot survive the CLI metadata wait.""" + from types import SimpleNamespace + private, source, entry, raw, admission = bootstrap_case(tmp_path) + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + dependency = tmp_path/'dependency' + dependency.write_bytes(b'ACCEPTED_STDLIB_FIXTURE') + admission.update(scope=scope, installation_id=7, app_id=11) + admission['dependencies'][str(dependency)] = hashlib.sha256(dependency.read_bytes()).hexdigest() + producer.materialize_tooling(source, private/'tooling', raw, + accepted_digest=hashlib.sha256(raw).hexdigest(), admission=admission, private_parent=private) + manifest_file, admission_file = tmp_path/'manifest', tmp_path/'admission' + manifest_file.write_bytes(raw); admission_file.write_text(json.dumps(admission)) + jwt = private/'prepare-app.jwt';jwt.write_bytes(b'FIXTURE_ONLY');jwt.chmod(0o600) + calls=[] + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}}, + 'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + def metadata(*args, **kwargs): + """Return valid fixture PR but revoke one pinned dependency during waiting.""" + dependency.write_bytes(b'REVOKED_RUNTIME_SUPPLIER') + return SimpleNamespace(stdout=json.dumps(pr)) + def auth(*args, **kwargs): + """Expose whether revoked authority ever reaches credential transport.""" + def request(*args): + calls.append(args) + return b'{}' + return request + monkeypatch.setattr(producer.subprocess,'run',metadata) + monkeypatch.setattr(producer,'authenticated_transport',auth) + argv=['--issue-prepare','--private-parent',str(private),'--source-root',str(source), + '--manifest',str(manifest_file),'--accepted-digest',hashlib.sha256(raw).hexdigest(), + '--admission',str(admission_file),'--admission-digest',hashlib.sha256(admission_file.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'], + '--app-jwt-file',str(jwt)] + assert producer.main(argv)==1 + assert calls==[], 'runtime dependency drift reached issuer after live metadata wait' + + +@pytest.mark.parametrize('mutation', ['manifest', 'tooling']) +def test_post_live_metadata_pin_drift_never_reaches_authenticated_issuer(tmp_path, monkeypatch, mutation): + """Source and manifest are re-read after the actual metadata wait too.""" + from types import SimpleNamespace + private, source, entry, raw, admission = bootstrap_case(tmp_path) + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + admission.update(scope=scope, installation_id=7, app_id=11) + producer.materialize_tooling(source, private/'tooling', raw, + accepted_digest=hashlib.sha256(raw).hexdigest(), admission=admission, private_parent=private) + manifest_file, admission_file = tmp_path/'manifest', tmp_path/'admission' + manifest_file.write_bytes(raw); admission_file.write_text(json.dumps(admission)) + jwt=private/'prepare-app.jwt';jwt.write_bytes(b'FIXTURE_ONLY');jwt.chmod(0o600) + calls=[] + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}}, + 'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + def metadata(*args, **kwargs): + """Replace a source pin after the pre-wait validation succeeded.""" + if mutation=='manifest':manifest_file.write_bytes(b'{}') + else:(private/'tooling'/entry.relative_to(source)).write_bytes(b'# STALE CHILD\n') + return SimpleNamespace(stdout=json.dumps(pr)) + def auth(*args, **kwargs): + """Assert no credential read, not merely no final response file.""" + calls.append('credential') + return lambda *args:b'{}' + monkeypatch.setattr(producer.subprocess,'run',metadata) + monkeypatch.setattr(producer,'authenticated_transport',auth) + argv=['--issue-prepare','--private-parent',str(private),'--source-root',str(source), + '--manifest',str(manifest_file),'--accepted-digest',hashlib.sha256(raw).hexdigest(), + '--admission',str(admission_file),'--admission-digest',hashlib.sha256(admission_file.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'], + '--app-jwt-file',str(jwt)] + assert producer.main(argv)==1 + assert calls==[], 'post-live source drift reached credential acquisition' + + +def test_new_cli_producer_original_responses_feed_actual_controller_children(tmp_path, monkeypatch): + """Exercise real CLI functions plus native children; only HTTP/metadata is inert.""" + from types import SimpleNamespace + from scripts.ci import premerge_review_tooling as controller + import subprocess + import sys + import urllib.request + private, source, entry, _, admission = bootstrap_case(tmp_path) + scope={'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + envelope={'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'], + 'reviewer_actor':scope['reviewer'],'pull_request_author':'writer', + 'verdict':{'decision':'approve'}} + entry.write_text('import os,sys\nfrom pathlib import Path\n' + 'a=sys.argv\n' + 'if "--prepare-verdict-file" in a:\n' + ' assert os.environ["GH_TOKEN"]=="NEW_FIXTURE_READ"\n' + ' assert os.environ["NOEMA_LLM_MODEL"]=="auto"\n' + ' p=Path(a[a.index("--prepare-verdict-file")+1]);p.write_text('+repr(json.dumps(envelope))+');p.chmod(0o600)\n' + 'else:\n' + ' assert os.environ["GH_TOKEN"]=="NEW_FIXTURE_WRITE"\n' + ' assert "LLM_GATEWAY_API_KEY" not in os.environ and "NOEMA_LLM_API_KEY" not in os.environ\n' + ' p=Path(a[a.index("--publish-verdict-file")+1]);Path(str(p)+".marker").write_text("NEW_NATIVE_FIXTURE");p.unlink()\n') + manifest={'schema_version':1,'repository':'ContextualWisdomLab/.github','commit':'c'*40, + 'tree':'d'*40,'entrypoint':entry.relative_to(source).as_posix(), + 'files':{entry.relative_to(source).as_posix():hashlib.sha256(entry.read_bytes()).hexdigest()}} + raw=json.dumps(manifest).encode() + admission.update(scope=scope,installation_id=7,app_id=11) + mf,af=tmp_path/'manifest',tmp_path/'admission';mf.write_bytes(raw);af.write_text(json.dumps(admission)) + common=['--private-parent',str(private),'--source-root',str(source), + '--manifest',str(mf),'--accepted-digest',hashlib.sha256(raw).hexdigest(), + '--admission',str(af),'--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head']] + events=[] + class Response: + """Hold raw response bytes with exact declared length and native cleanup.""" + def __init__(self,raw): + self.raw=raw;self.headers={'Content-Length':str(len(raw))} + def __enter__(self):return self + def __exit__(self,*args):return False + def read(self,size):return self.raw + def open_fixture(req,timeout): + """Return exact API fixture bodies to the real fixed-origin adapter.""" + if req.full_url.endswith('/app'):return Response(b'{"id":11,"slug":"cwl-noema-review"}') + if req.full_url.endswith('/installation'):return Response(b'{"id":7,"app_id":11}') + body=json.loads(req.data);phase=body['permissions']['pull_requests'];events.append(phase) + assert body['repositories']==['example'] + if phase=='write':assert (private/'verdict.json').exists() + return Response(json.dumps({'token':'NEW_FIXTURE_READ' if phase=='read' else 'NEW_FIXTURE_WRITE', + 'permissions':body['permissions'],'repositories':[{'full_name':scope['repository']}]}).encode()) + monkeypatch.setattr(urllib.request,'build_opener',lambda *args:SimpleNamespace(open=open_fixture)) + native_run=subprocess.run + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}}, + 'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + def metadata_or_native(argv,**kwargs): + """Fixture only gh metadata; execute the real isolated Python child.""" + if argv[0]=='gh':return SimpleNamespace(stdout=json.dumps(pr)) + return native_run(argv,**kwargs) + monkeypatch.setattr(subprocess,'run',metadata_or_native) + monkeypatch.setenv('LLM_GATEWAY_API_KEY','NEW_FIXTURE_INFERENCE') + assert producer.main(['--materialize',*common])==0 + for phase in ('prepare','publish'): + jwt=private/(phase+'-app.jwt');jwt.write_bytes(b'NEW_FIXTURE_JWT');jwt.chmod(0o600) + if phase=='publish': + assert producer.main(['--revalidate-publish',*common,'--verdict-file',str(private/'verdict.json')])==0 + argv=['--issue-'+phase,*common,'--app-jwt-file',str(jwt)] + if phase=='publish':argv+=['--verdict-file',str(private/'verdict.json')] + assert producer.main(argv)==0 + response=json.loads((private/(phase+'.json')).read_bytes()) + assert response['permissions']['pull_requests']==('read' if phase=='prepare' else 'write') + assert controller.production_main(['--'+phase+'-production', + '--tooling-root',str(private/'tooling'), + '--manifest',str(mf),'--accepted-digest',hashlib.sha256(raw).hexdigest(), + '--admission',str(af),'--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'], + '--phase-issuance',str(private/(phase+'.json')), + '--verdict-file',str(private/'verdict.json')])==0 + assert events==['read','write'] + assert (private/'verdict.json.marker').read_text()=='NEW_NATIVE_FIXTURE' diff --git a/tests/test_noema_native_metadata_credentials.py b/tests/test_noema_native_metadata_credentials.py index a69fff03f5..eccef79460 100644 --- a/tests/test_noema_native_metadata_credentials.py +++ b/tests/test_noema_native_metadata_credentials.py @@ -34,7 +34,11 @@ def test_native_metadata_token_reaches_both_head_reads(tmp_path: Path, name: str "secrets.OPENCODE_APPROVE_TOKEN": "synthetic-approve" if source == "approve" else "", "github.token": "workflow-only", } - token = next(credentials.get(term.strip()) for term in expression.group(1).split("||") if credentials.get(term.strip())) + expected_legacy = "secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token" + expected_kcsap = "env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_metadata_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (" + expected_legacy + ")) || ''" + assert expression.group(1) == expected_kcsap + legacy_terms = expected_legacy.split(" || ") + token = next((credentials.get(term.strip(), "") for term in legacy_terms if credentials.get(term.strip(), "")), "") gh = tmp_path / "gh" live_head = ("a" if current else "c") * 40 payload = json.dumps({"state": "open", "head": {"sha": live_head}, "base": {"sha": "b" * 40}}) diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 8de7551814..7d88b8188a 100644 --- a/tests/test_noema_orchestrator_workflow_contract.py +++ b/tests/test_noema_orchestrator_workflow_contract.py @@ -10,6 +10,13 @@ import textwrap from pathlib import Path +import pytest + +from tests.test_noema_draft_admission_before_sidecar import ( + EXPECTED_UPLOAD_CONDITION, + _parsed_noema_job, + _sidecar_upload_is_fail_closed, +) from tests.test_required_workflow_queue_contract import workflow_step, workflow_text @@ -194,15 +201,19 @@ def test_noema_review_credentials_and_llm_use_orchestrator_free() -> None: assert "NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }}" in workflow assert "OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}" in workflow assert "OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}" in workflow - assert 'export NOEMA_LLM_MODEL="orchestrator/free"' in workflow + assert 'export NOEMA_LLM_MODEL="orchestrator/auto"' in workflow prepare = workflow_step(workflow, "Prepare Noema model verdict") publish = workflow_step(workflow, "Publish prepared Noema verdict on the exact live head") - assert '.github/actions/noema-review/two_phase.py' in prepare - assert '--prepare-verdict-file "$verdict_file"' in prepare + assert 'scripts/ci/noema_existing_app_issuer.cjs' in prepare + assert '--phase prepare' in prepare + assert '--verdict-file "$verdict_file"' in prepare + assert '--private-parent "$RUNNER_TEMP/noema-private-bootstrap"' in prepare assert "NOEMA_TRANSPORT_RETRY_ATTEMPT" in prepare assert "toJSON(github.event.client_payload.transport_retry_attempt)" in prepare - assert '.github/actions/noema-review/two_phase.py' in publish - assert '--publish-verdict-file "$verdict_file"' in publish + assert 'scripts/ci/noema_existing_app_issuer.cjs' in publish + assert '--phase publish' in publish + assert '--verdict-file "$verdict_file"' in publish + assert '--private-parent "$RUNNER_TEMP/noema-private-bootstrap"' in publish redispatch = workflow_step(workflow, "Schedule bounded Noema transport re-dispatch") assert "needs.noema-review.outputs.transport_capacity_unavailable == 'true'" in workflow assert "needs.noema-review.outputs.transport_retry_eligible == 'true'" in workflow @@ -460,10 +471,20 @@ def test_noema_private_admission_uses_existing_metadata_credentials( case = tmp_path / str(index) case.mkdir() values = {"github.token": "workflow-only", **credentials} + # This fixture targets private-example, not the App-only k-csap path. + # Validate the branch guard before evaluating its legacy OR chain. + guarded = re.fullmatch( + r"env\.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' " + r"&& steps\.noema_metadata_app_token\.outputs\.token \|\| " + r"\(env\.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' " + r"&& \((.*?)\)\) \|\| ''", + expression, + ) + assert guarded is not None token = next( ( values.get(term.strip(), "") - for term in expression.split("||") + for term in guarded.group(1).split("||") if values.get(term.strip(), "") ), "", @@ -506,14 +527,8 @@ def test_noema_private_admission_uses_existing_metadata_credentials( workflow, "Reject a stale trigger before credential or model setup" ) guard_expression = re.search(r"GH_TOKEN: \$\{\{ (.*?) \}\}", guard).group(1) - guard_token = next( - ( - values.get(term.strip(), "") - for term in guard_expression.split("||") - if values.get(term.strip(), "") - ), - "", - ) + assert guard_expression == expression + guard_token = token guard_result = subprocess.run( [ shutil.which("bash") or "/bin/bash", @@ -592,7 +607,7 @@ def test_strix_gateway_default_and_noema_sidecar_fail_closed(tmp_path: Path) -> env={ **os.environ, "GITHUB_OUTPUT": str(strix_output), - "STRIX_MODEL": "contextual-orchestrator/orchestrator/free", + "STRIX_MODEL": "auto", "STRIX_MODEL_REQUESTED": "", }, capture_output=True, @@ -601,12 +616,12 @@ def test_strix_gateway_default_and_noema_sidecar_fail_closed(tmp_path: Path) -> ) assert strix.returncode == 0, strix.stderr assert { - "strix_model=contextual-orchestrator/orchestrator/free", + "strix_model=auto", "enabled=true", "provider_mode=contextual_orchestrator", } <= set(strix_output.read_text().splitlines()) assert ( - "STRIX_MODEL: contextual-orchestrator/orchestrator/free" + "STRIX_MODEL: auto" in workflow_text("strix.yml") ) assert ( @@ -700,28 +715,166 @@ def test_noema_review_job_has_no_job_level_timeout() -> None: def test_noema_review_retains_sanitized_sidecar_evidence_after_any_outcome() -> None: - """Retain existing sanitized evidence on success, failure and cancellation. + """Only current non-Draft terminal producers retain the two sanitized paths. - A forced runner shutdown can still prevent upload; this contract only - removes the failure-only gate without adding raw logs or new files. + Forced runner shutdown may still prevent upload. This local contract does + not prove hosted retention, inference, or a qualifying App-owned approval. """ workflow = workflow_text("noema-review.yml") name = "Upload contextual-orchestrator sidecar evidence" - step = workflow_step(workflow, name) - assert "if: always() && env.PR_NUMBER != ''" in step + uploads = [ + step for step in _parsed_noema_job(workflow)["steps"] + if str(step.get("uses", "")).casefold().startswith("actions/upload-artifact@") + ] + assert len(uploads) == 1 + step = uploads[0] + assert _sidecar_upload_is_fail_closed(workflow) + assert step["if"] == EXPECTED_UPLOAD_CONDITION strix_pin = re.search( r"actions/upload-artifact@([0-9a-f]{40})", workflow_text("strix.yml") ).group(1) - assert f"actions/upload-artifact@{strix_pin}" in step - assert "name: noema-sidecar-evidence" in step - assert "strix_runs/contextual-orchestrator-sidecar.stderr.log" in step - assert "strix_runs/contextual-orchestrator-preflight.json" in step - assert "if-no-files-found: ignore" in step - assert "retention-days: 5" in step + assert step["uses"] == f"actions/upload-artifact@{strix_pin}" + assert step["with"] == { + "name": "noema-sidecar-evidence", + "path": "strix_runs/contextual-orchestrator-sidecar.stderr.log\n" + "strix_runs/contextual-orchestrator-preflight.json\n", + "if-no-files-found": "ignore", + "retention-days": "5", + } prepare = workflow.index(" - name: Prepare Noema model verdict\n") upload = workflow.index(f" - name: {name}\n") refresh = workflow.index( " - name: Refresh repository-scoped Noema GitHub App token for publication\n" ) assert prepare < upload < refresh - assert workflow.count("actions/upload-artifact@") == 1 + + +OWNED_EVIDENCE = ( + "contextual-orchestrator-preflight.json", + "contextual-orchestrator-sidecar.stderr.log", +) + + +def _evidence_lifecycle_steps() -> tuple[dict, dict]: + """Read initialization and the real producer by parsed identities, in order.""" + steps = _parsed_noema_job(workflow_text("noema-review.yml"))["steps"] + producer = next(step for step in steps if step.get("id") == "noema_sidecar") + # Until initialization exists, execute the defective producer prefix for RED. + initializer = next((step for step in steps if step.get("id") == "noema_evidence"), producer) + return initializer, producer + + +def _run_evidence_lifecycle(workspace: Path, phase: str, marker: Path) -> subprocess.CompletedProcess[str]: + """Execute actual setup/run bodies with a harmless early-failure sidecar.""" + initializer, producer = _evidence_lifecycle_steps() + sidecar = workspace / "scripts/ci/contextual_orchestrator_review_sidecar.sh" + sidecar.parent.mkdir(parents=True, exist_ok=True) + sidecar.write_text( + '#!/bin/bash\nset -euo pipefail\nprintf reached >"$PHASE_MARKER"\n' + 'if [ "$PHASE" = early ]; then exit 23; fi\n' + 'printf "current sanitized diagnostic\\n" >"$GITHUB_WORKSPACE/strix_runs/contextual-orchestrator-sidecar.stderr.log"\n' + 'printf \'{"contract":"strix-plain-chat-preflight-v2","ready_count":0,"probed_count":1,\n' + '"routes":[{"status":"rejected","http_status":429}]}\\n\' ' + '>"$GITHUB_WORKSPACE/strix_runs/contextual-orchestrator-preflight.json"\n' + 'if [ "$PHASE" = failure ]; then exit 23; fi\n', + encoding="utf-8", + ) + bodies = [initializer["run"]] if initializer is producer else [initializer["run"], producer["run"]] + env = {**os.environ, "GITHUB_WORKSPACE": str(workspace), "PHASE": phase, "PHASE_MARKER": str(marker)} + result = None + for body in bodies: + result = subprocess.run( + [shutil.which("bash") or "/bin/bash", "-c", body], + env=env, capture_output=True, text=True, check=False, timeout=5, + ) + if result.returncode: + break # Actions default success() skips the producer after setup failure. + assert result is not None + return result + + +def test_evidence_initialization_failure_skips_actual_producer() -> None: + """An unsafe setup must not become a terminal producer eligible for upload.""" + initializer, producer = _evidence_lifecycle_steps() + steps = _parsed_noema_job(workflow_text("noema-review.yml"))["steps"] + assert initializer.get("id") == "noema_evidence" + assert steps.index(initializer) < steps.index(producer) + assert initializer["if"] == producer["if"] + assert "always()" not in producer["if"] + assert "continue-on-error" not in initializer + assert "continue-on-error" not in producer + + +@pytest.mark.parametrize("phase", ["early", "failure", "success"]) +def test_evidence_lifecycle_clears_stale_files_before_sidecar(tmp_path: Path, phase: str) -> None: + """Repeat actual initialization; preserve sentinels and genuine current diagnostics.""" + workspace = tmp_path / "workspace" + evidence = workspace / "strix_runs" + evidence.mkdir(parents=True) + sentinel = evidence / "unrelated-sentinel" + sentinel.write_text("keep unrelated\n") + marker = tmp_path / "producer-reached" + for _ in range(2): + marker.unlink(missing_ok=True) + for name in OWNED_EVIDENCE: + (evidence / name).write_text("old evidence\n") + result = _run_evidence_lifecycle(workspace, phase, marker) + assert marker.read_text() == "reached" + assert result.returncode == (0 if phase == "success" else 23), result.stderr + assert sentinel.read_text() == "keep unrelated\n" + if phase == "early": + assert all(not (evidence / name).exists() for name in OWNED_EVIDENCE) + else: + assert (evidence / OWNED_EVIDENCE[1]).read_text() == "current sanitized diagnostic\n" + report = json.loads((evidence / OWNED_EVIDENCE[0]).read_text()) + assert report["contract"] == "strix-plain-chat-preflight-v2" + assert report["routes"][0]["http_status"] == 429 + assert sorted(p.name for p in evidence.iterdir()) == ( + ["unrelated-sentinel"] if phase == "early" else sorted((*OWNED_EVIDENCE, "unrelated-sentinel")) + ) + + +@pytest.mark.parametrize("alias", ["linked-parent", "linked-workspace", "linked-evidence", "dotdot", + "symlink-preflight", "symlink-stderr", "hardlink-preflight", "hardlink-stderr", + "directory-preflight", "directory-stderr"]) +def test_evidence_initialization_rejects_path_aliases_without_deletion(tmp_path: Path, alias: str) -> None: + """Reject aliases before deleting either owned file or reaching the sidecar.""" + actual = tmp_path / "real/workspace" + evidence = actual / "strix_runs" + evidence.mkdir(parents=True) + sentinel = tmp_path / "unrelated-sentinel" + sentinel.write_text("keep outside\n") + for name in OWNED_EVIDENCE: + (evidence / name).write_text("old evidence\n") + workspace = actual + if alias == "linked-parent": + (tmp_path / "alias").symlink_to(actual.parent, target_is_directory=True) + workspace = tmp_path / "alias/workspace" + elif alias == "linked-workspace": + workspace = tmp_path / "alias" + workspace.symlink_to(actual, target_is_directory=True) + elif alias == "linked-evidence": + evidence.rename(actual / "outside") + evidence.symlink_to(actual / "outside", target_is_directory=True) + elif alias == "dotdot": + (actual / "child").mkdir() + workspace = actual / "child/.." + else: + index = 0 if alias.endswith("preflight") else 1 + target = evidence / OWNED_EVIDENCE[index] + target.unlink() + if alias.startswith("symlink"): + target.symlink_to(sentinel) + elif alias.startswith("hardlink"): + target.hardlink_to(sentinel) + else: + target.mkdir() + marker = tmp_path / "producer-reached" + result = _run_evidence_lifecycle(workspace, "early", marker) + assert result.returncode != 0 + assert not marker.exists(), result.stderr + assert sentinel.read_text() == "keep outside\n" + assert all((evidence / name).exists() for name in OWNED_EVIDENCE) + for path in evidence.iterdir(): + if path.is_file() and not path.is_symlink() and path.stat().st_nlink == 1: + assert path.read_text() == "old evidence\n" diff --git a/tests/test_noema_post_wait_interpreter_pin.py b/tests/test_noema_post_wait_interpreter_pin.py new file mode 100644 index 0000000000..990597f6f8 --- /dev/null +++ b/tests/test_noema_post_wait_interpreter_pin.py @@ -0,0 +1,55 @@ +"""New post-wait interpreter admission controls, with no operational authority.""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.mark.parametrize('drift', ['app', 'installation', 'issuance', 'controller', 'same_bytes_inode', 'none']) +def test_post_wait_shared_child_refuses_changed_interpreter(tmp_path, drift): + """Actual issuer main must reject drift before either shared spawn boundary.""" + script = tmp_path/'probe.cjs' + script.write_text(r''' +const assert=require('node:assert/strict'),fs=require('node:fs'),crypto=require('node:crypto'),https=require('node:https'); +const {EventEmitter}=require('node:events');const issuer=require(process.argv[2]); +(async()=>{ + const root=process.argv[3],drift=process.argv[4],parent=root+'/private';fs.mkdirSync(parent,{mode:0o700}); + const marker=root+'/launched',foreign=root+'/foreign',python=root+'/python'; + const original='#!/bin/sh\nprintf X >> "'+marker+'"\n'+(drift==='controller'?'cp "'+root+'/replacement" "'+python+'"\n':'')+'exit 0\n'; + const replacement='#!/bin/sh\nprintf BAD >> "'+foreign+'"\nexit 0\n'; + fs.writeFileSync(python,original,{mode:0o700});fs.writeFileSync(root+'/replacement',replacement); + const digest=raw=>crypto.createHash('sha256').update(raw).digest('hex'); + const admission={scope:{repository:'ContextualWisdomLab/example',number:7,head:'a'.repeat(40),base:'b'.repeat(40),author:'writer',reviewer:'cwl-noema-review[bot]'},app_id:11,installation_id:7, + interpreter:{path:python,sha256:digest(fs.readFileSync(python))},dependencies:{[fs.realpathSync(process.execPath)]:digest(fs.readFileSync(process.execPath))}}; + const af=root+'/admission';fs.writeFileSync(af,JSON.stringify(admission));let deletes=0,changed=false; + https.request=(opts,callback)=>{const req=new EventEmitter();let body='';req.setTimeout=()=>{};req.write=c=>body+=c;req.end=()=>{ + let raw; + if(opts.method==='DELETE'){deletes++;raw=Buffer.alloc(0);} + else if(opts.path==='/app')raw=Buffer.from('{"id":11,"slug":"cwl-noema-review"}'); + else if(opts.path.endsWith('/installation'))raw=Buffer.from('{"id":7,"app_id":11}'); + else raw=Buffer.from(JSON.stringify({token:'POSTWAIT_FIXTURE',expires_at:new Date(Date.now()+3600000).toISOString(),permissions:JSON.parse(body).permissions,repositories:[{full_name:admission.scope.repository}]})); + const stage=opts.path==='/app'?'app':opts.path.endsWith('/installation')?'installation':'issuance'; + if(!changed && opts.method!=='DELETE' && (drift===stage || (drift==='same_bytes_inode' && stage==='issuance'))){ + changed=true;if(drift==='same_bytes_inode'){fs.renameSync(python,root+'/old');fs.writeFileSync(python,original,{mode:0o700});} + else fs.writeFileSync(python,replacement); + } + const res=new EventEmitter();res.complete=true;res.statusCode=opts.method==='DELETE'?204:opts.method==='POST'?201:200;res.headers={'content-length':String(raw.length)};callback(res); + queueMicrotask(()=>{res.emit('data',raw);res.emit('end')});};return req;}; + const key=crypto.generateKeyPairSync('rsa',{modulusLength:2048}).privateKey.export({type:'pkcs8',format:'pem'}); + const args=['--phase','prepare','--private-parent',parent,'--source-root',root+'/source','--manifest',root+'/manifest','--accepted-digest','0'.repeat(64),'--admission',af,'--admission-digest',digest(fs.readFileSync(af)),'--repo',admission.scope.repository,'--pr-number','7','--expected-head',admission.scope.head,'--verdict-file',parent+'/verdict.json']; + let rejected=false;try{await issuer.main(args,{PATH:'/usr/bin:/bin',NOEMA_GITHUB_APP_CLIENT_ID:'Iv1.fixture',NOEMA_GITHUB_APP_PRIVATE_KEY:key,LLM_GATEWAY_API_KEY:'INERT_INFERENCE'});}catch{rejected=true;} + const launches=fs.existsSync(marker)?fs.readFileSync(marker,'utf8').length:0; + assert(!fs.existsSync(foreign),'unadmitted post-wait interpreter executed'); + if(drift==='none'){assert(!rejected);assert.equal(launches,2);} + else {assert(rejected,'post-wait interpreter drift was admitted');assert.equal(launches,drift==='controller'?1:0);} + assert.equal(deletes,1);assert(!fs.existsSync(parent+'/prepare.json')); + console.log('POST_WAIT_CONTROL_PASS'); +})().catch(error=>{console.error(error.message);process.exitCode=1}); +''') + result = subprocess.run([shutil.which('node'), str(script), str(ROOT/'scripts/ci/noema_existing_app_issuer.cjs'), str(tmp_path), drift], + env={'PATH':'/usr/bin:/bin'}, capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and result.stdout.strip() == 'POST_WAIT_CONTROL_PASS', result.stderr diff --git a/tests/test_noema_premerge_production_caller.py b/tests/test_noema_premerge_production_caller.py new file mode 100644 index 0000000000..65d4d49ea1 --- /dev/null +++ b/tests/test_noema_premerge_production_caller.py @@ -0,0 +1,157 @@ +"""Production premerge caller regressions; no live credentials or inference.""" +from pathlib import Path +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] + + +def test_workflow_phases_use_controller_and_preserve_refresh_order(): + """Both real phase steps must enter the controller, with refresh between them.""" + source = (ROOT / '.github/workflows/noema-review.yml').read_text() + prepare = source.split(' - name: Prepare Noema model verdict\n', 1)[1].split(' - name:', 1)[0] + publish = source.split(' - name: Publish prepared Noema verdict on the exact live head\n', 1)[1].split('\n continue-noema-transport:', 1)[0] + issuer = (ROOT / 'scripts/ci/noema_existing_app_issuer.cjs').read_text() + for step, phase in [(prepare, 'prepare'), (publish, 'publish')]: + assert 'noema_existing_app_issuer.cjs' in step, 'phase bypasses issuer/controller' + assert '--phase ' + phase in step + assert "child('premerge_review_tooling.py',args,env)" in issuer + assert "'--'+phase+'-production'" in issuer + assert "'--phase-issuance',file" in issuer + assert '.github/actions/noema-review/two_phase.py"' not in step + assert source.index('Prepare Noema model verdict\n') < source.index('Refresh repository-scoped Noema GitHub App token for publication\n') < source.index('Publish prepared Noema verdict on the exact live head\n') + + + +def test_production_noema_requires_pinned_admission_before_reviewer_credentials(): + """Production must invoke admission before minting model/publication authority.""" + source = (ROOT / '.github/workflows/noema-review.yml').read_text() + name = ' - name: Admit independently pinned premerge tooling and execution\n' + assert name in source, 'production Noema has no premerge controller admission caller' + step = source.split(name, 1)[1].split(' - name:', 1)[0] + assert 'scripts/ci/premerge_review_tooling.py' in step + assert '--admit-production' in step + assert 'vars.NOEMA_TOOLING_MANIFEST_SHA256' in step + assert 'vars.NOEMA_EXECUTION_ADMISSION_SHA256' in step + assert source.index(name) < source.index(' - name: Select fail-closed Noema reviewer credential\n') + assert 'LLM_GATEWAY_API_KEY' not in step + assert '--accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256"' in step + + +def test_actual_cli_refuses_missing_independent_pins(tmp_path): + """An unconfigured production caller cannot silently return success.""" + result = subprocess.run( + [sys.executable, '-I', '-B', str(ROOT / 'scripts/ci/premerge_review_tooling.py'), + '--admit-production', '--tooling-root', str(tmp_path), + '--manifest', str(tmp_path / 'manifest.json'), '--accepted-digest', '', + '--admission', str(tmp_path / 'admission.json'), '--admission-digest', '', + '--repo', 'ContextualWisdomLab/example', '--pr-number', '7', + '--expected-head', 'a' * 40], + env={'PATH': '/usr/bin:/bin'}, capture_output=True, text=True, timeout=15, + ) + assert result.returncode != 0, 'CLI ignored unconfigured independent admission' + assert 'independent admission pins' in result.stderr + + +def test_actual_production_cli_binds_manifest_and_live_scope(tmp_path): + """Real CLI reaches an inert metadata executable only after independent pins.""" + import hashlib + import json + + root = tmp_path / 'tooling' + entry = root / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + entry.write_bytes(b'# INERT SOURCE FIXTURE\n') + manifest = {'schema_version': 1, 'repository': 'ContextualWisdomLab/.github', + 'commit': 'b' * 40, 'tree': 'c' * 40, + 'entrypoint': '.github/actions/noema-review/two_phase.py', + 'files': {'.github/actions/noema-review/two_phase.py': hashlib.sha256(entry.read_bytes()).hexdigest()}} + scope = {'repository': 'ContextualWisdomLab/example', 'number': 7, 'head': 'a' * 40, + 'base': 'd' * 40, 'author': 'writer', 'reviewer': 'cwl-noema-review[bot]'} + admission = {'scope': scope, **{name: True for name in + ('isolated_clean_job', 'native_custody', 'supplier_cost', 'supplier_retention', 'issuer_phase_separation')}} + manifest_path = tmp_path / 'manifest.json' + admission_path = tmp_path / 'admission.json' + manifest_path.write_text(json.dumps(manifest)) + admission_path.write_text(json.dumps(admission)) + pr = {'base': {'sha': scope['base'], 'repo': {'full_name': scope['repository']}}, + 'head': {'sha': scope['head']}, 'number': 7, 'user': {'login': 'writer'}, + 'state': 'open', 'draft': False} + gh = tmp_path / 'gh' + gh.write_text('#!/bin/sh\nprintf \'%s\' \'' + json.dumps(pr) + '\'\n') + gh.chmod(0o700) + command = [sys.executable, '-I', '-B', str(ROOT / 'scripts/ci/premerge_review_tooling.py'), + '--admit-production', '--tooling-root', str(root), '--manifest', str(manifest_path), + '--accepted-digest', hashlib.sha256(manifest_path.read_bytes()).hexdigest(), + '--admission', str(admission_path), '--admission-digest', hashlib.sha256(admission_path.read_bytes()).hexdigest(), + '--repo', scope['repository'], '--pr-number', '7', '--expected-head', scope['head']] + result = subprocess.run(command, env={'PATH': str(tmp_path) + ':/usr/bin:/bin'}, + capture_output=True, text=True, timeout=15) + assert result.returncode == 0, result.stderr + # An otherwise-valid independently pinned NOT_HELD receipt cannot pass. + admission['native_custody'] = False + admission_path.write_text(json.dumps(admission)) + command[command.index('--admission-digest') + 1] = hashlib.sha256(admission_path.read_bytes()).hexdigest() + gh.write_text('#!/bin/sh\nexit 99\n') + result = subprocess.run(command, env={'PATH': str(tmp_path) + ':/usr/bin:/bin'}, + capture_output=True, text=True, timeout=15) + assert result.returncode == 1 + assert 'custody/scope evidence unavailable' in result.stderr + + +def test_production_run_invokes_controller_with_phase_scoped_issuance(tmp_path, monkeypatch): + """Full caller reaches controller, loader and independent phase environments.""" + import importlib.util + import hashlib + import json + from types import SimpleNamespace + + spec = importlib.util.spec_from_file_location('production_caller_test', ROOT / 'scripts/ci/premerge_review_tooling.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + root = tmp_path / 'source' + entry = root / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + entry.write_bytes(b'# FIXTURE ONLY\n') + files = {'.github/actions/noema-review/two_phase.py': entry.read_bytes()} + manifest = {'schema_version': 1, 'repository': 'ContextualWisdomLab/.github', 'commit': 'b'*40, + 'tree': 'c'*40, 'entrypoint': next(iter(files)), + 'files': {name: hashlib.sha256(data).hexdigest() for name, data in files.items()}} + scope = {'repository': 'ContextualWisdomLab/example', 'number': 7, 'head': 'a'*40, + 'base': 'd'*40, 'author': 'writer', 'reviewer': 'cwl-noema-review[bot]'} + admission = {'scope': scope, 'installation_id': 7, **{name: True for name in + ('isolated_clean_job','native_custody','supplier_cost','supplier_retention','issuer_phase_separation')}} + paths = {} + for name, data in [('manifest', manifest), ('admission', admission), + ('prepare', {'token': 'SYNTHETIC_READ', 'permissions': {'contents':'read','metadata':'read','pull_requests':'read'}, 'repositories':[{'full_name':scope['repository']}]}), + ('publish', {'token': 'SYNTHETIC_WRITE', 'permissions': {'contents':'read','metadata':'read','pull_requests':'write'}, 'repositories':[{'full_name':scope['repository']}]})]: + paths[name] = tmp_path / (name + '.json') + paths[name].write_text(json.dumps(data)) + paths[name].chmod(0o600) + pr = {'base': {'sha':scope['base'],'repo':{'full_name':scope['repository']}}, + 'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + monkeypatch.setattr(module.subprocess, 'run', lambda *_args, **_kwargs: SimpleNamespace(stdout=json.dumps(pr))) + monkeypatch.setenv('LLM_GATEWAY_API_KEY','SYNTHETIC_INFERENCE') + events = [] + + def run_controller(raw, **kwargs): + events.append('controller') + kwargs['require_execution_admission']() + tooling = kwargs['load_tooling']() + assert tooling.phase_environment('prepare')['GH_TOKEN'] == 'SYNTHETIC_READ' + assert tooling.phase_environment('prepare')['NOEMA_LLM_MODEL'] == 'auto' + kwargs['refresh_publisher']() + env = tooling.phase_environment('publish') + assert env['GH_TOKEN'] == 'SYNTHETIC_WRITE' + assert 'NOEMA_LLM_API_KEY' not in env + return 0 + + monkeypatch.setattr(module, 'run_review', run_controller) + argv = ['--run-production','--tooling-root',str(root),'--manifest',str(paths['manifest']), + '--accepted-digest',hashlib.sha256(paths['manifest'].read_bytes()).hexdigest(), + '--admission',str(paths['admission']),'--admission-digest',hashlib.sha256(paths['admission'].read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'], + '--prepare-issuance',str(paths['prepare']),'--publish-issuance',str(paths['publish']), + '--private-parent',str(tmp_path)] + assert module.production_main(argv) == 0 + assert events == ['controller'], 'production caller returned without controller execution' diff --git a/tests/test_noema_prepare_transport_admission.py b/tests/test_noema_prepare_transport_admission.py new file mode 100644 index 0000000000..0c3d831d4a --- /dev/null +++ b/tests/test_noema_prepare_transport_admission.py @@ -0,0 +1,55 @@ +"""New fail-closed controls for explicitly admitted prepare transport.""" +import hashlib +import json +from pathlib import Path + +import pytest +from scripts.ci import premerge_review_tooling as caller +from tests.test_noema_child_environment_composition import composition + + +@pytest.mark.parametrize("mutation", ["ambient", "retry", "output_mode", "output_symlink", "reader", "node", "incomplete"]) +def test_rejected_transport_never_reaches_child(tmp_path, monkeypatch, mutation): + """Invalid accepted transport rejects before any child or output write.""" + args, paths, output, children, context = composition(tmp_path, monkeypatch, "hwp") + if mutation == "ambient": + context["NODE_OPTIONS"] = "--require=ambient" + elif mutation == "retry": + context["NOEMA_TRANSPORT_RETRY_ATTEMPT"] = "3" + elif mutation == "output_mode": + output.chmod(0o666) + elif mutation == "output_symlink": + link = tmp_path / "output-link" + link.symlink_to(output) + context["GITHUB_OUTPUT"] = str(link) + elif mutation == "reader": + (Path(context["NOEMA_HWP_MCP_SOURCE"]) / "dist/tools/read.js").write_text("changed supplier") + elif mutation == "node": + context["node_sha256"] = "0" * 64 + else: + context.pop("NOEMA_NODE_EXECUTABLE") + admission = json.loads(paths["admission"].read_text()) + admission["prepare_transport"] = context + paths["admission"].write_text(json.dumps(admission)) + args[args.index("--admission-digest") + 1] = hashlib.sha256(paths["admission"].read_bytes()).hexdigest() + assert caller.production_main(args) == 1 + assert children == [] + assert output.read_bytes() == b"" + assert not paths["verdict"].exists() + + +def test_publisher_never_receives_prepare_transport_or_inference(tmp_path, monkeypatch): + """A publisher ignores even a prepare transport object and receives no loader.""" + _, _, _, _, context = composition(tmp_path, monkeypatch, "hwp") + scope = {"repository": "ContextualWisdomLab/example", "reviewer": "cwl-noema-review[bot]"} + response = {"token": "SYNTHETIC_WRITE", "permissions": { + "contents": "read", "metadata": "read", "pull_requests": "write"}, + "repositories": [{"full_name": scope["repository"]}]} + environment = caller.phase_environment_from_issuance( + "publish", response, scope=scope, reviewer_actor=scope["reviewer"], + installation_id=7, inference_key="SYNTHETIC_NOT_FOR_PUBLISH", prepare_transport=context, + ) + caller.require_phase_environment("publish", environment) + assert set(environment) == {"PATH", "GH_TOKEN", "NOEMA_REVIEW_ACTOR", + "NOEMA_REVIEW_INSTALLATION_ID", "NOEMA_REVIEW_TOKEN_SOURCE"} + assert environment["PATH"] == "/usr/bin:/bin" diff --git a/tests/test_noema_prepare_transport_path_identity.py b/tests/test_noema_prepare_transport_path_identity.py new file mode 100644 index 0000000000..f53584110b --- /dev/null +++ b/tests/test_noema_prepare_transport_path_identity.py @@ -0,0 +1,54 @@ +"""Regression controls for admission and consumer path identity.""" +import hashlib +import json +import shutil +from pathlib import Path + +import pytest +from scripts.ci import premerge_review_tooling as caller +from tests.test_noema_child_environment_composition import composition + + +@pytest.mark.parametrize("field", ["GITHUB_OUTPUT", "NOEMA_HWP_MCP_SOURCE"]) +@pytest.mark.parametrize("suffix", [" ", "\t", "\n", "\r", "\u00a0", "\x01", "\x7f"]) +def test_consumer_alias_rejected_before_child_or_output_write(tmp_path, monkeypatch, field, suffix): + """Valid distinct private raw/stripped names cannot redirect accepted capability.""" + scenario = "retry" if field == "GITHUB_OUTPUT" else "hwp" + args, paths, output, children, context = composition(tmp_path, monkeypatch, scenario) + ordinary = Path(context[field]) + suffixed = ordinary.with_name(ordinary.name + suffix) + if field == "GITHUB_OUTPUT": + suffixed.write_bytes(b"RAW_OUTPUT_SENTINEL") + suffixed.chmod(0o600) + else: + shutil.copytree(ordinary, suffixed) + # Keep the admitted raw inventory valid while distinguishing its sibling. + (ordinary / "dist/tools/read.js").write_text("throw new Error('UNADMITTED_SIBLING');\n") + context[field] = str(suffixed) + admission = json.loads(paths["admission"].read_text()) + admission["prepare_transport"] = context + paths["admission"].write_text(json.dumps(admission)) + args[args.index("--admission-digest") + 1] = hashlib.sha256(paths["admission"].read_bytes()).hexdigest() + before = {p: p.read_bytes() for p in tmp_path.rglob("*") if p.is_file()} + assert caller.production_main(args) == 1 + assert children == [] + assert not paths["verdict"].exists() + assert all(p.read_bytes() == raw for p, raw in before.items()) + assert output.read_bytes() == b"" + + +@pytest.mark.parametrize("scenario", ["retry", "hwp"]) +def test_ordinary_capability_identity_still_reaches_actual_consumer(tmp_path, monkeypatch, scenario): + """An unchanged accepted identity stays nonempty through the real child seam.""" + args, paths, output, children, context = composition(tmp_path, monkeypatch, scenario) + assert caller.production_main(args) == (1 if scenario == "retry" else 0) + assert len(children) == 1 + assert children[0]["GITHUB_OUTPUT"] == context["GITHUB_OUTPUT"] + assert children[0]["NOEMA_HWP_MCP_SOURCE"] == context["NOEMA_HWP_MCP_SOURCE"] + assert children[0]["NOEMA_LLM_MODEL"] == "auto" + assert "NODE_OPTIONS" not in children[0] + if scenario == "retry": + assert "transport_retry_next_attempt=2\n" in output.read_text() + assert not paths["verdict"].exists() + else: + assert paths["verdict"].exists() diff --git a/tests/test_noema_production_cli_coverage.py b/tests/test_noema_production_cli_coverage.py new file mode 100644 index 0000000000..591ca7bfdb --- /dev/null +++ b/tests/test_noema_production_cli_coverage.py @@ -0,0 +1,128 @@ +"""In-process production caller controls with synthetic authority only.""" +import hashlib +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest +from scripts.ci import premerge_review_tooling as caller + + +def setup_caller(tmp_path, monkeypatch, mode): + """Build private inert pins and intercept metadata without authenticating.""" + root = tmp_path / 'tooling' + entry = root / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + entry.write_bytes(b'# inert fixture\n') + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + manifest = {'schema_version':1,'repository':'ContextualWisdomLab/.github','commit':'c'*40, + 'tree':'d'*40,'entrypoint':'.github/actions/noema-review/two_phase.py', + 'files':{'.github/actions/noema-review/two_phase.py':hashlib.sha256(entry.read_bytes()).hexdigest()}} + admission = {'scope':scope,'installation_id':7,**{k:True for k in + ('isolated_clean_job','native_custody','supplier_cost','supplier_retention','issuer_phase_separation')}} + paths = {k:tmp_path/k for k in ['manifest','admission','prepare','publish','verdict']} + for name, value in [('manifest',manifest),('admission',admission)]: + paths[name].write_text(json.dumps(value)) + for phase in ['prepare','publish']: + paths[phase].write_text(json.dumps({'token':'SYNTHETIC_ONLY', + 'permissions':{'contents':'read','metadata':'read','pull_requests':'read' if phase=='prepare' else 'write'}, + 'repositories':[{'full_name':scope['repository']}]})) + paths[phase].chmod(0o600) + payload = {'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'], + 'pull_request_author':'writer','reviewer_actor':scope['reviewer'],'verdict':{'decision':'approve'}} + pr = {'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}}, + 'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + monkeypatch.setattr(caller.subprocess,'run',lambda *_a,**_k:SimpleNamespace(stdout=json.dumps(pr))) + monkeypatch.setenv('LLM_GATEWAY_API_KEY','SYNTHETIC_INFERENCE') + events = [] + + def prepare(repo, number, head, path): + """Write an inert bounded envelope, not a model response.""" + path.write_text(json.dumps(payload));path.chmod(0o600);events.append('prepare');return 0 + + def publish(repo, number, head, path): + """Observe phase admission at a recording-only sink.""" + events.append('publish');return 0 + + monkeypatch.setattr(caller,'load_noema_tooling',lambda *_a,**_k:SimpleNamespace(prepare_verdict=prepare,publish_verdict=publish)) + args = ['--'+mode+'-production','--tooling-root',str(root),'--manifest',str(paths['manifest']), + '--accepted-digest',hashlib.sha256(paths['manifest'].read_bytes()).hexdigest(), + '--admission',str(paths['admission']),'--admission-digest',hashlib.sha256(paths['admission'].read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head']] + if mode in ['prepare','publish']: + args += ['--phase-issuance',str(paths[mode]),'--verdict-file',str(paths['verdict'])] + if mode == 'run': + args += ['--prepare-issuance',str(paths['prepare']),'--publish-issuance',str(paths['publish']),'--private-parent',str(tmp_path)] + return args,paths,payload,events + + +@pytest.mark.parametrize('mode',['prepare','publish','run','admit']) +def test_production_mode_positive(tmp_path,monkeypatch,mode): + """Exercise caller control flow while keeping all network/model sinks inert.""" + args,paths,payload,events=setup_caller(tmp_path,monkeypatch,mode) + if mode=='publish': + paths['verdict'].write_text(json.dumps(payload));paths['verdict'].chmod(0o600) + assert caller.production_main(args)==0 + assert events == {'prepare':['prepare'],'publish':['publish'],'run':['prepare','publish'],'admit':[]}[mode] + + +@pytest.mark.parametrize('mutation',['pins','digest','target','native','missing_phase','custody','missing_run','run_custody','admission_drift']) +def test_production_denial_controls(tmp_path,monkeypatch,mutation): + """Missing or changed private authority refuses before later publication.""" + mode='run' if mutation in ['missing_run','run_custody','admission_drift'] else 'prepare' + args,paths,payload,events=setup_caller(tmp_path,monkeypatch,mode) + if mutation=='pins':args[args.index('--accepted-digest')+1]='' + if mutation=='digest':args[args.index('--admission-digest')+1]='0'*64 + if mutation in ['target','native']: + data=json.loads(paths['admission'].read_text()) + if mutation=='target':data['scope']['number']=8 + else:data['native_custody']=False + paths['admission'].write_text(json.dumps(data));args[args.index('--admission-digest')+1]=hashlib.sha256(paths['admission'].read_bytes()).hexdigest() + if mutation in ['missing_phase','missing_run']: + flag='--phase-issuance' if mutation=='missing_phase' else '--private-parent' + pos=args.index(flag);del args[pos:pos+2] + if mutation in ['custody','run_custody']:paths['prepare'].chmod(0o644) + if mutation=='admission_drift': + def stale(*_a,**kwargs): + """Mutate accepted custody before the controller revalidation.""" + paths['admission'].write_text('{}') + kwargs['require_execution_admission']() + monkeypatch.setattr(caller,'run_review',stale) + assert caller.production_main(args)==1 + assert events==[] + + +@pytest.mark.parametrize('mode',['prepare','publish']) +def test_phase_admission_drift_during_live_read_blocks_execution(tmp_path,monkeypatch,mode): + """A live metadata wait cannot preserve authority revoked during that wait.""" + args,paths,payload,events=setup_caller(tmp_path,monkeypatch,mode) + if mode=='publish': + paths['verdict'].write_text(json.dumps(payload));paths['verdict'].chmod(0o600) + original=caller.subprocess.run + + def revoke(*args,**kwargs): + """Return real fixture metadata but replace the previously accepted receipt.""" + response=original(*args,**kwargs) + paths['admission'].write_text('{}') + return response + + monkeypatch.setattr(caller.subprocess,'run',revoke) + assert caller.production_main(args)==1 + assert events==[] + + +def test_main_entrypoint_rejects_empty_pins_without_network(monkeypatch,tmp_path): + """Execute the real __main__ boundary in process with no external authority.""" + import runpy + import sys + + monkeypatch.setattr(sys,'argv',[str(Path(caller.__file__)), '--admit-production', + '--tooling-root',str(tmp_path),'--manifest',str(tmp_path/'missing'), + '--accepted-digest','','--admission',str(tmp_path/'missing-admission'), + '--admission-digest','','--repo','ContextualWisdomLab/example', + '--pr-number','7','--expected-head','a'*40]) + with pytest.raises(SystemExit) as stopped: + runpy.run_path(caller.__file__,run_name='__main__') + assert stopped.value.code == 1 diff --git a/tests/test_noema_production_phase_execution.py b/tests/test_noema_production_phase_execution.py new file mode 100644 index 0000000000..1fc836fe8c --- /dev/null +++ b/tests/test_noema_production_phase_execution.py @@ -0,0 +1,105 @@ +"""Execute production CLI phase children with only inert fixture authority.""" +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +def test_source_drift_during_credential_acquisition_blocks_child(tmp_path): + """Credential acquisition cannot stale the source check before child spawn.""" + import importlib.util + + spec = importlib.util.spec_from_file_location('credential_wait_boundary', ROOT / 'scripts/ci/premerge_review_tooling.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + entry = tmp_path / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + entry.write_bytes(b'# accepted fixture\n') + manifest = {'schema_version':1,'repository':'ContextualWisdomLab/.github', + 'commit':'a'*40,'tree':'b'*40, + 'entrypoint':'.github/actions/noema-review/two_phase.py', + 'files':{'.github/actions/noema-review/two_phase.py':hashlib.sha256(entry.read_bytes()).hexdigest()}} + raw = json.dumps(manifest).encode() + calls = [] + + def acquire(phase): + entry.write_bytes(b'# changed during authority acquisition\n') + return {'NOEMA_LLM_MODEL':'auto'} + + def child(*args, **kwargs): + from types import SimpleNamespace + calls.append(args) + return SimpleNamespace(returncode=0) + + tooling = module.load_noema_tooling(tmp_path, raw, + accepted_digest=hashlib.sha256(raw).hexdigest(), interpreter=Path(sys.executable), + phase_environment=acquire, child_runner=child) + with pytest.raises(ValueError, match='source inventory'): + tooling.prepare_verdict('ContextualWisdomLab/example',7,'c'*40,tmp_path.parent/'unused-verdict') + assert calls == [] + + + +@pytest.mark.parametrize('mutation', ['none', 'source', 'permissions', 'custody']) +def test_real_phase_children_and_fail_closed_boundaries(tmp_path, mutation): + """Prepare then separately publish; corrupted boundary never invokes publisher.""" + root = tmp_path / 'tooling' + entry = root / '.github/actions/noema-review/two_phase.py' + entry.parent.mkdir(parents=True) + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'d'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + envelope = {'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'], + 'pull_request_author':'writer','reviewer_actor':scope['reviewer'], + 'verdict':{'decision':'approve','summary':'INERT FIXTURE ONLY'}} + entry.write_text('import sys,json,os\nfrom pathlib import Path\n' + 'args=sys.argv\n' + 'if "--prepare-verdict-file" in args:\n' + ' assert os.environ["GH_TOKEN"] == "SYNTHETIC_READ"\n' + ' assert os.environ["NOEMA_LLM_MODEL"] == "auto"\n' + ' p=Path(args[args.index("--prepare-verdict-file")+1])\n' + ' p.write_text(' + repr(json.dumps(envelope)) + ')\n p.chmod(0o600)\n' + 'else:\n' + ' assert os.environ["GH_TOKEN"] == "SYNTHETIC_WRITE"\n' + ' assert "NOEMA_LLM_API_KEY" not in os.environ\n' + ' p=Path(args[args.index("--publish-verdict-file")+1])\n' + ' Path(str(p)+".published").write_text("FIXTURE ONLY")\n p.unlink()\n') + manifest = {'schema_version':1,'repository':'ContextualWisdomLab/.github','commit':'b'*40, + 'tree':'c'*40,'entrypoint':'.github/actions/noema-review/two_phase.py', + 'files':{'.github/actions/noema-review/two_phase.py':hashlib.sha256(entry.read_bytes()).hexdigest()}} + admission = {'scope':scope,'installation_id':7,**{name:True for name in + ('isolated_clean_job','native_custody','supplier_cost','supplier_retention','issuer_phase_separation')}} + mf=tmp_path/'manifest';ad=tmp_path/'admission';issuer=tmp_path/'issuer';verdict=tmp_path/'verdict' + mf.write_text(json.dumps(manifest));ad.write_text(json.dumps(admission)) + pr={'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}},'head':{'sha':scope['head']}, + 'number':7,'user':{'login':'writer'},'state':'open','draft':False} + gh=tmp_path/'gh';gh.write_text('#!/bin/sh\nprintf \'%s\' \''+json.dumps(pr)+'\'\n');gh.chmod(0o700) + common=['--tooling-root',str(root),'--manifest',str(mf),'--accepted-digest',hashlib.sha256(mf.read_bytes()).hexdigest(), + '--admission',str(ad),'--admission-digest',hashlib.sha256(ad.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'], + '--phase-issuance',str(issuer),'--verdict-file',str(verdict)] + for phase in ('prepare','publish'): + response={'token':'SYNTHETIC_READ' if phase=='prepare' else 'SYNTHETIC_WRITE', + 'permissions':{'contents':'read','metadata':'read','pull_requests':'read' if phase=='prepare' else 'write'}, + 'repositories':[{'full_name':scope['repository']}]} + if phase=='publish': + if mutation=='source':entry.write_text('# substituted\n') + if mutation=='permissions':response['permissions']['contents']='write' + issuer.write_text(json.dumps(response));issuer.chmod(0o644 if phase=='publish' and mutation=='custody' else 0o600) + env={'PATH':str(tmp_path)+':/usr/bin:/bin'} + if phase=='prepare':env['LLM_GATEWAY_API_KEY']='SYNTHETIC_INFERENCE' + completed=subprocess.run([sys.executable,'-I','-B',str(ROOT/'scripts/ci/premerge_review_tooling.py'), + '--'+phase+'-production',*common],env=env,capture_output=True,text=True,timeout=20) + if phase=='prepare' or mutation=='none': + assert completed.returncode==0,completed.stderr + else: + assert completed.returncode==1 + assert Path(str(verdict)+'.published').exists() is (mutation=='none') + assert 'SYNTHETIC_READ' not in completed.stdout+completed.stderr + assert 'SYNTHETIC_WRITE' not in completed.stdout+completed.stderr diff --git a/tests/test_noema_quality_contract_negatives.py b/tests/test_noema_quality_contract_negatives.py new file mode 100644 index 0000000000..56d60af85c --- /dev/null +++ b/tests/test_noema_quality_contract_negatives.py @@ -0,0 +1,28 @@ +"""New sensitivity controls for the two failed final-quality contracts.""" +from pathlib import Path +import pytest +from tests import test_noema_issuer_producer_connection as contract + + +@pytest.mark.parametrize('mutation', ['bypass', 'early_write', 'action_write', 'wide_issuer']) +def test_revised_contracts_reject_security_regressions(tmp_path, monkeypatch, mutation): + """Actual revised consumers must reject each narrowly changed owned input.""" + root=Path(contract.ROOT) + workflow=(root/'.github/workflows/noema-review.yml').read_text() + issuer=(root/'scripts/ci/noema_existing_app_issuer.cjs').read_text() + if mutation=='bypass': + workflow=workflow.replace('node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs"','node bypass.cjs',1) + elif mutation=='early_write': + issuer=issuer.replace('await fence(read.token,phase);','/* fence removed */',1) + elif mutation=='action_write': + marker=' - name: Refresh repository-scoped Noema GitHub App token for publication\n' + prefix,rest=workflow.split(marker,1) + workflow=prefix+marker+rest.replace('permission-pull-requests: read','permission-pull-requests: write',1) + else: + issuer=issuer.replace("contents:'read',metadata:'read',pull_requests:grant", "contents:'write',metadata:'read',pull_requests:grant",1) + wf=tmp_path/'.github/workflows/noema-review.yml';wf.parent.mkdir(parents=True);wf.write_text(workflow) + helper=tmp_path/'scripts/ci/noema_existing_app_issuer.cjs';helper.parent.mkdir(parents=True);helper.write_text(issuer) + monkeypatch.setattr(contract,'ROOT',tmp_path) + target=contract.test_workflow_invokes_real_bootstrap_and_phase_producer_before_controller if mutation in ('bypass','early_write') else contract.test_actual_publish_request_has_only_controller_write_permissions + with pytest.raises(AssertionError): + target() diff --git a/tests/test_noema_refreshed_app_identity.py b/tests/test_noema_refreshed_app_identity.py index 2339fdba88..788bd7ca0f 100644 --- a/tests/test_noema_refreshed_app_identity.py +++ b/tests/test_noema_refreshed_app_identity.py @@ -65,3 +65,54 @@ def test_unrecognized_source_is_not_normalized(monkeypatch: pytest.MonkeyPatch) with pytest.raises(RuntimeError, match="Noema GitHub App identity binding is invalid"): module._reviewer_actor(allow_refreshed_app=True) assert os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] == "untrusted-app-alias" + + +def test_actual_refreshed_publication_counts_only_as_exact_head_independent_app( + monkeypatch, tmp_path, +): + """Compose the real two-phase publisher and scheduler without remote writes.""" + import json + from copy import deepcopy + from scripts.ci import pr_review_merge_scheduler as scheduler + from tests.test_review_ai_app_independence import app_review, pull_request, HEAD + + template = app_review(monkeypatch) + module = _load_module() + monkeypatch.setenv("NOEMA_REVIEW_ACTOR", "cwl-noema-review[bot]") + monkeypatch.setenv("NOEMA_REVIEW_INSTALLATION_ID", "146401636") + monkeypatch.setenv("NOEMA_REVIEW_TOKEN_SOURCE", module.REFRESHED_APP_TOKEN_SOURCE) + live = {"state": "OPEN", "headRefOid": HEAD, "baseRefOid": "b" * 40, + "isDraft": False, "author": {"login": "seonghobae"}, "reviews": {"nodes": []}} + monkeypatch.setattr(module.gate, "fetch_pr", lambda *_: live) + captured = [] + monkeypatch.setattr(module.gate, "run", lambda args, stdin=None: captured.append(json.loads(stdin))) + envelope = tmp_path / "verdict.json" + # Recover a substantive verdict through the existing fixture publisher's contract. + verdict = { + "decision": "approve", "summary": "Inspected exact-head publication.", "findings": [], + "reviewed_lines": [{"path": "scripts/ci/example.py", "line": 2, "side": "RIGHT", + "analysis": "The exact-head guard rejects stale publication."}], + "adversarial_validation": {"residual_risk": "Installation permission remains external.", + "probes": [{"path": "scripts/ci/example.py", "line": 2, "side": "RIGHT", + "outcome": "falsified", "hypothesis": "Stale publication could pass.", + "evidence": "The live head comparison rejects stale publication."}]}, + } + module._write_envelope(envelope, {"schema_version": 1, + "repository": "ContextualWisdomLab/example", "pull_request_number": 7, + "expected_head": HEAD, "expected_base": "b" * 40, + "reviewer_actor": "cwl-noema-review[bot]", "pull_request_author": "seonghobae", + "verdict": verdict}) + assert module.publish_verdict("ContextualWisdomLab/example", 7, HEAD, envelope) == 0 + assert len(captured) == 1 and captured[0]["commit_id"] == HEAD + assert captured[0]["event"] == "APPROVE" + assert module.REFRESHED_APP_TOKEN_SOURCE in captured[0]["body"] + template["body"] = captured[0]["body"] + pr = pull_request(template) + assert scheduler.has_independent_current_head_approval(pr) + for key, value in (("state", "COMMENTED"), ("commit", {"oid": "c" * 40}), + ("author", {"login": "unknown[bot]", "__typename": "Bot"})): + rejected = deepcopy(pr) + rejected["reviews"]["nodes"][0][key] = value + assert not scheduler.has_independent_current_head_approval(rejected) + assert not envelope.exists() + assert os.environ["NOEMA_REVIEW_TOKEN_SOURCE"] == module.REFRESHED_APP_TOKEN_SOURCE diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 5d3fb66605..64b75c4df4 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -4,7 +4,6 @@ ) import base64 import hashlib -import http.client import io import json import os @@ -673,6 +672,7 @@ def make_pr(**overrides): "number": 7, "title": "Noema", "body": "", + "author": {"login": "fixture-pr-author"}, "isDraft": False, "state": "OPEN", "headRefOid": "head", diff --git a/tests/test_noema_reviewer_token_lifetime.py b/tests/test_noema_reviewer_token_lifetime.py index 7081171257..c040da5965 100644 --- a/tests/test_noema_reviewer_token_lifetime.py +++ b/tests/test_noema_reviewer_token_lifetime.py @@ -2,6 +2,8 @@ from pathlib import Path +import pytest + ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / ".github" / "workflows" / "noema-review.yml" @@ -21,6 +23,65 @@ def _step_block(text: str, name: str) -> str: return text[start:min(ends)] if ends else text[start:] +def test_prepublication_metadata_uses_fresh_read_only_app_token(tmp_path: Path) -> None: + """An expired pre-model token never masks the new post-model read grant.""" + import json + import os + import re + import shutil + import subprocess + from tests.test_strix_repository_visibility_contract import _extract_run_block + workflow = WORKFLOW.read_text(encoding="utf-8") + refresh_name = "Refresh read-only Noema GitHub App metadata token after model" + revalidate_name = "Revalidate prepared scope before publisher credential refresh" + import hashlib + import sys + from scripts.ci import noema_bootstrap_producer as producer + from tests.test_noema_issuer_producer_controls import bootstrap_case + revalidate = _step_block(workflow, revalidate_name) + expression = re.search(r"GH_TOKEN: \$\{\{ (.*?) \}\}", revalidate) + tokens = {'steps.noema_metadata_app_token.outputs.token': 'EXPIRED_PRE_MODEL', + 'steps.noema_publication_metadata_token.outputs.token': 'FRESH_READ_ONLY', + 'github.token': 'OTHER_IDENTITY'} + token = next((tokens.get(term.strip(), '') for term in expression.group(1).split(' || ') + if tokens.get(term.strip(), '')), '') + private, source, entry, raw, admission = bootstrap_case(tmp_path) + scope = {'repository':'ContextualWisdomLab/example','number':7,'head':'a'*40, + 'base':'b'*40,'author':'writer','reviewer':'cwl-noema-review[bot]'} + admission.update(scope=scope, installation_id=7, app_id=11) + producer.materialize_tooling(source, private/'tooling', raw, + accepted_digest=hashlib.sha256(raw).hexdigest(), admission=admission, private_parent=private) + mf, af = tmp_path/'manifest', tmp_path/'admission' + mf.write_bytes(raw); af.write_text(json.dumps(admission)) + envelope = {'schema_version':1,'repository':scope['repository'],'pull_request_number':7, + 'expected_head':scope['head'],'expected_base':scope['base'], + 'reviewer_actor':scope['reviewer'],'pull_request_author':'writer', + 'verdict':{'decision':'approve'}} + verdict = private/'verdict.json'; verdict.write_text(json.dumps(envelope)); verdict.chmod(0o600) + pr = {'base':{'sha':scope['base'],'repo':{'full_name':scope['repository']}}, + 'head':{'sha':scope['head']},'number':7,'user':{'login':'writer'},'state':'open','draft':False} + gh = tmp_path/'gh' + gh.write_text('#!/bin/sh\n[ "$GH_TOKEN" = FRESH_READ_ONLY ] || exit 77\n' + + "printf '%s' '" + json.dumps(pr) + "'\n") + gh.chmod(0o755) + args = ['--revalidate-publish','--private-parent',str(private),'--source-root',str(source), + '--manifest',str(mf),'--accepted-digest',hashlib.sha256(raw).hexdigest(), + '--admission',str(af),'--admission-digest',hashlib.sha256(af.read_bytes()).hexdigest(), + '--repo',scope['repository'],'--pr-number','7','--expected-head',scope['head'], + '--verdict-file',str(verdict)] + env = {'PATH':str(tmp_path)+':/usr/bin:/bin','GH_TOKEN':token} + result = subprocess.run([sys.executable,'-I','-B',str(ROOT/'scripts/ci/noema_bootstrap_producer.py'),*args], + env=env,capture_output=True,text=True,timeout=20) + assert result.returncode == 0, 'expired pre-model metadata token blocks publisher refresh' + assert not (private/'publish.json').exists(), 'write grant issued before readonly fence' + refresh = _step_block(workflow, refresh_name) + writer = _step_block(workflow, "Refresh repository-scoped Noema GitHub App token for publication") + assert workflow.index(" - name: Prepare Noema model verdict") < workflow.index(refresh) < workflow.index(revalidate) < workflow.index(writer) + assert APP_TOKEN_ACTION in refresh + assert all(f"permission-{p}: read" in refresh for p in ('contents','metadata','pull-requests')) + assert ': write' not in refresh and 'github.token' not in revalidate + + def test_noema_remints_repository_scoped_app_token_after_model_before_publication() -> None: """A long model call must not publish with its predecessor App token.""" workflow = WORKFLOW.read_text(encoding="utf-8") @@ -29,11 +90,16 @@ def test_noema_remints_repository_scoped_app_token_after_model_before_publicatio publish = _step_block(workflow, "Publish prepared Noema verdict on the exact live head") assert APP_TOKEN_ACTION in refresh - assert "--prepare-verdict-file" in prepare - assert "--publish-verdict-file" in publish + assert "--phase prepare" in prepare + assert "--phase publish" in publish + assert 'scripts/ci/noema_existing_app_issuer.cjs' in prepare + assert 'scripts/ci/noema_existing_app_issuer.cjs' in publish + assert '--verdict-file "$verdict_file"' in prepare + assert '--verdict-file "$verdict_file"' in publish + assert workflow.index(prepare) < workflow.index(refresh) < workflow.index(publish) assert '--expected-head "$EXPECTED_HEAD_SHA"' in prepare assert '--expected-head "$EXPECTED_HEAD_SHA"' in publish - assert 'export NOEMA_LLM_MODEL="orchestrator/free"' in prepare + assert 'export NOEMA_LLM_MODEL="orchestrator/auto"' in prepare assert "steps.noema_prepare.outputs.prepared == 'true'" in refresh assert "steps.noema_credential.outputs.source == 'github-app'" in refresh assert "steps.noema_prepare.outputs.prepared == 'true'" in publish @@ -47,9 +113,15 @@ def test_publication_step_uses_fresh_app_token_without_authority_fallback() -> N assert "owner: ContextualWisdomLab" in refresh assert "repositories: ${{ steps.noema_credential.outputs.repository }}" in refresh - assert "permission-pull-requests: write" in refresh + assert "permission-pull-requests: read" in refresh + assert "permission-pull-requests: write" not in refresh assert "permission-contents: read" in refresh - assert "permission-actions: read" in refresh + assert "permission-actions: read" not in refresh + assert "permission-checks: read" not in refresh + assert "permission-security-events: read" not in refresh + assert "permission-statuses: read" not in refresh + assert "permission-vulnerability-alerts: read" not in refresh + assert "permission-metadata: read" in refresh assert "steps.noema_github_app_publication_token.outputs.token" in publish assert "steps.noema_github_app_token.outputs.token" not in publish assert "secrets.NOEMA_REVIEW_TOKEN" in publish @@ -63,5 +135,28 @@ def test_prepare_and_publish_are_the_only_model_verdict_execution_path() -> None workflow = WORKFLOW.read_text(encoding="utf-8") assert "Run Noema LLM review and submit verdict" not in workflow assert "python3 -m scripts.ci.noema_review_gate" not in workflow - assert workflow.count("--prepare-verdict-file") == 1 - assert workflow.count("--publish-verdict-file") == 1 + assert workflow.count("--phase prepare") == 1 + assert workflow.count("--phase publish") == 1 + assert '--phase-issuance' not in workflow + issuer = (ROOT / 'scripts/ci/noema_existing_app_issuer.cjs').read_text() + assert "'--'+phase+'-production'" in issuer + assert "'--phase-issuance',file" in issuer + assert '--prepare-verdict-file' not in workflow + assert '--publish-verdict-file' not in workflow + + +@pytest.mark.parametrize("old_flag", ["--prepare-verdict-file", "--publish-verdict-file"]) +def test_phase_contract_rejects_obsolete_flags_with_current_path_retained( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, old_flag: str, +) -> None: + """An extra obsolete flag must be rejected even beside valid phase calls.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + mutated = tmp_path / "noema-review.yml" + current_flag = "--phase prepare" if old_flag == "--prepare-verdict-file" else "--phase publish" + assert workflow.count(current_flag) == 1 + mutated.write_text( + workflow.replace(current_flag, f"{current_flag} {old_flag}"), encoding="utf-8", + ) + monkeypatch.setitem(globals(), "WORKFLOW", mutated) + with pytest.raises(AssertionError): + test_prepare_and_publish_are_the_only_model_verdict_execution_path() diff --git a/tests/test_noema_two_phase_handoff.py b/tests/test_noema_two_phase_handoff.py index 0e2c5e2fe5..7a5f4a07d3 100644 --- a/tests/test_noema_two_phase_handoff.py +++ b/tests/test_noema_two_phase_handoff.py @@ -25,6 +25,7 @@ def _load_module() -> ModuleType: def _patch_live_gate(monkeypatch: pytest.MonkeyPatch, module: ModuleType) -> None: + """Bind the gate's PR lookup to an author-bearing immutable fixture.""" monkeypatch.setattr( module.gate, "fetch_pr", @@ -32,6 +33,7 @@ def _patch_live_gate(monkeypatch: pytest.MonkeyPatch, module: ModuleType) -> Non "isDraft": False, "headRefOid": HEAD, "baseRefOid": BASE, + "author": {"login": "seonghobae"}, }, ) monkeypatch.setattr(module.gate, "require_expected_head", lambda _pr, _head: None) @@ -40,7 +42,15 @@ def _patch_live_gate(monkeypatch: pytest.MonkeyPatch, module: ModuleType) -> Non monkeypatch.setattr(module.gate, "existing_noema_review", lambda _pr, _actor: False) -def test_prepare_seals_validated_verdict_without_publishing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +def test_prepare_query_requests_the_pr_author_identity() -> None: + """The model/publication gate queries the author instead of inferring it.""" + module = _load_module() + assert "\n author { login }\n" in module.gate.PR_QUERY + + +def test_prepare_seals_validated_verdict_without_publishing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: """Preparation performs model work but cannot submit GitHub review evidence.""" module = _load_module() _patch_live_gate(monkeypatch, module) @@ -56,10 +66,12 @@ def test_prepare_seals_validated_verdict_without_publishing(tmp_path: Path, monk payload = module._read_envelope(envelope) assert payload["verdict"] == verdict assert payload["expected_base"] == BASE + assert payload["reviewer_actor"] == "cwl-noema-review[bot]" + assert payload["pull_request_author"] == "seonghobae" def test_publish_refetches_exact_head_and_base_with_fresh_actor_and_removes_envelope(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """Publication rebinds repository/head/base/actor and consumes the private handoff.""" + """Publication rebinds repository/head/base/author/actor and consumes the private handoff.""" module = _load_module() _patch_live_gate(monkeypatch, module) envelope = tmp_path / "verdict.json" @@ -70,6 +82,8 @@ def test_publish_refetches_exact_head_and_base_with_fresh_actor_and_removes_enve "pull_request_number": 7, "expected_head": HEAD, "expected_base": BASE, + "reviewer_actor": "cwl-noema-review[bot]", + "pull_request_author": "seonghobae", "verdict": verdict, }) submitted: list[tuple[object, ...]] = [] @@ -108,6 +122,8 @@ def stale(_pr: object, _head: str) -> None: "pull_request_number": 7, "expected_head": HEAD, "expected_base": BASE, + "reviewer_actor": "cwl-noema-review[bot]", + "pull_request_author": "seonghobae", "verdict": {"decision": "approve"}, }) @@ -135,6 +151,8 @@ def test_publish_rejects_base_drift_with_unchanged_head(tmp_path: Path, monkeypa "pull_request_number": 7, "expected_head": HEAD, "expected_base": BASE, + "reviewer_actor": "cwl-noema-review[bot]", + "pull_request_author": "seonghobae", "verdict": {"decision": "approve", "summary": "stale base"}, }) monkeypatch.setattr(module.gate, "submit_review", lambda *_args: pytest.fail("base-drifted evidence must not publish")) @@ -153,6 +171,7 @@ def test_prepare_skip_creates_no_publishable_envelope(tmp_path: Path, monkeypatc "isDraft": True, "headRefOid": HEAD, "baseRefOid": BASE, + "author": {"login": "seonghobae"}, }, ) monkeypatch.setattr(module.gate, "require_expected_head", lambda _pr, _head: None) @@ -267,6 +286,55 @@ def raise_capacity(*_args: object, **_kwargs: object) -> None: assert "automatic re-dispatch budget is exhausted" in capsys.readouterr().out +@pytest.mark.parametrize("author", (None, "cwl-noema-review[bot]")) +def test_prepare_rejects_missing_or_self_author_before_model( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, author: str | None +) -> None: + """An unknown or author-owned reviewer identity fails before inference.""" + module = _load_module() + _patch_live_gate(monkeypatch, module) + monkeypatch.setattr(module.gate, "fetch_pr", lambda *_: { + "state": "OPEN", "isDraft": False, "headRefOid": HEAD, "baseRefOid": BASE, + "author": {"login": author} if author is not None else None, + }) + monkeypatch.setattr(module.gate, "fetch_diff", lambda *_: ("diff", False)) + monkeypatch.setattr(module.gate, "fetch_changed_files", lambda *_: [("src/a.py", "MODIFIED")]) + monkeypatch.setattr(module.gate, "build_review_context", lambda *_: "context") + monkeypatch.setattr(module.gate, "call_llm", lambda *_: pytest.fail("model reached")) + envelope = tmp_path / "verdict.json" + with pytest.raises(RuntimeError, match="author"): + module.prepare_verdict("ContextualWisdomLab/example", 7, HEAD, envelope) + assert not envelope.exists() + + +@pytest.mark.parametrize("author,actor", ( + ("cwl-noema-review[bot]", "cwl-noema-review[bot]"), + (None, "cwl-noema-review[bot]"), + ("seonghobae", "other-review[bot]"), +)) +def test_publication_rejects_author_or_reviewer_identity_drift( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, author: str | None, actor: str +) -> None: + """A refreshed actor must match preparation and remain distinct from the author.""" + module = _load_module() + _patch_live_gate(monkeypatch, module) + source = {"state": "OPEN", "isDraft": False, "headRefOid": HEAD, "baseRefOid": BASE, + "author": {"login": "seonghobae"}} + monkeypatch.setattr(module.gate, "fetch_pr", lambda *_: source) + monkeypatch.setattr(module.gate, "fetch_diff", lambda *_: ("diff", False)) + monkeypatch.setattr(module.gate, "fetch_changed_files", lambda *_: [("src/a.py", "MODIFIED")]) + monkeypatch.setattr(module.gate, "build_review_context", lambda *_: "context") + monkeypatch.setattr(module.gate, "call_llm", lambda *_: {"decision": "approve", "summary": "bounded"}) + monkeypatch.setattr(module.gate, "submit_review", lambda *_: pytest.fail("invalid actor must not publish")) + envelope = tmp_path / "verdict.json" + assert module.prepare_verdict("ContextualWisdomLab/example", 7, HEAD, envelope) == 0 + source["author"] = {"login": author} if author is not None else None + monkeypatch.setattr(module.gate, "current_actor", lambda: actor) + with pytest.raises(RuntimeError, match="author|actor"): + module.publish_verdict("ContextualWisdomLab/example", 7, HEAD, envelope) + assert not envelope.exists() + + 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() diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 8a80d65461..43c7517a4d 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -120,9 +120,9 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "opencode-free/qwen3.6-plus-free ' || '' }}" ) candidates_text = candidates_match.group(1) - if candidates_text == "contextual-orchestrator/orchestrator/free": - assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free"' in workflow - assert 'MODEL: contextual-orchestrator/orchestrator/free' in workflow + if candidates_text == "contextual-orchestrator/orchestrator/auto": + assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto"' in workflow + assert 'MODEL: contextual-orchestrator/orchestrator/auto' in workflow assert '.enabled_providers = ["contextual-orchestrator"]' in workflow return assert candidates_text.startswith(conditional_public_candidate) @@ -795,6 +795,12 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'candidate_count=$((candidate_count + 1))' not in measure_step assert 'printf \'%s\\n\' "$python_root" >>"$path_file"' in measure_step assert 'chmod -R a+rX /opt/base-vcs-dependencies "$path_file"' in measure_step + # Cargo crate archives keep modes such as 0640 through COPY, so the + # coverage image must normalize the base-rust dependency tree before + # unprivileged sandbox execution. + assert measure_step.index( + "COPY base-rust-dependencies /opt/base-rust-dependencies" + ) < measure_step.index("RUN chmod -R a+rX /opt/base-rust-dependencies") assert "docker build --pull --no-cache --network=default" in measure_step assert '"$coverage_build_dir"' in measure_step assert measure_step.index("docker build --pull --no-cache") < measure_step.index( @@ -1285,7 +1291,7 @@ def test_opencode_python_coverage_never_resolves_pr_dependency_manifests(): assert "[ -d src ] && printf src:. || printf ." in measure assert "PYTHONPATH=. python3 -m pytest tests/test_docstrings.py" not in measure assert ( - 'PYTHONPATH="$([ -d src ] && printf src:. || printf .)" ' + 'PYTHONPATH=\\"\\$([ -d src ] && printf src:. || printf .)\\" ' "python3 -m pytest tests/test_docstrings.py" ) in measure @@ -2076,7 +2082,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): "Skipping publish-step failed-check OpenCode diagnosis for central review-process self-repair" in workflow ) - assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free"' in workflow + assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto"' in workflow assert 'OPENCODE_MODEL_ATTEMPTS: "1"' in workflow assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "180"' in workflow assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow @@ -2113,7 +2119,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'gh api -X GET "repos/${GH_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate' not in publish_step ) - assert "MODEL: contextual-orchestrator/orchestrator/free" in publish_step + assert "MODEL: contextual-orchestrator/orchestrator/auto" in publish_step assert 'OPENCODE_RUN_TIMEOUT_SECONDS: "120"' not in publish_step assert "${OPENCODE_RUN_TIMEOUT_SECONDS:-120}s" not in publish_step assert ( @@ -2192,7 +2198,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert ( 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) - assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free"' in workflow + assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto"' in workflow assert "${{ runner.temp }}/opencode-review-model-pool.md" in workflow assert re.search( r'check-runs" \\\n\s+-f per_page=100 \\\n\s+--paginate \\\n\s+--slurp \|\n\s+jq -r "\$jq_filter"', @@ -2201,7 +2207,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert not re.search(r"--slurp\s*\\\n\s*--jq", workflow) assert ( workflow.count( - '["opencode-review","coverage-evidence","metadata-only gate evaluation"]' + '[\\"opencode-review\\",\\"coverage-evidence\\",\\"metadata-only gate evaluation\\"]' ) >= 2 ) @@ -2217,9 +2223,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): not in failed_check_collector ) assert ( - '["opencode-review", "coverage-evidence", "coverage-source-tree", ' - '"required-workflow-bootstrap", "metadata-only gate evaluation", ' - '"scan-pr-queue"]' in workflow + '[\\"opencode-review\\", \\"coverage-evidence\\", \\"coverage-source-tree\\", ' + '\\"required-workflow-bootstrap\\", \\"metadata-only gate evaluation\\", ' + '\\"scan-pr-queue\\"]' in workflow ) assert "falling back to current-head REST check-runs" in workflow @@ -2306,7 +2312,8 @@ def test_opencode_excludes_queue_self_check_from_every_failed_check_path(): # Both failed-check collectors and both pending-check collectors exclude the # scheduler check by name, independently of its current state or conclusion. - assert workflow.count(unconditional_filter) >= 5 + escaped_filter = 'select((.name // \\"\\") != \\"scan-pr-queue\\")' + assert workflow.count(unconditional_filter) + workflow.count(escaped_filter) >= 5 assert cancelled_only_filter not in workflow failed_check_collector = Path( "scripts/ci/collect_failed_check_evidence.sh" @@ -2909,7 +2916,7 @@ def test_opencode_approve_review_publication_failure_fails_closed(): )[1].split(" - name: Publish OpenCode review outcome", 1)[0] assert "continue-on-error: true" in fast_approval assert "def latest_peer_checks:" in fast_approval - assert 'group_by([.app.slug // "", .name // ""])' in fast_approval + assert 'group_by([.app.slug // \\"\\", .name // \\"\\"])' in fast_approval assert fast_approval.count("latest_peer_checks") == 3 assert "CENTRAL_FAST_APPROVAL_WAITING_FOR_CHECKS" in workflow assert "CENTRAL_FAST_APPROVAL_CODE_SCANNING_ALERTS" in workflow @@ -3012,7 +3019,7 @@ def test_opencode_review_language_signal_is_throttle_proof(): workflow, ) # The preferred-language marker and its Korean/English detection remain. - assert "- Preferred review language: `%s`" in workflow + assert "- Preferred review language: \\`%s\\`" in workflow assert "grep -Eq '[가-힣]'" in workflow diff --git a/tests/test_opencode_append_command_lint.py b/tests/test_opencode_append_command_lint.py new file mode 100644 index 0000000000..e809444919 --- /dev/null +++ b/tests/test_opencode_append_command_lint.py @@ -0,0 +1,54 @@ +"""Keep command-summary publication byte-exact with one append open.""" + +import subprocess +from pathlib import Path + +import pytest + +WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" + + +def _append_function(): + """Read the complete shipped shell function, not a replacement fixture.""" + text = WORKFLOW.read_text(encoding="utf-8") + start = text.index(" append_command() {") + end = text.index("\n emit_captured_log() {", start) + return text[start:end] + + +def test_command_summary_opens_destination_once(): + """One enclosing redirection avoids separate command-summary opens.""" + assert _append_function().count('>>"$summary_file"') == 1 + + +@pytest.mark.parametrize("args", [ + [], ["simple"], ["space value", "", "*"], + ["'quote'", '"double"', "$literal", "`literal`"], + ["newline\nvalue", "tab\tvalue", "한글"], +]) +def test_command_summary_preserves_exact_escaped_bytes(tmp_path, args): + """Native Bash percent-q output and an existing prefix remain intact.""" + summary = tmp_path / "summary" + summary.write_bytes(b"existing\n") + result = subprocess.run( + ["bash", "-c", 'summary_file="$1"; shift\n' + _append_function() + '\nappend_command "$@"', "bash", str(summary), *args], + capture_output=True, timeout=10, + ) + expected = subprocess.run( + ["bash", "-c", "printf '$ '; printf '%q ' \"$@\"; printf '\\n'", "bash", *args], + capture_output=True, timeout=10, + ) + assert result.returncode == expected.returncode == 0 + assert result.stderr == b"" + assert summary.read_bytes() == b"existing\n" + expected.stdout + + +def test_command_summary_refuses_directory_destination(tmp_path): + """An unopenable destination rejects publication without invoking printf.""" + result = subprocess.run( + ["bash", "-c", 'summary_file="$1"\n' + _append_function() + '\nappend_command sentinel', "bash", str(tmp_path)], + capture_output=True, timeout=10, + ) + assert result.returncode != 0 + assert result.stdout == b"" + assert not list(tmp_path.iterdir()) diff --git a/tests/test_opencode_gateway_route_integration.py b/tests/test_opencode_gateway_route_integration.py index 3893914c0e..e1fa618c07 100644 --- a/tests/test_opencode_gateway_route_integration.py +++ b/tests/test_opencode_gateway_route_integration.py @@ -63,7 +63,7 @@ def do_POST(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API "id": "stub", "object": "chat.completion", "created": 0, - "model": "orchestrator/free", + "model": "orchestrator/auto", "choices": [ { "index": 0, @@ -128,8 +128,8 @@ def _run_opencode( json.dumps( { "$schema": "https://opencode.ai/config.json", - "model": "contextual-orchestrator/orchestrator/free", - "small_model": "contextual-orchestrator/orchestrator/free", + "model": "contextual-orchestrator/orchestrator/auto", + "small_model": "contextual-orchestrator/orchestrator/auto", "enabled_providers": ["contextual-orchestrator"], "provider": {"contextual-orchestrator": provider}, } @@ -154,7 +154,7 @@ def _run_opencode( "reply with ok", "--pure", "--model", - "contextual-orchestrator/orchestrator/free", + "contextual-orchestrator/orchestrator/auto", ], cwd=project, env=environment, diff --git a/tests/test_opencode_pr_workload_runner_boundary.py b/tests/test_opencode_pr_workload_runner_boundary.py new file mode 100644 index 0000000000..05be5e4dfe --- /dev/null +++ b/tests/test_opencode_pr_workload_runner_boundary.py @@ -0,0 +1,76 @@ +"""Keep PR merge-tree execution off privileged OpenCode/control runners.""" + +from copy import deepcopy +from pathlib import Path +import re + +import pytest +import yaml + + +WORKFLOW = Path('.github/workflows/opencode-review-dispatch.yml') +ISOLATED = { + 'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated'], +} + + +def assert_pr_workload_isolated(jobs: dict) -> None: + """Require every identified PR coverage executor to use the isolated route.""" + observed = 0 + for job_id, job in jobs.items(): + runs = '\n'.join(str(step.get('run', '')) for step in job.get('steps', [])) + executes_tests = re.search(r'\b(pytest|coverage run|npm (?:run |)test|pnpm (?:run |)test)\b', runs) + if job_id == 'coverage-evidence' or ('COVERAGE_SOURCE_WORKDIR' in runs and executes_tests): + observed += 1 + assert job.get('runs-on') == ISOLATED, ( + f'{job_id}: PR merge-tree execution requires the exact isolated runner mapping; ' + f'got {job.get("runs-on")!r}' + ) + assert observed, 'No PR merge-tree workload was checked' + + +def test_actual_pr_merge_tree_execution_requires_isolated_runner() -> None: + """Reject the actual workflow's privileged-pool PR test execution.""" + assert_pr_workload_isolated(yaml.safe_load(WORKFLOW.read_text())['jobs']) + + +@pytest.mark.parametrize('runner', [ + {'group': 'CWL central OpenCode', 'labels': ['self-hosted', 'linux', 'x64']}, + {'group': 5, 'labels': ['self-hosted', 'linux', 'x64']}, + {'group': 'CWL central control', 'labels': ['self-hosted', 'linux', 'x64']}, + {'group': 'CWL CI isolated', 'labels': ['self-hosted', 'linux', 'x64']}, + '${{ fromJSON(inputs.runner) }}', +]) +def test_equivalent_renamed_pr_workload_rejects_nonisolated_mapping(runner) -> None: + """A renamed executor cannot evade group or required-label admission.""" + job = { + 'runs-on': deepcopy(ISOLATED), + 'steps': [{'run': 'cd "$COVERAGE_SOURCE_WORKDIR" && python3 -m pytest tests'}], + } + assert_pr_workload_isolated({'renamed-pr-tests': job}) + job['runs-on'] = runner + with pytest.raises(AssertionError, match='requires the exact isolated runner mapping'): + assert_pr_workload_isolated({'renamed-pr-tests': job}) + + +def test_metadata_and_publisher_keep_privileged_permissions_separate() -> None: + """Preserve trusted token jobs and the PR executor's read-only artifact access.""" + jobs = yaml.safe_load(WORKFLOW.read_text())['jobs'] + for job_id in ('validate-pr-metadata', 'opencode-review-target'): + assert jobs[job_id]['runs-on'] == { + 'group': 'CWL central OpenCode', 'labels': ['self-hosted', 'linux', 'x64'], + } + assert jobs['validate-pr-metadata']['permissions'] == { + 'contents': 'read', 'pull-requests': 'read', 'id-token': 'write', + } + coverage = jobs['coverage-evidence'] + assert coverage['permissions'] == {'actions': 'read'} + assert coverage['needs'] == ['validate-pr-metadata'] + assert coverage['timeout-minutes'] == 300 + assert coverage['outputs'] == { + 'coverage_summary': '${{ steps.measure.outputs.coverage_summary }}', + } + assert jobs['opencode-review-target']['needs'] == ['validate-pr-metadata', 'coverage-evidence'] + assert jobs['opencode-review-target']['permissions']['pull-requests'] == 'write' + assert jobs['opencode-review-target']['permissions']['id-token'] == 'write' diff --git a/tests/test_opencode_python_coverage_case_lint.py b/tests/test_opencode_python_coverage_case_lint.py new file mode 100644 index 0000000000..96e5f2b930 --- /dev/null +++ b/tests/test_opencode_python_coverage_case_lint.py @@ -0,0 +1,50 @@ +"""Exercise the actual Python-coverage case without redundant alternatives.""" + +import fnmatch +import subprocess +from pathlib import Path + +import pytest + +WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" + + +def _case_script(): + """Extract only the shipped basename case, retaining its loop break.""" + text = WORKFLOW.read_text(encoding="utf-8") + start = text.index(' case "$changed_basename" in') + end = text.index(" esac", start) + len(" esac") + return ( + "#!/bin/bash\npython_coverage_required=0\n" + "while IFS= read -r changed_basename; do\n" + + text[start:end] + + '\ndone\nprintf "%s\\n" "$python_coverage_required"\n' + ) + + +def test_python_coverage_case_has_no_shadowed_alternative(): + """Reject literal alternatives already admitted by an earlier glob.""" + case = _case_script().split('case "$changed_basename" in', 1)[1] + alternatives = case.split(")", 1)[0].replace("\\\n", "").strip().split("|") + earlier = [] + for alternative in alternatives: + alternative = alternative.strip() + if not any(character in alternative for character in "*?["): + assert not any(fnmatch.fnmatchcase(alternative, pattern) for pattern in earlier), alternative + earlier.append(alternative) + + +@pytest.mark.parametrize("name,expected", [ + ("setup.py", "1"), ("module.py", "1"), ("setup.cfg", "1"), + ("pyproject.toml", "1"), ("requirements-dev.txt", "1"), + ("environment.yaml", "1"), ("README.md", "0"), + ("setup.py.bak", "0"), ("package.json", "0"), +]) +def test_python_coverage_case_keeps_admission(name, expected): + """Removing the redundant setup.py token must retain actual admission.""" + result = subprocess.run( + ["bash", "-c", _case_script()], input=name + "\n", + capture_output=True, text=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == expected diff --git a/tests/test_opencode_required_rerun_capacity.py b/tests/test_opencode_required_rerun_capacity.py index c85bc24e3c..33f72c073d 100644 --- a/tests/test_opencode_required_rerun_capacity.py +++ b/tests/test_opencode_required_rerun_capacity.py @@ -8,7 +8,11 @@ from pathlib import Path import subprocess -from tests.test_opencode_required_verdict_regression import HEAD, fail_closed_script +from tests.test_opencode_required_verdict_regression import ( + HEAD, + RECEIPT_HELPER, + fail_closed_script, +) REQUIRED = Path(".github/workflows/opencode-review.yml") @@ -64,6 +68,8 @@ def test_missing_verdict_fails_after_one_review_read(tmp_path: Path) -> None: printf '%s\n' "$*" >>"$CALLS" if [[ "$*" == "api repos/owner/repo/pulls/7" ]]; then printf '%s' "$LIVE_PR" +elif [[ "$*" == *"contents/scripts/ci/opencode_review_receipt_gate.py"* ]]; then + base64 <"$REAL_RECEIPT_HELPER" elif [[ "$*" == *"/pulls/7/reviews?per_page=100"* ]]; then printf '[]' else @@ -85,6 +91,9 @@ def test_missing_verdict_fails_after_one_review_read(tmp_path: Path) -> None: "HEAD_SHA": HEAD, "PR_ACTION": "synchronize", "PR_DRAFT": "false", + "CHANGED_SCOPE_CODE": "true", + "WORKFLOW_SHA": "6a37e4cdfbd8bf6f3a60645a0ad7c13e1b9c1ae3", + "REAL_RECEIPT_HELPER": str(RECEIPT_HELPER.resolve()), "LIVE_PR": json.dumps( {"draft": False, "head": {"sha": HEAD}, "state": "open"} ), @@ -98,5 +107,6 @@ def test_missing_verdict_fails_after_one_review_read(tmp_path: Path) -> None: assert "will rerun this failed job" in result.stdout assert calls.read_text(encoding="utf-8").splitlines() == [ "api repos/owner/repo/pulls/7", - "api --paginate repos/owner/repo/pulls/7/reviews?per_page=100", + "api --paginate --slurp repos/owner/repo/pulls/7/reviews?per_page=100", + "api repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=6a37e4cdfbd8bf6f3a60645a0ad7c13e1b9c1ae3 --jq .content", ] diff --git a/tests/test_opencode_required_verdict_regression.py b/tests/test_opencode_required_verdict_regression.py index c764ad0ad2..c5e735023d 100644 --- a/tests/test_opencode_required_verdict_regression.py +++ b/tests/test_opencode_required_verdict_regression.py @@ -12,6 +12,8 @@ import shutil import subprocess import textwrap +import tempfile +import sys from pathlib import Path import pytest @@ -42,7 +44,9 @@ def fail_closed_script() -> str: step = workflow.split( " - name: Fail closed without a current-head OpenCode verdict\n", 1 )[1] - return textwrap.dedent(step.split(" run: |\n", 1)[1]) + return textwrap.dedent(step.split(" run: |\n", 1)[1].split( + "\n cancel-superseded-opencode-review-runs:", 1 + )[0]) def admission_script() -> str: @@ -97,31 +101,56 @@ def test_opencode_dispatch_uses_the_same_target_repo_pr_group() -> None: def review(*, state: str, commit_id: str = HEAD, body: str = "") -> dict[str, object]: """Build one Reviews API record from the OpenCode GitHub App.""" return { + "id": 1, "user": {"login": "opencode-agent[bot]"}, "state": state, "commit_id": commit_id, - "body": body, + "body": body or f"## Pull request overview\n- Head SHA: `{commit_id}`\n", } def runtime_verdict(reviews: list[dict[str, object]], head_sha: str = HEAD) -> str: - """Execute the jq program embedded in the required workflow.""" - jq = shutil.which("jq") - if jq is None: - pytest.skip("jq is required to execute the production verdict filter") - workflow = WORKFLOW.read_text(encoding="utf-8") - marker = """jq -r -s --arg sha "$HEAD_SHA" '""" - start = workflow.index(marker) + len(marker) - end = workflow.index("\n ')", start) - result = subprocess.run( - [jq, "-r", "-s", "--arg", "sha", head_sha, workflow[start:end]], - input=json.dumps(reviews), - text=True, - capture_output=True, - check=False, - ) - assert result.returncode == 0, result.stderr - return result.stdout.strip() + """Execute the actual verdict step with strict offline-only GitHub reads.""" + import base64 + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + fake_gh = root / "gh" + fake_gh.write_text( + f"#!{sys.executable}\n" + Path(__file__).with_name("fixture_github_reads.py").read_text() + ) + fake_gh.chmod(0o700) + fixture = root / "fixture.json" + fixture.write_text(json.dumps({ + "pull": {"head": {"sha": head_sha}, "draft": False, "state": "open"}, + "reviews": reviews, "lookup_fail": False, "malformed": False, + "calls": str(root / "calls.jsonl"), + "helper_base64": base64.b64encode(RECEIPT_HELPER.read_bytes()).decode(), + })) + bash = shutil.which("bash") + assert bash is not None + result = subprocess.run( + [bash, "-c", fail_closed_script()], cwd=root, + env={ + "PATH": str(root) + os.pathsep + str(Path(bash).parent) + os.pathsep + str(Path(sys.executable).parent) + os.pathsep + os.defpath, + "HOME": str(root), "TMPDIR": str(root), "FIXTURE_FILE": str(fixture), + "TARGET_REPOSITORY": "ContextualWisdomLab/fast-mlsirm", "PR_NUMBER": "2018", + "HEAD_SHA": head_sha, "PR_ACTION": "synchronize", "PR_DRAFT": "false", + "CHANGED_SCOPE_CODE": "true", "WORKFLOW_SHA": "6a37e4cdfbd8bf6f3a60645a0ad7c13e1b9c1ae3", + "PYTHONDONTWRITEBYTECODE": "1", + }, + capture_output=True, text=True, timeout=15, + ) + calls = [json.loads(line) for line in (root / "calls.jsonl").read_text().splitlines()] + assert len(calls) == 3, result.stdout + result.stderr + assert all("POST" not in call and "-X" not in call for call in calls) + assert result.returncode in (0, 1), result.stderr + if result.returncode: + assert "No current-head formal OpenCode verdict:" in result.stderr + return "" + match = re.search(r"Current-head OpenCode verdict: (APPROVED|CHANGES_REQUESTED)\.", result.stdout) + assert match is not None, result.stdout + result.stderr + return match.group(1) @pytest.mark.parametrize("state", ("APPROVED", "CHANGES_REQUESTED")) @@ -324,7 +353,14 @@ def test_required_workflow_cannot_succeed_with_an_echo_only_placeholder() -> Non assert "while :; do" not in target_job assert "poll_interval_seconds" not in target_job assert "180 minutes of polling" not in target_job - assert 'gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100"' in workflow + admission = fail_closed_script() + assert 'gh api --paginate --slurp "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100"' in admission + assert 'contents/scripts/ci/opencode_review_receipt_gate.py?ref=${WORKFLOW_SHA}' in admission + assert 'gate.evaluate_receipts(reviews, head_sha, is_draft=False)' in admission + step_env = workflow.split( + ' - name: Fail closed without a current-head OpenCode verdict\n', 1 + )[1].split(' run: |\n', 1)[0] + assert 'WORKFLOW_SHA: ${{ github.workflow_sha }}' in step_env assert "github.event.pull_request.head.sha" in workflow assert "will rerun this failed job" in workflow assert ( diff --git a/tests/test_opencode_review_receipt_gate.py b/tests/test_opencode_review_receipt_gate.py index c971e2128a..a7437c5bca 100644 --- a/tests/test_opencode_review_receipt_gate.py +++ b/tests/test_opencode_review_receipt_gate.py @@ -49,6 +49,91 @@ def test_afipc_230_stale_changes_requested_are_not_current() -> None: assert "formal review" in reason +@pytest.mark.parametrize("login", sorted(receipt.FORMAL_AUTHORS)) +def test_newer_same_publisher_dismissal_retires_older_approval(login: str) -> None: + """A current-head dismissal must not resurrect its publisher's older approval.""" + approved = review( + commit=receipt.AFIPC_230_HEAD, state="APPROVED", login=login, review_id=1 + ) + dismissed = review( + commit=receipt.AFIPC_230_HEAD, state="DISMISSED", login=login, review_id=2 + ) + dismissed["body"] = "" + found, reason = receipt.evaluate_receipts( + [approved, dismissed], receipt.AFIPC_230_HEAD + ) + assert found is None + assert "dismissed" in reason.lower() + + +@pytest.mark.parametrize( + "case", + ["dismissed-only", "other-publisher", "newer-approved", "newer-changes-requested"], +) +def test_dismissal_preserves_independent_and_newer_receipts(case: str) -> None: + """Dismissal retires older same-publisher evidence, not independent or fresh verdicts.""" + approved = review(commit=receipt.AFIPC_230_HEAD, state="APPROVED", review_id=1) + dismissed = review(commit=receipt.AFIPC_230_HEAD, state="DISMISSED", review_id=2) + if case == "dismissed-only": + found, reason = receipt.evaluate_receipts([dismissed], receipt.AFIPC_230_HEAD) + assert found is None + assert "dismissed" in reason + elif case == "other-publisher": + dismissed["user"] = {"login": "github-actions[bot]"} + found, _ = receipt.evaluate_receipts([approved, dismissed], receipt.AFIPC_230_HEAD) + assert found is approved + else: + latest = review( + commit=receipt.AFIPC_230_HEAD, + state="APPROVED" if case == "newer-approved" else "CHANGES_REQUESTED", + review_id=3, + ) + found, _ = receipt.evaluate_receipts( + [approved, dismissed, latest], receipt.AFIPC_230_HEAD + ) + assert found is latest + + +@pytest.mark.parametrize("invalid_field", ["author", "commit", "body-head", "id"]) +def test_unbound_dismissal_does_not_retire_valid_receipt(invalid_field: str) -> None: + """Only a recognized publisher's identified exact-head dismissal retires evidence.""" + approved = review(commit=receipt.AFIPC_230_HEAD, state="APPROVED", review_id=1) + dismissed = review(commit=receipt.AFIPC_230_HEAD, state="DISMISSED", review_id=2) + if invalid_field == "author": + dismissed["user"] = {"login": "seonghobae"} + elif invalid_field == "commit": + dismissed["commit_id"] = "b" * 40 + elif invalid_field == "body-head": + dismissed["body"] = f"## Pull request overview\nHead SHA: `{'b' * 40}`" + else: + dismissed.pop("id") + found, _ = receipt.evaluate_receipts([approved, dismissed], receipt.AFIPC_230_HEAD) + assert found is approved + + +def test_graphql_dismissal_fails_closed_through_receipt_cli( + tmp_path: Path, capsys, monkeypatch +) -> None: + """REST approval plus newer GraphQL dismissal produces a failing required-gate exit.""" + approved = review(commit=receipt.AFIPC_230_HEAD, state="APPROVED", review_id=1) + dismissed = { + "id": 2, + "state": "DISMISSED", + "author": {"login": "opencode-agent[bot]"}, + "commit": {"oid": receipt.AFIPC_230_HEAD}, + "body": "", + } + path = tmp_path / "reviews.json" + path.write_text(json.dumps([approved, dismissed]), encoding="utf-8") + monkeypatch.delenv("GITHUB_STEP_SUMMARY", raising=False) + assert receipt.main( + ["--head-sha", receipt.AFIPC_230_HEAD, "--reviews-file", str(path)] + ) == 1 + captured = capsys.readouterr() + assert captured.out == "" + assert "dismissed" in captured.err + + def test_kaefa_79_stub_has_no_current_head_formal_receipt() -> None: """A 3-second green stub without a product-file review stays fail-closed.""" found, reason = receipt.evaluate_receipts([], receipt.KAEFA_79_HEAD) diff --git a/tests/test_opencode_shellcheck_literals.py b/tests/test_opencode_shellcheck_literals.py new file mode 100644 index 0000000000..f705b67657 --- /dev/null +++ b/tests/test_opencode_shellcheck_literals.py @@ -0,0 +1,32 @@ +"""Bind each lint quote repair to the original literal bytes and actual Bash.""" + +import hashlib +import json +import os +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github/workflows/opencode-review-dispatch.yml" +OPERANDS = json.loads((ROOT / "tests/fixtures/opencode_shell_literal_operands.json").read_text()) + + +@pytest.mark.parametrize("row", OPERANDS, ids=lambda row: row["literal_sha256"][:12]) +def test_migrated_operand_preserves_original_literal(row): + """Inspect each shipped replacement, then expand it without its consumer.""" + text = WORKFLOW.read_text(encoding="utf-8") + assert text.count(row["after"]) == row["count"] + assert hashlib.sha256(row["expected"].encode()).hexdigest() == row["literal_sha256"] + # No bash/jq/GraphQL consumer runs here; only operand expansion into printf. + start = text.index(row["after"]) + actual = text[start:start + len(row["after"])] + result = subprocess.run( + ["bash", "-eu", "-c", "printf '%s' " + actual], + env={"PATH": os.defpath, "LC_ALL": "C", "n": "SENTINEL", "owner": "SENTINEL"}, + capture_output=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + assert result.stderr == b"" + assert result.stdout == row["expected"].encode() diff --git a/tests/test_opencode_vulkan_path_selection.py b/tests/test_opencode_vulkan_path_selection.py new file mode 100644 index 0000000000..249424fcff --- /dev/null +++ b/tests/test_opencode_vulkan_path_selection.py @@ -0,0 +1,58 @@ +"""Exercise the shipped ICD selector without installing or running a GPU.""" + +import os +import subprocess +from pathlib import Path + +import pytest + +WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" + + +def _adapter_function(icd_root): + """Change only the fixed system pathname to a fixture-owned directory.""" + text = WORKFLOW.read_text(encoding="utf-8") + start = text.index(" ensure_rust_gpu_adapter() {") + end = text.index("\n ensure_rust_desktop_deps() {", start) + function = text[start:end] + assert function.count("/usr/share/vulkan/icd.d/") >= 1 + return function.replace("/usr/share/vulkan/icd.d/", str(icd_root) + "/") + + +@pytest.mark.parametrize("names,expected", [ + ([], None), + (["lvp_icd.a.json"], "lvp_icd.a.json"), + (["lvp_icd.b.json", "lvp_icd.a.json"], "lvp_icd.a.json"), + (["lvp_icd.a b.json"], "lvp_icd.a b.json"), + (["lvp_icd.a\nb.json"], "lvp_icd.a\nb.json"), +]) +def test_adapter_keeps_complete_path(tmp_path, names, expected): + """The actual selector retains the whole selected filename and both exports.""" + for name in names: + (tmp_path / name).write_text("{}", encoding="utf-8") + script = "append() { :; }\n" + _adapter_function(tmp_path) + script += '\nensure_rust_gpu_adapter\nprintf "%s\\0%s" "${VK_ICD_FILENAMES:-}" "${VK_DRIVER_FILES:-}"' + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", script], + env={"PATH": os.defpath, "LC_ALL": "C"}, + capture_output=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + selected = str(tmp_path / expected).encode() if expected else b"" + assert result.stdout == selected + b"\0" + selected + assert result.stderr == b"" + + +def test_adapter_does_not_select_matching_directory(tmp_path): + """Only a regular ICD file can be exported as a driver configuration.""" + (tmp_path / "lvp_icd.a.json").mkdir() + (tmp_path / "lvp_icd.b.json").write_text("{}", encoding="utf-8") + script = "append() { :; }\n" + _adapter_function(tmp_path) + script += '\nensure_rust_gpu_adapter\nprintf "%s" "${VK_ICD_FILENAMES:-}"' + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", script], + env={"PATH": os.defpath, "LC_ALL": "C"}, + capture_output=True, timeout=10, + ) + assert result.returncode == 0, result.stderr + assert result.stdout == str(tmp_path / "lvp_icd.b.json").encode() diff --git a/tests/test_personal_review_route.py b/tests/test_personal_review_route.py new file mode 100644 index 0000000000..4317810d91 --- /dev/null +++ b/tests/test_personal_review_route.py @@ -0,0 +1,110 @@ +"""Personal wire auto is distinct from an orchestrator alias; policy fails closed.""" +import importlib +import json +from pathlib import Path +import pytest + + +def module(): + assert importlib.util.find_spec('scripts.ci.personal_review_route') is not None, 'Missing personal wire-auto route' + return importlib.import_module('scripts.ci.personal_review_route') + + +def test_config_primary_small_reset_preserves_permissions_and_wire_auto(tmp_path): + route = module() + path = tmp_path / 'opencode.jsonc' + path.write_text(json.dumps({'model': 'unsafe/a', 'small_model': 'unsafe/b', + 'permission': {'edit': 'deny'}, 'agent': {'ci-review': {'steps': 4}}})) + route.configure_opencode(path) + result = json.loads(path.read_text()) + assert result['model'] == result['small_model'] == 'personal-litellm/auto' + assert result['provider']['personal-litellm']['models'].keys() == {'auto'} + options = result['provider']['personal-litellm']['options'] + assert options == {'baseURL': 'https://litellm.poinnetworks.net/v1', + 'apiKey': '{env:LLM_GATEWAY_API_KEY}'} + assert result['permission'] == {'edit': 'deny'} + assert result['agent']['ci-review']['steps'] == 4 + + +@pytest.mark.parametrize('cost,zdr,private', [(False, True, False), (True, False, True), (False, False, True)]) +def test_private_target_without_retention_attestation_stops_before_configuration(cost,zdr,private): + with pytest.raises(RuntimeError, match='STOP'): + module().require_admission(zero_cost=cost, zero_retention=zdr, private_target=private) + + +def test_public_and_private_attested_admission_are_distinct(): + module().require_admission(zero_cost=True, zero_retention=False, private_target=False) + module().require_admission(zero_cost=True, zero_retention=True, private_target=True) + + +def test_actual_review_workflows_consume_named_existing_secret_and_auto_route(): + root = Path(__file__).resolve().parents[1] + noema = (root / '.github/workflows/noema-review.yml').read_text() + dispatch = (root / '.github/workflows/opencode-review-dispatch.yml').read_text() + for text in (noema, dispatch): + assert 'LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }}' in text + assert 'scripts.ci.personal_review_route' in text + assert 'PERSONAL_REVIEW_ZERO_COST_ATTESTED' in text + assert 'PERSONAL_REVIEW_ZDR_ATTESTED' in text + assert 'export NOEMA_LLM_MODEL="auto"' in noema + assert 'https://litellm.poinnetworks.net/v1/chat/completions' in noema + assert '--opencode-config "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc"' in dispatch + assert 'OPENCODE_MODEL_CANDIDATES="personal-litellm/auto"' in dispatch + assert 'MODEL="personal-litellm/auto"' in dispatch + + +@pytest.mark.parametrize('configure', [False, True]) +def test_cli_consumes_explicit_policy_and_preserves_config(tmp_path, configure): + path = tmp_path / 'config.jsonc' + path.write_text('// trusted comment\n{"permission":{"edit":"deny"}}') + args = ['--zero-cost', 'true', '--zero-retention', 'true', '--private-target', 'true'] + if configure: + args += ['--opencode-config', str(path)] + assert module().main(args) == 0 + if configure: + assert json.loads(path.read_text())['model'] == 'personal-litellm/auto' + else: + assert path.read_text().startswith('// trusted comment') + + +def test_cli_stop_does_not_write_config_or_assume_attestation(tmp_path): + path = tmp_path / 'config.jsonc' + original = '{"permission":{"edit":"deny"}}' + path.write_text(original) + with pytest.raises(RuntimeError, match='zero-cost supplier'): + module().main(['--zero-cost', 'false', '--zero-retention', 'false', + '--private-target', 'false', '--opencode-config', str(path)]) + assert path.read_text() == original + + +def test_module_cli_entry_point_stops_without_policy_inputs(monkeypatch): + import runpy + import sys + monkeypatch.setattr(sys, 'argv', [str(Path(module().__file__))]) + with pytest.raises(SystemExit) as result: + runpy.run_path(str(Path(module().__file__)), run_name='__main__') + assert result.value.code == 2 + + +def test_actual_noema_payload_uses_literal_auto_without_sampling_override(monkeypatch): + from scripts.ci import noema_review_gate as gate + from tests.test_noema_review_gate import FakeResponse + captured = [] + monkeypatch.setenv('NOEMA_LLM_API_URL', 'https://litellm.poinnetworks.net/v1/chat/completions') + monkeypatch.setenv('NOEMA_LLM_MODEL', 'auto') + monkeypatch.setenv('NOEMA_LLM_API_KEY', 'synthetic-test-only') + monkeypatch.setattr(gate, 'validate_substantive_verdict', lambda *_: None) + monkeypatch.setattr(gate.socket, 'getaddrinfo', lambda *_: []) + class Opener: + def open(self, request): + captured.append((request.full_url, json.loads(request.data))) + return FakeResponse({'model': 'auto', 'choices': [{'message': {'content': json.dumps({ + 'decision': 'comment', 'summary': 'Controlled source-only payload probe.', 'findings': []})}}]}) + monkeypatch.setattr(gate.urllib.request, 'build_opener', lambda *_: Opener()) + verdict = gate.call_llm('ContextualWisdomLab/.github', 2565, {'headRefOid': 'a'*40}, + '', False, 'a'*40) + assert verdict['decision'] == 'comment' + url, body = captured[0] + assert url == 'https://litellm.poinnetworks.net/v1/chat/completions' + assert body['model'] == 'auto' and 'temperature' not in body + assert body['response_format']['type'] == 'json_schema' diff --git a/tests/test_place_maturin_extension.py b/tests/test_place_maturin_extension.py index 2b7460e440..c62d2896e9 100644 --- a/tests/test_place_maturin_extension.py +++ b/tests/test_place_maturin_extension.py @@ -2,7 +2,10 @@ from __future__ import annotations +import os import runpy +import shlex +import subprocess import sys import zipfile from pathlib import Path @@ -100,8 +103,17 @@ def test_offline_maturin_build_places_the_extension_for_pytest() -> None: Path(__file__).resolve().parents[1] / ".github/workflows/opencode-review-dispatch.yml" ).read_text(encoding="utf-8") build = workflow.split("build_maturin_extension_if_needed() {", 1)[1].split("\n }", 1)[0] - assert 'python3 "$2" "$dist_dir"/*.whl .' in build - assert '"${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py"' in build + operand = build.split("bash -c ", 1)[1].rsplit(' bash "$project_dir"', 1)[0] + expanded = subprocess.run( + ["bash", "-eu", "-c", "printf '%s' " + operand], + env={"PATH": os.defpath}, capture_output=True, text=True, timeout=10, + ) + assert expanded.returncode == 0, expanded.stderr + assert 'python3 "$2" "$dist_dir"/*.whl .' in expanded.stdout + command = shlex.split(build.split("bash -c ", 1)[1]) + assert command[1:] == [ + "bash", "$project_dir", "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py" + ] def test_cli_rejects_wrong_arity_and_reports_placement_failure( diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 1bfc13292e..3c1c9f8d61 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 = "09bbf8181a443f7a5630ec91ca958440c5dcfc68" +REVIEW_DISPATCH_BLOB_SHA = "443888bf1f82e07910af2943f405c8eb74f11b9a" def _workflow_text(path: Path) -> str: diff --git a/tests/test_pr_review_fix_scheduler_control_runner.py b/tests/test_pr_review_fix_scheduler_control_runner.py index b6eaa916e7..b71b011522 100644 --- a/tests/test_pr_review_fix_scheduler_control_runner.py +++ b/tests/test_pr_review_fix_scheduler_control_runner.py @@ -33,10 +33,14 @@ def _job() -> dict: def test_control_pool_only_for_central_main_caller() -> None: runs_on = _job()["runs-on"] - assert runs_on == ( - f"${{{{ github.workflow_ref == '{CENTRAL_CALLER}' && {CONTROL} " - "|| fromJSON('[\"ubuntu-24.04\"]') }}" + expression = ( + f"github.workflow_ref == '{CENTRAL_CALLER}' && {CONTROL} " + "|| fromJSON('{\"group\":\"CWL CI isolated\",\"labels\":[\"self-hosted\",\"linux\",\"x64\",\"cwlab-ci-isolated\"]}')" ) + assert runs_on == { + "group": "${{ (" + expression + ").group }}", + "labels": "${{ (" + expression + ").labels }}", + } def test_dispatch_job_never_checks_out_pull_request_content() -> None: diff --git a/tests/test_premerge_review_tooling.py b/tests/test_premerge_review_tooling.py new file mode 100644 index 0000000000..26a6dc459d --- /dev/null +++ b/tests/test_premerge_review_tooling.py @@ -0,0 +1,564 @@ +"""Source-only admission tests; synthetic pins are never operational approval.""" + +from __future__ import annotations + +import hashlib +import importlib.util +import json +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +MODULE = ROOT / "scripts" / "ci" / "premerge_review_tooling.py" + + +def _load(): + """Require the actual controller boundary rather than a fixture substitute.""" + assert MODULE.is_file(), "missing trusted premerge tooling admission boundary" + spec = importlib.util.spec_from_file_location("premerge_review_tooling", MODULE) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _fixture(): + """Return inert source bytes and an independently supplied fixture digest.""" + files = {".github/actions/noema-review/two_phase.py": b"# FIXTURE ONLY\n"} + manifest = { + "schema_version": 1, + "repository": "ContextualWisdomLab/.github", + "commit": "a" * 40, + "tree": "b" * 40, + "entrypoint": ".github/actions/noema-review/two_phase.py", + "files": {name: hashlib.sha256(data).hexdigest() for name, data in files.items()}, + } + raw = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode() + return raw, files, hashlib.sha256(raw).hexdigest() + + +def test_unaccepted_tooling_rejected_before_source_consumption(): + """A caller cannot replace independent acceptance with its own digest.""" + module = _load() + raw, files, _accepted = _fixture() + with pytest.raises(ValueError, match="accepted manifest digest"): + module.admit_tooling(raw, accepted_digest="0" * 64, source_files=files) + + +def test_source_inventory_must_equal_accepted_manifest(): + """Even a pinned manifest cannot admit substituted or extra executable bytes.""" + module = _load() + raw, files, accepted = _fixture() + for changed in [ + {next(iter(files)): b"# substituted\n"}, + {**files, "scripts/ci/hidden.py": b"# extra\n"}, + {}, + ]: + with pytest.raises(ValueError, match="source inventory"): + module.admit_tooling(raw, accepted_digest=accepted, source_files=changed) + + +def test_complete_pinned_source_inventory_admitted(): + """Nonempty exact source bytes have a useful positive admission control.""" + raw, files, accepted = _fixture() + assert _load().admit_tooling(raw, accepted_digest=accepted, source_files=files) is None + + +@pytest.mark.parametrize("change", [ + {"schema_version": True}, + {"schema_version": 2}, + {"repository": "foreign/repo"}, + {"commit": "main"}, + {"tree": "b" * 39}, + {"entrypoint": "../outside.py"}, + {"entrypoint": "scripts/ci/not-listed.py"}, + {"unexpected": "field"}, + {"files": {}}, + {"files": {"../outside.py": "c" * 64}}, +]) +def test_pinned_but_invalid_manifest_refused(change): + """Independent digest acceptance is necessary but does not replace schema checks.""" + module = _load() + raw, files, _accepted = _fixture() + manifest = {**json.loads(raw), **change} + raw = json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode() + with pytest.raises(ValueError, match="manifest"): + module.admit_tooling(raw, accepted_digest=hashlib.sha256(raw).hexdigest(), source_files=files) + + +@pytest.mark.parametrize("name", ["/absolute.py", "a//b.py", "a/./b.py", "a/../b.py", "a\\b.py", "a\x00b.py"]) +def test_noncanonical_inventory_path_refused(name): + """Self-consistent unsafe paths cannot leave the immutable tooling namespace.""" + module = _load() + raw, _files, _accepted = _fixture() + manifest = json.loads(raw) + files = {name: b"# inert\n"} + manifest["files"] = {name: hashlib.sha256(files[name]).hexdigest()} + manifest["entrypoint"] = name + raw = json.dumps(manifest).encode() + with pytest.raises(ValueError, match="manifest"): + module.admit_tooling(raw, accepted_digest=hashlib.sha256(raw).hexdigest(), source_files=files) + + +def test_duplicate_manifest_field_refused(): + """Ambiguous authenticated JSON is not a second interpretation of an accepted source.""" + raw, files, _accepted = _fixture() + raw = raw[:-1] + b',"schema_version":1}' + with pytest.raises(ValueError, match="manifest"): + _load().admit_tooling(raw, accepted_digest=hashlib.sha256(raw).hexdigest(), source_files=files) + + +@pytest.mark.parametrize("raw", [b"[]", b"null", b"{", b"{}" + b" " * (2 * 1024 * 1024)]) +def test_malformed_or_oversized_pinned_manifest_refused(raw): + """A trusted digest cannot make malformed or unbounded metadata usable.""" + with pytest.raises(ValueError, match="manifest"): + _load().admit_tooling(raw, accepted_digest=hashlib.sha256(raw).hexdigest(), source_files={}) + + +def _scope(): + """A fixture scope contains explicit reviewed PR and independent reviewer identities.""" + return { + "repository": "ContextualWisdomLab/example", "number": 7, + "head": "c" * 40, "base": "d" * 40, + "author": "seonghobae", "reviewer": "cwl-noema-review[bot]", + } + + +@pytest.mark.parametrize("change", [ + {"head": "e" * 40}, {"base": "e" * 40}, {"author": "other"}, + {"reviewer": "other[bot]"}, {"repository": "ContextualWisdomLab/other"}, + {"number": 8}, {"state": "CLOSED"}, {"draft": True}, +]) +def test_review_scope_drift_rejected(change): + """Full live scope, not head alone, defines premerge evidence applicability.""" + module = _load() + assert hasattr(module, "require_review_scope"), "missing full live review scope admission" + live = {**_scope(), "state": "OPEN", "draft": False, **change} + with pytest.raises(ValueError, match="review scope"): + module.require_review_scope(_scope(), live) + + +def test_current_full_review_scope_allowed(): + """A live independently authored non-draft PR remains reviewable.""" + module = _load() + assert hasattr(module, "require_review_scope"), "missing full live review scope admission" + module.require_review_scope(_scope(), {**_scope(), "state": "OPEN", "draft": False}) + + +@pytest.mark.parametrize("change", [ + {"number": True}, {"number": 0}, {"head": "main"}, {"base": "d" * 39}, + {"repository": "foreign/repo"}, {"author": ""}, {"reviewer": ""}, + {"reviewer": "seonghobae"}, {"reviewer": "SeongHoBae"}, +]) +def test_matching_invalid_or_self_review_scope_refused(change): + """Equality cannot make malformed identity or self approval admissible.""" + scope = {**_scope(), **change} + with pytest.raises(ValueError, match="review scope"): + _load().require_review_scope(scope, {**scope, "state": "OPEN", "draft": False}) + + +def test_controller_refuses_runtime_admission_before_loading_tooling(tmp_path): + """A reviewed source pin never implies runner or supplier admission.""" + module = _load() + assert hasattr(module, "run_review"), "missing executable premerge controller" + raw, files, accepted = _fixture() + events = [] + + def admission(): + events.append("admission") + raise RuntimeError("native/policy NOT_HELD") + + def load(): + pytest.fail("unadmitted tooling must not load") + + with pytest.raises(RuntimeError, match="NOT_HELD"): + module.run_review( + raw, accepted_digest=accepted, source_files=files, scope=_scope(), + fetch_live=lambda: {**_scope(), "state": "OPEN", "draft": False}, + require_execution_admission=admission, load_tooling=load, + refresh_publisher=lambda: pytest.fail("no credentials before admission"), + private_parent=tmp_path, + ) + assert events == ["admission"] + assert not list(tmp_path.iterdir()) + + +def test_controller_composes_actual_compatible_two_phase_modules(tmp_path, monkeypatch): + """Real prepare/seal/read/publish runs with inert network/model collaborators only.""" + module = _load() + two_path = ROOT / ".github/actions/noema-review/two_phase.py" + spec = importlib.util.spec_from_file_location("trusted_two_phase_fixture", two_path) + two = importlib.util.module_from_spec(spec) + spec.loader.exec_module(two) + scope = _scope() + pr = {"state": "OPEN", "isDraft": False, "headRefOid": scope["head"], + "baseRefOid": scope["base"], "author": {"login": scope["author"]}} + events = [] + monkeypatch.setattr(two.gate, "fetch_pr", lambda *_: pr) + monkeypatch.setattr(two.gate, "current_actor", lambda: scope["reviewer"]) + monkeypatch.setattr(two.gate, "existing_noema_review", lambda *_: False) + monkeypatch.setattr(two.gate, "fetch_diff", lambda *_: ("fixture diff", False)) + monkeypatch.setattr(two.gate, "fetch_changed_files", lambda *_: [("src/a.py", "MODIFIED")]) + monkeypatch.setattr(two.gate, "build_review_context", lambda *_: "fixture context") + + def model(*_): + events.append("actual_prepare_model_boundary") + return {"decision": "approve", "summary": "FIXTURE ONLY: not a real model verdict"} + + monkeypatch.setattr(two.gate, "call_llm", model) + monkeypatch.setattr(two.gate, "submit_review", lambda *_: events.append("actual_publisher_sink")) + + def load(): + events.append("load_accepted_fixture_tooling") + return two + + def refresh(): + events.append("independent_publisher_refresh_fixture") + + raw, files, accepted = _fixture() + result = module.run_review( + raw, accepted_digest=accepted, source_files=files, scope=scope, + fetch_live=lambda: {**scope, "state": "OPEN", "draft": False}, + require_execution_admission=lambda: events.append("admission_fixture"), + load_tooling=load, refresh_publisher=refresh, private_parent=tmp_path, + ) + assert result == 0 + assert events == ["admission_fixture", "load_accepted_fixture_tooling", + "actual_prepare_model_boundary", "admission_fixture", + "independent_publisher_refresh_fixture", "actual_publisher_sink"] + assert not list(tmp_path.iterdir()) + + +def test_controller_rejects_forged_runner_local_envelope(tmp_path): + """The controller never forwards a SOURCE_PASS or arbitrary envelope as model evidence.""" + from types import SimpleNamespace + + raw, files, accepted = _fixture() + + def prepare(_repo, _number, _head, path): + path.write_text(json.dumps({"SOURCE_PASS": True})) + return 0 + + with pytest.raises(ValueError, match="prepared envelope"): + _load().run_review( + raw, accepted_digest=accepted, source_files=files, scope=_scope(), + fetch_live=lambda: {**_scope(), "state": "OPEN", "draft": False}, + require_execution_admission=lambda: None, + load_tooling=lambda: SimpleNamespace(prepare_verdict=prepare, + publish_verdict=lambda *_: pytest.fail("must not publish")), + refresh_publisher=lambda: pytest.fail("must not refresh"), private_parent=tmp_path, + ) + assert not list(tmp_path.iterdir()) + + +@pytest.mark.parametrize("phase", ["missing", "failed", "scope_drift", "admission_revoked", "refresh_drift"]) +def test_controller_rejects_unpublishable_preparation(tmp_path, phase): + """No refresh or publication follows a missing verdict or expired scope/admission.""" + from types import SimpleNamespace + + module = _load() + raw, files, accepted = _fixture() + observations = 0 + admissions = 0 + refreshed = False + + def live(): + nonlocal observations + observations += 1 + return {**_scope(), "state": "OPEN", "draft": False, + **({"base": "e" * 40} if (phase == "scope_drift" and observations > 1) or refreshed else {})} + + def refresh(): + nonlocal refreshed + if phase != "refresh_drift": + pytest.fail("must not refresh") + refreshed = True + + def admission(): + nonlocal admissions + admissions += 1 + if phase == "admission_revoked" and admissions > 1: + raise RuntimeError("admission revoked") + + def prepare(_repo, _number, _head, path): + if phase != "missing": + scope = _scope() + payload = { + "schema_version": 1, "repository": scope["repository"], + "pull_request_number": scope["number"], "expected_head": scope["head"], + "expected_base": scope["base"], "reviewer_actor": scope["reviewer"], + "pull_request_author": scope["author"], "verdict": {"decision": "approve"}, + } + path.write_text(json.dumps(payload)) + path.chmod(0o600) + return 1 if phase == "failed" else 0 + + with pytest.raises((RuntimeError, ValueError)): + module.run_review( + raw, accepted_digest=accepted, source_files=files, scope=_scope(), + fetch_live=live, require_execution_admission=admission, + load_tooling=lambda: SimpleNamespace(prepare_verdict=prepare, + publish_verdict=lambda *_: pytest.fail("must not publish")), + refresh_publisher=refresh, private_parent=tmp_path, + ) + assert not list(tmp_path.iterdir()) + + +def test_tooling_loader_rejects_mutated_source_before_child(tmp_path): + """Live module bytes, not a retained mapping alone, are checked at spawn.""" + module = _load() + assert hasattr(module, "load_noema_tooling"), "missing bound executable tooling loader" + raw, files, accepted = _fixture() + path = tmp_path / next(iter(files)) + path.parent.mkdir(parents=True) + path.write_bytes(b"# substituted\n") + with pytest.raises(ValueError, match="source inventory"): + module.load_noema_tooling( + tmp_path, raw, accepted_digest=accepted, interpreter=Path(__import__('sys').executable), + phase_environment={}, child_runner=lambda *_args, **_kwargs: pytest.fail("must not spawn"), + ).prepare_verdict("ContextualWisdomLab/example", 7, "c" * 40, tmp_path / "verdict.json") + + +@pytest.mark.parametrize("phase,environment", [ + ("publish", {"NOEMA_LLM_API_KEY": "SYNTHETIC_ONLY"}), + ("prepare", {"NOEMA_LLM_MODEL": "orchestrator/free"}), + ("prepare", {"NOEMA_LLM_MODEL": "auto", "PYTHONPATH": "/untrusted"}), + ("prepare", {"NOEMA_LLM_MODEL": "auto", "LD_PRELOAD": "/untrusted"}), +]) +def test_phase_environment_rejected_before_child(tmp_path, phase, environment): + """Publisher gets no inference secret and model child gets no injection overrides.""" + import sys + + module = _load() + raw, files, accepted = _fixture() + for name, data in files.items(): + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + tooling = module.load_noema_tooling( + tmp_path, raw, accepted_digest=accepted, interpreter=Path(sys.executable), + phase_environment=lambda _: environment, + child_runner=lambda *_args, **_kwargs: pytest.fail("forbidden environment reached child"), + ) + with pytest.raises(ValueError, match="phase environment"): + getattr(tooling, phase + "_verdict")("ContextualWisdomLab/example", 7, "c" * 40, tmp_path.parent / "unused.json") + + +def test_live_boolean_number_cannot_equal_admitted_integer(): + """Python equality must not treat a malformed live PR number as scope equality.""" + expected = {**_scope(), "number": 1} + with pytest.raises(ValueError, match="review scope"): + _load().require_review_scope(expected, {**expected, "number": True, "state": "OPEN", "draft": False}) + + +def _token_response(phase): + """Synthetic GitHub installation-token issuance response, never a live token.""" + return {"token": "SYNTHETIC_FIXTURE_ONLY", + "permissions": {"contents": "read", "metadata": "read", + "pull_requests": "read" if phase == "prepare" else "write"}, + "repositories": [{"full_name": _scope()["repository"]}]} + + +@pytest.mark.parametrize("phase,change", [ + ("prepare", {"permissions": {"contents": "read", "pull_requests": "write"}}), + ("prepare", {"permissions": {"contents": "write", "pull_requests": "read"}}), + ("publish", {"permissions": {"contents": "read", "pull_requests": "read"}}), + ("prepare", {"repositories": [{"full_name": "ContextualWisdomLab/other"}]}), + ("prepare", {"repositories": [{"full_name": _scope()["repository"]}, {"full_name": "ContextualWisdomLab/other"}]}), + ("prepare", {"token": ""}), +]) +def test_issued_credential_scope_rejected_before_phase_environment(phase, change): + """Trusted issuance response must prove exact phase permissions and repository.""" + module = _load() + assert hasattr(module, "phase_environment_from_issuance"), "missing issuance-scoped consumer" + response = {**_token_response(phase), **change} + with pytest.raises(ValueError, match="credential issuance"): + module.phase_environment_from_issuance( + phase, response, scope=_scope(), reviewer_actor="cwl-noema-review[bot]", + installation_id=7, inference_key="SYNTHETIC_INFERENCE_ONLY", + ) + + +@pytest.mark.parametrize("phase,response,actor,installation,key", [ + ("invalid", {}, "cwl-noema-review[bot]", 7, "SYNTHETIC"), + ("prepare", None, "cwl-noema-review[bot]", 7, "SYNTHETIC"), + ("prepare", _token_response("prepare"), "foreign[bot]", 7, "SYNTHETIC"), + ("prepare", _token_response("prepare"), "cwl-noema-review[bot]", True, "SYNTHETIC"), + ("prepare", _token_response("prepare"), "cwl-noema-review[bot]", 7, None), +]) +def test_issuance_identity_and_inference_characterization(phase, response, actor, installation, key): + """Unsupported or missing authenticated authority metadata remains a refusal.""" + with pytest.raises(ValueError, match="credential issuance"): + _load().phase_environment_from_issuance(phase, response, scope=_scope(), + reviewer_actor=actor, installation_id=installation, inference_key=key) + + +def test_issued_read_and_write_credentials_are_separated(): + """Only prepared read authority receives inference; publisher remains independent.""" + module = _load() + assert hasattr(module, "phase_environment_from_issuance"), "missing issuance-scoped consumer" + for phase in ("prepare", "publish"): + env = module.phase_environment_from_issuance( + phase, _token_response(phase), scope=_scope(), reviewer_actor="cwl-noema-review[bot]", + installation_id=7, inference_key="SYNTHETIC_INFERENCE_ONLY", + ) + assert env["GH_TOKEN"] == "SYNTHETIC_FIXTURE_ONLY" + assert ("NOEMA_LLM_API_KEY" in env) is (phase == "prepare") + module.require_phase_environment(phase, env) + + +def _envelope_payload(): + """An otherwise-valid synthetic eight-field producer output.""" + scope = _scope() + return {"schema_version": 1, "repository": scope["repository"], + "pull_request_number": scope["number"], "expected_head": scope["head"], + "expected_base": scope["base"], "pull_request_author": scope["author"], + "reviewer_actor": scope["reviewer"], "verdict": {"decision": "approve"}} + + +@pytest.mark.parametrize("kind", ["empty", "oversize", "malformed", "missing", "wrong_keys", "version", "identity", "verdict"]) +def test_envelope_characterization_rejections(tmp_path, kind): + """Exercise each bounded local-envelope refusal without inference or publication.""" + payload = _envelope_payload() + if kind == "version": + payload["schema_version"] = True + if kind == "identity": + payload["reviewer_actor"] = "foreign[bot]" + if kind == "verdict": + payload["verdict"] = None + if kind == "wrong_keys": + payload = {} + raw = json.dumps(payload).encode() + if kind == "empty": + raw = b"" + if kind == "oversize": + raw = b" " * (2 * 1024 * 1024 + 1) + if kind == "malformed": + raw = b"{" + path = tmp_path / "envelope" + if kind != "missing": + path.write_bytes(raw) + path.chmod(0o600) + with pytest.raises(ValueError, match="prepared envelope"): + _load().require_prepared_envelope(path, _scope()) + + +@pytest.mark.parametrize("kind", ["relative", "symlink", "fifo", "entrypoint", "interpreter"]) +def test_loader_characterization_rejections(tmp_path, kind): + """Invalid namespace and interpreter controls never reach a child process.""" + import os + import sys + + raw, files, accepted = _fixture() + root = tmp_path / "source" + root.mkdir() + for name, data in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + interpreter = Path(sys.executable) + if kind == "relative": + root = Path("relative") + if kind == "symlink": + (root / "alias").symlink_to(next(iter(files))) + if kind == "fifo": + os.mkfifo(root / "pipe") + if kind == "entrypoint": + manifest = json.loads(raw) + manifest["entrypoint"] = "other.py" + (root / next(iter(files))).unlink() + (root / "other.py").write_bytes(b"# inert") + manifest["files"] = {"other.py": hashlib.sha256(b"# inert").hexdigest()} + raw = json.dumps(manifest).encode() + accepted = hashlib.sha256(raw).hexdigest() + if kind == "interpreter": + interpreter = Path("relative-python") + with pytest.raises(ValueError): + _load().load_noema_tooling(root, raw, accepted_digest=accepted, interpreter=interpreter, + phase_environment=lambda _: {"NOEMA_LLM_MODEL": "auto"}, + child_runner=lambda *_args, **_kwargs: pytest.fail("must not execute") + ).prepare_verdict("ContextualWisdomLab/example", 7, "c" * 40, tmp_path / "verdict") + + +def test_scope_missing_fields_and_unsupported_phase_characterization(): + """Incomplete authority records and unknown phase names fail closed.""" + module = _load() + with pytest.raises(ValueError, match="review scope"): + module.require_review_scope({}, {}) + with pytest.raises(ValueError, match="phase environment"): + module.require_phase_environment("foreign", {}) + + +def test_bound_tooling_runs_real_two_phase_cli_with_inert_fixture_gate(tmp_path): + """Execute actual CLI producer/publisher bytes, not live inference or GitHub.""" + import sys + + tooling_root = tmp_path / "tooling" + files = { + ".github/actions/noema-review/two_phase.py": (ROOT / ".github/actions/noema-review/two_phase.py").read_bytes(), + "scripts/__init__.py": b"", + "scripts/ci/__init__.py": b"", + "scripts/ci/noema_review_gate.py": b'''"""INERT FIXTURE ONLY: not the production gate or provider.""" +import json +import os +from pathlib import Path +PRIMARY_REVIEW_AUTHORS = {"opencode-agent[bot]"} +def fetch_pr(repo, number): + return {"state":"OPEN","isDraft":False,"headRefOid":"c"*40,"baseRefOid":"d"*40,"author":{"login":"seonghobae"}} +def require_expected_head(pr, head): + assert pr["headRefOid"] == head +def current_actor(): + return "cwl-noema-review[bot]" +def require_distinct_pr_author(pr, actor): + assert pr["author"]["login"] != actor + return pr["author"]["login"] +def existing_noema_review(*args): + return False +def fetch_diff(*args): + return "FIXTURE DIFF", False +def fetch_changed_files(*args): + return [("src/a.py", "MODIFIED")] +def build_review_context(*args): + return "FIXTURE CONTEXT" +def call_llm(*args): + assert os.environ["FIXTURE_PHASE"] == "prepare" + return {"decision":"approve", "summary":"FIXTURE ONLY"} +def submit_review(repo, number, pr, actor, verdict): + assert os.environ["FIXTURE_PHASE"] == "publish" + Path(os.environ["FIXTURE_SINK"]).write_text(json.dumps({"fixture_only":True,"head":pr["headRefOid"],"actor":actor,"verdict":verdict})) +''', + } + for name, data in files.items(): + path = tooling_root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + raw, _, _ = _fixture() + manifest = json.loads(raw) + manifest["files"] = {name: hashlib.sha256(data).hexdigest() for name, data in files.items()} + raw = json.dumps(manifest).encode() + accepted = hashlib.sha256(raw).hexdigest() + sink = tmp_path / "fixture-sink.json" + + def phase_environment(phase): + environment = _load().phase_environment_from_issuance( + phase, _token_response(phase), scope=_scope(), + reviewer_actor="cwl-noema-review[bot]", installation_id=7, + inference_key="SYNTHETIC_INFERENCE_ONLY", + ) + environment.update(FIXTURE_PHASE=phase, FIXTURE_SINK=str(sink)) + return environment + + loader = lambda: _load().load_noema_tooling( + tooling_root, raw, accepted_digest=accepted, interpreter=Path(sys.executable), + phase_environment=phase_environment, + ) + assert _load().run_review( + raw, accepted_digest=accepted, source_files=files, scope=_scope(), + fetch_live=lambda: {**_scope(), "state": "OPEN", "draft": False}, + require_execution_admission=lambda: None, load_tooling=loader, + refresh_publisher=lambda: None, private_parent=tmp_path, + ) == 0 + assert json.loads(sink.read_text())["fixture_only"] is True + assert not list(tmp_path.glob("premerge-review-*")) + assert {name: (tooling_root / name).read_bytes() for name in files} == files diff --git a/tests/test_r_isolated_platform_admission.py b/tests/test_r_isolated_platform_admission.py new file mode 100644 index 0000000000..06ed2de59f --- /dev/null +++ b/tests/test_r_isolated_platform_admission.py @@ -0,0 +1,71 @@ +"""Source and executable matrix-admission contract; no native R execution.""" +import json +from pathlib import Path +import subprocess +import sys +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +class Admission(unittest.TestCase): + def test_matrix_admission(self): + script = ROOT / 'scripts/ci/r_isolated_matrix.py' + self.assertTrue(script.is_file(), 'Missing supported-platform admission') + rows = [{'os': 'ubuntu-latest', 'r': 'release'}, + {'os': 'macos-latest', 'r': 'release'}, + {'os': 'windows-latest', 'r': 'release'}, + {'os': 'ubuntu-24.04-arm', 'r': 'release'}] + result = subprocess.run([sys.executable, str(script), json.dumps(rows)], + capture_output=True, text=True, check=False) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, '') + self.assertIn('STOP', result.stderr) + for invalid in ([], rows[1:], [{'os': 'ubuntu-evil', 'r': 'release'}], + [{'os': 'ubuntu-latest', 'r': ''}], {}): + denied = subprocess.run([sys.executable, str(script), json.dumps(invalid)], + capture_output=True, text=True, check=False) + self.assertNotEqual(denied.returncode, 0) + self.assertEqual(denied.stdout, '') + + def test_r_selector_is_static_and_matrix_is_admitted(self): + text = (ROOT / '.github/workflows/r-package-check.yml').read_text() + self.assertIn('needs: admit-platforms', text) + self.assertIn('config: ${{ fromJSON(needs.admit-platforms.outputs.matrix) }}', text) + self.assertIn('labels: [self-hosted, Linux, X64, cwlab-ci-isolated]', text) + self.assertNotIn('toJSON(matrix.config.os)', text) + inline = text.split("<<'PY'\n", 1)[1].split('\n PY', 1)[0] + emitted = '\n'.join(line[10:] for line in inline.splitlines()) + '\n' + self.assertEqual(emitted, (ROOT / 'scripts/ci/r_isolated_matrix.py').read_text()) + + +def test_supported_matrix_and_cli_branches_are_exercised_in_process(monkeypatch, capsys): + """Cover actual admission and CLI errors without executing native R jobs.""" + import pytest + import runpy + from scripts.ci.r_isolated_matrix import admit + + rows = [{'os': 'ubuntu-latest', 'r': 'release'}, + {'os': 'ubuntu-24.04', 'r': '4.4'}] + assert admit(json.dumps(rows)) == rows + for value in ({}, [], [None], [{'os': 1, 'r': 'release'}], + [{'os': 'ubuntu-latest', 'r': ''}], + [{'os': 'ubuntu-latest', 'r': 1}], + [{'os': 'windows-latest', 'r': 'release'}]): + with pytest.raises(ValueError): + admit(json.dumps(value)) + script = str(ROOT / 'scripts/ci/r_isolated_matrix.py') + monkeypatch.setattr(sys, 'argv', [script, json.dumps(rows)]) + runpy.run_path(script, run_name='__main__') + assert json.loads(capsys.readouterr().out) == rows + for argv in ([script], [script, 'not-json']): + monkeypatch.setattr(sys, 'argv', argv) + with pytest.raises(SystemExit) as error: + runpy.run_path(script, run_name='__main__') + assert error.value.code == 1 + captured = capsys.readouterr() + assert captured.out == '' and captured.err + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_r_package_check_reusable_workflow_contract.py b/tests/test_r_package_check_reusable_workflow_contract.py index 2d3ad49711..87bb07aa21 100644 --- a/tests/test_r_package_check_reusable_workflow_contract.py +++ b/tests/test_r_package_check_reusable_workflow_contract.py @@ -10,6 +10,8 @@ from __future__ import annotations +import json +import re from pathlib import Path _WORKFLOW = Path(".github/workflows/r-package-check.yml") @@ -108,14 +110,23 @@ def test_uniform_fields_are_hardcoded_not_parameterized() -> None: def test_matrix_is_driven_by_the_r_matrix_input() -> None: - """The strategy matrix must come from fromJSON(inputs.r_matrix), not a fixed list.""" + """Caller data is admitted before expanding supported Linux matrix legs.""" workflow = _workflow_text() - assert "config: ${{ fromJSON(inputs.r_matrix) }}" in workflow - assert "runs-on: ${{ matrix.config.os }}" in workflow - assert "r-version: ${{ matrix.config.r }}" in workflow + assert 'R_MATRIX: ${{ inputs.r_matrix }}' in workflow + assert 'config: ${{ fromJSON(needs.admit-platforms.outputs.matrix) }}' in workflow + assert 'r-version: ${{ matrix.config.r }}' in workflow assert "http-user-agent: ${{ matrix.config['http-user-agent'] }}" in workflow +def test_matrix_runner_format_renders_valid_group_scoped_json() -> None: + """Both admission and R execution use the fixed isolated Linux selector.""" + workflow = _workflow_text() + assert workflow.count('group: CWL CI isolated') == 2 + assert workflow.count('labels: [self-hosted, Linux, X64, cwlab-ci-isolated]') == 2 + assert 'toJSON(matrix.config.os)' not in workflow + assert 'STOP unsupported platform leg:' in workflow + + def test_pre_check_hook_is_bounded_data_not_caller_shell_source() -> None: """A reusable caller must not inject arbitrary Bash source into the trusted job.""" workflow = _workflow_text() @@ -129,3 +140,15 @@ def test_pre_check_hook_is_bounded_data_not_caller_shell_source() -> None: assert '"$PRE_CHECK_TEST_FILE" == *".."*' in workflow assert '"$PRE_CHECK_TEST_FILE" == /*' in workflow assert 'testthat::test_file(Sys.getenv("PRE_CHECK_TEST_FILE"))' in workflow + + +def test_current_routing_document_matches_linux_only_admission() -> None: + """Current coverage must not claim unsupported platforms are queued checks.""" + source = Path('docs/doctoring/all-self-hosted-runner-routing-20261003.md').read_text() + paragraph = next(p for p in source.split('\n\n') if p.startswith('R reusable jobs')) + assert 'Linux/x64-only' in paragraph + assert 'ubuntu-latest' in paragraph and 'ubuntu-24.04' in paragraph + assert 'STOP' in paragraph and 'unsupported-only' in paragraph + assert 'not platform coverage' in paragraph + assert 'remain queued' not in paragraph + assert 'entire R-version matrix' not in paragraph diff --git a/tests/test_required_review_runner_image_contract.py b/tests/test_required_review_runner_image_contract.py index ab7ede7aa4..65412229bb 100644 --- a/tests/test_required_review_runner_image_contract.py +++ b/tests/test_required_review_runner_image_contract.py @@ -17,14 +17,10 @@ class RequiredReviewRunnerImageContract(unittest.TestCase): """Keep required review jobs off the observed starved floating image.""" def assert_explicit_supported_image(self, path: Path) -> None: - """Require every job runner declaration to pin Ubuntu 24.04.""" - runs_on = { - line.strip() - for line in path.read_text(encoding="utf-8").splitlines() - if line.strip().startswith("runs-on:") - } - self.assertTrue(runs_on) - self.assertEqual(runs_on, {"runs-on: ubuntu-24.04"}) + """Require every ordinary job to use isolated self-hosted Linux runners.""" + workflow = path.read_text(encoding="utf-8") + self.assertIn("group: CWL CI isolated", workflow) + self.assertIn("labels: [self-hosted, linux, x64, cwlab-ci-isolated]", workflow) def test_strix_uses_explicit_supported_image(self) -> None: """Route trusted metadata to control while preserving the scan image.""" @@ -36,25 +32,26 @@ def test_strix_uses_explicit_supported_image(self) -> None: 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.assertIn('fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')', 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.assertIn("group: CWL CI isolated", scan) + self.assertIn("labels: [self-hosted, linux, x64, cwlab-ci-isolated]", 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.""" - workflow = OPENCODE_REVIEW.read_text(encoding="utf-8") + workflow = "\n".join(line for line in OPENCODE_REVIEW.read_text(encoding="utf-8").splitlines() if not line.strip().startswith("labels:")) self.assertEqual(workflow.count('"group":"CWL central control"'), 6) self.assertEqual(workflow.count('"labels":["self-hosted","linux","x64"]'), 6) self.assertNotIn("runs-on: ubuntu-24.04", workflow) self.assertNotIn("actions/checkout", workflow) self.assertEqual(workflow.count("github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main'"), 6) - self.assertEqual(workflow.count("fromJSON('[\"ubuntu-24.04\"]')"), 6) + self.assertEqual(workflow.count('fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')'), 6) 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") + workflow = "\n".join(line for line in NOEMA_REVIEW.read_text(encoding="utf-8").splitlines() if not line.strip().startswith("labels:")) 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) @@ -62,7 +59,7 @@ def test_noema_review_uses_explicit_supported_image(self) -> None: 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.assertEqual(workflow.count('fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')'), 5) self.assertNotIn("runs-on: ubuntu-24.04", workflow) self.assertEqual(workflow.count('"cwlab-control"'), 4) for name in ("admit-current-head", "changed-scope", "cancel-closed-pr-runs", "continue-noema-transport"): @@ -74,19 +71,12 @@ def test_noema_review_uses_explicit_supported_image(self) -> None: self.assertNotIn("cwlab-control", review) def test_opencode_review_dispatch_uses_explicit_supported_image(self) -> None: - """Require OpenCode dispatch jobs to use the compatible dedicated group. - - This is the workflow the required `opencode-review` check's - `repository_dispatch` actually lands on to run the OpenCode CLI and - post the exact-head verdict; a starved floating image here queues - the real review work for hours just as surely as on the required - check itself (see docs/product-technical-gap-baseline.md's - 2026-09-01 entry, whose own "Residual" note flagged this exact - follow-up sweep as still open). - """ + """Keep trusted OpenCode jobs separate from isolated PR test execution.""" workflow = OPENCODE_REVIEW_DISPATCH.read_text(encoding="utf-8") - self.assertEqual(workflow.count("group: CWL central OpenCode"), 3) - self.assertEqual(workflow.count("labels: [self-hosted, linux, x64]"), 3) + self.assertEqual(workflow.count("group: CWL central OpenCode"), 2) + self.assertEqual(workflow.count("labels: [self-hosted, linux, x64]"), 2) + self.assertEqual(workflow.count("group: CWL CI isolated"), 1) + self.assertEqual(workflow.count("labels: [self-hosted, linux, x64, cwlab-ci-isolated]"), 1) self.assertNotIn("runs-on: ubuntu-latest", workflow) self.assertNotIn("runs-on: ubuntu-24.04", workflow) @@ -97,8 +87,8 @@ def test_opencode_review_dispatch_uses_explicit_supported_image(self) -> None: def test_codeql_pr_routes_trusted_main_to_control_and_pr_revisions_to_hosted() -> None: """Separate short metadata work from model work without granting PR runner access.""" - workflow = Path(".github/workflows/codeql-pr.yml").read_text() + workflow = "\n".join(line for line in Path(".github/workflows/codeql-pr.yml").read_text().splitlines() if not line.strip().startswith("labels:")) assert workflow.count('"group":"CWL central control"') == 3 assert workflow.count("github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main'") == 3 - assert workflow.count("|| '\"ubuntu-24.04\"'") == 3 + assert workflow.count("|| '{\"group\":\"CWL CI isolated\",\"labels\":[\"self-hosted\",\"linux\",\"x64\",\"cwlab-ci-isolated\"]}'") == 3 assert '"group":"CWL MCP remediation"' not in workflow diff --git a/tests/test_required_security_runner_image_contract.py b/tests/test_required_security_runner_image_contract.py index d20c0c3a98..fb0f7d57e9 100644 --- a/tests/test_required_security_runner_image_contract.py +++ b/tests/test_required_security_runner_image_contract.py @@ -21,10 +21,11 @@ def test_security_scan_uses_explicit_supported_image(self) -> None: """ workflow = SECURITY_SCAN.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 6) + self.assertEqual(workflow.count("group: CWL CI isolated"), 6) + self.assertEqual(workflow.count("labels: [self-hosted, linux, x64, cwlab-ci-isolated]"), 6) def test_sast_semgrep_uses_explicit_supported_image(self) -> None: - """Require the SAST Semgrep job to use explicit Ubuntu 24.04. + """Require the SAST Semgrep job to use isolated self-hosted Linux. `#1656` removed the sibling `cancel-closed-pr-runs` no-op job (it only duplicated PR-stable workflow concurrency), leaving one runner @@ -36,7 +37,8 @@ def test_sast_semgrep_uses_explicit_supported_image(self) -> None: """ workflow = SAST_SEMGREP.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 1) + self.assertEqual(workflow.count("group: CWL CI isolated"), 1) + self.assertEqual(workflow.count("labels: [self-hosted, linux, x64, cwlab-ci-isolated]"), 1) if __name__ == "__main__": diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index a24b3c7d0c..71f03174db 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -7,7 +7,6 @@ import subprocess import sys import textwrap -import time from pathlib import Path import pytest @@ -25,7 +24,7 @@ def test_central_dispatch_and_control_jobs_use_dedicated_groups() -> None: """Central-only workflows cannot fall back into the general Ubuntu pool.""" for name, group, jobs in ( ("codeql-scan-dispatch.yml", "CWL central CodeQL", 3), - ("opencode-review-dispatch.yml", "CWL central OpenCode", 3), + ("opencode-review-dispatch.yml", "CWL central OpenCode", 2), ("agent-mention-router.yml", "CWL central control", 2), ("hourly-review-repair.yml", "CWL central control", 1), ): @@ -1276,7 +1275,21 @@ def test_noema_review_credentials_and_orchestrator_configuration_fail_closed() - assert "https://integrate.api.nvidia.com/v1/chat/completions" not in workflow assert "nvidia/nemotron-3-ultra-550b-a55b" not in workflow assert "contextual_orchestrator_review_sidecar.sh" in workflow - assert 'export NOEMA_LLM_MODEL="orchestrator/free"' in workflow + prepare = workflow_step(workflow, "Prepare Noema model verdict") + assert 'export NOEMA_LLM_MODEL="orchestrator/free"' not in prepare + personal, sidecar = prepare.split(" else\n", 1) + assert 'if [ "${PERSONAL_ROUTE_SELECTED:-}" = \'true\' ]; then' in personal + assert 'test -n "${LLM_GATEWAY_API_KEY:-}"' in personal + assert 'export NOEMA_LLM_MODEL="auto"' in personal + assert 'https://litellm.poinnetworks.net/v1/chat/completions' in personal + assert 'export NOEMA_LLM_MODEL="orchestrator/auto"' in sidecar + admission = workflow_step(workflow, "Admit existing personal LiteLLM auto route") + assert 'scripts.ci.personal_review_route' in admission + assert '--zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED"' in admission + assert '--zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED"' in admission + assert '--private-target "$PRIVATE_TARGET"' in admission + assert "vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false'" in admission + assert "vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false'" in admission assert ( "contextual-orchestrator review sidecar must be provisioned before Noema LLM review." in workflow @@ -1314,7 +1327,7 @@ def test_strix_gateway_default_and_noema_sidecar_fail_closed( env={ **os.environ, "GITHUB_OUTPUT": str(strix_output), - "STRIX_MODEL": "contextual-orchestrator/orchestrator/free", + "STRIX_MODEL": "auto", "STRIX_MODEL_REQUESTED": "", }, capture_output=True, @@ -1323,12 +1336,12 @@ def test_strix_gateway_default_and_noema_sidecar_fail_closed( ) assert strix.returncode == 0, strix.stderr assert { - "strix_model=contextual-orchestrator/orchestrator/free", + "strix_model=auto", "enabled=true", "provider_mode=contextual_orchestrator", } <= set(strix_output.read_text().splitlines()) assert ( - "STRIX_MODEL: contextual-orchestrator/orchestrator/free" + "STRIX_MODEL: auto" in workflow_text("strix.yml") ) assert ( @@ -1392,15 +1405,15 @@ def test_noema_review_supports_review_token_pat_fallback() -> None: """ workflow = workflow_text("noema-review.yml") - assert "NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN }}" in workflow + assert 'NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || \'\' }}' in workflow assert 'if [ -n "${NOEMA_REVIEW_TOKEN:-}" ]; then' in workflow assert ( "Noema reviewer using the NOEMA_REVIEW_TOKEN secret fallback identity." in workflow ) - # The review step must prefer the PAT over the exchanged app token. + # Non-kcsap consumers retain PAT preference; k-csap is App-only. assert ( - "GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }}" + "GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }}" in workflow ) assert "steps.noema_credential.outputs.source == 'github-app'" in workflow @@ -1420,17 +1433,17 @@ def test_noema_review_mints_a_least_privilege_github_app_token() -> None: assert "private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }}" in workflow assert "owner: ContextualWisdomLab" in workflow assert "repositories: ${{ steps.noema_credential.outputs.repository }}" in workflow - for permission in ( - "permission-actions: read", - "permission-checks: read", - "permission-contents: read", - "permission-metadata: read", - "permission-pull-requests: write", - "permission-security-events: read", - "permission-statuses: read", - "permission-vulnerability-alerts: read", - ): - assert permission in workflow + prepare = workflow_step(workflow, "Mint repository-scoped Noema GitHub App token") + publish = workflow_step(workflow, "Refresh repository-scoped Noema GitHub App token for publication") + for permission in ("permission-contents: read", "permission-metadata: read"): + assert permission in prepare and permission in publish + assert "permission-pull-requests: read" in prepare + assert "permission-pull-requests: read" in publish + assert "permission-pull-requests: write" not in publish + for permission in ("permission-actions:", "permission-checks:", + "permission-security-events:", "permission-statuses:", + "permission-vulnerability-alerts:"): + assert permission not in prepare and permission not in publish def test_opencode_dispatch_hands_approved_head_to_noema_before_merge() -> None: @@ -1862,6 +1875,24 @@ def test_security_scan_osv_upload_uses_pr_head_for_pr_head_sarif() -> None: assert "wait-for-processing: false" in upload_step +def test_osv_debug_artifact_quota_does_not_fail_the_scanner_gate() -> None: + """Keep optional debug evidence from overriding a completed OSV gate.""" + workflow = workflow_text("security-scan.yml") + upload_step = workflow_step(workflow, "Upload OSV debug artifacts") + + assert "actions/upload-artifact" in upload_step + assert "continue-on-error: true" in upload_step + + +def test_trivy_scan_does_not_restore_runner_specific_binary_cache() -> None: + """Install the pinned Trivy binary on each heterogeneous runner.""" + workflow = workflow_text("security-scan.yml") + scan_step = workflow_step(workflow, "Trivy filesystem scan") + + assert "aquasecurity/trivy-action" in scan_step + assert "cache: false" in scan_step + + def test_pr_sarif_upload_rate_limits_do_not_mask_scanner_gates() -> None: """Scanner hard gates must run even when GitHub code-scanning upload is busy.""" cases = ( diff --git a/tests/test_review_ai_app_independence.py b/tests/test_review_ai_app_independence.py new file mode 100644 index 0000000000..eb4b484b60 --- /dev/null +++ b/tests/test_review_ai_app_independence.py @@ -0,0 +1,86 @@ +"""Known second-review App evidence, not arbitrary bot or human prerequisites.""" +from copy import deepcopy +import json + +import pytest + +from scripts.ci import noema_review_gate as noema +from scripts.ci import pr_review_merge_scheduler as scheduler + +HEAD = 'a' * 40 + + +def app_review(monkeypatch): + """Capture the actual publisher body without contacting GitHub or a model.""" + payloads = [] + monkeypatch.setenv('NOEMA_REVIEW_TOKEN_SOURCE', 'noema-review-github-app') + monkeypatch.setattr(noema, 'run', lambda args, stdin=None: payloads.append(json.loads(stdin))) + verdict = { + 'decision': 'approve', 'summary': 'Inspected the changed authorization boundary.', + 'findings': [], + 'reviewed_lines': [{'path': 'scripts/ci/example.py', 'line': 2, 'side': 'RIGHT', + 'analysis': 'The exact-head guard rejects a stale commit before publication.'}], + 'adversarial_validation': {'residual_risk': 'App installation permissions are externally enforced.', + 'probes': [{'path': 'scripts/ci/example.py', 'line': 2, 'side': 'RIGHT', + 'outcome': 'falsified', 'hypothesis': 'A stale head could bypass admission.', + 'evidence': 'Changed-line inspection found the stale-head rejection before mutation.'}]}, + } + noema.submit_review('ContextualWisdomLab/example', 1, {'headRefOid': HEAD}, + 'cwl-noema-review[bot]', verdict) + return {'author': {'login': 'cwl-noema-review', '__typename': 'Bot'}, + 'state': 'APPROVED', 'commit': {'oid': HEAD}, 'body': payloads[0]['body']} + + +def pull_request(review): + """Keep GitHub's independent policy decision separate from review identity.""" + return {'author': {'login': 'source-writer'}, 'headRefOid': HEAD, + 'reviewDecision': 'APPROVED', 'reviews': {'nodes': [review]}} + + +def test_known_noema_app_real_publisher_body_counts_as_independent(monkeypatch): + pr = pull_request(app_review(monkeypatch)) + assert scheduler.has_independent_current_head_approval(pr) + assert scheduler.merge_approval_block_reason(pr) is None + pr['reviewDecision'] = 'REVIEW_REQUIRED' + assert scheduler.merge_approval_block_reason(pr) is not None + + +@pytest.mark.parametrize('mutation', ['author', 'unknown', 'primary', 'stale', 'no_commit', + 'empty', 'no_analysis', 'no_probe', 'no_credential', 'no_actor', 'marker_mismatch']) +def test_known_app_admission_preserves_negative_boundaries(monkeypatch, mutation): + review = app_review(monkeypatch) + pr = pull_request(review) + if mutation == 'author': + pr['author']['login'] = 'cwl-noema-review[bot]' + elif mutation == 'unknown': + review['author']['login'] = 'arbitrary-reviewer' + elif mutation == 'primary': + review['author']['login'] = 'opencode-agent' + elif mutation == 'stale': + review['commit']['oid'] = 'b' * 40 + elif mutation == 'no_commit': + review['commit'] = None + elif mutation == 'empty': + review['body'] = '' + elif mutation == 'no_analysis': + review['body'] = review['body'].replace('### Reviewed changed lines', '### Missing') + elif mutation == 'no_probe': + review['body'] = review['body'].replace('### Adversarial validation', '### Missing') + elif mutation == 'no_credential': + review['body'] = review['body'].replace('noema-review-github-app', 'untrusted-token') + elif mutation == 'no_actor': + review['body'] = review['body'].replace('- Actor: `cwl-noema-review[bot]`', '') + else: + review['body'] = review['body'].replace('decision=approve', 'decision=request_changes') + assert not scheduler.has_independent_current_head_approval(pr) + + +@pytest.mark.parametrize('state', ['CHANGES_REQUESTED', 'DISMISSED', 'APPROVED']) +def test_latest_same_app_policy_state_revokes_old_approval(monkeypatch, state): + review = app_review(monkeypatch) + later = deepcopy(review) + later['author']['login'] = 'cwl-noema-review[bot]' + later.update(state=state, body='not a substantive model review') + pr = pull_request(review) + pr['reviews']['nodes'].append(later) + assert not scheduler.has_independent_current_head_approval(pr) diff --git a/tests/test_review_auto_cost_admission.py b/tests/test_review_auto_cost_admission.py new file mode 100644 index 0000000000..5ac0a1a871 --- /dev/null +++ b/tests/test_review_auto_cost_admission.py @@ -0,0 +1,110 @@ +"""Exercise complete launcher selection with inert library/provider collaborators.""" +import json +from pathlib import Path +import runpy +import sys +from types import ModuleType, SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.mark.parametrize('only_priced', [False, True]) +def test_auto_zero_cost_main_never_preflights_or_serves_priced_models(tmp_path, monkeypatch, only_priced): + namespace = runpy.run_path(str(ROOT / 'scripts/ci/contextual_orchestrator_review_launcher.py')) + main = namespace['main'] + globalns = main.__globals__ + free = SimpleNamespace(provider_name='openrouter', model_id='qwen/qwen3-coder:free', + agent_id='openrouter_free_model', output_modalities=('text',), + prompt_price_per_1k=0.0, completion_price_per_1k=0.0, + currency_code='USD') + priced = SimpleNamespace(provider_name='openai', model_id='priced-model', + agent_id='openai_priced_model', output_modalities=('text',), + prompt_price_per_1k=0.1, completion_price_per_1k=0.2, + currency_code='USD') + observed = {'preflight': [], 'served': []} + modules = { + 'credentials': {'get_credential': lambda _: 'synthetic-only'}, + 'chat_capability': {'is_general_chat_agent_model_id': lambda _: True}, + 'model_discovery': {'discover_all_models': lambda: ([priced] if only_priced else [free, priced], {}), + 'free_discovered_models': lambda rows: [m for m in rows if m is free]}, + 'review_gateway': {'REVIEW_AUTH_CREDENTIAL_NAME': 'fixture', + 'register_review_credentials': lambda _: ['OPENROUTER_API_KEY']}, + 'debug_logging': {'configure_logging': lambda **_: None}, + } + def load_agents(path): + return json.loads(Path(path).read_text())['agents'] + class ModelClient: + def __init__(self, **kwargs): + pass + class TaskOrchestrator: + def __init__(self, agents, **kwargs): + self.agents = agents + modules['orchestrator'] = {'load_agents': load_agents, 'ModelClient': ModelClient, + 'TaskOrchestrator': TaskOrchestrator} + modules['server'] = {'SecurityConfig': lambda **kwargs: kwargs, + 'serve': lambda orchestrator, **kwargs: observed['served'].extend(orchestrator.agents)} + package = ModuleType('contextual_orchestrator') + package.__path__ = [] + monkeypatch.setitem(sys.modules, 'contextual_orchestrator', package) + for name, values in modules.items(): + module = ModuleType('contextual_orchestrator.' + name) + module.__dict__.update(values) + monkeypatch.setitem(sys.modules, module.__name__, module) + monkeypatch.setitem(globalns, '_configure_sidecar_logging', lambda _: None) + monkeypatch.setitem(globalns, '_log_discovery_errors', lambda _: None) + def preflight(primary, fallback, **kwargs): + assert not fallback, 'zero-cost mode may not prepare paid fallback' + assert all(a['model'] != 'priced-model' for a in primary) + observed['preflight'].extend(primary) + return primary, {'ready': True}, False + monkeypatch.setitem(globalns, '_preflight_with_fallback', preflight) + argv = ['--pool', 'auto', '--zero-cost-only'] + for flag in ('discovery-out', 'catalog-out', 'report-out', 'preflight-out'): + argv.extend(['--' + flag, str(tmp_path / (flag + '.json'))]) + if only_priced: + with pytest.raises(SystemExit, match='no eligible models; orchestrator/auto'): + main(argv) + assert not observed['preflight'] and not observed['served'] + else: + assert main(argv) == 0 + assert [a['model'] for a in observed['served']] == [free.model_id] + report = json.loads((tmp_path / 'report-out.json').read_text()) + assert report['requested_pool'] == 'orchestrator/auto' + assert report['zero_cost_only'] is True + assert report['priced_selected_count'] == report['unknown_selected_count'] == 0 + + +@pytest.mark.parametrize('private_target', [False, True]) +def test_fenced_auto_rejects_primary_failure_without_priced_fallback( + tmp_path, monkeypatch, private_target, +): + """Execute real launcher preflight/policy with controlled provider calls.""" + from scripts.ci import contextual_orchestrator_review_launcher as launcher + from tests.test_contextual_orchestrator_review_launcher import ( + _discovered_model, _install_owner_runtime, _launcher_arguments, + ) + free = _discovered_model() + priced = _discovered_model( + provider_name='openai', model_id='gpt-priced', agent_id='openai_priced', + credential_name='OPENAI_API_KEY', prompt_price=1.0, completion_price=2.0, + ) + state = _install_owner_runtime( + monkeypatch, discovered_models=[free, priced], + registered_credentials=['BYTEZ_API_KEY', 'OPENAI_API_KEY'], + failing_agent_ids=frozenset({'bytez_free'}), + ) + extra = ['--pool', 'auto', '--zero-cost-only'] + if private_target: + extra.append('--require-zdr') + from scripts.ci.contextual_orchestrator_review_policy import PolicyError + rejection = PolicyError if private_target else SystemExit + with pytest.raises(rejection): + launcher.main(_launcher_arguments(tmp_path, *extra)) + assert not state.served_requests + catalog = tmp_path / 'catalog.json' + if catalog.exists(): + agents = json.loads(catalog.read_text())['agents'] + assert all(a['model'] != 'gpt-priced' for a in agents) + assert not (tmp_path / 'catalog.json.priced').exists() diff --git a/tests/test_review_auto_route.py b/tests/test_review_auto_route.py new file mode 100644 index 0000000000..c0ba439389 --- /dev/null +++ b/tests/test_review_auto_route.py @@ -0,0 +1,145 @@ +"""Execute source-only auto selection; never contact a provider or publish a review.""" +import json +import os +from pathlib import Path +import re +import shutil +import subprocess + +import pytest + +from tests.test_agent_review_runtime_quality_consolidation import ( + test_ghas_runtime_selector_executes_the_actual_shell as execute_runtime_selector, +) + +ROOT = Path(__file__).resolve().parents[1] +SIDECAR = ROOT / 'scripts/ci/contextual_orchestrator_review_sidecar.sh' + + +def _strix_model_steps(source, root, requested): + """Execute only the two real model-selection shells without model calls.""" + import textwrap + + gate = source.split(' - name: Gate Strix secrets\n', 1)[1].split(' - name:', 1)[0] + default = re.search(r'^ STRIX_MODEL: (\S+)$', gate, re.M) + assert default is not None + output = root / 'output' + result = subprocess.run( + [shutil.which('bash'), '-eu', '-c', textwrap.dedent(gate.split(' run: |\n', 1)[1])], + env={'PATH': os.environ['PATH'], 'STRIX_MODEL': default[1], + 'STRIX_MODEL_REQUESTED': requested, 'GITHUB_OUTPUT': str(output)}, + capture_output=True, text=True, timeout=5, + ) + if result.returncode: + return result.returncode, None, None + values = dict(line.split('=', 1) for line in output.read_text().splitlines()) + primary = values['strix_model'] + prepare = source.split(' - name: Prepare Strix model input file\n', 1)[1].split(' - name:', 1)[0] + result = subprocess.run( + [shutil.which('bash'), '-eu', '-c', textwrap.dedent(prepare.split(' run: |\n', 1)[1])], + env={'PATH': os.environ['PATH'], 'STRIX_MODEL': primary, + 'RUNNER_TEMP': str(root), 'GITHUB_ENV': str(root / 'env')}, + capture_output=True, text=True, timeout=5, + ) + return result.returncode, primary, (root / 'strix_llm.txt').read_text() if not result.returncode else None + + +def _require_exact_strix_auto(result): + """Neither primary output nor scanner input may fall back to free.""" + assert result == (0, 'auto', 'auto'), result + + +@pytest.mark.parametrize('requested', ['', 'auto', 'orchestrator/auto', 'contextual-orchestrator/orchestrator/auto']) +def test_strix_primary_and_scanner_input_are_literal_auto(requested, tmp_path): + source = (ROOT / '.github/workflows/strix.yml').read_text() + _require_exact_strix_auto(_strix_model_steps(source, tmp_path, requested)) + + +@pytest.mark.parametrize('requested', ['free', 'orchestrator/free', 'contextual-orchestrator/orchestrator/free', 'external/unsafe']) +def test_strix_rejects_free_and_direct_override(requested, tmp_path): + source = (ROOT / '.github/workflows/strix.yml').read_text() + assert _strix_model_steps(source, tmp_path, requested)[0] != 0 + + +@pytest.mark.parametrize('position', ['primary', 'scanner']) +def test_strix_auto_oracle_rejects_free_fallback(position, tmp_path): + """Run an otherwise-valid source with one actual output changed to free.""" + source = (ROOT / '.github/workflows/strix.yml').read_text() + if position == 'primary': + before, after = 'echo "strix_model=$strix_model"', 'echo "strix_model=free"' + else: + before, after = "printf '%s' 'auto' > \"$strix_llm_file\"", "printf '%s' 'free' > \"$strix_llm_file\"" + assert source.count(before) == 1 + mutated = source.replace(before, after, 1) + result = _strix_model_steps(mutated, tmp_path, '') + with pytest.raises(AssertionError): + _require_exact_strix_auto(result) + + +def test_sidecar_accepts_auto_with_zero_cost_fence(): + text = SIDECAR.read_text() + start = text.index('orchestrator_pool=') + body = text[start:text.index('\nlog "starting review sidecar', start)] + result = subprocess.run([shutil.which('bash'), '-c', + 'fail() { exit 7; }; ' + body + + '\nprintf "%s\\n" "${pool_args[@]}"'], + env={'CONTEXTUAL_ORCHESTRATOR_POOL': 'auto'}, + capture_output=True, text=True, timeout=5) + assert result.returncode == 0, 'auto request rejected by source pool guard' + assert result.stdout.splitlines() == ['--pool', 'auto', '--zero-cost-only'] + + +def test_opencode_generated_primary_aux_reset_and_diagnosis_use_auto(tmp_path): + source = (ROOT / '.github/workflows/opencode-review-dispatch.yml').read_text() + # Execute the actual two jq expressions with an inert workspace; no workflow/API/model. + initial = re.search(r"jq -n '(\{\n.*?\n \})' >\"\$\{OPENCODE_REVIEW_WORKDIR\}/opencode.jsonc\"", source, re.S) + assert initial + reset = re.search(r"gateway_config=.*?\n jq '(.*?)' \"\$\{OPENCODE_REVIEW_WORKDIR\}/opencode.jsonc\"", source, re.S) + assert reset + jq = shutil.which('jq') + assert jq + first = subprocess.run([jq, '-n', initial[1]], capture_output=True, text=True, timeout=5) + assert first.returncode == 0 + data = json.loads(first.stdout) + assert data['model'] == data['small_model'] == 'contextual-orchestrator/orchestrator/auto' + # Force conflicting preceding values; the real reset must restore both to auto. + data.update(model='external/unsafe', small_model='external/unsafe') + second = subprocess.run([jq, reset[1]], input=json.dumps(data), capture_output=True, text=True, timeout=5) + assert second.returncode == 0 + data = json.loads(second.stdout) + assert data['model'] == data['small_model'] == 'contextual-orchestrator/orchestrator/auto' + assert set(data['provider']['contextual-orchestrator']['models']) == {'orchestrator/auto'} + assert 'OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto"' in source + assert 'MODEL: contextual-orchestrator/orchestrator/auto' in source + + +def test_noema_and_opencode_sidecar_pool_match_model_alias(): + for name in ('noema-review.yml', 'opencode-review-dispatch.yml'): + source = (ROOT / '.github/workflows' / name).read_text() + provision = source.split(' - name: Provision contextual-orchestrator review sidecar\n', 1)[1] + provision = provision.split(' - name:', 1)[0] + assert re.search(r'^ CONTEXTUAL_ORCHESTRATOR_POOL: auto$', provision, re.M) + assert 'export NOEMA_LLM_MODEL="orchestrator/auto"' in (ROOT / '.github/workflows/noema-review.yml').read_text() + + +def test_tracked_opencode_defaults_do_not_restore_free(): + source = (ROOT / 'opencode.jsonc').read_text() + config = json.loads('\n'.join(line for line in source.splitlines() if not line.lstrip().startswith('//'))) + assert config['model'] == config['small_model'] == 'contextual-orchestrator/orchestrator/auto' + assert 'orchestrator/auto' in config['provider']['contextual-orchestrator']['models'] + + +@pytest.mark.parametrize('path', [ + 'tests/test_review_auto_route.py', + 'tests/test_review_auto_cost_admission.py', + 'tests/test_review_ai_app_independence.py', +]) +def test_auto_new_regressions_reach_existing_quality_selector_and_command(path, tmp_path): + source = (ROOT / '.github/workflows/agent-review-runtime-quality-ci.yml').read_text() + trigger = source.split('on:\n', 1)[1].split('\nconcurrency:', 1)[0] + assert f' - "{path}"' in trigger + execute_runtime_selector(path, {'review_repair'}, tmp_path) + step = source.split('- name: Verify scheduler and contextual-orchestrator review-repair contracts', 1)[1] + script = step.split(' run: |\n', 1)[1].split(' - name:', 1)[0] + first_pytest = script.split('python -m interrogate', 1)[0] + assert path in first_pytest diff --git a/tests/test_runner_fallback_checker_sensitivity.py b/tests/test_runner_fallback_checker_sensitivity.py new file mode 100644 index 0000000000..c580c21fc2 --- /dev/null +++ b/tests/test_runner_fallback_checker_sensitivity.py @@ -0,0 +1,126 @@ +"""Exercise the retained fallback checker with valid and reversed routing.""" +import json +from pathlib import Path +import pytest +import yaml +from tests.test_all_self_hosted_runner_contract import test_isolated_fallback_never_selects_privileged_group as check_fallback +from tests.test_all_self_hosted_runner_contract import test_nonprivileged_runners_require_isolated_label as check_static + +ISOLATED = {'group':'CWL CI isolated','labels':['self-hosted','linux','x64','cwlab-ci-isolated']} +CONTROL = {'group':'CWL central control','labels':['self-hosted','linux','x64']} + + +def install_fixture(tmp_path, monkeypatch, typed, suffix, unsafe): + first, fallback = (ISOLATED, CONTROL) if unsafe else (CONTROL, ISOLATED) + expression = "github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/example.yml@refs/heads/main' && fromJSON('" + json.dumps(first,separators=(',',':')) + "') || fromJSON('" + json.dumps(fallback,separators=(',',':')) + "')" + selector = {'group':'${{ ('+expression+').group }}','labels':'${{ ('+expression+').labels }}'} if typed else '${{ '+expression+' }}' + workflows = tmp_path/'.github/workflows' + workflows.mkdir(parents=True) + (workflows/f'example.{suffix}').write_text(yaml.safe_dump({'jobs':{f'job{n}':{'runs-on':selector} for n in range(29)}}, width=10000)) + monkeypatch.chdir(tmp_path) + + +@pytest.mark.parametrize('typed',[False,True]) +@pytest.mark.parametrize('suffix',['yml','yaml']) +def test_checker_rejects_privileged_fallback(tmp_path,monkeypatch,typed,suffix): + install_fixture(tmp_path,monkeypatch,typed,suffix,True) + with pytest.raises(AssertionError): + check_fallback() + + +@pytest.mark.parametrize('typed',[False,True]) +@pytest.mark.parametrize('suffix',['yml','yaml']) +def test_checker_accepts_same_shape_isolated_fallback(tmp_path,monkeypatch,typed,suffix): + install_fixture(tmp_path,monkeypatch,typed,suffix,False) + check_fallback() + + +def test_checker_refuses_empty_conditional_inventory(tmp_path, monkeypatch): + workflows = tmp_path/'.github/workflows' + workflows.mkdir(parents=True) + (workflows/'example.yaml').write_text(yaml.safe_dump({'jobs':{'job':{'runs-on':ISOLATED}}})) + monkeypatch.chdir(tmp_path) + with pytest.raises(AssertionError, match='coverage'): + check_fallback() + + +def test_checker_rejects_privileged_group_even_with_isolation_label(tmp_path, monkeypatch): + install_fixture(tmp_path,monkeypatch,True,'yaml',False) + path = tmp_path/'.github/workflows/example.yaml' + text = path.read_text() + path.write_text(text.replace('CWL CI isolated', 'CWL central control')) + with pytest.raises(AssertionError, match='unsafe fallback'): + check_fallback() + + +def test_checker_exceptions_are_not_counted_as_rejection(tmp_path, monkeypatch): + install_fixture(tmp_path,monkeypatch,True,'yaml',True) + def broken_parser(*args): + raise RuntimeError('fixture parser failure') + monkeypatch.setattr(yaml, 'safe_load', broken_parser) + with pytest.raises(RuntimeError, match='fixture parser failure'): + check_fallback() + + +def install_static_fixture(tmp_path, monkeypatch, suffix, labels, group='CWL CI isolated', block=False): + """Write an ordinary selector as actual YAML with a nonstandard valid indent.""" + workflows = tmp_path / '.github/workflows' + workflows.mkdir(parents=True) + selector = {'group': group, 'labels': labels} + document = {'jobs': {'ordinary': {'runs-on': selector}}} + (workflows / f'ordinary.{suffix}').write_text( + yaml.safe_dump(document, default_flow_style=not block, indent=4), + ) + monkeypatch.chdir(tmp_path) + + +@pytest.mark.parametrize('suffix', ['yml', 'yaml']) +@pytest.mark.parametrize('block', [False, True]) +@pytest.mark.parametrize('missing', ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated']) +def test_static_checker_rejects_each_missing_required_label(tmp_path, monkeypatch, suffix, block, missing): + """The same isolated group must not hide any missing required label.""" + labels = [label for label in ISOLATED['labels'] if label != missing] + install_static_fixture(tmp_path, monkeypatch, suffix, labels, block=block) + with pytest.raises(AssertionError): + check_static() + + +@pytest.mark.parametrize('suffix', ['yml', 'yaml']) +@pytest.mark.parametrize('block', [False, True]) +def test_static_checker_accepts_intact_isolated_mapping(tmp_path, monkeypatch, suffix, block): + """Accept the exact positive through the same parser and ordinary-job gate.""" + install_static_fixture(tmp_path, monkeypatch, suffix, list(ISOLATED['labels']), block=block) + check_static() + + +@pytest.mark.parametrize('suffix', ['yml', 'yaml']) +@pytest.mark.parametrize('group', ['CWL central control', 'CWL central CodeQL', 'CWL central OpenCode']) +def test_static_checker_preserves_existing_dedicated_groups(tmp_path, monkeypatch, suffix, group): + """Retain the existing dedicated static exceptions without an isolation label.""" + install_static_fixture(tmp_path, monkeypatch, suffix, list(CONTROL['labels']), group=group) + check_static() + + +@pytest.mark.parametrize('suffix', ['yml', 'yaml']) +@pytest.mark.parametrize('labels', [None, 'self-hosted linux x64 cwlab-ci-isolated', ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated', None]]) +def test_static_checker_rejects_malformed_label_shapes(tmp_path, monkeypatch, suffix, labels): + """A substring or malformed list must not substitute for typed label data.""" + install_static_fixture(tmp_path, monkeypatch, suffix, labels) + with pytest.raises(AssertionError, match='static labels'): + check_static() + + +@pytest.mark.parametrize('suffix', ['yml', 'yaml']) +def test_static_checker_requires_exact_group_name(tmp_path, monkeypatch, suffix): + """Reject a group that only contains the required group name as a substring.""" + install_static_fixture(tmp_path, monkeypatch, suffix, list(ISOLATED['labels']), group='CWL CI isolated extra') + with pytest.raises(AssertionError, match='unsupported static group'): + check_static() + + +def test_static_checker_requires_nonempty_inventory(tmp_path, monkeypatch): + """An empty workflow population is not proof of ordinary runner admission.""" + (tmp_path / '.github/workflows').mkdir(parents=True) + monkeypatch.chdir(tmp_path) + with pytest.raises(AssertionError, match='coverage is empty'): + check_static() diff --git a/tests/test_runner_mapping_schema_contract.py b/tests/test_runner_mapping_schema_contract.py new file mode 100644 index 0000000000..15a7343d07 --- /dev/null +++ b/tests/test_runner_mapping_schema_contract.py @@ -0,0 +1,24 @@ +"""Require typed group/label fields instead of object-valued runs-on expressions.""" +from pathlib import Path +import re +import yaml + + +def test_dynamic_group_routing_uses_explicit_yaml_mapping() -> None: + observed = 0 + paths = sorted(set(Path('.github/workflows').glob('*.yml')) | set(Path('.github/workflows').glob('*.yaml'))) + for path in paths: + jobs = yaml.safe_load(path.read_text())['jobs'] + for job_id, job in jobs.items(): + runner = job.get('runs-on') + if isinstance(runner, str) and '"group":' in runner: + raise AssertionError(f'{path}:{job_id}: object-valued expression is not a typed runner mapping') + if isinstance(runner, dict) and str(runner.get('group', '')).startswith('${{ ('): + observed += 1 + group = runner['group'] + labels = runner['labels'] + assert group.endswith(').group }}'), (path, job_id, group) + assert labels.endswith(').labels }}'), (path, job_id, labels) + assert group.removesuffix(').group }}') == labels.removesuffix(').labels }}'), (path, job_id) + assert 'CWL CI isolated' in group and 'cwlab-ci-isolated' in labels, (path, job_id) + assert observed == 29, observed diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 2231f48ede..7db73bf436 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -1610,7 +1610,21 @@ def test_isolated_command_rejects_executable_outside_bound_roots(monkeypatch, tm ) -def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(monkeypatch, tmp_path): +@pytest.fixture +def isolated_host_home(monkeypatch, tmp_path): + """Separate synthetic host home from scratch without relaxing production denial. + + TMPDIR may legitimately live under the caller's home on macOS. These + path-resolution fixtures model a workspace outside the denied host home; + the separate host-home rejection test continues to use the real home. + """ + home = tmp_path / "host-home" + home.mkdir() + monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + return home + + +def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(monkeypatch, tmp_path, isolated_host_home): """A ./gradlew-style repository launcher resolves against the sandboxed cwd. ``shutil.which`` resolves any command string containing a path separator @@ -1641,7 +1655,7 @@ def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(mon assert command.endswith("./gradlew build") -def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypatch, tmp_path): +def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypatch, tmp_path, isolated_host_home): """A bare command resolves when PATH itself has a relative entry. ``shutil.which`` joins a relative ``PATH`` entry with the *calling @@ -1674,7 +1688,7 @@ def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypa assert command.endswith("tool") -def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_mount(monkeypatch, tmp_path): +def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_mount(monkeypatch, tmp_path, isolated_host_home): """An absolute copied-repo launcher path is rewritten to its /workspace equivalent. ``isolated_command`` binds ``sandbox_root`` at ``SANDBOX_MOUNT`` inside @@ -1720,6 +1734,23 @@ def test_which_relative_to_cwd_returns_none_for_empty_path(tmp_path): assert result is None +def test_isolated_command_rejects_explicit_host_home_even_inside_workspace(monkeypatch, tmp_path): + """An allowed scratch workspace cannot turn a host-home executable into code.""" + monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) + sandbox = tmp_path / "sandbox" + repo = sandbox / "repo" + repo.mkdir(parents=True) + launcher = repo / "launch.sh" + launcher.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + launcher.chmod(0o755) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + with pytest.raises(RuntimeError, match="host home directory"): + sandboxed_web_e2e.isolated_command( + str(launcher), backend="/usr/bin/bwrap", cwd=repo, + sandbox_root=sandbox, env={"PATH": "/usr/bin"}, + ) + + def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypatch, tmp_path): """A relative PATH entry cannot be used to search the real host filesystem outside the copy. @@ -1743,7 +1774,7 @@ def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypat ) -def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path): +def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path, isolated_host_home): """A repo-local launcher path that lexically escapes the sandbox root is still rejected.""" monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" @@ -1763,7 +1794,7 @@ def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_pat ) -def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path): +def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path, isolated_host_home): """An explicit absolute path outside the workspace and bind roots is still rejected.""" monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" 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 42efce621e..dda5761619 100644 --- a/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py +++ b/tests/test_scheduler_and_codeql_dispatch_runner_image_contract.py @@ -27,20 +27,21 @@ class SchedulerAndCodeqlDispatchRunnerImageContract(unittest.TestCase): """Keep these central callers/dispatchers off the observed starved image.""" def assert_explicit_supported_image(self, path: Path) -> None: - """Require every job runner declaration to pin Ubuntu 24.04.""" + """Require every ordinary job to use isolated self-hosted Linux runners.""" workflow = path.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow, path) - self.assertIn("runs-on: ubuntu-24.04", workflow, path) + self.assertIn("group: CWL CI isolated", workflow, path) + self.assertIn("labels: [self-hosted, linux, x64, cwlab-ci-isolated]", workflow, path) def test_pr_review_autofix_uses_explicit_supported_image(self) -> None: - """Require the PR Review Autofix job to use explicit Ubuntu 24.04.""" + """Require the PR Review Autofix job to use isolated self-hosted Linux.""" self.assert_explicit_supported_image(PR_REVIEW_AUTOFIX) def test_pr_review_fix_scheduler_uses_explicit_supported_image(self) -> None: - """Require the fix-scheduler's hosted fallback to pin Ubuntu 24.04.""" + """Require the fix-scheduler's fallback to use isolated self-hosted Linux.""" workflow = PR_REVIEW_FIX_SCHEDULER.read_text(encoding="utf-8") self.assertNotIn("ubuntu-latest", workflow) - self.assertIn("fromJSON('[\"ubuntu-24.04\"]')", workflow) + self.assertIn('fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')', workflow) def test_hourly_review_repair_uses_explicit_supported_image(self) -> None: """Require hourly control jobs to use the dedicated central group.""" @@ -49,12 +50,12 @@ 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 trusted-main control routing and Ubuntu fallback for all three jobs.""" + """Require trusted-main control routing and isolated fallback for all three jobs.""" workflow = CODEQL_PR.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) selectors = [ line.strip() for line in workflow.splitlines() - if line.strip().startswith("runs-on:") + if line.startswith(" group:") ] self.assertEqual(len(selectors), 3) for selector in selectors: @@ -64,7 +65,7 @@ def test_codeql_pr_uses_explicit_supported_image(self) -> None: ) self.assertIn('"group":"CWL central control"', selector) self.assertIn('"labels":["self-hosted","linux","x64"]', selector) - self.assertIn("|| '\"ubuntu-24.04\"'", selector) + self.assertIn("|| '{\"group\":\"CWL CI isolated\",\"labels\":[\"self-hosted\",\"linux\",\"x64\",\"cwlab-ci-isolated\"]}'", selector) def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: """Require validation, scan, and attempt wake jobs in the dedicated group.""" @@ -74,10 +75,11 @@ def test_codeql_scan_dispatch_uses_explicit_supported_image(self) -> None: self.assertEqual(workflow.count("labels: [self-hosted, linux, x64]"), 3) def test_python_security_uses_explicit_supported_image(self) -> None: - """Require all three Python Security jobs to pin Ubuntu 24.04.""" + """Require all three Python Security jobs to select isolated self-hosted Linux.""" workflow = PYTHON_SECURITY.read_text(encoding="utf-8") self.assertNotIn("runs-on: ubuntu-latest", workflow) - self.assertEqual(workflow.count("runs-on: ubuntu-24.04"), 3) + self.assertEqual(workflow.count("group: CWL CI isolated"), 3) + self.assertEqual(workflow.count("labels: [self-hosted, linux, x64, cwlab-ci-isolated]"), 3) if __name__ == "__main__": diff --git a/tests/test_strix_auto_consumer.py b/tests/test_strix_auto_consumer.py new file mode 100644 index 0000000000..6207c6abcd --- /dev/null +++ b/tests/test_strix_auto_consumer.py @@ -0,0 +1,96 @@ +"""Connected Strix auto routing; real shell helpers, no scanner or network.""" +from __future__ import annotations + +import os +from pathlib import Path +import subprocess + +import pytest + +from tests.test_strix_openai_fallback_api_base import _function_block + +ROOT = Path(__file__).resolve().parents[1] +GATE = ROOT / "scripts/ci/strix_quick_gate.sh" +UTILS = ROOT / "scripts/ci/strix_model_utils.sh" + + +def _consume(tmp_path: Path, base: str | None, provider: str = "contextual_orchestrator") -> subprocess.CompletedProcess[str]: + """Execute normalize -> trusted API-base resolver -> actual child qualifier.""" + gate = GATE.read_text(encoding="utf-8") + utils = UTILS.read_text(encoding="utf-8") + names = ( + "is_vertex_model", "is_contextual_orchestrator_model", + "is_contextual_orchestrator_api_base", "is_github_models_model", + "is_github_models_api_base", "is_known_foreign_provider_api_base", + "is_github_models_api_compatible_model", "is_explicit_openai_model", + "resolve_trusted_input_file", "resolved_llm_api_base_for_model", + "child_model_for_api_base", + ) + helpers = [_function_block(gate, name) for name in names] + helpers += [_function_block(utils, name) for name in ( + "trim_whitespace", "sanitize_provider_name", "is_vertex_resource_path", + "extract_vertex_model_id", "normalize_model", + )] + env = {"PATH": os.environ["PATH"], "HOME": str(tmp_path), + "DEFAULT_PROVIDER": provider, "STRIX_INPUT_FILE_ROOT": str(tmp_path)} + if base is not None: + path = tmp_path / "base.txt" + path.write_text(base, encoding="utf-8") + env["LLM_API_BASE_FILE"] = str(path) + script = "\n".join([ + "set -euo pipefail", *helpers, + 'model=$(normalize_model auto)', + 'printf "normalized=%s\\n" "$model"', + 'base=$(resolved_llm_api_base_for_model "$model")', + 'printf "base=%s\\n" "$base"', + 'child=$(child_model_for_api_base "$model" "$base")', + 'printf "child=%s\\n" "$child"', + ]) + return subprocess.run(["bash", "-c", script], env=env, capture_output=True, + text=True, timeout=10) + + +def test_workflow_auto_reaches_child_as_exact_auto(tmp_path: Path) -> None: + result = _consume(tmp_path, "http://127.0.0.1:18080/v1") + assert result.returncode == 0, result.stderr + assert result.stdout.splitlines() == [ + "normalized=auto", + "base=http://127.0.0.1:18080/v1", "child=openai/auto", + ] + # openai/ is the LiteLLM connector prefix, not part of the gateway model ID. + child = result.stdout.splitlines()[-1].removeprefix("child=") + assert child.removeprefix("openai/") == "auto" + assert "free" not in result.stdout + + +@pytest.mark.parametrize("base", [None, "", "http://127.0.0.1:18081/v1", + "http://localhost:18080/v1", "https://example.com/v1"]) +def test_auto_cannot_escape_pinned_gateway(tmp_path: Path, base: str | None) -> None: + result = _consume(tmp_path, base) + assert result.returncode == 2, (result.stdout, result.stderr) + assert "child=" not in result.stdout + + +@pytest.mark.parametrize("provider", ["", "vertex_ai", "openai"]) +def test_auto_requires_explicit_gateway_provider(tmp_path: Path, provider: str) -> None: + result = _consume(tmp_path, "http://127.0.0.1:18080/v1", provider) + assert result.returncode == 2, (result.stdout, result.stderr) + assert "normalized=" not in result.stdout + assert "child=" not in result.stdout + + +def test_required_smoke_admits_repaired_literal_auto() -> None: + result = subprocess.run( + ["bash", str(ROOT / "scripts/ci/strix_required_workflow_smoke.sh")], + cwd=ROOT, env={"PATH": os.environ["PATH"]}, + capture_output=True, text=True, timeout=20, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_strix_sidecar_pool_matches_auto_consumer() -> None: + workflow = (ROOT / ".github/workflows/strix.yml").read_text(encoding="utf-8") + step = workflow.split(" - name: Provision contextual-orchestrator Strix sidecar\n", 1)[1].split(" - name:", 1)[0] + assert " CONTEXTUAL_ORCHESTRATOR_POOL: auto\n" in step + assert "CONTEXTUAL_ORCHESTRATOR_POOL: free" not in step + assert "CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR:" in step diff --git a/tests/test_strix_contextual_orchestrator_contract.py b/tests/test_strix_contextual_orchestrator_contract.py index 52763ecc88..52bf617a58 100644 --- a/tests/test_strix_contextual_orchestrator_contract.py +++ b/tests/test_strix_contextual_orchestrator_contract.py @@ -24,9 +24,10 @@ def setUp(self) -> None: self.smoke = SMOKE.read_text(encoding="utf-8") def test_default_scan_provisions_the_existing_gateway_sidecar(self) -> None: - """Every scan uses the five-provider gateway, never a direct pool.""" + """Every scan uses literal auto through the gateway, never a direct pool.""" self.assertIn("Provision contextual-orchestrator Strix sidecar", self.workflow) - self.assertIn("STRIX_MODEL: contextual-orchestrator/orchestrator/free", self.workflow) + self.assertIn("STRIX_MODEL: auto", self.workflow) + self.assertIn("CONTEXTUAL_ORCHESTRATOR_POOL: auto", self.workflow) self.assertIn("provider_mode=contextual_orchestrator", self.workflow) self.assertIn("STRIX_FALLBACK_MODELS: \"\"", self.workflow) self.assertNotIn( @@ -48,7 +49,7 @@ def test_model_override_cannot_escape_the_gateway(self) -> None: """A dispatch payload cannot select a direct provider route.""" self.assertIn("github.event.client_payload.strix_llm", self.workflow) self.assertIn( - "Strix model overrides are limited to contextual-orchestrator/orchestrator/free", + "Strix model overrides must select literal auto", self.workflow, ) for direct_route in ("nvidia_nim/*)", "openrouter/free", "openai-direct/gpt-5.4"): @@ -75,7 +76,7 @@ def test_required_smoke_pins_the_gateway_default(self) -> None: self.assertIn("contextual-orchestrator Strix sidecar", self.smoke) self.assertIn("active_strix_models=", self.smoke) self.assertIn( - '"$active_strix_models" = "contextual-orchestrator/orchestrator/free"', + '"$active_strix_models" = "auto"', self.smoke, ) self.assertIn("Strix does not resolve a direct provider outside the gateway", self.smoke) diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index ba8344455b..7a4d2dbb7d 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -190,7 +190,7 @@ def test_workflow_routes_all_scans_through_contextual_orchestrator(self) -> None workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") self.assertIn("Provision contextual-orchestrator Strix sidecar", workflow) - self.assertIn("STRIX_MODEL: contextual-orchestrator/orchestrator/free", workflow) + self.assertIn("STRIX_MODEL: auto", workflow) self.assertIn("provider_mode=contextual_orchestrator", workflow) self.assertIn("STRIX_LLM_DEFAULT_PROVIDER: contextual_orchestrator", workflow) self.assertNotIn("Resolve live NVIDIA NIM Strix models", workflow) @@ -201,7 +201,7 @@ def test_workflow_rejects_non_gateway_model_overrides(self) -> None: workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") self.assertIn("STRIX_MODEL_REQUESTED", workflow) - self.assertIn("Strix model overrides are limited to contextual-orchestrator/orchestrator/free.", workflow) + self.assertIn("Strix model overrides must select literal auto; free and direct-provider routes are not admitted.", workflow) self.assertIn("STRIX_FALLBACK_MODELS: \"\"", workflow) def test_outer_workflow_requires_litellm_context_for_nvidia_404(self) -> None: diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py index b5bf0cacb0..8063b2bcdb 100644 --- a/tests/test_strix_openai_fallback_api_base.py +++ b/tests/test_strix_openai_fallback_api_base.py @@ -326,13 +326,18 @@ def test_workflow_gateway_base_is_the_only_http_exception(self) -> None: ) self.assertEqual(rc, 2) - # 2026-08-30: orchestrator/auto is no longer a recognized Strix gateway - # model (owner decision superseding ADR-0003's auto default) -- the - # gate must now reject it rather than resolve it, the same as any - # other unrecognized virtual pool. - rc, _ = _resolve_api_base( + # Literal auto now uses the same pinned origin as the retained legacy + # free routes; unrecognized virtual pools still fail closed. + rc, api_base = _resolve_api_base( {"LLM_API_BASE_FILE": "http://127.0.0.1:18080/v1"}, - "orchestrator/auto", + "auto", + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "http://127.0.0.1:18080/v1") + + rc, _ = _resolve_api_base( + {"LLM_API_BASE_FILE": "http://127.0.0.1:18081/v1"}, + "auto", ) self.assertEqual(rc, 2) @@ -343,9 +348,10 @@ def test_workflow_gateway_base_is_the_only_http_exception(self) -> None: self.assertEqual(rc, 2) def test_gateway_child_model_preserves_selected_virtual_pool(self) -> None: - """LiteLLM qualification must not rewrite the selected free pool.""" + """LiteLLM qualification preserves auto and standalone legacy free models.""" expected_child_models = { + "auto": "openai/auto", "orchestrator/free": "openai/orchestrator/free", "contextual-orchestrator/orchestrator/free": "openai/orchestrator/free", } diff --git a/tests/test_strix_report_scope.py b/tests/test_strix_report_scope.py index 955557e214..1a608cc864 100644 --- a/tests/test_strix_report_scope.py +++ b/tests/test_strix_report_scope.py @@ -194,6 +194,35 @@ def test_bare_repository_directory_or_scope_root_is_rejected( ) +@pytest.mark.parametrize("report_text", [ + "Assessed `python/fast_mlsirm/report.py@backup`; no findings.", + "Assessed `python/fast_mlsirm/report.py追加`; no findings.", + "Assessed `other/python/fast_mlsirm/report.py`; no findings.", + "Scope: `/workspace/strix-pr-scope.ABC123/python/fast_mlsirm@backup`.", + "Scope: `python/fast_mlsirm/../unrelated`. Reviewed `report.py`.", +]) +def test_longer_or_traversing_report_identity_cannot_bind_scope( + tmp_path: Path, report_text: str +) -> None: + """Otherwise-complete reports cannot authorize a partial path token.""" + _write_report(tmp_path, report_text=report_text) + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +@pytest.mark.parametrize("report_text", [ + "Assessed `python/fast_mlsirm/report.py:12:3`; no findings.", + "Assessed python/fast_mlsirm/report.py. No findings.", + "Directory `python/fast_mlsirm`; reviewed `report.py`.", +]) +def test_exact_report_identity_with_context_is_accepted( + tmp_path: Path, report_text: str +) -> None: + """Line references and separately named directory/file remain valid scope.""" + _write_report(tmp_path, report_text=report_text) + report_scope.validate(tmp_path, [CHANGED_PATH]) + + def test_cli_reports_validation_error_and_accepts_scoped_success( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/tests/test_tokyo_review_consumer.py b/tests/test_tokyo_review_consumer.py new file mode 100644 index 0000000000..44c765d088 --- /dev/null +++ b/tests/test_tokyo_review_consumer.py @@ -0,0 +1,32 @@ +"""Tokyo admission reuses central dispatch, never a leaf worker or unknown App.""" +import json +from pathlib import Path + +from scripts.ci import noema_review_handoff as handoff + +ROOT = Path(__file__).resolve().parents[1] +TOKYO = 'ContextualWisdomLab/tokyo-travel-2026' + + +def test_tokyo_source_target_is_explicit_and_unique(): + targets = json.loads((ROOT / 'scripts/ci/opencode_repository_dispatch_targets.json').read_text())['targets'] + assert targets.count(TOKYO) == 1 + + +def test_leaf_handoff_dispatches_central_handler_with_exact_target_head(): + calls = [] + handoff.dispatch_noema(TOKYO, 3, 'a' * 40, + runner=lambda args, stdin: calls.append((args, json.loads(stdin)))) + assert calls[0][0] == ['api', '-X', 'POST', 'repos/ContextualWisdomLab/.github/dispatches', '--input', '-'] + assert calls[0][1] == {'event_type': 'noema-review', 'client_payload': { + 'target_repository': TOKYO, 'pr_number': 3, 'pr_head_sha': 'a' * 40}} + + +def test_tokyo_noema_selects_only_existing_self_hosted_groups(): + source = (ROOT / '.github/workflows/noema-review.yml').read_text() + selectors = [line for line in source.splitlines() if line.strip().startswith(('group: ${{', 'labels: ${{'))] + assert selectors + for line in selectors: + assert "github.repository == '" + TOKYO + "'" in line + assert 'CWL CI isolated' in line + assert 'ubuntu-24.04' not in line diff --git a/tests/test_trusted_uv_company_gate_wiring.py b/tests/test_trusted_uv_company_gate_wiring.py new file mode 100644 index 0000000000..3effdf2850 --- /dev/null +++ b/tests/test_trusted_uv_company_gate_wiring.py @@ -0,0 +1,49 @@ +"""Verify company trust-gate changes reach the existing complete quality gate.""" +from fnmatch import fnmatchcase +from pathlib import Path +import shlex + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +QUALITY = ROOT / '.github/workflows/trusted-uv-materializer-quality-ci.yml' +SURFACES = ( + '.github/workflows/company-default-head-trust-gate.yml', + 'tests/test_company_default_head_trust_gate.py', +) + + +def quality(): + return yaml.safe_load(QUALITY.read_text()) + + +@pytest.mark.parametrize('event', ['pull_request', 'push']) +@pytest.mark.parametrize('surface', SURFACES) +def test_trust_gate_change_selects_existing_quality(event, surface): + data = quality() + triggers = data.get('on', data.get(True)) + patterns = triggers[event]['paths'] + assert any(fnmatchcase(surface, pattern) for pattern in patterns), ( + event, surface, 'Trust-gate change does not select existing full quality gate') + # Each new exact surface is necessary: prove selector sensitivity offline. + without = [p for p in patterns if p != surface] + assert not any(fnmatchcase(surface, p) for p in without) + + +def test_selected_quality_command_discovers_trust_gate_regression(): + data = quality() + job = data['jobs']['full-quality-gate'] + step = next(s for s in job['steps'] + if s.get('name') == 'Run complete central test and branch coverage gate') + commands = [shlex.split(line) for line in step['run'].splitlines() if line.strip()] + assert ['python', '-m', 'coverage', 'run', '-m', 'pytest', 'tests', '-q'] in commands + assert ['python', '-m', 'coverage', 'report'] in commands + assert ['unset', 'COVERAGE_RCFILE'] in commands + module = ROOT / SURFACES[1] + assert module.is_file() and module.name.startswith('test_') and module.suffix == '.py' + assert module.parent == ROOT / 'tests' + assert data['permissions'] == {'contents': 'read'} + assert job['name'] == 'Python 3.14 full quality gate' + assert job['runs-on'] == {'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated']} diff --git a/tests/test_trusted_uv_company_settlement_validate.py b/tests/test_trusted_uv_company_settlement_validate.py new file mode 100644 index 0000000000..6789f74034 --- /dev/null +++ b/tests/test_trusted_uv_company_settlement_validate.py @@ -0,0 +1,83 @@ +"""Validate the central settlement workload callee source boundary.""" +import hashlib +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / '.github/workflows/company-settlement-validate.yml' +QUALITY = ROOT / '.github/workflows/trusted-uv-materializer-quality-ci.yml' +EXPECTED_STEPS_HASH = '0d549f21d485446af03f440c374fa701720340c6961bbac1e02c3987d89522b0' +EXPECTED_TEST_MODULES = ( + 'tests/test_issue119_pgcat_endpoint_resolver.py', + 'tests/test_issue119_settlement_live_verify.py', + 'tests/test_issue119_settlement_replica_routing.py', + 'tests/test_issue119_settlement_review_regressions.py', + 'tests/test_multi_instance_runtime_contract.py', +) + + +def load(path: Path): + assert path.is_file(), f'missing {path.relative_to(ROOT)}' + return yaml.safe_load(path.read_text()) + + +def test_settlement_callee_has_fixed_isolated_selector_and_no_caller_selected_runner(): + data = load(WORKFLOW) + trigger = data.get('on', data.get(True)) + assert set(trigger) == {'workflow_call'} + call = trigger['workflow_call'] + assert call.get('inputs', {}) == {} + assert call.get('secrets', {}) == {} + assert data['permissions'] == {'contents': 'read'} + assert set(data['jobs']) == {'validate'} + job = data['jobs']['validate'] + assert job['runs-on'] == { + 'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'Linux', 'X64', 'cwlab-ci-isolated'], + } + assert 'runner_group' not in WORKFLOW.read_text() + assert 'runner_labels_json' not in WORKFLOW.read_text() + assert 'fromJSON' not in WORKFLOW.read_text() + + +def test_settlement_callee_preserves_reviewed_workload_bytes_and_security_boundary(): + data = load(WORKFLOW) + job = data['jobs']['validate'] + assert len(job['steps']) == 7 + body = WORKFLOW.read_bytes() + steps_bytes = body[body.index(b' steps:\n'):] + # Supplier's trailing blank line is not workload; preserve every actual step byte. + assert hashlib.sha256(steps_bytes.rstrip(b'\n') + b'\n\n').hexdigest() == EXPECTED_STEPS_HASH + assert job['steps'][0]['uses'] == 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' + assert job['steps'][0]['with'] == { + 'persist-credentials': False, + 'ref': '${{ github.event.pull_request.head.sha || github.sha }}', + } + assert job['steps'][1]['uses'] == 'actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97' + assert job['steps'][1]['with'] == {'python-version': '3.13'} + assert job['steps'][2]['uses'] == 'astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4' + verify = job['steps'][5] + assert verify['env'] == {'SETTLEMENT_VERIFY_REQUIRE_POSTGRES': '1'} + for module in EXPECTED_TEST_MODULES: + assert module in verify['run'] + assert 'DATABASE_' not in yaml.safe_dump(job) + assert 'secrets.' not in yaml.safe_dump(job) + + +def test_settlement_callee_is_selected_by_existing_complete_quality_gate(): + data = load(QUALITY) + trigger = data.get('on', data.get(True)) + for event in ('pull_request', 'push'): + paths = trigger[event]['paths'] + assert '.github/workflows/company-settlement-validate.yml' in paths + assert 'tests/test_trusted_uv*.py' in paths + assert Path(__file__).match('test_trusted_uv*.py') + full = data['jobs']['full-quality-gate'] + step = next(s for s in full['steps'] if s.get('name') == 'Run complete central test and branch coverage gate') + assert 'python -m coverage run -m pytest tests -q' in step['run'] + assert full['runs-on'] == { + 'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated'], + } + assert data['permissions'] == {'contents': 'read'}