Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/strix-changed-path-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
66 changes: 56 additions & 10 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restoring workflow_dispatch on this privileged file re-opens GitHub's Branch dropdown / gh workflow run --ref path (GitHub, n.d., Manually running a workflow). After this YAML is on the default branch, the selected revision supplies the workflow definition before the trusted-source checkout, with scanner secrets and id-token: write.

A job-level if: on main does not stop a malicious selected revision. Write access is the only control. Either record that residual in docs/doctoring/strix-scan-mode-dual-flow.md, or move Deep off this pull_request_target file. Do not add target_repository / pr_number inputs to “fix” it.

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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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 Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services.
- Run the bounded DiskSage repair heartbeat at minute 37 of every hour, dispatch no more than one exact-head repair, and wait two hours before redispatching an unchanged head so legitimate OpenCode or NVIDIA NIM latency does not create duplicate writers.
Expand All @@ -36,6 +37,8 @@ Semantic Versioning where the repository publishes a release.
- Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109).
- Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109).
- Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories.
- Bound the Strix dual-flow mapping cases to the pinned GitHub expression and recorded that restored `workflow_dispatch` still lets a repository writer choose the workflow revision (`gh workflow run --ref`); write access is the only control, and `target_repository` / `pr_number` inputs stay forbidden.
- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context.
- Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367).
- Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics.
- Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed.
Expand Down
66 changes: 66 additions & 0 deletions docs/doctoring/strix-scan-mode-dual-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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.

## Residual: workflow_dispatch revision selection

GitHub's manual-run UI and `gh workflow run --ref` let a repository writer choose which revision supplies the workflow definition *before* the trusted-source checkout (GitHub, n.d., *Manually running a workflow*). After this YAML is on the default branch, that selected revision runs with scanner secrets and `id-token: write`. A job-level `if:` on `main` does not stop a malicious selected revision. Write access is the only control.

This residual is accepted because CWL has no RC-tag convention and Deep must remain available by hand on this file. Do not add `target_repository` or `pr_number` inputs to "fix" it; those would widen the privileged surface. Privileged same-head retries stay on default-branch `repository_dispatch` type `strix-scan`.

`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
12 changes: 9 additions & 3 deletions scripts/ci/strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
5 changes: 5 additions & 0 deletions scripts/ci/strix_required_workflow_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading