From 559b6f16955d6c907b73d9c8dd7b3f5acac88f06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 20:47:52 +0900 Subject: [PATCH 1/5] fix(automation): restore hourly fleet coordination --- ...organization-commercial-readiness-loop.yml | 73 ++++++++++++++++++- CHANGELOG.md | 11 +++ .../organization-commercial-readiness-loop.md | 8 +- pyproject.toml | 1 + ...cial_readiness_loop_credential_contract.py | 12 ++- ...zation_commercial_readiness_loop_policy.py | 7 +- ...mercial_readiness_loop_receipt_contract.py | 1 + ..._commercial_readiness_loop_secret_scope.py | 15 +++- 8 files changed, 119 insertions(+), 9 deletions(-) diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml index 521495617..c8fd97399 100644 --- a/.github/workflows/organization-commercial-readiness-loop.yml +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -18,6 +18,9 @@ jobs: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) runs-on: ubuntu-24.04 timeout-minutes: 25 + permissions: + contents: read + id-token: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" ORGANIZATION: ContextualWisdomLab @@ -32,6 +35,7 @@ jobs: egress-policy: block allowed-endpoints: >- api.github.com:443 + api.opencode.ai:443 github.com:443 objects.githubusercontent.com:443 release-assets.githubusercontent.com:443 @@ -50,13 +54,78 @@ jobs: with: python-version: "3.14" + - name: Exchange OpenCode app token for bounded fleet coordination + id: opencode_app_token + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "OpenCode app token exchange unavailable: OIDC request environment is missing." + mark_unavailable + exit 0 + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + + if ! oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + echo "OpenCode app token exchange unavailable: OIDC token request did not complete." + mark_unavailable + exit 0 + fi + + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "OpenCode app token exchange unavailable: OIDC token response was empty." + mark_unavailable + exit 0 + fi + + if ! token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + echo "OpenCode app token exchange unavailable: app token request did not complete." + mark_unavailable + exit 0 + fi + + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "OpenCode app token exchange unavailable: app token response was empty." + mark_unavailable + exit 0 + fi + + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + - name: Coordinate one bounded fleet pass env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || steps.opencode_app_token.outputs.token }} shell: bash --noprofile --norc -e -o pipefail {0} run: | if [ -z "${GH_TOKEN:-}" ]; then - echo "::error::PR_REVIEW_MERGE_TOKEN is required; neither the reviewer credential nor repository-scoped GITHUB_TOKEN is accepted." + echo "::error::PR_REVIEW_MERGE_TOKEN or the job-bound OpenCode App token exchange is required; neither the reviewer credential nor repository-scoped GITHUB_TOKEN is accepted." exit 1 fi echo "::add-mask::$GH_TOKEN" diff --git a/CHANGELOG.md b/CHANGELOG.md index 35b938271..b520236ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,17 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Restored the hourly organization commercial-readiness loop after five + consecutive startup failures caused by its mandatory but unprovisioned + `PR_REVIEW_MERGE_TOKEN`. Protected scheduled jobs now prefer that maintainer + secret and otherwise exchange their job-bound GitHub OIDC identity for the + existing short-lived OpenCode GitHub App installation token, without + accepting `OPENCODE_APPROVE_TOKEN`, `GITHUB_TOKEN`, provider credentials, or + exposing either cross-repository credential to checkout, setup, or artifact + actions. Declared `pip` in the project-local development environment so the + existing isolated wheel/hash preflight test no longer depends on an + accidentally pre-populated virtual environment. + - Replaced nonexistent `job.workflow_repository` / `job.workflow_sha` / `job.workflow_ref` / `job.workflow_file_path` context references (actionlint: "property ... is not defined in object type") in `pr-review-fix-scheduler.yml`'s called-workflow source verification and `exact-artifact-sbom-attestation.yml`'s trusted-verifier checkout. Both always failed closed on the missing properties (ContextualWisdomLab/.github#1212) or, for the SBOM attestation checkout, silently resolved an empty repository/ref instead of the pinned trusted source (downstream `gh attestation verify --signer-repo`/`--signer-workflow`, using the separately hardcoded `SIGNER_REPOSITORY` constant rather than any workflow_ref, still failed closed on the resulting empty signer identity). `github.workflow_ref`/`github.workflow_sha` are real, documented properties, but for a `workflow_call` target they reflect the top-level *calling* workflow, not the reusable workflow's own file — a prefix match against the reusable workflow's own path can never succeed. `exact-artifact-sbom-attestation.yml`'s checkout now uses `github.workflow_sha` (correct today: it has no callers yet); `pr-review-fix-scheduler.yml`'s identity check instead validates `github.repository`, since every current caller uses a local, same-repo `uses: ./...` where caller and callee share one commit and `github.workflow_sha` is still the right pin. Tracked follow-up for the SBOM attestation checkout once a real (potentially cross-repo) caller exists: ContextualWisdomLab/.github#1228. - Used the receiving repository's workflow token for same-repository scheduler Actions inventory and read calls, while retaining the established mutation diff --git a/docs/doctoring/organization-commercial-readiness-loop.md b/docs/doctoring/organization-commercial-readiness-loop.md index 76ef1fce5..6549edd04 100644 --- a/docs/doctoring/organization-commercial-readiness-loop.md +++ b/docs/doctoring/organization-commercial-readiness-loop.md @@ -10,7 +10,7 @@ The coordinator may dispatch at most one review-repair workflow and one product- A single workflow cannot safely write every repository merely because it runs in the organization `.github` repository. GitHub's default `GITHUB_TOKEN` is scoped to the repository containing the workflow; cross-repository Actions dispatch therefore requires an explicitly provisioned user or GitHub App credential with the required repository and Actions permissions. This control does not make every repository directly writable. It only considers repositories the live API reports as organization-owned, non-fork, enabled, non-archived, default-branch-bearing, and writable by the authenticated installation. -The central job therefore refuses both repository-scoped and reviewer-scoped token fallbacks. It requires the maintainer-scoped `PR_REVIEW_MERGE_TOKEN`; `OPENCODE_APPROVE_TOKEN` remains isolated to the reviewer credential chain and `GITHUB_TOKEN` is not accepted for cross-repository coordination. The maintainer token is exposed only to the final dispatch shell step, not checkout, setup, artifact upload, or other third-party actions. The coordinator itself receives neither `NVIDIA_NIM_API_KEY` nor `COPILOT_GITHUB_TOKEN`. Model credentials remain inside separately reviewed repository-local or central workers. +The central job therefore refuses both repository-scoped and reviewer-scoped token fallbacks. It prefers the maintainer-scoped `PR_REVIEW_MERGE_TOKEN`; when that optional long-lived secret is absent, the protected scheduled job exchanges its GitHub OIDC identity for the existing short-lived OpenCode GitHub App installation token. `OPENCODE_APPROVE_TOKEN` remains isolated to the reviewer credential chain and `GITHUB_TOKEN` is not accepted for cross-repository coordination. Both accepted credentials are exposed only to the final dispatch shell step, not checkout, setup, artifact upload, or other third-party actions. The OIDC exchange receives only `id-token: write`, which permits requesting the job-bound JWT but grants no repository write authority by itself. The exchanged installation token remains bounded by the App installation's selected repositories and permissions; GitHub still requires Contents write for `repository_dispatch` and Actions write for `workflow_dispatch`, so a missing installation permission fails closed. The coordinator itself receives neither `NVIDIA_NIM_API_KEY` nor `COPILOT_GITHUB_TOKEN`. Model credentials remain inside separately reviewed repository-local or central workers. ## Dynamic repository-writer lease @@ -44,6 +44,8 @@ The repository-local entrypoint remains responsible for its own bounded editable ## Failure, evidence, and operations +Runs `32560132644`, `32562851784`, `32565331074`, `32567859925`, and `32570355777` reproduced the same startup failure: the workflow required `PR_REVIEW_MERGE_TOKEN`, but neither the repository nor organization exposed that secret. The coordinator therefore completed no inventory or dispatch work for five consecutive hourly heartbeats. The OIDC installation-token fallback repairs that configuration deadlock without copying a personal token, accepting the repository-scoped `GITHUB_TOKEN`, or reusing `OPENCODE_APPROVE_TOKEN`. + The schedule runs at minute 7 rather than minute 0 to reduce exposure to the documented start-of-hour GitHub Actions load spike. The central workflow has no `workflow_dispatch` entrypoint, so branch-selected coordinator source cannot be executed; scheduled execution occurs only from protected default `main`. Local operators may use the script's `--dry-run` mode from a reviewed checkout without adding a central manual workflow entrypoint. Organization, workflow, active-run, and pull-request inventories are paginated. One inaccessible repository is recorded as an inspection error while other independently safe repositories continue. A run fails nonzero when every selected repository inspection fails or when every planned dispatch fails; partial, independently contained failures remain visible without discarding successful work. @@ -60,10 +62,14 @@ GitHub. (n.d.). *Automatic token authentication*. GitHub Docs. Retrieved August GitHub. (n.d.). *Events that trigger workflows*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows +GitHub. (n.d.). *OpenID Connect reference*. GitHub Docs. Retrieved August 22, 2026, from https://docs.github.com/en/actions/reference/security/oidc + GitHub. (n.d.). *REST API endpoints for artifacts*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/rest/actions/artifacts GitHub. (n.d.). *REST API endpoints for workflows*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/rest/actions/workflows +GitHub. (n.d.). *REST API endpoints for repositories*. GitHub Docs. Retrieved August 22, 2026, from https://docs.github.com/en/rest/repos/repos + GitHub. (n.d.). *REST API endpoints for workflow runs*. GitHub Docs. Retrieved August 8, 2026, from https://docs.github.com/en/rest/actions/workflow-runs National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 diff --git a/pyproject.toml b/pyproject.toml index 1954a2aaf..bfa28f150 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ dependencies = [] [dependency-groups] dev = [ + "pip>=25.0", "pytest>=8.0.0", "pytest-cov>=7.1.0", "interrogate>=1.7.0", diff --git a/tests/test_organization_commercial_readiness_loop_credential_contract.py b/tests/test_organization_commercial_readiness_loop_credential_contract.py index 3225d5832..ca3435da9 100644 --- a/tests/test_organization_commercial_readiness_loop_credential_contract.py +++ b/tests/test_organization_commercial_readiness_loop_credential_contract.py @@ -10,12 +10,20 @@ def test_central_schedule_has_no_branch_selected_or_reviewer_credential_path() -> None: - """The fleet coordinator must be schedule-only and use maintainer authority.""" + """The fleet coordinator uses schedule-bound maintainer or App authority.""" source = WORKFLOW_PATH.read_text(encoding="utf-8") assert "workflow_dispatch:" not in source - assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in source + assert ( + "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " + "steps.opencode_app_token.outputs.token }}" + ) in source + assert "id-token: write" in source + assert "OIDC_AUDIENCE: opencode-github-action" in source + assert "https://api.opencode.ai/exchange_github_app_token" not in source + assert '"${OPENCODE_API_BASE_URL}/exchange_github_app_token"' in source assert "persist-credentials: false" in source assert "OPENCODE_APPROVE_TOKEN" not in source + assert "|| github.token" not in source assert "DRY_RUN" not in source assert "inputs.dry_run" not in source diff --git a/tests/test_organization_commercial_readiness_loop_policy.py b/tests/test_organization_commercial_readiness_loop_policy.py index 920f8072f..26963f70c 100644 --- a/tests/test_organization_commercial_readiness_loop_policy.py +++ b/tests/test_organization_commercial_readiness_loop_policy.py @@ -159,7 +159,12 @@ def test_workflow_and_doctoring_contracts() -> None: assert "cancel-in-progress: false" in workflow_source assert 'MAX_REVIEW_DISPATCHES: "1"' in workflow_source assert 'MAX_DEVELOPMENT_DISPATCHES: "1"' in workflow_source - assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in workflow_source + assert ( + "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " + "steps.opencode_app_token.outputs.token }}" + ) in workflow_source + assert "id-token: write" in workflow_source + assert "OIDC_AUDIENCE: opencode-github-action" in workflow_source assert "OPENCODE_APPROVE_TOKEN" not in workflow_source assert "workflow_dispatch:" not in workflow_source assert "|| github.token" not in workflow_source diff --git a/tests/test_organization_commercial_readiness_loop_receipt_contract.py b/tests/test_organization_commercial_readiness_loop_receipt_contract.py index ce0956bba..ac5d3b7f0 100644 --- a/tests/test_organization_commercial_readiness_loop_receipt_contract.py +++ b/tests/test_organization_commercial_readiness_loop_receipt_contract.py @@ -43,3 +43,4 @@ def test_json_receipt_is_retained_as_an_immutable_short_lived_artifact() -> None assert "results-receiver.actions.githubusercontent.com:443" in source assert "*.actions.githubusercontent.com:443" in source assert "*.blob.core.windows.net:443" in source + assert "api.opencode.ai:443" in source diff --git a/tests/test_organization_commercial_readiness_loop_secret_scope.py b/tests/test_organization_commercial_readiness_loop_secret_scope.py index b47c2cadc..47d0ec2ae 100644 --- a/tests/test_organization_commercial_readiness_loop_secret_scope.py +++ b/tests/test_organization_commercial_readiness_loop_secret_scope.py @@ -9,8 +9,8 @@ ) -def test_maintainer_token_is_scoped_only_to_the_dispatch_step() -> None: - """Third-party setup actions must never receive the cross-repository token.""" +def test_coordinator_token_is_scoped_only_to_the_dispatch_step() -> None: + """Third-party actions never receive either cross-repository credential.""" source = WORKFLOW_PATH.read_text(encoding="utf-8") before_dispatch, dispatch_step = source.split( " - name: Coordinate one bounded fleet pass\n", maxsplit=1 @@ -18,4 +18,13 @@ def test_maintainer_token_is_scoped_only_to_the_dispatch_step() -> None: assert "PR_REVIEW_MERGE_TOKEN" not in before_dispatch assert "GH_TOKEN:" not in before_dispatch - assert "env:\n GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in dispatch_step + assert ( + "env:\n GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " + "steps.opencode_app_token.outputs.token }}" + ) in dispatch_step + artifact_step = dispatch_step.split( + " - name: Preserve the exact fleet receipt\n", maxsplit=1 + )[1] + assert "GH_TOKEN:" not in artifact_step + assert "PR_REVIEW_MERGE_TOKEN" not in artifact_step + assert "steps.opencode_app_token.outputs.token" not in artifact_step From 6daa442c7d0f2c4872f06ea9d0b66c5ef0fd6971 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:43:12 +0900 Subject: [PATCH 2/5] fix(loop): harden OIDC credential exchange --- ...organization-commercial-readiness-loop.yml | 111 ++++++++---------- CHANGELOG.md | 6 +- .../organization-commercial-readiness-loop.md | 12 ++ ...cial_readiness_loop_credential_contract.py | 22 +++- ...zation_commercial_readiness_loop_policy.py | 6 +- ..._commercial_readiness_loop_secret_scope.py | 6 +- 6 files changed, 89 insertions(+), 74 deletions(-) diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml index c8fd97399..9aed7f970 100644 --- a/.github/workflows/organization-commercial-readiness-loop.yml +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -54,76 +54,65 @@ jobs: with: python-version: "3.14" - - name: Exchange OpenCode app token for bounded fleet coordination - id: opencode_app_token + - name: Coordinate one bounded fleet pass env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} OIDC_AUDIENCE: opencode-github-action OPENCODE_API_BASE_URL: https://api.opencode.ai shell: bash --noprofile --norc -e -o pipefail {0} run: | - mark_unavailable() { - echo "available=false" >>"$GITHUB_OUTPUT" + exchange_unavailable() { + echo "::error::OpenCode app token exchange unavailable: $1" + exit 1 } - if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then - echo "OpenCode app token exchange unavailable: OIDC request environment is missing." - mark_unavailable - exit 0 - fi - - request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" - separator="&" - case "$request_url" in - *\?*) ;; - *) separator="?" ;; - esac - - if ! oidc_response="$( - curl -fsS \ - -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ - "${request_url}${separator}audience=${OIDC_AUDIENCE}" - )"; then - echo "OpenCode app token exchange unavailable: OIDC token request did not complete." - mark_unavailable - exit 0 - fi - - oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" - if [ -z "$oidc_token" ]; then - echo "OpenCode app token exchange unavailable: OIDC token response was empty." - mark_unavailable - exit 0 - fi - - if ! token_response="$( - curl -fsS \ - -X POST \ - -H "Authorization: Bearer ${oidc_token}" \ - "${OPENCODE_API_BASE_URL}/exchange_github_app_token" - )"; then - echo "OpenCode app token exchange unavailable: app token request did not complete." - mark_unavailable - exit 0 - fi - - app_token="$(jq -r '.token // empty' <<<"$token_response")" - if [ -z "$app_token" ]; then - echo "OpenCode app token exchange unavailable: app token response was empty." - mark_unavailable - exit 0 + if [ -z "${GH_TOKEN:-}" ]; then + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + exchange_unavailable "OIDC request environment is missing." + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + + if ! oidc_response="$( + curl -fsS \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + exchange_unavailable "OIDC token request did not complete." + fi + + if ! oidc_token="$(jq -r '.value // empty' <<<"$oidc_response" 2>/dev/null)"; then + exchange_unavailable "OIDC token response was malformed." + fi + if [ -z "$oidc_token" ]; then + exchange_unavailable "OIDC token response was empty." + fi + echo "::add-mask::$oidc_token" + + if ! token_response="$( + curl -fsS \ + -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + exchange_unavailable "app token request did not complete." + fi + + if ! app_token="$(jq -r '.token // empty' <<<"$token_response" 2>/dev/null)"; then + exchange_unavailable "app token response was malformed." + fi + if [ -z "$app_token" ]; then + exchange_unavailable "app token response was empty." + fi + echo "::add-mask::$app_token" + export GH_TOKEN="$app_token" fi - echo "::add-mask::$app_token" - { - echo "available=true" - echo "token=$app_token" - } >>"$GITHUB_OUTPUT" - - - name: Coordinate one bounded fleet pass - env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || steps.opencode_app_token.outputs.token }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | if [ -z "${GH_TOKEN:-}" ]; then echo "::error::PR_REVIEW_MERGE_TOKEN or the job-bound OpenCode App token exchange is required; neither the reviewer credential nor repository-scoped GITHUB_TOKEN is accepted." exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index df8e1f13c..0484e2c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,11 @@ Semantic Versioning where the repository publishes a release. exposing either cross-repository credential to checkout, setup, or artifact actions. Declared `pip` in the project-local development environment so the existing isolated wheel/hash preflight test no longer depends on an - accidentally pre-populated virtual environment. + accidentally pre-populated virtual environment. The exchange now runs only + when the preferred maintainer secret is absent, masks the OIDC JWT before the + second request, and converts malformed successful JSON responses into the + existing explicit unavailable output instead of exiting early under + `errexit`. - Retried the Strix scan up to `STRIX_TRANSIENT_RETRY_PER_MODEL` times, same model, when the log shows the upstream strix-agent Caido sandbox bootstrap timing race (`loginAsGuest failed after N attempts` / `Failed to connect to 127.0.0.1 port `; tracked upstream as usestrix/strix#1036, #1037, #1056). A slow CI runner can exceed strix-agent's fixed 10-attempt sandbox-login budget before its local intercepting proxy is reachable, even though the penetration test itself never started and no vulnerability evidence was produced or lost; the Docker image is already cached from the failed attempt, so a same-model retry is cheap and typically clears the one-off boot race. Not wired into cross-model fallback, since switching LLM models cannot change local sandbox container boot timing. - Replaced nonexistent `job.workflow_repository` / `job.workflow_sha` / `job.workflow_ref` / `job.workflow_file_path` context references (actionlint: "property ... is not defined in object type") in `pr-review-fix-scheduler.yml`'s called-workflow source verification and `exact-artifact-sbom-attestation.yml`'s trusted-verifier checkout. Both always failed closed on the missing properties (ContextualWisdomLab/.github#1212) or, for the SBOM attestation checkout, silently resolved an empty repository/ref instead of the pinned trusted source (downstream `gh attestation verify --signer-repo`/`--signer-workflow`, using the separately hardcoded `SIGNER_REPOSITORY` constant rather than any workflow_ref, still failed closed on the resulting empty signer identity). `github.workflow_ref`/`github.workflow_sha` are real, documented properties, but for a `workflow_call` target they reflect the top-level *calling* workflow, not the reusable workflow's own file — a prefix match against the reusable workflow's own path can never succeed. `exact-artifact-sbom-attestation.yml`'s checkout now uses `github.workflow_sha` (correct today: it has no callers yet); `pr-review-fix-scheduler.yml`'s identity check instead validates `github.repository`, since every current caller uses a local, same-repo `uses: ./...` where caller and callee share one commit and `github.workflow_sha` is still the right pin. Tracked follow-up for the SBOM attestation checkout once a real (potentially cross-repo) caller exists: ContextualWisdomLab/.github#1228. diff --git a/docs/doctoring/organization-commercial-readiness-loop.md b/docs/doctoring/organization-commercial-readiness-loop.md index 6549edd04..b507d68f8 100644 --- a/docs/doctoring/organization-commercial-readiness-loop.md +++ b/docs/doctoring/organization-commercial-readiness-loop.md @@ -50,6 +50,18 @@ The schedule runs at minute 7 rather than minute 0 to reduce exposure to the doc Organization, workflow, active-run, and pull-request inventories are paginated. One inaccessible repository is recorded as an inspection error while other independently safe repositories continue. A run fails nonzero when every selected repository inspection fails or when every planned dispatch fails; partial, independently contained failures remain visible without discarding successful work. +Exact-head reviews found three exchange-path gaps before activation. A +malformed HTTP-success OIDC or App-token response made `jq` exit under shell +`errexit` before the workflow could publish its explicit unavailable result; +the OIDC JWT was not registered with the runner masker; and the App exchange +ran even when the preferred maintainer secret was already present. The final +coordinator shell step now performs the exchange only when its preferred +`GH_TOKEN` input is empty, guards both JSON parses with a bounded fail-closed +diagnostic, and masks the OIDC JWT immediately after validation. Keeping +selection and exchange in that final first-party shell step preserves the rule +that no checkout, setup, artifact, or other third-party action receives either +credential, and neither response body is logged. + Each run writes one deterministic JSON receipt and the same bounded evidence to the GitHub Actions job summary. The JSON is uploaded through the immutable, SHA-pinned artifact action with a three-day retention period. Artifact upload receives no maintainer or model credential. The receipt proves only coordinator observations and downstream dispatch acceptance; it is not merge, release, or product-quality evidence. No queued, pending, skipped-required, cancelled, absent, stale-head, predecessor-head, synthetic-merge-only, or failed check is converted to passing evidence. The coordinator's successful dispatch means only that exact state was revalidated and a bounded downstream workflow was accepted by GitHub. diff --git a/tests/test_organization_commercial_readiness_loop_credential_contract.py b/tests/test_organization_commercial_readiness_loop_credential_contract.py index ca3435da9..7f312606e 100644 --- a/tests/test_organization_commercial_readiness_loop_credential_contract.py +++ b/tests/test_organization_commercial_readiness_loop_credential_contract.py @@ -14,10 +14,8 @@ def test_central_schedule_has_no_branch_selected_or_reviewer_credential_path() - source = WORKFLOW_PATH.read_text(encoding="utf-8") assert "workflow_dispatch:" not in source - assert ( - "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " - "steps.opencode_app_token.outputs.token }}" - ) in source + assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in source + assert "steps.opencode_app_token.outputs.token" not in source assert "id-token: write" in source assert "OIDC_AUDIENCE: opencode-github-action" in source assert "https://api.opencode.ai/exchange_github_app_token" not in source @@ -27,3 +25,19 @@ def test_central_schedule_has_no_branch_selected_or_reviewer_credential_path() - assert "|| github.token" not in source assert "DRY_RUN" not in source assert "inputs.dry_run" not in source + + +def test_opencode_exchange_fails_closed_without_wasting_or_exposing_credentials() -> None: + """Malformed exchanges diagnose safely and the preferred secret skips OIDC.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + + coordinate = source.split( + " - name: Coordinate one bounded fleet pass\n", maxsplit=1 + )[1] + assert 'if [ -z "${GH_TOKEN:-}" ]; then' in coordinate + assert 'export GH_TOKEN="$app_token"' in coordinate + assert 'if ! oidc_token="$(jq -r' in source + assert 'if ! app_token="$(jq -r' in source + assert "OIDC token response was malformed" in source + assert "app token response was malformed" in source + assert 'echo "::add-mask::$oidc_token"' in source diff --git a/tests/test_organization_commercial_readiness_loop_policy.py b/tests/test_organization_commercial_readiness_loop_policy.py index 26963f70c..ae9f63291 100644 --- a/tests/test_organization_commercial_readiness_loop_policy.py +++ b/tests/test_organization_commercial_readiness_loop_policy.py @@ -159,10 +159,8 @@ def test_workflow_and_doctoring_contracts() -> None: assert "cancel-in-progress: false" in workflow_source assert 'MAX_REVIEW_DISPATCHES: "1"' in workflow_source assert 'MAX_DEVELOPMENT_DISPATCHES: "1"' in workflow_source - assert ( - "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " - "steps.opencode_app_token.outputs.token }}" - ) in workflow_source + assert "GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in workflow_source + assert 'export GH_TOKEN="$app_token"' in workflow_source assert "id-token: write" in workflow_source assert "OIDC_AUDIENCE: opencode-github-action" in workflow_source assert "OPENCODE_APPROVE_TOKEN" not in workflow_source diff --git a/tests/test_organization_commercial_readiness_loop_secret_scope.py b/tests/test_organization_commercial_readiness_loop_secret_scope.py index 47d0ec2ae..9d10d493b 100644 --- a/tests/test_organization_commercial_readiness_loop_secret_scope.py +++ b/tests/test_organization_commercial_readiness_loop_secret_scope.py @@ -18,10 +18,8 @@ def test_coordinator_token_is_scoped_only_to_the_dispatch_step() -> None: assert "PR_REVIEW_MERGE_TOKEN" not in before_dispatch assert "GH_TOKEN:" not in before_dispatch - assert ( - "env:\n GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || " - "steps.opencode_app_token.outputs.token }}" - ) in dispatch_step + assert "env:\n GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in dispatch_step + assert "steps.opencode_app_token.outputs.token" not in source artifact_step = dispatch_step.split( " - name: Preserve the exact fleet receipt\n", maxsplit=1 )[1] From e237b2d998a41ac169f49343cde8e9755c8dbf80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:46:58 +0900 Subject: [PATCH 3/5] fix(automation): bound app token exchange requests --- .github/workflows/organization-commercial-readiness-loop.yml | 4 ++++ CHANGELOG.md | 4 +++- docs/doctoring/organization-commercial-readiness-loop.md | 5 ++++- ...nization_commercial_readiness_loop_credential_contract.py | 2 ++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml index 9aed7f970..f187b033e 100644 --- a/.github/workflows/organization-commercial-readiness-loop.yml +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -80,6 +80,8 @@ jobs: if ! oidc_response="$( curl -fsS \ + --connect-timeout 10 \ + --max-time 30 \ -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ "${request_url}${separator}audience=${OIDC_AUDIENCE}" )"; then @@ -96,6 +98,8 @@ jobs: if ! token_response="$( curl -fsS \ + --connect-timeout 10 \ + --max-time 30 \ -X POST \ -H "Authorization: Bearer ${oidc_token}" \ "${OPENCODE_API_BASE_URL}/exchange_github_app_token" diff --git a/CHANGELOG.md b/CHANGELOG.md index e04c36deb..453691d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,9 @@ Semantic Versioning where the repository publishes a release. when the preferred maintainer secret is absent, masks the OIDC JWT before the second request, and converts malformed successful JSON responses into the existing explicit unavailable output instead of exiting early under - `errexit`. + `errexit`. Both OIDC and App-token HTTP requests now use a 10-second connect + timeout and 30-second total timeout so a stalled exchange fails within the + bounded coordinator step. - Publish only the sanitized cumulative Strix report tree, avoiding a later copy of relative scanner output that could reintroduce known internal warning text into uploaded security evidence. diff --git a/docs/doctoring/organization-commercial-readiness-loop.md b/docs/doctoring/organization-commercial-readiness-loop.md index b507d68f8..da4764007 100644 --- a/docs/doctoring/organization-commercial-readiness-loop.md +++ b/docs/doctoring/organization-commercial-readiness-loop.md @@ -60,7 +60,10 @@ coordinator shell step now performs the exchange only when its preferred diagnostic, and masks the OIDC JWT immediately after validation. Keeping selection and exchange in that final first-party shell step preserves the rule that no checkout, setup, artifact, or other third-party action receives either -credential, and neither response body is logged. +credential, and neither response body is logged. Both HTTP calls reuse the +central scheduler's 10-second connection and 30-second total request bounds, so +an unavailable identity or exchange endpoint cannot consume the entire +25-minute coordinator budget. Each run writes one deterministic JSON receipt and the same bounded evidence to the GitHub Actions job summary. The JSON is uploaded through the immutable, SHA-pinned artifact action with a three-day retention period. Artifact upload receives no maintainer or model credential. The receipt proves only coordinator observations and downstream dispatch acceptance; it is not merge, release, or product-quality evidence. diff --git a/tests/test_organization_commercial_readiness_loop_credential_contract.py b/tests/test_organization_commercial_readiness_loop_credential_contract.py index 7f312606e..7927e74df 100644 --- a/tests/test_organization_commercial_readiness_loop_credential_contract.py +++ b/tests/test_organization_commercial_readiness_loop_credential_contract.py @@ -41,3 +41,5 @@ def test_opencode_exchange_fails_closed_without_wasting_or_exposing_credentials( assert "OIDC token response was malformed" in source assert "app token response was malformed" in source assert 'echo "::add-mask::$oidc_token"' in source + assert coordinate.count("--connect-timeout 10") == 2 + assert coordinate.count("--max-time 30") == 2 From dfb8e261c81705841111dd4ad1712a9fb6c767d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 04:01:11 -0700 Subject: [PATCH 4/5] fix(strix): gate dependency manifest updates (#935) * fix(strix): trigger quality CI on dependency manifest updates Replay the unique Strix lock-trigger contract onto current main so a manifest-only lock change cannot skip install, policy, and full-suite evidence. * ci: refresh audit and scheduler contracts * fix(strix): preflight production dependency lock * fix(strix): mirror production lock semantics --------- Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> --- .../strix-changed-path-quality-ci.yml | 20 ++++++++++++ CHANGELOG.md | 3 ++ .../strix-dependency-manifest-trigger.md | 32 +++++++++++++++++++ .../test_strix_workflow_dependency_hashes.py | 27 ++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 docs/doctoring/strix-dependency-manifest-trigger.md diff --git a/.github/workflows/strix-changed-path-quality-ci.yml b/.github/workflows/strix-changed-path-quality-ci.yml index 31924910a..3521836e8 100644 --- a/.github/workflows/strix-changed-path-quality-ci.yml +++ b/.github/workflows/strix-changed-path-quality-ci.yml @@ -10,6 +10,8 @@ on: - "docs/doctoring/strix-legal-git-paths.md" - "docs/doctoring/strix-model-behavior-error.md" - "docs/doctoring/strix-quality-timeout-fixtures.md" + - "docs/doctoring/strix-dependency-manifest-trigger.md" + - "requirements-strix-ci-hashes.txt" - "scripts/ci/strix_quick_gate.sh" - "scripts/ci/test_strix_quick_gate.sh" - "tests/test_strix_changed_path_policy.py" @@ -73,3 +75,21 @@ jobs: python -m compileall -q tests/test_strix_changed_path_policy.py tests/test_strix_model_behavior_error.py tests/test_strix_nvidia_nim_not_found_fallback.py tests/test_strix_workflow_dependency_hashes.py tests/test_strix_quality_timeout_fixture_budget.py bash -n scripts/ci/strix_quick_gate.sh git diff --exit-code + + - name: Set up production Strix lock Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + + - name: Preflight exact hashed Strix dependency closure + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python -m pip install \ + --dry-run \ + --ignore-installed \ + --no-deps \ + --require-hashes \ + -r requirements-strix-ci-hashes.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 453691d4f..93104bcb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,9 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Bound the Strix changed-path quality gate to the complete production hash + lock and mirrored production's deliberate `--no-deps` security-override + boundary without rejecting source distributions that production accepts. - Restored the hourly organization commercial-readiness loop after five consecutive startup failures caused by its mandatory but unprovisioned `PR_REVIEW_MERGE_TOKEN`. Protected scheduled jobs now prefer that maintainer diff --git a/docs/doctoring/strix-dependency-manifest-trigger.md b/docs/doctoring/strix-dependency-manifest-trigger.md new file mode 100644 index 000000000..7d01041b8 --- /dev/null +++ b/docs/doctoring/strix-dependency-manifest-trigger.md @@ -0,0 +1,32 @@ +# Strix dependency-manifest quality trigger + +## Incident and buyer impact + +`requirements-strix-ci-hashes.txt` is executable supply-chain input for the +organization-required Strix gate. The permanent changed-path quality +workflow did not list that file. A Dependabot lock-only pull request could +therefore merge without running the Strix install, policy, shell-regression, +and full-suite contract. + +## Decision + +Add the exact repository-root manifest path to +`.github/workflows/strix-changed-path-quality-ci.yml` and bind it with +`test_strix_workflow_reruns_when_dependency_manifest_changes`. The same gate +uses production Python 3.13 to perform a hash-enforced dry-run of every pinned +lock entry. It mirrors production's deliberate `--no-deps` boundary because +the reviewed `cryptography==50.0.0` security override is newer than the range +declared by `strix-agent==1.5.3`; every installed entry is still version- and +hash-pinned. The preflight permits source distributions because production +does too, so it does not invent a stricter platform contract. Scanner models, +credentials, timeouts, and result semantics are unchanged. + +## References + +National Institute of Standards and Technology. (2024). *Cybersecurity +supply chain risk management practices for systems and organizations* +(NIST Special Publication 800-161 Rev. 1). +https://doi.org/10.6028/NIST.SP.800-161r1 + +Open Source Security Foundation. (2025). *SLSA specification version 1.2*. +https://slsa.dev/spec/v1.2/ diff --git a/tests/test_strix_workflow_dependency_hashes.py b/tests/test_strix_workflow_dependency_hashes.py index e2509c18b..dbdc74548 100644 --- a/tests/test_strix_workflow_dependency_hashes.py +++ b/tests/test_strix_workflow_dependency_hashes.py @@ -40,6 +40,33 @@ def test_strix_workflow_reruns_when_hash_contract_changes() -> None: assert ' - "tests/test_strix_workflow_dependency_hashes.py"' in workflow +def test_strix_workflow_reruns_when_dependency_manifest_changes() -> None: + """Changing the Strix dependency lock must trigger its install contract.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + + assert (ROOT / "requirements-strix-ci-hashes.txt").is_file() + assert ' - "requirements-strix-ci-hashes.txt"' in workflow + assert ' - "docs/doctoring/strix-dependency-manifest-trigger.md"' in workflow + + +def test_strix_workflow_preflights_dependency_manifest_hashes() -> None: + """The specialized gate resolves the production lock with enforced hashes.""" + + workflow = WORKFLOW.read_text(encoding="utf-8") + preflight = workflow.split( + " - name: Preflight exact hashed Strix dependency closure\n", 1 + )[1].split("\n - name:", 1)[0] + + assert 'python-version: "3.13"' in workflow + assert "python -m pip install \\" in preflight + assert "--dry-run \\" in preflight + assert "--ignore-installed \\" in preflight + assert "--no-deps \\" in preflight + assert "--only-binary=:all:" not in preflight + assert "--require-hashes \\" in preflight + assert "-r requirements-strix-ci-hashes.txt" in preflight + + def test_strix_workflow_rejects_branch_selected_manual_dispatch() -> None: """Central executable workflows load no branch-selected manual source.""" workflow = WORKFLOW.read_text(encoding="utf-8") From 9cda8fa219a2dbfa172cc05edb20ff7d6f08eb75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:12:51 +0900 Subject: [PATCH 5/5] fix(strix): avoid untrusted dependency build hooks --- .../strix-changed-path-quality-ci.yml | 18 ----------------- CHANGELOG.md | 12 +++++------ .../strix-dependency-manifest-trigger.md | 20 ++++++++++--------- pyproject.toml | 1 - .../test_strix_workflow_dependency_hashes.py | 19 +++++------------- 5 files changed, 22 insertions(+), 48 deletions(-) diff --git a/.github/workflows/strix-changed-path-quality-ci.yml b/.github/workflows/strix-changed-path-quality-ci.yml index 3521836e8..3c333624c 100644 --- a/.github/workflows/strix-changed-path-quality-ci.yml +++ b/.github/workflows/strix-changed-path-quality-ci.yml @@ -75,21 +75,3 @@ jobs: python -m compileall -q tests/test_strix_changed_path_policy.py tests/test_strix_model_behavior_error.py tests/test_strix_nvidia_nim_not_found_fallback.py tests/test_strix_workflow_dependency_hashes.py tests/test_strix_quality_timeout_fixture_budget.py bash -n scripts/ci/strix_quick_gate.sh git diff --exit-code - - - name: Set up production Strix lock Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.13" - - - name: Preflight exact hashed Strix dependency closure - env: - PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_INPUT: "1" - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - python -m pip install \ - --dry-run \ - --ignore-installed \ - --no-deps \ - --require-hashes \ - -r requirements-strix-ci-hashes.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 93104bcb1..f785eff68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,8 +56,10 @@ Semantic Versioning where the repository publishes a release. ### Fixed - Bound the Strix changed-path quality gate to the complete production hash - lock and mirrored production's deliberate `--no-deps` security-override - boundary without rejecting source distributions that production accepts. + lock while keeping pull-request-controlled source distributions out of pip's + executable metadata/build boundary. Production lock resolution stays in the + trusted default-branch Strix workflow; lock changes still trigger the + permanent policy, regression, and security gates. - Restored the hourly organization commercial-readiness loop after five consecutive startup failures caused by its mandatory but unprovisioned `PR_REVIEW_MERGE_TOKEN`. Protected scheduled jobs now prefer that maintainer @@ -65,10 +67,8 @@ Semantic Versioning where the repository publishes a release. existing short-lived OpenCode GitHub App installation token, without accepting `OPENCODE_APPROVE_TOKEN`, `GITHUB_TOKEN`, provider credentials, or exposing either cross-repository credential to checkout, setup, or artifact - actions. Declared `pip` in the project-local development environment so the - existing isolated wheel/hash preflight test no longer depends on an - accidentally pre-populated virtual environment. The exchange now runs only - when the preferred maintainer secret is absent, masks the OIDC JWT before the + actions. The exchange now runs only when the preferred maintainer secret is + absent, masks the OIDC JWT before the second request, and converts malformed successful JSON responses into the existing explicit unavailable output instead of exiting early under `errexit`. Both OIDC and App-token HTTP requests now use a 10-second connect diff --git a/docs/doctoring/strix-dependency-manifest-trigger.md b/docs/doctoring/strix-dependency-manifest-trigger.md index 7d01041b8..a9da16fb6 100644 --- a/docs/doctoring/strix-dependency-manifest-trigger.md +++ b/docs/doctoring/strix-dependency-manifest-trigger.md @@ -5,20 +5,22 @@ `requirements-strix-ci-hashes.txt` is executable supply-chain input for the organization-required Strix gate. The permanent changed-path quality workflow did not list that file. A Dependabot lock-only pull request could -therefore merge without running the Strix install, policy, shell-regression, -and full-suite contract. +therefore merge without running the Strix policy, shell-regression, security, +and full-suite contracts. ## Decision Add the exact repository-root manifest path to `.github/workflows/strix-changed-path-quality-ci.yml` and bind it with -`test_strix_workflow_reruns_when_dependency_manifest_changes`. The same gate -uses production Python 3.13 to perform a hash-enforced dry-run of every pinned -lock entry. It mirrors production's deliberate `--no-deps` boundary because -the reviewed `cryptography==50.0.0` security override is newer than the range -declared by `strix-agent==1.5.3`; every installed entry is still version- and -hash-pinned. The preflight permits source distributions because production -does too, so it does not invent a stricter platform contract. Scanner models, +`test_strix_workflow_reruns_when_dependency_manifest_changes`. Do not pass the +pull-request-controlled lock to `pip` in this job. A hash-matching source +distribution can execute its PEP 517 build backend while pip prepares metadata, +even for `--dry-run --no-deps --require-hashes`. Hosted Strix run `32643804284` +reproduced that execution path at the exact pull-request head. Requiring wheels +is not an equivalent repair because the production closure includes packages +without a compatible wheel. Production resolution therefore remains inside the +trusted default-branch Strix boundary, while every lock change still triggers +the permanent policy, regression, and security review gates. Scanner models, credentials, timeouts, and result semantics are unchanged. ## References diff --git a/pyproject.toml b/pyproject.toml index bfa28f150..1954a2aaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ dependencies = [] [dependency-groups] dev = [ - "pip>=25.0", "pytest>=8.0.0", "pytest-cov>=7.1.0", "interrogate>=1.7.0", diff --git a/tests/test_strix_workflow_dependency_hashes.py b/tests/test_strix_workflow_dependency_hashes.py index dbdc74548..63cc21f65 100644 --- a/tests/test_strix_workflow_dependency_hashes.py +++ b/tests/test_strix_workflow_dependency_hashes.py @@ -49,22 +49,13 @@ def test_strix_workflow_reruns_when_dependency_manifest_changes() -> None: assert ' - "docs/doctoring/strix-dependency-manifest-trigger.md"' in workflow -def test_strix_workflow_preflights_dependency_manifest_hashes() -> None: - """The specialized gate resolves the production lock with enforced hashes.""" +def test_strix_workflow_does_not_execute_pr_dependency_build_hooks() -> None: + """The PR gate must not resolve its untrusted production lock with pip.""" workflow = WORKFLOW.read_text(encoding="utf-8") - preflight = workflow.split( - " - name: Preflight exact hashed Strix dependency closure\n", 1 - )[1].split("\n - name:", 1)[0] - - assert 'python-version: "3.13"' in workflow - assert "python -m pip install \\" in preflight - assert "--dry-run \\" in preflight - assert "--ignore-installed \\" in preflight - assert "--no-deps \\" in preflight - assert "--only-binary=:all:" not in preflight - assert "--require-hashes \\" in preflight - assert "-r requirements-strix-ci-hashes.txt" in preflight + + assert "Preflight exact hashed Strix dependency closure" not in workflow + assert "-r requirements-strix-ci-hashes.txt" not in workflow def test_strix_workflow_rejects_branch_selected_manual_dispatch() -> None: