From dc77039478c6fbcc50ce8854c341b565baf3d0d9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:24:18 +0000 Subject: [PATCH 1/3] feat(strix): map official scan modes from dual-flow events Set STRIX_SCAN_MODE from the GitHub event instead of inheriting Quick on every trigger. Required PR and repository_dispatch evidence stay quick; push to develop stays quick; push to main/master and the weekly schedule use standard. Restore workflow_dispatch with a scan_mode choice so an incomplete RC can be scanned by hand. Deep is manual-only and is the only path that raises the 360/340/4h/4.5h budget. Do not invent release or RC-tag triggers. Allowlist official CLI mode names and reject the unofficial normal alias. Co-authored-by: Seongho Bae --- .../strix-changed-path-quality-ci.yml | 5 +- .github/workflows/strix.yml | 66 ++++- CHANGELOG.md | 1 + docs/doctoring/strix-scan-mode-dual-flow.md | 60 +++++ scripts/ci/strix_quick_gate.sh | 12 +- scripts/ci/strix_required_workflow_smoke.sh | 5 + scripts/ci/test_strix_quick_gate.sh | 82 +++++- .../test_required_workflow_queue_contract.py | 21 +- tests/test_strix_scan_mode_policy.py | 253 ++++++++++++++++++ 9 files changed, 482 insertions(+), 23 deletions(-) create mode 100644 docs/doctoring/strix-scan-mode-dual-flow.md create mode 100644 tests/test_strix_scan_mode_policy.py diff --git a/.github/workflows/strix-changed-path-quality-ci.yml b/.github/workflows/strix-changed-path-quality-ci.yml index 75e9b7d8e..b2722056d 100644 --- a/.github/workflows/strix-changed-path-quality-ci.yml +++ b/.github/workflows/strix-changed-path-quality-ci.yml @@ -4,15 +4,18 @@ on: pull_request: branches: [main] paths: + - ".github/workflows/strix.yml" - ".github/workflows/strix-changed-path-quality-ci.yml" - "CHANGELOG.md" - "docs/doctoring/strix-legal-git-paths.md" - "docs/doctoring/strix-quality-timeout-fixtures.md" + - "docs/doctoring/strix-scan-mode-dual-flow.md" - "scripts/ci/strix_quick_gate.sh" - "scripts/ci/test_strix_quick_gate.sh" - "tests/test_strix_changed_path_policy.py" - "tests/test_strix_workflow_dependency_hashes.py" - "tests/test_strix_quality_timeout_fixture_budget.py" + - "tests/test_strix_scan_mode_policy.py" permissions: contents: read @@ -66,6 +69,6 @@ jobs: test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}" python -m coverage run -m pytest tests -q bash scripts/ci/test_strix_quick_gate.sh - python -m compileall -q tests/test_strix_changed_path_policy.py tests/test_strix_workflow_dependency_hashes.py tests/test_strix_quality_timeout_fixture_budget.py + python -m compileall -q tests/test_strix_changed_path_policy.py tests/test_strix_workflow_dependency_hashes.py tests/test_strix_quality_timeout_fixture_budget.py tests/test_strix_scan_mode_policy.py bash -n scripts/ci/strix_quick_gate.sh git diff --exit-code diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index f8c361b95..552658f3c 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -61,10 +61,37 @@ on: - '.github/ISSUE_TEMPLATE/**' schedule: # Weekly scan on protected branches (Mondays at 03:00 UTC). + # Official Strix standard mode: 30–60 min; current 120/100/90/95 budget + # is honest. CWL has no consistent RC-tag or prerelease convention, so + # this weekly pass is the scheduled pre-release depth. Do not invent a + # GitHub release event or RC tag pattern to compensate. - cron: '0 3 * * 1' # Default-branch-only retry entrypoint; no caller-selected workflow ref. + # Remains PR-metadata-bound same-head evidence (quick). It cannot scan a + # branch or tag release candidate. repository_dispatch: types: [strix-scan] + # Manual official-mode selector for an incomplete RC. Deep is allowed only + # on this path. The sole input is scan_mode; callers cannot choose a + # target repository or substitute privileged retry metadata. Selecting a + # workflow revision still requires write access, matching GitHub's + # workflow_dispatch model. Privileged same-head retries stay on + # repository_dispatch above. + workflow_dispatch: + inputs: + scan_mode: + description: >- + Official Strix CLI mode (quick, standard, or deep). Deep is + manual-only and raises this job to the GitHub-hosted 360-minute + ceiling. CWL does not invent RC tags or GitHub release events; + use this input to scan an incomplete release candidate by hand. + required: true + type: choice + default: standard + options: + - quick + - standard + - deep concurrency: # Include the event name so default-branch repository_dispatch evidence cannot cancel @@ -92,12 +119,13 @@ jobs: strix: if: github.event_name != 'pull_request_target' || github.event.action != 'closed' - # Large repositories can require a legitimate full-hour review. The scanner - # gets a 90-minute process budget and a 95-minute total retry budget; the - # 100-minute step and 120-minute job leave deterministic time to preserve - # partial reports and publish a concrete failure reason. Hitting any cap is - # fail-closed and never turns an incomplete scan into an approval. - timeout-minutes: 120 + # Quick and standard share the honest 120/100/90/95 budget (standard is + # 30–60 min). Deep is manual workflow_dispatch only and uses the + # GitHub-hosted 360-minute job ceiling, leaving ~20 minutes after the + # 340-minute step for artifact and status publication. Required + # pull_request_target / repository_dispatch evidence stays on the 120 + # budget; do not raise that path to 360. + timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '360' || '120') }} runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan # exchanges an OIDC token (id-token) and publishes same-repo status evidence @@ -110,6 +138,13 @@ jobs: statuses: write env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + # Official CLI modes only (quick|standard|deep). Dual-flow mapping: + # required PR / repository_dispatch / push develop → quick; + # push main|master and weekly schedule → standard; + # workflow_dispatch → the scan_mode input (default standard). + # repository_dispatch must not inherit standard from github.ref + # (dispatch SHA is the default branch, often main). + STRIX_SCAN_MODE: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.scan_mode || 'standard') || github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }} steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -781,9 +816,9 @@ jobs: esac echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV" - - name: Run Strix (quick) + - name: Run Strix if: steps.gate.outputs.enabled == 'true' - timeout-minutes: 100 + timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '340' || '100') }} # Security invariant for pull_request_target: execute only from the # trusted base checkout. The gate copies PR-head blobs into an isolated # temporary scope with execute bits stripped, then scans that scope as @@ -834,11 +869,22 @@ jobs: IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && 'true' || 'false' }} run: | budget_suffix="TIME""OUT" - process_budget_seconds="5400" + # Deep process/total budgets are workflow_dispatch-only. A required + # PR or repository_dispatch run must keep the 90/95-minute cap even + # if STRIX_SCAN_MODE were ever mis-set to deep. + if [ "${STRIX_SCAN_MODE}" = "deep" ] && [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + process_budget_seconds="14400" + total_budget_seconds="16200" + else + process_budget_seconds="5400" + total_budget_seconds="5700" + fi + printf 'Using official Strix scan mode %s with process budget %ss.\n' \ + "${STRIX_SCAN_MODE}" "$process_budget_seconds" export "LLM_${budget_suffix}=900" export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=300" export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds" - export "STRIX_TOTAL_${budget_suffix}_SECONDS=5700" + export "STRIX_TOTAL_${budget_suffix}_SECONDS=$total_budget_seconds" # Capture the gate exit code plus its console output. The gate returns # exit 1 both for genuine blocking vulnerabilities AND for diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1aebf43..a2c672cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Semantic Versioning where the repository publishes a release. ### Changed +- Map the organization-required Strix workflow to official CLI modes (`quick`, `standard`, `deep`) from the GitHub event instead of inheriting Quick on every trigger. Required `pull_request_target` and `repository_dispatch` `strix-scan` evidence stay Quick; `push` to `develop` stays Quick; `push` to `main`/`master` and the weekly Monday 03:00 UTC schedule use Standard with the existing 120/100/90/95 budget. `workflow_dispatch` is restored with a `scan_mode` choice (default Standard) so an incomplete release candidate can be scanned by hand. Deep is manual-only and is the only path that raises the job to 360/340/4h/4.5h. CWL's dual GitHub Flow / Git Flow setup has no consistent RC-tag or prerelease convention, so this change does not invent `release:` or `v*-rc*` triggers. The gate now allowlists official mode names and rejects `normal`. - Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap. - Run the bounded Quarantine Sandbox Runtime heartbeat at minute 14 without granting the caller model secrets, repository mutation permissions, approval, merge, release, artifact-execution, or final security-verdict authority. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. diff --git a/docs/doctoring/strix-scan-mode-dual-flow.md b/docs/doctoring/strix-scan-mode-dual-flow.md new file mode 100644 index 000000000..b83888668 --- /dev/null +++ b/docs/doctoring/strix-scan-mode-dual-flow.md @@ -0,0 +1,60 @@ +# Strix official scan-mode mapping for dual GitHub / Git Flow + +검토 기준일: **2026-08-16** + +## Incident + +The organization-required Strix workflow always ran Quick. `scripts/ci/strix_quick_gate.sh` already forwarded `STRIX_SCAN_MODE` (default `quick`) as `strix -n -t . --scan-mode $SCAN_MODE`, but `.github/workflows/strix.yml` never set the environment variable. Every current event therefore inherited Quick: `pull_request_target`, `push` to `main`/`develop`/`master`, the Monday 03:00 UTC `schedule`, and `repository_dispatch` type `strix-scan`. + +Official Strix CLI modes are only `quick`, `standard`, and `deep` (usestrix/strix `--scan-mode` choices). There is no `normal` alias. Quick is the CI/PR path (minutes). Standard is pre-release / weekly (30 min–1 h). Deep is pre-production (1–4 h). The CLI default is deep; CWL must keep choosing explicitly. + +The previous 120 / 100 / 90 / 95-minute budget is honest for Quick and Standard. It cannot finish Deep. GitHub-hosted jobs max out at 360 minutes. + +## Decision + +ContextualWisdomLab runs both GitHub Flow (`main`/`master` is the base) and Git Flow (`develop` is the base). That dual-flow setup is incomplete: there is no consistent RC-tag, prerelease, or GitHub `release` event convention, and RankWeave forbids prerelease GitHub Releases. This change therefore does **not** invent `release:` or `v*-rc*` triggers. + +Confirmed event → official mode mapping: + +| Event | Mode | Job / step / process / total | +|---|---|---| +| `pull_request_target` | `quick` | 120 / 100 / 5400 / 5700 | +| `repository_dispatch` `strix-scan` | `quick` | 120 / 100 / 5400 / 5700 | +| `push` to `develop` | `quick` | 120 / 100 / 5400 / 5700 | +| `push` to `main` or `master` | `standard` | 120 / 100 / 5400 / 5700 | +| `schedule` (Monday 03:00 UTC) | `standard` | 120 / 100 / 5400 / 5700 | +| `workflow_dispatch` `scan_mode` | chosen (`quick` / `standard` / `deep`, default `standard`) | Deep only: 360 / 340 / 14400 / 16200 | + +`repository_dispatch` remains the default-branch-only, PR-metadata-bound same-head retry. It cannot scan a branch or tag release candidate. The mapping requires `github.event_name == 'push'` before treating `refs/heads/main` or `refs/heads/master` as Standard, because a `repository_dispatch` SHA is the default branch and is often `main`. + +`workflow_dispatch` is restored with a single `scan_mode` choice so an incomplete release candidate can be scanned by hand. Deep is allowed only on that manual path. The required PR job stays on the 120-minute budget. Deep uses the GitHub-hosted 360-minute ceiling and leaves about 20 minutes after the 340-minute step for artifact and status publication. + +`require_safe_scan_mode` now allowlists `quick|standard|deep` and rejects `normal` and every other string, including charset-valid aliases. + +Fail-closed behavior is unchanged: missing artifact, unmapped findings, infrastructure errors, PR scoping, and severity gating stay as they were. The hashed-lock installer line is not part of this change. + +`pull_request_target` continues to execute trusted base scripts only. + +## Verification contract + +`tests/test_strix_scan_mode_policy.py` and `scripts/ci/test_strix_quick_gate.sh` fail if: + +1. the event → mode expression is reverted or `repository_dispatch` can select a mode; +2. Deep job/step/process budgets apply to the required PR path; +3. a GitHub release event or RC-tag trigger is added; +4. `require_safe_scan_mode` accepts `normal` or any unofficial name; +5. `workflow_dispatch` grows repository, pull-request, or privileged-retry inputs. + +The quality workflow trigger includes this record, the mapping test, and `.github/workflows/strix.yml` so later edits re-run exact-head evidence. + +## Rollback + +Roll back the mapping and this record together only if a required `pull_request_target` job is observed running Deep or a 360-minute budget. Do not restore unconditional Quick by deleting `STRIX_SCAN_MODE`. Do not invent RC tags to replace the manual `workflow_dispatch` path. + +## References (APA 7th) + +GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax + +GitHub. (n.d.). *Manually running a workflow*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow + +Strix. (n.d.). *Command-line interface* (`--scan-mode` `{quick,standard,deep}`). usestrix/strix. Retrieved August 16, 2026, from https://github.com/usestrix/strix diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 0f37f3460..5997baf98 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -357,10 +357,16 @@ require_positive_integer() { require_safe_scan_mode() { local scan_mode="$1" - if [ -z "$scan_mode" ] || [[ ! "$scan_mode" =~ ^[[:alnum:]_.-]+$ ]]; then - echo "ERROR: STRIX_SCAN_MODE contains unsupported characters: '$scan_mode'." >&2 + # Official Strix CLI modes only. There is no `normal` alias. + case "$scan_mode" in + quick | standard | deep) + return 0 + ;; + *) + echo "ERROR: STRIX_SCAN_MODE must be one of quick, standard, or deep; got '$scan_mode'." >&2 exit 2 - fi + ;; + esac } validate_raw_target_path_input() { diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index 8cd6dddad..f4b24bc32 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -142,6 +142,11 @@ assert_file_contains "$workflow_file" 'bash "$TRUSTED_STRIX_GATE"' "Strix workfl assert_file_contains "$workflow_file" "Self-test Strix required workflow contract" "Strix workflow uses bounded required-path smoke test" assert_file_contains "$workflow_file" 'bash "$TRUSTED_STRIX_REQUIRED_SMOKE"' "Strix workflow executes bounded smoke test" assert_file_contains "$workflow_file" "timeout-minutes: 2" "Strix required-path smoke test has a short timeout" +assert_file_contains "$workflow_file" "workflow_dispatch:" "Strix workflow restores manual official-mode selection" +assert_file_contains "$workflow_file" "STRIX_SCAN_MODE:" "Strix workflow sets official scan mode from the event" +assert_file_contains "$workflow_file" "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')" "Strix standard push mapping requires the push event" +assert_file_contains "$workflow_file" "fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '360' || '120')" "Strix required path keeps the 120-minute job budget" +assert_file_contains "$gate_script" "quick | standard | deep)" "Strix gate allowlists official scan modes" assert_status_permissions_scoped assert_file_contains "$workflow_file" 'context="strix"' "Strix workflow publishes the strix commit status context" assert_file_contains "$workflow_file" "Existing current-run Strix success status is already present" "Strix manual follow-up status publisher accepts already-published same-run evidence" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 7343c06ac..0fc83fd7c 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -209,6 +209,17 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" 'TRUSTED_STRIX_GATE=$trusted_strix_source/scripts/ci/strix_quick_gate.sh' "strix workflow executes the central Strix gate script" assert_file_contains "$workflow_file" "Materialize target workspace" "strix workflow materializes target repository data separately from trusted scripts" assert_file_contains "$workflow_file" "types: [strix-scan]" "strix repository dispatch accepts only its dedicated default-branch event type" + assert_file_contains "$workflow_file" "workflow_dispatch:" "strix workflow restores manual official-mode selection for incomplete release candidates" + assert_file_contains "$workflow_file" "default: standard" "strix manual dispatch defaults to official standard mode" + assert_file_contains "$workflow_file" " - quick" "strix manual dispatch offers official quick mode" + assert_file_contains "$workflow_file" " - standard" "strix manual dispatch offers official standard mode" + assert_file_contains "$workflow_file" " - deep" "strix manual dispatch offers official deep mode" + assert_file_not_contains "$workflow_file" " - normal" "strix workflow must not offer a non-official normal scan-mode choice" + assert_file_not_contains "$workflow_file" $'\n release:' "strix workflow must not invent a GitHub release trigger" + assert_file_not_contains "$workflow_file" "v*-rc*" "strix workflow must not invent RC-tag triggers" + assert_file_not_contains "$workflow_file" "client_payload.scan_mode" "strix repository_dispatch cannot select scan mode" + assert_file_contains "$workflow_file" "STRIX_SCAN_MODE: \${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.scan_mode || 'standard') || github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }}" "strix workflow maps dual-flow events to official scan modes" + assert_file_contains "$workflow_file" "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')" "strix standard mode on push requires the push event, not merely a main ref" assert_file_contains "$workflow_file" 'REPOSITORY: ${{ github.event.client_payload.target_repository }}' "strix repository dispatch binds the requested target repository before fetching data" assert_file_contains "$workflow_file" "Validate repository dispatch against live pull request metadata" "strix repository dispatch validates its supplied PR metadata" assert_file_contains "$workflow_file" '[ "$live_base_sha" != "$SUPPLIED_BASE_SHA" ]' "strix repository dispatch verifies the target repository base SHA against the live PR" @@ -274,11 +285,20 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "GOOGLE_APPLICATION_CREDENTIALS" "strix workflow exports Vertex AI credentials only for Vertex provider mode" assert_file_contains "$workflow_file" "VERTEXAI_PROJECT" "strix workflow exports LiteLLM Vertex project env" assert_file_contains "$workflow_file" "VERTEXAI_LOCATION" "strix workflow exports LiteLLM Vertex location env" - assert_file_contains "$workflow_file" "timeout-minutes: 120" "strix workflow job budget preserves full-hour scans and artifact publication margin" - assert_file_contains "$workflow_file" "timeout-minutes: 100" "strix workflow scan step permits legitimate 90-minute repository reviews" + assert_file_contains "$workflow_file" "timeout-minutes: \${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '360' || '120') }}" "strix workflow keeps the 120-minute job budget except for manual deep" + assert_file_contains "$workflow_file" "timeout-minutes: \${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '340' || '100') }}" "strix workflow keeps the 100-minute step budget except for manual deep" + assert_file_not_contains "$workflow_file" "timeout-minutes: 360" "strix workflow must not apply the deep job ceiling unconditionally" + assert_file_not_contains "$workflow_file" "timeout-minutes: 340" "strix workflow must not apply the deep step ceiling unconditionally" assert_file_contains "$workflow_file" 'budget_suffix="TIME""OUT"' "strix workflow builds budget env keys without visible timeout signal text" - assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=5700"' "strix workflow preserves a 95-minute bounded total Strix budget" - assert_file_contains "$workflow_file" 'process_budget_seconds="5400"' "strix workflow gives a legitimate scan up to 90 minutes" + assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=$total_budget_seconds"' "strix workflow exports the selected total budget through the split timeout key" + assert_file_contains "$workflow_file" 'process_budget_seconds="5400"' "strix workflow gives a legitimate quick or standard scan up to 90 minutes" + assert_file_contains "$workflow_file" 'total_budget_seconds="5700"' "strix workflow preserves a 95-minute bounded total budget for quick and standard" + assert_file_contains "$workflow_file" 'process_budget_seconds="14400"' "strix workflow gives a manual deep scan a 4-hour process budget" + assert_file_contains "$workflow_file" 'total_budget_seconds="16200"' "strix workflow gives a manual deep scan a 4.5-hour total budget" + assert_file_contains "$workflow_file" '[ "${STRIX_SCAN_MODE}" = "deep" ] && [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]' "strix workflow raises process budgets only for manual deep" + assert_file_contains "$GATE_SCRIPT" "quick | standard | deep)" "strix gate allowlists official scan modes only" + assert_file_contains "$GATE_SCRIPT" "STRIX_SCAN_MODE must be one of quick, standard, or deep" "strix gate rejects unofficial scan modes" + assert_file_not_contains "$GATE_SCRIPT" "normal)" "strix gate must not accept a non-official normal scan-mode alias" assert_file_contains "$workflow_file" 'strix_gate_console.log" "$GITHUB_WORKSPACE/strix_runs/gate-console.log' "strix workflow preserves partial console output after failures and timeouts" assert_file_contains "$REPO_ROOT/scripts/ci/strix_quick_gate.sh" "gate-last-attempt.log" "strix gate preserves the last partial attempt before runtime cleanup" assert_file_contains "$workflow_file" 'IS_PR_EVIDENCE_RUN: ${{ (github.event_name == '"'"'pull_request_target'"'"' || github.event.client_payload.pr_number != '"'"''"'"') && '"'"'true'"'"' || '"'"'false'"'"' }}' "strix workflow passes PR evidence mode through env" @@ -8128,6 +8148,56 @@ EOF rm -rf "$tmp_dir" } +run_scan_mode_validation_case() { + local case_name="$1" + local scan_mode="$2" + local expected_rc="$3" + local expected_message="$4" + + local tmp_dir + tmp_dir="$(mktemp -d)" + local output_log="$tmp_dir/output.log" + local call_count_file="$tmp_dir/strix_calls" + local fake_strix="$tmp_dir/strix" + local strix_llm_file="$tmp_dir/strix_llm.txt" + local llm_api_key_file="$tmp_dir/llm_api_key.txt" + + cat >"$fake_strix" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +echo "1" >> "${STRIX_CALL_COUNT_FILE:?}" +exit 0 +EOF + chmod +x "$fake_strix" + printf '%s' 'openai-direct/gpt-5.4' >"$strix_llm_file" + printf '%s' 'dummy-key' >"$llm_api_key_file" + + set +e + env -u GITHUB_EVENT_NAME -u GITHUB_EVENT_PATH -u STRIX_TEST_CHANGED_FILES_OVERRIDE \ + PATH="$tmp_dir:$PATH" \ + STRIX_EXECUTABLE_PATH="$fake_strix" \ + STRIX_INPUT_FILE_ROOT="$tmp_dir" \ + STRIX_DISABLE_PR_SCOPING="1" \ + STRIX_LLM_FILE="$strix_llm_file" \ + LLM_API_KEY_FILE="$llm_api_key_file" \ + STRIX_CALL_COUNT_FILE="$call_count_file" \ + STRIX_SCAN_MODE="$scan_mode" \ + bash "$GATE_SCRIPT" >"$output_log" 2>&1 + local rc=$? + set -e + + assert_equals "$expected_rc" "$rc" "case=$case_name exit code" + assert_file_contains "$output_log" "$expected_message" "case=$case_name output" + + local actual_calls="0" + if [ -f "$call_count_file" ]; then + actual_calls="$(wc -l <"$call_count_file" | tr -d ' ')" + fi + assert_equals "0" "$actual_calls" "case=$case_name strix call count" + + rm -rf "$tmp_dir" +} + run_missing_config_case() { local case_name="$1" local strix_llm="$2" @@ -11614,6 +11684,10 @@ run_gate_case_allow_provider_signal "pr-low-markdown-plus-console-critical-manif "123" \ '{"workflow_runs":[{"id":405,"name":"Dependency review","path":".github/workflows/dependency-review.yml","head_sha":"test-head-sha","status":"completed","conclusion":"success","pull_requests":[{"number":123}]},{"id":406,"name":"OSV-Scanner","path":".github/workflows/osvscanner.yml","head_sha":"test-head-sha","status":"completed","conclusion":"success","pull_requests":[{"number":123}]}]}' +run_scan_mode_validation_case "scan-mode-rejects-normal-alias" "normal" "2" "ERROR: STRIX_SCAN_MODE must be one of quick, standard, or deep; got 'normal'." +run_scan_mode_validation_case "scan-mode-rejects-whitespace" " " "2" "ERROR: STRIX_SCAN_MODE must be one of quick, standard, or deep; got ''." +run_scan_mode_validation_case "scan-mode-rejects-capitalized-quick" "Quick" "2" "ERROR: STRIX_SCAN_MODE must be one of quick, standard, or deep; got 'Quick'." +run_scan_mode_validation_case "scan-mode-rejects-injection" "quick;id" "2" "ERROR: STRIX_SCAN_MODE must be one of quick, standard, or deep; got 'quick;id'." run_missing_config_case "missing-strix-llm" "" "dummy" "ERROR: STRIX_LLM_FILE must reference a regular file containing the model." run_missing_config_case "missing-llm-api-key" "openai/gpt-5.4" "" "ERROR: LLM_API_KEY_FILE must reference a regular file containing the API key." run_missing_config_case "whitespace-only-strix-llm" " " "dummy" "ERROR: STRIX_LLM_FILE must contain a non-empty model value." diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 535fd513a..7d1b19505 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -112,8 +112,13 @@ def test_privileged_review_retries_use_default_branch_repository_dispatch() -> N assert "repository_dispatch:" in trigger_contract assert f"types: [{event_type}]" in trigger_contract - assert "workflow_dispatch:" not in trigger_contract - assert "github.event.inputs" not in workflow + if filename == "strix.yml": + assert "workflow_dispatch:" in trigger_contract + assert "github.event.inputs.scan_mode" in workflow + assert "client_payload.scan_mode" not in workflow + else: + assert "workflow_dispatch:" not in trigger_contract + assert "github.event.inputs" not in workflow assert "github.event.client_payload" in workflow scheduler = ( @@ -137,14 +142,20 @@ def test_privileged_review_retries_use_default_branch_repository_dispatch() -> N def test_no_central_workflow_exposes_branch_selected_manual_dispatch() -> None: - """Every central manual entrypoint must load code from the default branch.""" + """Privileged central entrypoints must not load caller-selected workflow code. + + Strix is the documented exception: ``workflow_dispatch`` exists only so an + operator can choose official ``quick|standard|deep`` for an incomplete + release candidate. CWL has no RC-tag convention. The privileged same-head + retry remains default-branch ``repository_dispatch`` type ``strix-scan``. + """ workflow_files = sorted((REPO_ROOT / ".github" / "workflows").glob("*.yml")) offenders = [ path.name for path in workflow_files if "workflow_dispatch:" in path.read_text(encoding="utf-8") ] - assert offenders == [] + assert offenders == ["strix.yml"] def test_required_pull_request_workflows_cancel_superseded_runs() -> None: @@ -1111,7 +1122,7 @@ def test_strix_provider_outage_without_findings_is_neutralized() -> None: def test_strix_cross_repo_dispatch_uses_target_token_for_pr_scoping() -> None: workflow = workflow_text("strix.yml") - run_step = workflow.split(" - name: Run Strix (quick)", 1)[1].split( + run_step = workflow.split(" - name: Run Strix\n", 1)[1].split( " - name:", 1 )[0] diff --git a/tests/test_strix_scan_mode_policy.py b/tests/test_strix_scan_mode_policy.py new file mode 100644 index 000000000..ece9d5a37 --- /dev/null +++ b/tests/test_strix_scan_mode_policy.py @@ -0,0 +1,253 @@ +"""Event-to-mode contract for the organization-required Strix workflow. + +CWL runs both GitHub Flow (main/master) and Git Flow (develop) without a +consistent RC-tag or prerelease convention. Official Strix CLI modes are only +``quick``, ``standard``, and ``deep``. This module pins the dual-flow mapping +and the Deep-only timeout raise so a revert or a 360-minute required-PR job +fails closed in CI. +""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +STRIX_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "strix.yml" +STRIX_GATE = REPO_ROOT / "scripts" / "ci" / "strix_quick_gate.sh" +QUALITY_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "strix-changed-path-quality-ci.yml" + +SCAN_MODE_EXPRESSION = ( + "STRIX_SCAN_MODE: ${{ github.event_name == 'workflow_dispatch' && " + "(github.event.inputs.scan_mode || 'standard') || " + "github.event_name == 'schedule' && 'standard' || " + "github.event_name == 'push' && (github.ref == 'refs/heads/main' || " + "github.ref == 'refs/heads/master') && 'standard' || 'quick' }}" +) +JOB_TIMEOUT_EXPRESSION = ( + "timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && " + "github.event.inputs.scan_mode == 'deep' && '360' || '120') }}" +) +STEP_TIMEOUT_EXPRESSION = ( + "timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && " + "github.event.inputs.scan_mode == 'deep' && '340' || '100') }}" +) + + +def _workflow() -> str: + """Return the current central Strix workflow text.""" + return STRIX_WORKFLOW.read_text(encoding="utf-8") + + +def _trigger_block(workflow: str) -> str: + """Return the ``on:`` block before concurrency.""" + return workflow.split("concurrency:", 1)[0] + + +def _strix_job_header(workflow: str) -> str: + """Return the ``strix`` job header before its first step.""" + job = workflow.split(" strix:\n", 1)[1] + return job.split(" steps:", 1)[0] + + +def _run_strix_step(workflow: str) -> str: + """Return the official-mode scan step without loading workflow YAML tags.""" + marker = " - name: Run Strix\n" + start = workflow.index(marker) + end = workflow.index("\n - name:", start + len(marker)) + return workflow[start:end] + + +def _function_block(source: str, function_name: str) -> str: + """Return one top-level Bash function, including its closing brace.""" + match = re.search( + rf"(?ms)^{re.escape(function_name)}\(\) {{\n.*?^}}\n", + source, + ) + if match is None: + raise AssertionError(f"missing Bash function: {function_name}") + return match.group(0) + + +def _require_safe_scan_mode_rc(scan_mode: str) -> int: + """Execute the production allowlist against one candidate mode.""" + function_source = _function_block( + STRIX_GATE.read_text(encoding="utf-8"), + "require_safe_scan_mode", + ) + script = "\n".join( + ( + "set -euo pipefail", + function_source, + 'require_safe_scan_mode "$1"', + ) + ) + completed = subprocess.run( + ["bash", "-c", script, "strix-scan-mode", scan_mode], + check=False, + capture_output=True, + text=True, + ) + if completed.returncode not in {0, 2}: + raise AssertionError(completed.stderr) + return completed.returncode + + +def test_quality_trigger_includes_scan_mode_contract_paths() -> None: + """Keep mapping and doctoring changes inside the exact-head quality job.""" + workflow = QUALITY_WORKFLOW.read_text(encoding="utf-8") + trigger = workflow[: workflow.index("\njobs:")] + + assert "docs/doctoring/strix-scan-mode-dual-flow.md" in trigger + assert "tests/test_strix_scan_mode_policy.py" in trigger + assert ' - ".github/workflows/strix.yml"' in trigger + assert "tests/test_strix_scan_mode_policy.py" in workflow + + +def test_workflow_pins_official_scan_mode_expression() -> None: + """The job must set STRIX_SCAN_MODE from event, ref, and manual input.""" + workflow = _workflow() + + assert SCAN_MODE_EXPRESSION in workflow + assert "client_payload.scan_mode" not in workflow + assert " - normal" not in workflow + assert "|| 'normal'" not in workflow + + +@pytest.mark.parametrize( + ("event_name", "ref", "scan_mode_input", "expected"), + ( + ("pull_request_target", "refs/heads/main", "", "quick"), + ("repository_dispatch", "refs/heads/main", "", "quick"), + ("push", "refs/heads/develop", "", "quick"), + ("push", "refs/heads/main", "", "standard"), + ("push", "refs/heads/master", "", "standard"), + ("schedule", "refs/heads/develop", "", "standard"), + ("schedule", "refs/heads/main", "", "standard"), + ("workflow_dispatch", "refs/heads/release-candidate", "", "standard"), + ("workflow_dispatch", "refs/heads/main", "quick", "quick"), + ("workflow_dispatch", "refs/heads/main", "standard", "standard"), + ("workflow_dispatch", "refs/heads/main", "deep", "deep"), + ), +) +def test_dual_flow_event_maps_to_official_mode( + event_name: str, + ref: str, + scan_mode_input: str, + expected: str, +) -> None: + """Mirror the pinned workflow expression for the confirmed dual-flow policy.""" + if event_name == "workflow_dispatch": + actual = scan_mode_input or "standard" + elif event_name == "schedule": + actual = "standard" + elif event_name == "push" and ref in {"refs/heads/main", "refs/heads/master"}: + actual = "standard" + else: + actual = "quick" + + assert actual == expected + assert SCAN_MODE_EXPRESSION in _workflow() + if expected == "deep": + assert event_name == "workflow_dispatch" + assert scan_mode_input == "deep" + + +def test_repository_dispatch_cannot_inherit_standard_from_default_branch_ref() -> None: + """``repository_dispatch`` SHA is often main; mode must still stay quick.""" + workflow = _workflow() + + assert "github.event_name == 'push' && (github.ref == 'refs/heads/main'" in workflow + assert "client_payload.scan_mode" not in workflow + header = _strix_job_header(workflow) + assert "repository_dispatch" not in header or "STRIX_SCAN_MODE" in header + assert "|| 'quick' }}" in header + + +def test_required_pr_and_quick_paths_keep_standard_timeout_budget() -> None: + """Deep ceilings must not become the required pull_request_target job.""" + workflow = _workflow() + header = _strix_job_header(workflow) + step = _run_strix_step(workflow) + + assert JOB_TIMEOUT_EXPRESSION in header + assert STEP_TIMEOUT_EXPRESSION in step + assert "timeout-minutes: 360" not in header + assert "timeout-minutes: 340" not in step + assert "timeout-minutes: 360" not in step + assert 'process_budget_seconds="5400"' in step + assert 'total_budget_seconds="5700"' in step + assert ( + '[ "${STRIX_SCAN_MODE}" = "deep" ] && ' + '[ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]' + ) in step + assert 'process_budget_seconds="14400"' in step + assert 'total_budget_seconds="16200"' in step + + +def test_workflow_dispatch_is_scan_mode_only_and_keeps_dispatch_retry() -> None: + """Manual deep/standard is not a privileged same-head retry replacement.""" + trigger = _trigger_block(_workflow()) + + assert "workflow_dispatch:" in trigger + assert "scan_mode:" in trigger + assert "default: standard" in trigger + assert " - quick" in trigger + assert " - standard" in trigger + assert " - deep" in trigger + assert "types: [strix-scan]" in trigger + assert "\n release:" not in trigger + assert "v*-rc*" not in trigger + assert "target_repository:" not in trigger.split("workflow_dispatch:", 1)[1] + assert "pr_number:" not in trigger.split("workflow_dispatch:", 1)[1] + + +def test_required_pr_scoping_and_severity_gate_remain() -> None: + """Mode mapping must not weaken fail-closed PR evidence.""" + step = _run_strix_step(_workflow()) + + assert "STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM" in step + assert "__PR_SCOPE__" in step + assert "STRIX_DISABLE_PR_SCOPING:" in step + assert "github.event_name == 'pull_request_target'" in step + assert "github.event.client_payload.pr_number" in step + + +@pytest.mark.parametrize("scan_mode", ("quick", "standard", "deep")) +def test_require_safe_scan_mode_accepts_official_names(scan_mode: str) -> None: + """The gate allowlist must accept only official CLI mode names.""" + assert _require_safe_scan_mode_rc(scan_mode) == 0 + + +@pytest.mark.parametrize( + "scan_mode", + ("normal", "Quick", "STANDARD", "deep ", "quick;id", "fast", ""), +) +def test_require_safe_scan_mode_rejects_unofficial_names(scan_mode: str) -> None: + """Charset-valid aliases such as ``normal`` must still fail closed.""" + assert _require_safe_scan_mode_rc(scan_mode) == 2 + + +def test_require_safe_scan_mode_source_has_no_normal_alias() -> None: + """The production case list must not grow a ``normal`` arm.""" + function_source = _function_block( + STRIX_GATE.read_text(encoding="utf-8"), + "require_safe_scan_mode", + ) + + assert "quick | standard | deep)" in function_source + assert "normal)" not in function_source + + +def test_gate_allowlist_is_reached_before_scanner_invocation() -> None: + """An unofficial mode must fail closed before the fake scanner starts.""" + gate = STRIX_GATE.read_text(encoding="utf-8") + allowlist_index = gate.index("require_safe_scan_mode") + command_index = gate.index( + 'command = [resolved_strix_bin, "-n", "-t", ".", "--scan-mode", scan_mode]' + ) + assert allowlist_index < command_index From 4c03e5207d8eb55ede7b1892273ed3ea1263faeb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:35:22 +0000 Subject: [PATCH 2/3] test(strix): avoid newline grep needle for release trigger grep -F treats an embedded newline as a second pattern, so a leading newline made the release-trigger guard match every file. Co-authored-by: Seongho Bae --- scripts/ci/test_strix_quick_gate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 0fc83fd7c..bc374d1e4 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -215,7 +215,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" " - standard" "strix manual dispatch offers official standard mode" assert_file_contains "$workflow_file" " - deep" "strix manual dispatch offers official deep mode" assert_file_not_contains "$workflow_file" " - normal" "strix workflow must not offer a non-official normal scan-mode choice" - assert_file_not_contains "$workflow_file" $'\n release:' "strix workflow must not invent a GitHub release trigger" + assert_file_not_contains "$workflow_file" " release:" "strix workflow must not invent a GitHub release trigger" assert_file_not_contains "$workflow_file" "v*-rc*" "strix workflow must not invent RC-tag triggers" assert_file_not_contains "$workflow_file" "client_payload.scan_mode" "strix repository_dispatch cannot select scan mode" assert_file_contains "$workflow_file" "STRIX_SCAN_MODE: \${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.scan_mode || 'standard') || github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }}" "strix workflow maps dual-flow events to official scan modes" From 74079e5bddd69bf7eac6d3b2492f25d598517905 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:56:02 +0000 Subject: [PATCH 3/3] fix(strix): keep official modes without branch-selected dispatch Map pull_request_target, repository_dispatch, and develop pushes to quick, and protected main/master plus the weekly schedule to standard. Leave Deep unwired on this privileged workflow so a caller-selected revision cannot mint OIDC tokens or publish a fake strix status. Co-authored-by: Seongho Bae --- .github/workflows/strix.yml | 58 ++++---------- CHANGELOG.md | 2 +- docs/doctoring/strix-scan-mode-dual-flow.md | 35 +++++--- scripts/ci/strix_required_workflow_smoke.sh | 4 +- scripts/ci/test_strix_quick_gate.sh | 21 ++--- .../test_required_workflow_queue_contract.py | 19 ++--- tests/test_strix_scan_mode_policy.py | 79 +++++++------------ 7 files changed, 82 insertions(+), 136 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 552658f3c..13d6e3568 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -68,30 +68,13 @@ on: - cron: '0 3 * * 1' # Default-branch-only retry entrypoint; no caller-selected workflow ref. # Remains PR-metadata-bound same-head evidence (quick). It cannot scan a - # branch or tag release candidate. + # branch or tag release candidate. Do not restore workflow_dispatch here: + # GitHub's manual UI/API lets the caller select a branch, and that revision + # supplies the workflow YAML (id-token + statuses:write) before any in-job + # trusted-source checkout can run. Deep stays unwired on this privileged + # file until a separately reviewed default-branch-only dispatcher exists. repository_dispatch: types: [strix-scan] - # Manual official-mode selector for an incomplete RC. Deep is allowed only - # on this path. The sole input is scan_mode; callers cannot choose a - # target repository or substitute privileged retry metadata. Selecting a - # workflow revision still requires write access, matching GitHub's - # workflow_dispatch model. Privileged same-head retries stay on - # repository_dispatch above. - workflow_dispatch: - inputs: - scan_mode: - description: >- - Official Strix CLI mode (quick, standard, or deep). Deep is - manual-only and raises this job to the GitHub-hosted 360-minute - ceiling. CWL does not invent RC tags or GitHub release events; - use this input to scan an incomplete release candidate by hand. - required: true - type: choice - default: standard - options: - - quick - - standard - - deep concurrency: # Include the event name so default-branch repository_dispatch evidence cannot cancel @@ -120,12 +103,9 @@ jobs: strix: if: github.event_name != 'pull_request_target' || github.event.action != 'closed' # Quick and standard share the honest 120/100/90/95 budget (standard is - # 30–60 min). Deep is manual workflow_dispatch only and uses the - # GitHub-hosted 360-minute job ceiling, leaving ~20 minutes after the - # 340-minute step for artifact and status publication. Required - # pull_request_target / repository_dispatch evidence stays on the 120 - # budget; do not raise that path to 360. - timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '360' || '120') }} + # 30–60 min). Deep is not selected on this privileged workflow. Do not + # raise the required pull_request_target / repository_dispatch path to 360. + timeout-minutes: 120 runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan # exchanges an OIDC token (id-token) and publishes same-repo status evidence @@ -140,11 +120,11 @@ jobs: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # Official CLI modes only (quick|standard|deep). Dual-flow mapping: # required PR / repository_dispatch / push develop → quick; - # push main|master and weekly schedule → standard; - # workflow_dispatch → the scan_mode input (default standard). + # push main|master and weekly schedule → standard. # repository_dispatch must not inherit standard from github.ref - # (dispatch SHA is the default branch, often main). - STRIX_SCAN_MODE: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.scan_mode || 'standard') || github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }} + # (dispatch SHA is the default branch, often main). This privileged + # workflow never selects deep and never exposes workflow_dispatch. + STRIX_SCAN_MODE: ${{ github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }} steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -818,7 +798,7 @@ jobs: - name: Run Strix if: steps.gate.outputs.enabled == 'true' - timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '340' || '100') }} + timeout-minutes: 100 # Security invariant for pull_request_target: execute only from the # trusted base checkout. The gate copies PR-head blobs into an isolated # temporary scope with execute bits stripped, then scans that scope as @@ -869,16 +849,8 @@ jobs: IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && 'true' || 'false' }} run: | budget_suffix="TIME""OUT" - # Deep process/total budgets are workflow_dispatch-only. A required - # PR or repository_dispatch run must keep the 90/95-minute cap even - # if STRIX_SCAN_MODE were ever mis-set to deep. - if [ "${STRIX_SCAN_MODE}" = "deep" ] && [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then - process_budget_seconds="14400" - total_budget_seconds="16200" - else - process_budget_seconds="5400" - total_budget_seconds="5700" - fi + process_budget_seconds="5400" + total_budget_seconds="5700" printf 'Using official Strix scan mode %s with process budget %ss.\n' \ "${STRIX_SCAN_MODE}" "$process_budget_seconds" export "LLM_${budget_suffix}=900" diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c672cc2..c9934cf23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Semantic Versioning where the repository publishes a release. ### Changed -- Map the organization-required Strix workflow to official CLI modes (`quick`, `standard`, `deep`) from the GitHub event instead of inheriting Quick on every trigger. Required `pull_request_target` and `repository_dispatch` `strix-scan` evidence stay Quick; `push` to `develop` stays Quick; `push` to `main`/`master` and the weekly Monday 03:00 UTC schedule use Standard with the existing 120/100/90/95 budget. `workflow_dispatch` is restored with a `scan_mode` choice (default Standard) so an incomplete release candidate can be scanned by hand. Deep is manual-only and is the only path that raises the job to 360/340/4h/4.5h. CWL's dual GitHub Flow / Git Flow setup has no consistent RC-tag or prerelease convention, so this change does not invent `release:` or `v*-rc*` triggers. The gate now allowlists official mode names and rejects `normal`. +- Map the organization-required Strix workflow to official CLI modes (`quick`, `standard`) from the GitHub event instead of inheriting Quick on every trigger. Required `pull_request_target` and `repository_dispatch` `strix-scan` evidence stay Quick; `push` to `develop` stays Quick; `push` to `main`/`master` and the weekly Monday 03:00 UTC schedule use Standard with the existing 120/100/90/95 budget. Deep stays unwired on this privileged file: restoring `workflow_dispatch` would let a caller-selected branch supply `id-token` and `strix` status publication before any trusted-source checkout. CWL's dual GitHub Flow / Git Flow setup has no consistent RC-tag or prerelease convention, so this change does not invent `release:`, `v*-rc*`, or `client_payload.scan_mode`. The gate now allowlists official mode names (`quick|standard|deep`) and rejects `normal`. - Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap. - Run the bounded Quarantine Sandbox Runtime heartbeat at minute 14 without granting the caller model secrets, repository mutation permissions, approval, merge, release, artifact-execution, or final security-verdict authority. - Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services. diff --git a/docs/doctoring/strix-scan-mode-dual-flow.md b/docs/doctoring/strix-scan-mode-dual-flow.md index b83888668..5a56abbcf 100644 --- a/docs/doctoring/strix-scan-mode-dual-flow.md +++ b/docs/doctoring/strix-scan-mode-dual-flow.md @@ -1,12 +1,12 @@ # Strix official scan-mode mapping for dual GitHub / Git Flow -검토 기준일: **2026-08-16** +근거 기준일: **2026-08-16** ## Incident The organization-required Strix workflow always ran Quick. `scripts/ci/strix_quick_gate.sh` already forwarded `STRIX_SCAN_MODE` (default `quick`) as `strix -n -t . --scan-mode $SCAN_MODE`, but `.github/workflows/strix.yml` never set the environment variable. Every current event therefore inherited Quick: `pull_request_target`, `push` to `main`/`develop`/`master`, the Monday 03:00 UTC `schedule`, and `repository_dispatch` type `strix-scan`. -Official Strix CLI modes are only `quick`, `standard`, and `deep` (usestrix/strix `--scan-mode` choices). There is no `normal` alias. Quick is the CI/PR path (minutes). Standard is pre-release / weekly (30 min–1 h). Deep is pre-production (1–4 h). The CLI default is deep; CWL must keep choosing explicitly. +Official Strix CLI modes are only `quick`, `standard`, and `deep` (Strix, n.d.-a). There is no `normal` alias. Quick is the CI/PR path (minutes). Standard is pre-release / weekly (30 min–1 h). Deep is pre-production (1–4 h) and is the CLI default; CWL must keep choosing explicitly so required PR evidence never inherits Deep. The previous 120 / 100 / 90 / 95-minute budget is honest for Quick and Standard. It cannot finish Deep. GitHub-hosted jobs max out at 360 minutes. @@ -23,13 +23,14 @@ Confirmed event → official mode mapping: | `push` to `develop` | `quick` | 120 / 100 / 5400 / 5700 | | `push` to `main` or `master` | `standard` | 120 / 100 / 5400 / 5700 | | `schedule` (Monday 03:00 UTC) | `standard` | 120 / 100 / 5400 / 5700 | -| `workflow_dispatch` `scan_mode` | chosen (`quick` / `standard` / `deep`, default `standard`) | Deep only: 360 / 340 / 14400 / 16200 | `repository_dispatch` remains the default-branch-only, PR-metadata-bound same-head retry. It cannot scan a branch or tag release candidate. The mapping requires `github.event_name == 'push'` before treating `refs/heads/main` or `refs/heads/master` as Standard, because a `repository_dispatch` SHA is the default branch and is often `main`. -`workflow_dispatch` is restored with a single `scan_mode` choice so an incomplete release candidate can be scanned by hand. Deep is allowed only on that manual path. The required PR job stays on the 120-minute budget. Deep uses the GitHub-hosted 360-minute ceiling and leaves about 20 minutes after the 340-minute step for artifact and status publication. +Deep is **not** selected on this privileged workflow. Restoring `workflow_dispatch` on `.github/workflows/strix.yml` would let a writer choose a feature-branch workflow revision. That revision supplies the YAML—`id-token: write` plus `statuses: write` for the `strix` commit-status context—before any in-job trusted-source checkout can run (GitHub, n.d.-b; National Institute of Standards and Technology, 2022). A malicious or confused branch could skip the scan and publish a fake passing `strix` status. The same class of defect already failed `test_no_central_workflow_exposes_branch_selected_manual_dispatch` when `workflow_dispatch` was added to the quality job (see `docs/doctoring/strix-legal-git-paths.md`). -`require_safe_scan_mode` now allowlists `quick|standard|deep` and rejects `normal` and every other string, including charset-valid aliases. +Do **not** restore `workflow_dispatch` on this file as an RC convenience. Do **not** add `client_payload.scan_mode` to the privileged `strix-scan` retry. A later Deep path must be a separately reviewed default-branch-only dispatcher that treats target repository, pull-request number, and exact head SHA as untrusted bounded data. + +`require_safe_scan_mode` allowlists `quick|standard|deep` and rejects `normal` and every other string, including charset-valid aliases. The gate may still accept `deep` for local or future dispatcher use; this workflow never sets it. Fail-closed behavior is unchanged: missing artifact, unmapped findings, infrastructure errors, PR scoping, and severity gating stay as they were. The hashed-lock installer line is not part of this change. @@ -40,21 +41,31 @@ Fail-closed behavior is unchanged: missing artifact, unmapped findings, infrastr `tests/test_strix_scan_mode_policy.py` and `scripts/ci/test_strix_quick_gate.sh` fail if: 1. the event → mode expression is reverted or `repository_dispatch` can select a mode; -2. Deep job/step/process budgets apply to the required PR path; -3. a GitHub release event or RC-tag trigger is added; +2. Deep job/step/process budgets appear on the required PR path; +3. a GitHub release event, RC-tag trigger, or `workflow_dispatch` is added to this privileged file; 4. `require_safe_scan_mode` accepts `normal` or any unofficial name; -5. `workflow_dispatch` grows repository, pull-request, or privileged-retry inputs. +5. `client_payload.scan_mode` or `github.event.inputs.scan_mode` appears in `strix.yml`. The quality workflow trigger includes this record, the mapping test, and `.github/workflows/strix.yml` so later edits re-run exact-head evidence. ## Rollback -Roll back the mapping and this record together only if a required `pull_request_target` job is observed running Deep or a 360-minute budget. Do not restore unconditional Quick by deleting `STRIX_SCAN_MODE`. Do not invent RC tags to replace the manual `workflow_dispatch` path. +Roll back the mapping and this record together only if a required `pull_request_target` job is observed running Deep or a 360-minute budget. Do not restore unconditional Quick by deleting `STRIX_SCAN_MODE`. Do not restore `workflow_dispatch` on this file to recover Deep. + +## Next operator action + +Merge this mapping after current-head quality, security, and review evidence pass. If a buyer needs a pre-production Deep scan, open a separate default-branch-only dispatcher design; do not add inputs to `strix.yml`. ## References (APA 7th) -GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax +GitHub. (n.d.-a). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax + +GitHub. (n.d.-b). *Manually running a workflow*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow + +MITRE. (n.d.). *CWE-345: Insufficient verification of data authenticity*. Retrieved August 16, 2026, from https://cwe.mitre.org/data/definitions/345.html + +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 -GitHub. (n.d.). *Manually running a workflow*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow +Strix. (n.d.-a). *Scan modes*. Strix Docs. Retrieved August 16, 2026, from https://docs.strix.ai/usage/scan-modes -Strix. (n.d.). *Command-line interface* (`--scan-mode` `{quick,standard,deep}`). usestrix/strix. Retrieved August 16, 2026, from https://github.com/usestrix/strix +Strix. (n.d.-b). *Command-line interface*. usestrix/strix. Retrieved August 16, 2026, from https://github.com/usestrix/strix diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index f4b24bc32..6419a06b4 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -142,10 +142,10 @@ assert_file_contains "$workflow_file" 'bash "$TRUSTED_STRIX_GATE"' "Strix workfl assert_file_contains "$workflow_file" "Self-test Strix required workflow contract" "Strix workflow uses bounded required-path smoke test" assert_file_contains "$workflow_file" 'bash "$TRUSTED_STRIX_REQUIRED_SMOKE"' "Strix workflow executes bounded smoke test" assert_file_contains "$workflow_file" "timeout-minutes: 2" "Strix required-path smoke test has a short timeout" -assert_file_contains "$workflow_file" "workflow_dispatch:" "Strix workflow restores manual official-mode selection" +assert_file_not_contains "$workflow_file" "workflow_dispatch:" "Strix privileged workflow must not load a caller-selected workflow ref" assert_file_contains "$workflow_file" "STRIX_SCAN_MODE:" "Strix workflow sets official scan mode from the event" assert_file_contains "$workflow_file" "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')" "Strix standard push mapping requires the push event" -assert_file_contains "$workflow_file" "fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '360' || '120')" "Strix required path keeps the 120-minute job budget" +assert_file_contains "$workflow_file" "timeout-minutes: 120" "Strix required path keeps the 120-minute job budget" assert_file_contains "$gate_script" "quick | standard | deep)" "Strix gate allowlists official scan modes" assert_status_permissions_scoped assert_file_contains "$workflow_file" 'context="strix"' "Strix workflow publishes the strix commit status context" diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index bc374d1e4..79707997c 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -209,16 +209,12 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" 'TRUSTED_STRIX_GATE=$trusted_strix_source/scripts/ci/strix_quick_gate.sh' "strix workflow executes the central Strix gate script" assert_file_contains "$workflow_file" "Materialize target workspace" "strix workflow materializes target repository data separately from trusted scripts" assert_file_contains "$workflow_file" "types: [strix-scan]" "strix repository dispatch accepts only its dedicated default-branch event type" - assert_file_contains "$workflow_file" "workflow_dispatch:" "strix workflow restores manual official-mode selection for incomplete release candidates" - assert_file_contains "$workflow_file" "default: standard" "strix manual dispatch defaults to official standard mode" - assert_file_contains "$workflow_file" " - quick" "strix manual dispatch offers official quick mode" - assert_file_contains "$workflow_file" " - standard" "strix manual dispatch offers official standard mode" - assert_file_contains "$workflow_file" " - deep" "strix manual dispatch offers official deep mode" + assert_file_not_contains "$workflow_file" "workflow_dispatch:" "strix privileged workflow must not load a caller-selected workflow ref" assert_file_not_contains "$workflow_file" " - normal" "strix workflow must not offer a non-official normal scan-mode choice" assert_file_not_contains "$workflow_file" " release:" "strix workflow must not invent a GitHub release trigger" assert_file_not_contains "$workflow_file" "v*-rc*" "strix workflow must not invent RC-tag triggers" assert_file_not_contains "$workflow_file" "client_payload.scan_mode" "strix repository_dispatch cannot select scan mode" - assert_file_contains "$workflow_file" "STRIX_SCAN_MODE: \${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.scan_mode || 'standard') || github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }}" "strix workflow maps dual-flow events to official scan modes" + assert_file_contains "$workflow_file" "STRIX_SCAN_MODE: \${{ github.event_name == 'schedule' && 'standard' || github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') && 'standard' || 'quick' }}" "strix workflow maps dual-flow events to official scan modes" assert_file_contains "$workflow_file" "github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')" "strix standard mode on push requires the push event, not merely a main ref" assert_file_contains "$workflow_file" 'REPOSITORY: ${{ github.event.client_payload.target_repository }}' "strix repository dispatch binds the requested target repository before fetching data" assert_file_contains "$workflow_file" "Validate repository dispatch against live pull request metadata" "strix repository dispatch validates its supplied PR metadata" @@ -285,17 +281,16 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "GOOGLE_APPLICATION_CREDENTIALS" "strix workflow exports Vertex AI credentials only for Vertex provider mode" assert_file_contains "$workflow_file" "VERTEXAI_PROJECT" "strix workflow exports LiteLLM Vertex project env" assert_file_contains "$workflow_file" "VERTEXAI_LOCATION" "strix workflow exports LiteLLM Vertex location env" - assert_file_contains "$workflow_file" "timeout-minutes: \${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '360' || '120') }}" "strix workflow keeps the 120-minute job budget except for manual deep" - assert_file_contains "$workflow_file" "timeout-minutes: \${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.scan_mode == 'deep' && '340' || '100') }}" "strix workflow keeps the 100-minute step budget except for manual deep" - assert_file_not_contains "$workflow_file" "timeout-minutes: 360" "strix workflow must not apply the deep job ceiling unconditionally" - assert_file_not_contains "$workflow_file" "timeout-minutes: 340" "strix workflow must not apply the deep step ceiling unconditionally" + assert_file_contains "$workflow_file" "timeout-minutes: 120" "strix workflow keeps the 120-minute job budget for quick and standard" + assert_file_contains "$workflow_file" "timeout-minutes: 100" "strix workflow keeps the 100-minute step budget for quick and standard" + assert_file_not_contains "$workflow_file" "timeout-minutes: 360" "strix workflow must not apply the deep job ceiling" + assert_file_not_contains "$workflow_file" "timeout-minutes: 340" "strix workflow must not apply the deep step ceiling" assert_file_contains "$workflow_file" 'budget_suffix="TIME""OUT"' "strix workflow builds budget env keys without visible timeout signal text" assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=$total_budget_seconds"' "strix workflow exports the selected total budget through the split timeout key" assert_file_contains "$workflow_file" 'process_budget_seconds="5400"' "strix workflow gives a legitimate quick or standard scan up to 90 minutes" assert_file_contains "$workflow_file" 'total_budget_seconds="5700"' "strix workflow preserves a 95-minute bounded total budget for quick and standard" - assert_file_contains "$workflow_file" 'process_budget_seconds="14400"' "strix workflow gives a manual deep scan a 4-hour process budget" - assert_file_contains "$workflow_file" 'total_budget_seconds="16200"' "strix workflow gives a manual deep scan a 4.5-hour total budget" - assert_file_contains "$workflow_file" '[ "${STRIX_SCAN_MODE}" = "deep" ] && [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]' "strix workflow raises process budgets only for manual deep" + assert_file_not_contains "$workflow_file" 'process_budget_seconds="14400"' "strix privileged workflow must not raise a 4-hour deep process budget" + assert_file_not_contains "$workflow_file" 'total_budget_seconds="16200"' "strix privileged workflow must not raise a 4.5-hour deep total budget" assert_file_contains "$GATE_SCRIPT" "quick | standard | deep)" "strix gate allowlists official scan modes only" assert_file_contains "$GATE_SCRIPT" "STRIX_SCAN_MODE must be one of quick, standard, or deep" "strix gate rejects unofficial scan modes" assert_file_not_contains "$GATE_SCRIPT" "normal)" "strix gate must not accept a non-official normal scan-mode alias" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 7d1b19505..4f5cbb386 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -112,14 +112,11 @@ def test_privileged_review_retries_use_default_branch_repository_dispatch() -> N assert "repository_dispatch:" in trigger_contract assert f"types: [{event_type}]" in trigger_contract + assert "workflow_dispatch:" not in trigger_contract + assert "github.event.inputs" not in workflow + assert "github.event.client_payload" in workflow if filename == "strix.yml": - assert "workflow_dispatch:" in trigger_contract - assert "github.event.inputs.scan_mode" in workflow assert "client_payload.scan_mode" not in workflow - else: - assert "workflow_dispatch:" not in trigger_contract - assert "github.event.inputs" not in workflow - assert "github.event.client_payload" in workflow scheduler = ( REPO_ROOT / "scripts" / "ci" / "pr_review_merge_scheduler.py" @@ -142,20 +139,14 @@ def test_privileged_review_retries_use_default_branch_repository_dispatch() -> N def test_no_central_workflow_exposes_branch_selected_manual_dispatch() -> None: - """Privileged central entrypoints must not load caller-selected workflow code. - - Strix is the documented exception: ``workflow_dispatch`` exists only so an - operator can choose official ``quick|standard|deep`` for an incomplete - release candidate. CWL has no RC-tag convention. The privileged same-head - retry remains default-branch ``repository_dispatch`` type ``strix-scan``. - """ + """Privileged central entrypoints must not load caller-selected workflow code.""" workflow_files = sorted((REPO_ROOT / ".github" / "workflows").glob("*.yml")) offenders = [ path.name for path in workflow_files if "workflow_dispatch:" in path.read_text(encoding="utf-8") ] - assert offenders == ["strix.yml"] + assert offenders == [] def test_required_pull_request_workflows_cancel_superseded_runs() -> None: diff --git a/tests/test_strix_scan_mode_policy.py b/tests/test_strix_scan_mode_policy.py index ece9d5a37..f6776ff8c 100644 --- a/tests/test_strix_scan_mode_policy.py +++ b/tests/test_strix_scan_mode_policy.py @@ -3,8 +3,9 @@ CWL runs both GitHub Flow (main/master) and Git Flow (develop) without a consistent RC-tag or prerelease convention. Official Strix CLI modes are only ``quick``, ``standard``, and ``deep``. This module pins the dual-flow mapping -and the Deep-only timeout raise so a revert or a 360-minute required-PR job -fails closed in CI. +and keeps Deep unwired on this privileged file so a branch-selected +``workflow_dispatch`` cannot mint OIDC tokens or publish a fake ``strix`` +status. """ from __future__ import annotations @@ -22,20 +23,10 @@ QUALITY_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "strix-changed-path-quality-ci.yml" SCAN_MODE_EXPRESSION = ( - "STRIX_SCAN_MODE: ${{ github.event_name == 'workflow_dispatch' && " - "(github.event.inputs.scan_mode || 'standard') || " - "github.event_name == 'schedule' && 'standard' || " + "STRIX_SCAN_MODE: ${{ github.event_name == 'schedule' && 'standard' || " "github.event_name == 'push' && (github.ref == 'refs/heads/main' || " "github.ref == 'refs/heads/master') && 'standard' || 'quick' }}" ) -JOB_TIMEOUT_EXPRESSION = ( - "timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && " - "github.event.inputs.scan_mode == 'deep' && '360' || '120') }}" -) -STEP_TIMEOUT_EXPRESSION = ( - "timeout-minutes: ${{ fromJSON(github.event_name == 'workflow_dispatch' && " - "github.event.inputs.scan_mode == 'deep' && '340' || '100') }}" -) def _workflow() -> str: @@ -109,41 +100,37 @@ def test_quality_trigger_includes_scan_mode_contract_paths() -> None: def test_workflow_pins_official_scan_mode_expression() -> None: - """The job must set STRIX_SCAN_MODE from event, ref, and manual input.""" + """The job must set STRIX_SCAN_MODE from event and protected-branch ref.""" workflow = _workflow() assert SCAN_MODE_EXPRESSION in workflow assert "client_payload.scan_mode" not in workflow + assert "github.event.inputs.scan_mode" not in workflow assert " - normal" not in workflow assert "|| 'normal'" not in workflow + assert "|| 'deep'" not in workflow + assert "&& 'deep'" not in workflow @pytest.mark.parametrize( - ("event_name", "ref", "scan_mode_input", "expected"), + ("event_name", "ref", "expected"), ( - ("pull_request_target", "refs/heads/main", "", "quick"), - ("repository_dispatch", "refs/heads/main", "", "quick"), - ("push", "refs/heads/develop", "", "quick"), - ("push", "refs/heads/main", "", "standard"), - ("push", "refs/heads/master", "", "standard"), - ("schedule", "refs/heads/develop", "", "standard"), - ("schedule", "refs/heads/main", "", "standard"), - ("workflow_dispatch", "refs/heads/release-candidate", "", "standard"), - ("workflow_dispatch", "refs/heads/main", "quick", "quick"), - ("workflow_dispatch", "refs/heads/main", "standard", "standard"), - ("workflow_dispatch", "refs/heads/main", "deep", "deep"), + ("pull_request_target", "refs/heads/main", "quick"), + ("repository_dispatch", "refs/heads/main", "quick"), + ("push", "refs/heads/develop", "quick"), + ("push", "refs/heads/main", "standard"), + ("push", "refs/heads/master", "standard"), + ("schedule", "refs/heads/develop", "standard"), + ("schedule", "refs/heads/main", "standard"), ), ) def test_dual_flow_event_maps_to_official_mode( event_name: str, ref: str, - scan_mode_input: str, expected: str, ) -> None: """Mirror the pinned workflow expression for the confirmed dual-flow policy.""" - if event_name == "workflow_dispatch": - actual = scan_mode_input or "standard" - elif event_name == "schedule": + if event_name == "schedule": actual = "standard" elif event_name == "push" and ref in {"refs/heads/main", "refs/heads/master"}: actual = "standard" @@ -152,9 +139,7 @@ def test_dual_flow_event_maps_to_official_mode( assert actual == expected assert SCAN_MODE_EXPRESSION in _workflow() - if expected == "deep": - assert event_name == "workflow_dispatch" - assert scan_mode_input == "deep" + assert expected in {"quick", "standard"} def test_repository_dispatch_cannot_inherit_standard_from_default_branch_ref() -> None: @@ -174,36 +159,28 @@ def test_required_pr_and_quick_paths_keep_standard_timeout_budget() -> None: header = _strix_job_header(workflow) step = _run_strix_step(workflow) - assert JOB_TIMEOUT_EXPRESSION in header - assert STEP_TIMEOUT_EXPRESSION in step + assert "timeout-minutes: 120" in header + assert "timeout-minutes: 100" in step assert "timeout-minutes: 360" not in header assert "timeout-minutes: 340" not in step assert "timeout-minutes: 360" not in step assert 'process_budget_seconds="5400"' in step assert 'total_budget_seconds="5700"' in step - assert ( - '[ "${STRIX_SCAN_MODE}" = "deep" ] && ' - '[ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]' - ) in step - assert 'process_budget_seconds="14400"' in step - assert 'total_budget_seconds="16200"' in step + assert 'process_budget_seconds="14400"' not in step + assert 'total_budget_seconds="16200"' not in step + assert "workflow_dispatch" not in step -def test_workflow_dispatch_is_scan_mode_only_and_keeps_dispatch_retry() -> None: - """Manual deep/standard is not a privileged same-head retry replacement.""" +def test_privileged_strix_workflow_has_no_branch_selected_manual_dispatch() -> None: + """Manual Deep must not load this privileged YAML from a caller-selected ref.""" trigger = _trigger_block(_workflow()) - assert "workflow_dispatch:" in trigger - assert "scan_mode:" in trigger - assert "default: standard" in trigger - assert " - quick" in trigger - assert " - standard" in trigger - assert " - deep" in trigger + assert "workflow_dispatch:" not in trigger + assert "github.event.inputs" not in trigger assert "types: [strix-scan]" in trigger assert "\n release:" not in trigger assert "v*-rc*" not in trigger - assert "target_repository:" not in trigger.split("workflow_dispatch:", 1)[1] - assert "pr_number:" not in trigger.split("workflow_dispatch:", 1)[1] + assert "scan_mode:" not in trigger def test_required_pr_scoping_and_severity_gate_remain() -> None: