Skip to content

Strix Security Scan ContextualWisdomLab/fast-mlsirm#1779@d336fd398cbe35bc68976353f7a124b71765a653 #7626

Strix Security Scan ContextualWisdomLab/fast-mlsirm#1779@d336fd398cbe35bc68976353f7a124b71765a653

Strix Security Scan ContextualWisdomLab/fast-mlsirm#1779@d336fd398cbe35bc68976353f7a124b71765a653 #7626

name: Strix Security Scan
run-name: >-
Strix Security Scan ${{ github.event.client_payload.target_repository ||
github.event.pull_request.base.repo.full_name || github.repository }}#${{
github.event.client_payload.pr_number || github.event.pull_request.number || 'event' }}@${{
github.event.client_payload.pr_head_sha || github.event.pull_request.head.sha || github.sha }}
on:
push:
branches: [main, develop, master]
# Skip scans for changes that touch ONLY non-executable documentation and
# image assets. A change whose entire diff is these paths has no source,
# build, config, or workflow logic for a code security scanner to analyze,
# so skipping it loses no coverage while freeing shared runner capacity.
# Conservative by design: only file EXTENSIONS/paths that can never contain
# executable logic are listed (no source, no *.txt, no *.svg, no CODEOWNERS,
# no build scripts). A diff touching even one non-listed file still scans.
# The weekly full-tree schedule below re-scans protected branches with no
# path filter, backstopping every path.
# This filter is only evaluated for natively-triggered runs. Repositories
# covered by org ruleset 18156473 have every 'on:' filter ignored; the
# job-level gate below is what skips them.
paths-ignore:
- '**/*.md'
- '**/*.markdown'
- '**/*.rst'
- '**/*.png'
- '**/*.jpg'
- '**/*.jpeg'
- '**/*.gif'
- '**/*.webp'
- '**/*.bmp'
- '**/*.ico'
- 'LICENSE'
- 'LICENSE.*'
- 'COPYING'
- '.github/ISSUE_TEMPLATE/**'
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
# Same conservative doc/image-only skip for PR scans. GitHub evaluates these
# path filters only for natively-triggered runs -- i.e. in the three
# repositories ruleset 18156473 excludes (.github, noema,
# IRT-bibliography-set). In every other repository the ruleset ignores
# them, so the same doc/image-only decision is enforced by the
# changed-scope job below. The run-name
# includes the PR number and head SHA for status grouping, while the
# concurrency group is scoped per repository and event class to prevent
# shared-provider key rate-limit storms. Strix runs intentionally do not
# cancel in progress because a pre-job cancellation leaves no scanner log to
# review. GitHub keeps one active and one pending run per group; the merge
# scheduler re-dispatches exact-head evidence when a pending run is
# superseded. For PRs the merge scheduler manages, same-head Strix evidence
# is still forced at merge time via repository_dispatch (which paths-ignore
# does not affect), so merged code never loses evidence.
paths-ignore:
- '**/*.md'
- '**/*.markdown'
- '**/*.rst'
- '**/*.png'
- '**/*.jpg'
- '**/*.jpeg'
- '**/*.gif'
- '**/*.webp'
- '**/*.bmp'
- '**/*.ico'
- 'LICENSE'
- 'LICENSE.*'
- 'COPYING'
- '.github/ISSUE_TEMPLATE/**'
schedule:
# Weekly scan on protected branches (Mondays at 03:00 UTC).
- cron: '0 3 * * 1'
# Default-branch-only retry entrypoint; no caller-selected workflow ref.
repository_dispatch:
types: [strix-scan]
concurrency:
# Workflow-level admission is required: job-level groups are never evaluated
# while the whole run is queued behind the organization job ceiling.
group: >-
strix-security-scan-${{
github.event.pull_request.base.repo.full_name ||
github.event.client_payload.target_repository || github.repository }}-${{
github.event.pull_request.number ||
github.event.client_payload.pr_number || github.run_id }}
cancel-in-progress: true
# Scorecard Token-Permissions (alert #43): keep the workflow-level token
# read-only and scope same-repo status publication to the Strix scan job.
permissions:
actions: read
contents: read
models: read
jobs:
changed-scope:
name: Detect changed scope
# The org ruleset IGNORES every `on:` filter (paths, branches, types) when it
# runs this workflow in another repository, and a trigger-level skip would
# leave `.github`'s classic required contexts Pending forever. Both
# mechanisms honour a JOB-level skip, so the doc/image-only decision is made
# here and consumed through `needs`. See
# docs/doctoring/required-workflow-path-filter-boundary.md.
# Fails OPEN: an unreadable, empty, or truncated file list scans everything.
if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft')
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.scope.outputs.code }}
deps: ${{ steps.scope.outputs.deps }}
steps:
- name: Classify changed paths
id: scope
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR: ${{ github.event.pull_request.number }}
EXPECTED_FILES: ${{ github.event.pull_request.changed_files }}
shell: bash
run: |
set -uo pipefail
code=true
deps=true
if [ -n "${PR}" ] && [ -n "${EXPECTED_FILES}" ]; then
changed=""
for attempt in 1 2 3; do
if changed="$(gh api --paginate "repos/${REPO}/pulls/${PR}/files?per_page=100" --jq '.[].filename')" && [ -n "$changed" ]; then
break
fi
changed=""
sleep $((attempt * 3))
done
# GitHub caps /pulls/N/files at 3000 entries; a short list would hide
# source files behind a doc-only verdict, so require an exact count.
if [ -n "$changed" ] && [ "$(printf '%s\n' "$changed" | wc -l | tr -d ' ')" = "${EXPECTED_FILES}" ]; then
code=false
deps=false
while IFS= read -r changed_path; do
case "$changed_path" in
*.md|*.markdown|*.rst|*.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.ico|LICENSE|LICENSE.txt|COPYING|COPYING.txt|NOTICE|NOTICE.txt|.github/ISSUE_TEMPLATE/*) ;;
*) code=true ;;
esac
case "$changed_path" in
requirements*.txt|*/requirements*.txt|pyproject.toml|*/pyproject.toml|uv.lock|*/uv.lock|pylock.*.toml|*/pylock.*.toml|package.json|*/package.json|package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|yarn.lock|*/yarn.lock|Cargo.toml|*/Cargo.toml|Cargo.lock|*/Cargo.lock|go.mod|*/go.mod|go.sum|*/go.sum|pom.xml|*/pom.xml|build.gradle|*/build.gradle|build.gradle.kts|*/build.gradle.kts|DESCRIPTION|*/DESCRIPTION) deps=true ;;
esac
done <<<"$changed"
else
echo "::notice::changed-scope could not read a complete PR file list; scanning everything."
fi
fi
echo "code=${code}" >> "$GITHUB_OUTPUT"
echo "deps=${deps}" >> "$GITHUB_OUTPUT"
echo "changed-scope code=${code} deps=${deps}"
admit-current-head:
name: Admit current pull request head
if: >-
github.event_name != 'pull_request_target' ||
(github.event.action != 'closed' && github.event.action != 'converted_to_draft')
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
admitted: ${{ steps.admission.outputs.admitted }}
target_repository: ${{ steps.admission.outputs.target_repository }}
pr_number: ${{ steps.admission.outputs.pr_number }}
steps:
- name: Verify event metadata against the live pull request
id: admission
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
EVENT_NAME: ${{ github.event_name }}
TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}
TARGET_PR_NUMBER: ${{ github.event.client_payload.pr_number || github.event.pull_request.number }}
EXPECTED_BASE_REF: ${{ github.event.client_payload.pr_base_ref || github.event.pull_request.base.ref }}
EXPECTED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha || github.event.pull_request.base.sha }}
EXPECTED_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.event.client_payload.target_repository }}
EXPECTED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || github.event.pull_request.head.sha }}
shell: bash
run: |
set -euo pipefail
printf 'admitted=false\n' >> "$GITHUB_OUTPUT"
if [ "$EVENT_NAME" != "pull_request_target" ] && [ "$EVENT_NAME" != "repository_dispatch" ]; then
{
echo "admitted=true"
echo "target_repository=${TARGET_REPOSITORY}"
echo "pr_number=${GITHUB_RUN_ID}"
} >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ ! "$TARGET_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
[[ ! "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
[[ ! "$EXPECTED_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] ||
[[ ! "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Strix event metadata is incomplete or malformed."
exit 1
fi
pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}")"
live_tuple="$(jq -r '[.state // "", .base.repo.full_name // "", .base.ref // "", .base.sha // "", .head.repo.full_name // "", .head.sha // ""] | @tsv' <<<"$pull_request_json")"
expected_tuple="$(printf 'open\t%s\t%s\t%s\t%s\t%s' "$TARGET_REPOSITORY" "$EXPECTED_BASE_REF" "$EXPECTED_BASE_SHA" "$EXPECTED_HEAD_REPOSITORY" "$EXPECTED_HEAD_SHA")"
if [ "$live_tuple" != "$expected_tuple" ]; then
echo "::notice::Strix event does not match the live pull request head; skipping stale evidence."
exit 0
fi
{
echo "admitted=true"
echo "target_repository=${TARGET_REPOSITORY}"
echo "pr_number=${TARGET_PR_NUMBER}"
} >> "$GITHUB_OUTPUT"
cancel-superseded-pr-runs:
if: >-
github.event_name == 'pull_request_target' &&
(github.event.action == 'synchronize' || github.event.action == 'converted_to_draft' || github.event.action == 'closed')
# Idempotent per PR: a fresh sweep re-verifies live state (live_target_matches
# below) before selecting or cancelling anything, so it fully subsumes
# whatever an older, not-yet-run instance would have done. cancel-in-progress
# true is the right shape here (the merge scheduler's integrated exact-head
# coalescer instead uses its own admission-order queueing, since each instance
# carries a DIFFERENT specific expected-head only it can act on): it caps
# this job to one running + one queued per PR instead of letting a push
# burst pile up N independent, mutually-non-deduped sweeps that each cost a
# full admission slot under the shared 60-job ceiling. Matches
# codeql-pr.yml's established group-key style (PR-number scoped).
concurrency:
group: >-
cancel-superseded-pr-runs-${{
github.event.pull_request.base.repo.full_name || github.repository }}-${{
github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
runs-on: ubuntu-24.04
# Bound this gh-api-only cleanup job so a stuck call (rate limit, hung
# `gh api --paginate`) cannot silently occupy a runner for GitHub's
# 360-minute platform default -- exactly the window when a busy PR is
# producing the superseded runs this job exists to retire. Matches
# the merge scheduler's bounded run-cleanup shape (gh-api-only, no provider
# inference).
timeout-minutes: 10
# Prefer the established scheduler credential, but let the close event use
# its job-scoped token so abandoned scans are cancelled even when that
# optional secret is unavailable. This job never checks out PR code.
permissions:
actions: write
contents: read
pull-requests: read
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
TARGET_PR_NUMBER: ${{ github.event.pull_request.number }}
TARGET_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_ACTION: ${{ github.event.action }}
CURRENT_RUN_ID: ${{ github.run_id }}
steps:
- name: Cancel queued and running scans for superseded or inactive pull requests
shell: bash
run: |
set -euo pipefail
live_target_matches() {
local live_pr_json live_state live_draft live_head
if ! live_pr_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}" 2>/tmp/strix-cleanup-gh-error)"; then
echo "::warning::Strix cleanup could not verify the live pull request; leaving runs unchanged."
sed 's/^/ /' /tmp/strix-cleanup-gh-error >&2 || true
return 1
fi
live_state="$(jq -r '.state // ""' <<<"$live_pr_json")"
live_draft="$(jq -r '.draft // false' <<<"$live_pr_json")"
live_head="$(jq -r '.head.sha // ""' <<<"$live_pr_json")"
[ "$live_head" = "$TARGET_PR_HEAD_SHA" ] && {
{ [ "$PR_ACTION" = "closed" ] && [ "$live_state" = "closed" ]; } ||
{ [ "$PR_ACTION" = "converted_to_draft" ] && [ "$live_state" = "open" ] && [ "$live_draft" = "true" ]; } ||
{ [ "$PR_ACTION" = "synchronize" ] && [ "$live_state" = "open" ]; }
}
}
cancel_runs() {
local status="$1"
if ! live_target_matches; then
echo "::notice::Strix cleanup target changed before run selection; leaving runs unchanged."
return 0
fi
local runs_url="repos/${TARGET_REPOSITORY}/actions/runs?status=${status}&per_page=100"
local runs_json
if ! runs_json="$(gh api --paginate "$runs_url" 2>/tmp/strix-cleanup-gh-error)"; then
echo "::warning::Strix cleanup could not inspect ${TARGET_REPOSITORY}; leaving runs unchanged."
sed 's/^/ /' /tmp/strix-cleanup-gh-error >&2 || true
return 0
fi
local run_ids
if ! run_ids="$(jq -r --arg pr "$TARGET_PR_NUMBER" --arg head_sha "$TARGET_PR_HEAD_SHA" \
--arg action "$PR_ACTION" --arg repo "$TARGET_REPOSITORY" --arg current "$CURRENT_RUN_ID" '
.workflow_runs[]
| select((.id | tostring) != $current)
| select(.name == "Strix Security Scan")
| select(.event == "pull_request_target")
| ((.display_title // "") | startswith("Strix Security Scan " + $repo + "#" + $pr + "@")) as $title_matches
| ((.pull_requests // []) | any((.number | tostring) == $pr)) as $metadata_matches
| select($title_matches or $metadata_matches)
| ((.display_title // "") | endswith("@" + $head_sha)) as $title_is_current
| ((.pull_requests // []) | any(
((.number | tostring) == $pr)
and ((.head.sha // "") | ascii_downcase) == ($head_sha | ascii_downcase)
)) as $metadata_is_current
| ((.pull_requests // []) | any(
((.number | tostring) == $pr) and ((.head.sha // "") != "")
)) as $metadata_has_head
| select(
$action == "closed"
or $action == "converted_to_draft"
or (($title_matches or $metadata_has_head) and (($title_is_current or $metadata_is_current) | not))
)
| .id
' <<<"$runs_json")"; then
echo "::warning::Strix cleanup received invalid run data for ${TARGET_REPOSITORY}; leaving runs unchanged."
return 0
fi
while IFS= read -r run_id; do
[ -n "$run_id" ] || continue
if ! live_target_matches; then
echo "::notice::Strix cleanup target changed before cancellation; leaving runs unchanged."
return 0
fi
if gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null 2>/tmp/strix-cleanup-cancel-error ||
gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/force-cancel" >/dev/null 2>>/tmp/strix-cleanup-cancel-error; then
echo "Cancelled obsolete Strix run ${run_id} in ${TARGET_REPOSITORY} for PR #${TARGET_PR_NUMBER}."
else
echo "::warning::Strix cleanup could not cancel run ${run_id} in ${TARGET_REPOSITORY}; it may have finished or the credential lacks Actions write access."
sed 's/^/ /' /tmp/strix-cleanup-cancel-error >&2 || true
fi
done <<<"$run_ids"
}
for active_status in queued in_progress requested waiting pending; do
cancel_runs "$active_status"
done
strix:
needs: [changed-scope, admit-current-head]
if: needs.changed-scope.outputs.code == 'true' && needs.admit-current-head.outputs.admitted == 'true'
# Large, actively-growing repositories (e.g. contextual-orchestrator) can
# legitimately require well over two hours to scan -- this org's own
# standing operating directive accepts that central OpenCode/Strix/Noema
# scans may take more than two hours per model (docs/product-goal-directive.md).
# Inference has no wall-clock deadline; cancellation is reserved for an
# explicit operator action or a superseded head.
runs-on: ubuntu-24.04
# Least-privilege token scoped to this job (Scorecard alert #43): the scan
# exchanges an OIDC token (id-token) and publishes same-repo status evidence
# from the scan job only.
permissions:
actions: read
contents: read
id-token: write
models: read
statuses: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: Harden runner
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
with:
egress-policy: audit
disable-file-monitoring: true
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Resolve trusted Strix source ref
id: trusted_source
env:
JOB_CONTEXT_JSON: ${{ toJSON(job) }}
GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}
run: |
set -euo pipefail
python3 <<'PY' >>"$GITHUB_OUTPUT"
import json
import os
import re
import sys
try:
job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}")
github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}")
except json.JSONDecodeError as exc:
print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr)
raise SystemExit(1)
trusted_repository = str(
job_context.get("workflow_repository") or "ContextualWisdomLab/.github"
).strip()
trusted_ref = str(
job_context.get("workflow_sha") or github_context.get("workflow_sha") or ""
).strip()
workflow_ref = str(
job_context.get("workflow_ref") or github_context.get("workflow_ref") or ""
).strip()
if not trusted_ref:
trusted_ref = "main"
prefix = "ContextualWisdomLab/.github/.github/workflows/strix.yml@"
if workflow_ref.startswith(prefix):
trusted_ref = workflow_ref.split("@", 1)[1]
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", trusted_repository):
print("::error::Trusted workflow repository resolved to an invalid name.", file=sys.stderr)
raise SystemExit(1)
if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref):
print("::error::Trusted workflow ref resolved to an invalid value.", file=sys.stderr)
raise SystemExit(1)
print(f"repository={trusted_repository}")
print(f"ref={trusted_ref}")
PY
- name: Checkout trusted Strix source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ steps.trusted_source.outputs.repository }}
fetch-depth: 1
persist-credentials: false
ref: ${{ steps.trusted_source.outputs.ref }}
path: trusted-strix-source
- name: Export trusted Strix source paths
run: |
set -euo pipefail
trusted_strix_source="$GITHUB_WORKSPACE/trusted-strix-source"
test -f "$trusted_strix_source/scripts/ci/strix_quick_gate.sh"
test -f "$trusted_strix_source/scripts/ci/test_strix_quick_gate.sh"
test -f "$trusted_strix_source/scripts/ci/strix_required_workflow_smoke.sh"
{
echo "TRUSTED_STRIX_SOURCE=$trusted_strix_source"
echo "TRUSTED_STRIX_GATE=$trusted_strix_source/scripts/ci/strix_quick_gate.sh"
echo "TRUSTED_STRIX_GATE_TEST=$trusted_strix_source/scripts/ci/test_strix_quick_gate.sh"
echo "TRUSTED_STRIX_REQUIRED_SMOKE=$trusted_strix_source/scripts/ci/strix_required_workflow_smoke.sh"
} >> "$GITHUB_ENV"
- name: Exchange OpenCode app token for target repository reads
id: target_app_token
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
run: |
set -euo pipefail
mark_unavailable() {
echo "available=false" >>"$GITHUB_OUTPUT"
}
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "OpenCode app token exchange unavailable: OIDC request environment is missing."
mark_unavailable
exit 0
fi
request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}"
separator="&"
case "$request_url" in
*\?*) ;;
*) separator="?" ;;
esac
if ! oidc_response="$(
curl -fsS \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${request_url}${separator}audience=${OIDC_AUDIENCE}"
)"; then
echo "OpenCode app token exchange unavailable: OIDC token request did not complete."
mark_unavailable
exit 0
fi
oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")"
if [ -z "$oidc_token" ]; then
echo "OpenCode app token exchange unavailable: OIDC token response was empty."
mark_unavailable
exit 0
fi
if ! token_response="$(
curl -fsS \
-X POST \
-H "Authorization: Bearer ${oidc_token}" \
"${OPENCODE_API_BASE_URL}/exchange_github_app_token"
)"; then
echo "OpenCode app token exchange unavailable: app token request did not complete."
mark_unavailable
exit 0
fi
app_token="$(jq -r '.token // empty' <<<"$token_response")"
if [ -z "$app_token" ]; then
echo "OpenCode app token exchange unavailable: app token response was empty."
mark_unavailable
exit 0
fi
echo "::add-mask::$app_token"
{
echo "available=true"
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"
- name: Resolve target repository visibility
id: target_visibility
env:
GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.event.pull_request.base.repo.full_name || github.repository }}
EVENT_REPOSITORY_VISIBILITY: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.visibility || github.event_name != 'repository_dispatch' && github.event.repository.visibility || '' }}
run: |
set -euo pipefail
if [[ ! "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]]; then
echo "::error::Strix target repository must belong to ContextualWisdomLab."
exit 1
fi
case "$EVENT_REPOSITORY_VISIBILITY" in
PUBLIC | public) is_private=false ;;
PRIVATE | private | INTERNAL | internal) is_private=true ;;
"")
is_private=""
for target_visibility_attempt in 1 2 3 4 5 6; do
# The single-quoted jq program intentionally expands jq's
# `$visibility`, not a shell variable (ShellCheck SC2016).
# shellcheck disable=SC2016
if is_private="$(
gh api "repos/${TARGET_REPOSITORY}" --jq '
(.visibility // "" | ascii_downcase) as $visibility
| if $visibility == "public" then "false"
elif $visibility == "private" or $visibility == "internal" then "true"
else empty
end
'
)"; then
break
fi
is_private=""
if [ "$target_visibility_attempt" -lt 6 ]; then
echo "Repository visibility lookup failed (attempt ${target_visibility_attempt}/6), possibly a transient GitHub API rate limit; retrying after backoff." >&2
sleep "$(( target_visibility_attempt * 5 ))"
fi
done
;;
*)
echo "::error::Target repository event visibility was not public, private, or internal."
exit 1
;;
esac
case "$is_private" in
true | false) ;;
*)
echo "::error::Target repository visibility did not resolve to true or false after retries."
exit 1
;;
esac
echo "is_private=$is_private" >>"$GITHUB_OUTPUT"
- name: Materialize target workspace
if: github.event_name != 'repository_dispatch'
env:
GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
TARGET_WORKSPACE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
run: |
set -euo pipefail
trusted_workspace="$RUNNER_TEMP/trusted-workspace"
mkdir -p "$trusted_workspace"
git init -q "$trusted_workspace"
gh auth setup-git
git -C "$trusted_workspace" remote add origin "$GITHUB_SERVER_URL/$REPOSITORY.git"
git -C "$trusted_workspace" fetch --no-tags --depth=1 origin "$TARGET_WORKSPACE_SHA"
git -C "$trusted_workspace" checkout --detach --quiet "$TARGET_WORKSPACE_SHA"
git -C "$trusted_workspace" cat-file -e "$TARGET_WORKSPACE_SHA^{commit}"
echo "TRUSTED_WORKSPACE=$trusted_workspace" >> "$GITHUB_ENV"
- name: Validate repository dispatch against live pull request metadata
if: github.event_name == 'repository_dispatch'
env:
GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
REPOSITORY: ${{ github.event.client_payload.target_repository }}
PR_NUMBER: ${{ github.event.client_payload.pr_number }}
SUPPLIED_BASE_REF: ${{ github.event.client_payload.pr_base_ref }}
SUPPLIED_BASE_SHA: ${{ github.event.client_payload.pr_base_sha }}
SUPPLIED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }}
run: |
set -euo pipefail
if ! [[ "$REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$SUPPLIED_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$SUPPLIED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] ||
[ -z "$SUPPLIED_BASE_REF" ]; then
echo "::error::repository_dispatch Strix metadata is incomplete or malformed."
exit 1
fi
pull_request_json="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")"
live_state="$(jq -r '.state // empty' <<<"$pull_request_json")"
live_base_repository="$(jq -r '.base.repo.full_name // empty' <<<"$pull_request_json")"
live_head_repository="$(jq -r '.head.repo.full_name // empty' <<<"$pull_request_json")"
live_base_ref="$(jq -r '.base.ref // empty' <<<"$pull_request_json")"
live_base_sha="$(jq -r '.base.sha // empty' <<<"$pull_request_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")"
if [ "$live_state" != "open" ] ||
[ "$live_base_repository" != "$REPOSITORY" ] ||
[ "$live_head_repository" != "$REPOSITORY" ] ||
[ "$live_base_ref" != "$SUPPLIED_BASE_REF" ] ||
[ "$live_base_sha" != "$SUPPLIED_BASE_SHA" ] ||
[ "$live_head_sha" != "$SUPPLIED_HEAD_SHA" ]; then
printf '::error::repository_dispatch Strix metadata does not match live PR %s#%s. supplied base=%s/%s head=%s; live state=%s base_repo=%s base=%s/%s head_repo=%s head=%s.\n' \
"$REPOSITORY" "$PR_NUMBER" "$SUPPLIED_BASE_REF" "$SUPPLIED_BASE_SHA" "$SUPPLIED_HEAD_SHA" \
"${live_state:-missing}" "${live_base_repository:-missing}" "${live_base_ref:-missing}" "${live_base_sha:-missing}" \
"${live_head_repository:-missing}" "${live_head_sha:-missing}"
exit 1
fi
trusted_workspace="$RUNNER_TEMP/trusted-workspace"
mkdir -p "$trusted_workspace"
git init -q "$trusted_workspace"
gh auth setup-git
git -C "$trusted_workspace" remote add origin "$GITHUB_SERVER_URL/$REPOSITORY.git"
git -C "$trusted_workspace" fetch --no-tags --depth=1 origin "$live_base_sha"
git -C "$trusted_workspace" checkout --detach --quiet "$live_base_sha"
git -C "$trusted_workspace" cat-file -e "$live_base_sha^{commit}"
echo "TRUSTED_WORKSPACE=$trusted_workspace" >> "$GITHUB_ENV"
- name: Fetch pull request head for trusted scan
if: github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != ''
env:
GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.client_payload.pr_number }}
PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha }}
PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }}
run: |
set -euo pipefail
if [ -z "$PR_NUMBER" ] || [ -z "$PR_HEAD_SHA" ]; then
echo "::error::PR number and head SHA are required for trusted PR-scope Strix evidence."
exit 1
fi
gh auth setup-git
if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::PR head SHA must be a 40-character git SHA."
exit 1
fi
if [ -n "$PR_BASE_SHA" ] && ! [[ "$PR_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::PR base SHA must be a 40-character git SHA."
exit 1
fi
if [ -n "$PR_BASE_SHA" ]; then
git -C "$TRUSTED_WORKSPACE" fetch --no-tags --depth=1 origin "$PR_BASE_SHA"
git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_BASE_SHA^{commit}"
fi
# Fetching the expected head SHA directly avoids false failures when
# refs/pull/<n>/head has already advanced before this queued run starts.
if git -C "$TRUSTED_WORKSPACE" fetch --no-tags --depth=1 origin "$PR_HEAD_SHA"; then
git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA^{commit}"
if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:.github/workflows/strix.yml" 2>/dev/null; then
mkdir -p "$TRUSTED_WORKSPACE/.github/workflows"
git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:.github/workflows/strix.yml" > "$TRUSTED_WORKSPACE/.github/workflows/strix.yml"
echo "Materialized PR-head Strix workflow for self-test."
fi
if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" 2>/dev/null; then
mkdir -p "$TRUSTED_WORKSPACE/scripts/ci"
git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" > "$TRUSTED_WORKSPACE/scripts/ci/pr_review_merge_scheduler.py"
fi
git -C "$TRUSTED_WORKSPACE" update-ref "refs/remotes/pull/${PR_NUMBER}/head" "$PR_HEAD_SHA"
exit 0
fi
for pr_head_fetch_attempt in 1 2 3 4 5 6; do
git -C "$TRUSTED_WORKSPACE" fetch --no-tags --prune origin "+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head"
fetched_head_sha="$(git -C "$TRUSTED_WORKSPACE" rev-parse "refs/remotes/pull/${PR_NUMBER}/head")"
if [ "$fetched_head_sha" = "$PR_HEAD_SHA" ]; then
git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA^{commit}"
if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:.github/workflows/strix.yml" 2>/dev/null; then
mkdir -p "$TRUSTED_WORKSPACE/.github/workflows"
git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:.github/workflows/strix.yml" > "$TRUSTED_WORKSPACE/.github/workflows/strix.yml"
echo "Materialized PR-head Strix workflow for self-test."
fi
if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" 2>/dev/null; then
mkdir -p "$TRUSTED_WORKSPACE/scripts/ci"
git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:scripts/ci/pr_review_merge_scheduler.py" > "$TRUSTED_WORKSPACE/scripts/ci/pr_review_merge_scheduler.py"
fi
exit 0
fi
if [ "$pr_head_fetch_attempt" -lt 6 ]; then
echo "Fetched PR head $fetched_head_sha, expected $PR_HEAD_SHA; retrying after propagation delay." >&2
sleep 10
fi
done
echo "::error::PR head ref did not resolve to expected commit $PR_HEAD_SHA after retries." >&2
exit 1
- name: Self-test Strix required workflow contract
timeout-minutes: 2
working-directory: trusted-strix-source
run: |
set -euo pipefail
printf 'Running bounded Strix required-workflow smoke test.\n'
bash "$TRUSTED_STRIX_REQUIRED_SMOKE"
- name: Materialize central Strix dependency lock from PR head
if: >-
github.event_name == 'pull_request_target'
&& github.repository == 'ContextualWisdomLab/.github'
&& github.event.pull_request.base.repo.full_name == 'ContextualWisdomLab/.github'
&& github.event.pull_request.head.repo.full_name == 'ContextualWisdomLab/.github'
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::PR head SHA must be a 40-character git SHA."
exit 1
fi
if git -C "$TRUSTED_WORKSPACE" cat-file -e "$PR_HEAD_SHA:requirements-strix-ci-hashes.txt" 2>/dev/null; then
git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:requirements-strix-ci-hashes.txt" > "$TRUSTED_STRIX_SOURCE/requirements-strix-ci-hashes.txt"
printf 'Materialized central Strix dependency lock from same-repository PR head.\n'
fi
- name: Gate Strix secrets
id: gate
env:
STRIX_MODEL: contextual-orchestrator/orchestrator/free
STRIX_MODEL_REQUESTED: ${{ github.event.client_payload.strix_llm || '' }}
run: |
requested_model="$(printf '%s' "$STRIX_MODEL_REQUESTED" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$requested_model" in
""|orchestrator/free|contextual-orchestrator/orchestrator/free) ;;
*)
echo '::error::Strix model overrides are limited to contextual-orchestrator/orchestrator/free.'
exit 1
;;
esac
strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
{
echo "strix_model=$strix_model"
echo 'enabled=true'
echo 'provider_mode=contextual_orchestrator'
} >> "$GITHUB_OUTPUT"
- name: Provision contextual-orchestrator Strix sidecar
if: steps.gate.outputs.enabled == 'true'
env:
BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }}
NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}
NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.is_private }}
CONTEXTUAL_ORCHESTRATOR_POOL: free
run: |
set -euo pipefail
bash "$TRUSTED_STRIX_SOURCE/scripts/ci/contextual_orchestrator_review_sidecar.sh"
- name: Set up Python
if: steps.gate.outputs.enabled == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Install Strix
if: steps.gate.outputs.enabled == 'true'
working-directory: trusted-strix-source
run: |
set -euo pipefail
# GitHub-hosted runners may inherit a collaborative umask (0002),
# which makes pip-generated console scripts group-writable. Pin a
# private install umask before creating the credential-bearing Strix
# entry point; the runtime gate still rejects any later relaxation.
umask 022
# --no-deps: strix-agent declares cryptography<49, conflicting with this repo's
# cryptography==50.0.0 pin (CVE-2026-39892 fix, see requirements-strix-ci-overrides.txt).
# --require-hashes already pins every package (including transitive deps) to an exact,
# hash-verified version, so skipping pip's redundant declared-range resolution here is
# safe -- verified locally with --dry-run against this exact file before pushing.
python3 -m pip install --disable-pip-version-check --no-cache-dir --require-hashes --no-deps -r requirements-strix-ci-hashes.txt
strix_executable="$(command -v strix || true)"
if [ -z "$strix_executable" ] || [[ "$strix_executable" != /* ]] \
|| [ ! -f "$strix_executable" ] || [ -L "$strix_executable" ] \
|| [ ! -x "$strix_executable" ]; then
echo "::error::Pinned Strix installation did not produce a trusted absolute executable path."
exit 1
fi
case "$strix_executable" in
"$GITHUB_WORKSPACE"/*|"$RUNNER_TEMP"/*)
echo "::error::Refusing a Strix executable from a workspace or runner-temp path."
exit 1
;;
esac
strix_scripts_root="$(python3 -c 'import sysconfig; print(sysconfig.get_path("scripts"))')"
if [ -z "$strix_scripts_root" ] || [[ "$strix_scripts_root" != /* ]] \
|| [ ! -d "$strix_scripts_root" ] || [ -L "$strix_scripts_root" ]; then
echo "::error::Pinned Strix installation did not produce a trusted absolute scripts root."
exit 1
fi
case "$strix_executable" in
"$strix_scripts_root"/*) ;;
*)
echo "::error::Pinned Strix executable is outside the trusted scripts root."
exit 1
;;
esac
# pip and the hosted tool cache can preserve collaborative write bits
# even after a private install umask. Normalize both the containing
# scripts root and resolved console script before pinning their
# identity; the runtime gate still fails closed on later relaxation.
chmod go-w -- "$strix_scripts_root" "$strix_executable"
strix_executable_sha256="$(python3 - "$strix_executable" <<'PY'
import hashlib
from pathlib import Path
import sys
print(hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest())
PY
)"
{
printf 'STRIX_EXECUTABLE_PATH=%s\n' "$strix_executable"
printf 'STRIX_EXECUTABLE_ROOT=%s\n' "$strix_scripts_root"
printf 'STRIX_EXECUTABLE_SHA256=%s\n' "$strix_executable_sha256"
} >> "$GITHUB_ENV"
- name: Mask LLM API key
if: steps.gate.outputs.enabled == 'true'
env:
PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }}
run: |
set -euo pipefail
if [ "$PROVIDER_MODE" != "contextual_orchestrator" ]; then
echo '::error::Strix must use the contextual-orchestrator provider.'
exit 1
fi
source "$TRUSTED_STRIX_SOURCE/scripts/ci/load_contextual_orchestrator_token.sh"
# Sanitize CR/LF before masking to prevent broken ::add-mask::
# commands and potential workflow command injection.
sanitized="$(printf '%s' "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" | tr -d '\r\n')"
if [ -n "$sanitized" ]; then
echo "::add-mask::${sanitized}"
trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -n "$trimmed" ] && [ "$trimmed" != "$sanitized" ]; then
echo "::add-mask::${trimmed}"
fi
fi
- name: Prepare LLM API key input file
if: steps.gate.outputs.enabled == 'true'
env:
PROVIDER_MODE: ${{ steps.gate.outputs.provider_mode }}
run: |
set -euo pipefail
if [ "$PROVIDER_MODE" != "contextual_orchestrator" ]; then
echo '::error::Strix must use the contextual-orchestrator provider.'
exit 1
fi
source "$TRUSTED_STRIX_SOURCE/scripts/ci/load_contextual_orchestrator_token.sh"
sanitized="$(printf '%s' "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" | tr -d '\r\n')"
trimmed="$(printf '%s' "$sanitized" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -z "$trimmed" ]; then
echo '::error::CONTEXTUAL_ORCHESTRATOR_TOKEN is required for Strix scans.'
exit 1
fi
umask 077
llm_api_key_file="$RUNNER_TEMP/llm_api_key.txt"
printf '%s' "$trimmed" > "$llm_api_key_file"
echo "LLM_API_KEY_FILE=$llm_api_key_file" >> "$GITHUB_ENV"
- name: Prepare contextual-orchestrator API base
if: steps.gate.outputs.provider_mode == 'contextual_orchestrator'
run: |
set -euo pipefail
sidecar_base="${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}"
if [ "$sidecar_base" != "http://127.0.0.1:18080" ]; then
echo '::error::Strix sidecar base URL is not the pinned local gateway origin.'
exit 1
fi
umask 077
llm_api_base_file="$RUNNER_TEMP/llm_api_base.txt"
printf '%s/v1' "${sidecar_base%/}" > "$llm_api_base_file"
echo "LLM_API_BASE_FILE=$llm_api_base_file" >> "$GITHUB_ENV"
- name: Prepare Strix model input file
if: steps.gate.outputs.enabled == 'true'
env:
STRIX_MODEL: ${{ steps.gate.outputs.strix_model }}
run: |
umask 077
strix_llm_file="$RUNNER_TEMP/strix_llm.txt"
strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$strix_model" in
orchestrator/free | contextual-orchestrator/orchestrator/free)
printf '%s' 'orchestrator/free' > "$strix_llm_file"
;;
*)
echo '::error::STRIX_LLM must select contextual-orchestrator/orchestrator/free.'
exit 1
;;
esac
echo "STRIX_LLM_FILE=$strix_llm_file" >> "$GITHUB_ENV"
- name: Run Strix (quick)
if: steps.gate.outputs.enabled == 'true'
# 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
# data. PR evidence uses the __PR_SCOPE__ sentinel so the scanner target
# cannot accidentally remain the trusted base checkout.
working-directory: ${{ runner.temp }}/trusted-workspace
env:
STRIX_LLM_FILE: ${{ env.STRIX_LLM_FILE }}
STRIX_REPO_ROOT: ${{ runner.temp }}/trusted-workspace
LLM_API_BASE_FILE: ${{ env.LLM_API_BASE_FILE }}
STRIX_LLM_DEFAULT_PROVIDER: contextual_orchestrator
LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }}
STRIX_TARGET_PATH: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && '__PR_SCOPE__' || './' }}
STRIX_SOURCE_DIRS: ". backend frontend"
# The gateway auto pool is provider-diverse. Strix function tools
# must not send a provider-specific reasoning setting to every route.
STRIX_REASONING_EFFORT: none
# The gateway owns discovery and provider failover; Strix must not
# bypass its ZDR/privacy policy with an external fallback model.
STRIX_FALLBACK_MODELS: ""
STRIX_FAIL_ON_PROVIDER_SIGNAL: "1"
NPM_CONFIG_IGNORE_SCRIPTS: "true"
PNPM_CONFIG_IGNORE_SCRIPTS: "true"
YARN_ENABLE_SCRIPTS: "false"
BUN_CONFIG_IGNORE_SCRIPTS: "true"
STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM
STRIX_DISABLE_PR_SCOPING: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && '0' || '1' }}
# A repository_dispatch executes in this central repository, so its
# github.token cannot read the target repository's PR. Reuse the
# target-app token that already validated and fetched that exact PR;
# preserve the target-repository token for pull_request_target runs.
GH_TOKEN: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && (steps.target_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token) || github.event_name == 'pull_request_target' && github.token || '' }}
PR_NUMBER: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event.client_payload.pr_number }}
PR_BASE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha }}
PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }}
IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && 'true' || 'false' }}
run: |
export LLM_TIMEOUT=0
export STRIX_MEMORY_COMPRESSOR_TIMEOUT=0
export STRIX_PROCESS_TIMEOUT_SECONDS=0
export STRIX_TOTAL_TIMEOUT_SECONDS=0
# Recognized signals that the LLM backend was unavailable / starved.
# Defined before the gate loop so the bounded retry decision below
# can classify outcomes without duplicating the patterns later.
backend_unavailable_signal='STRIX_PROVIDER_UNAVAILABLE|RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*500[^[:cntrl:]]*internal_error|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404|Error during penetration test: loginAsGuest failed after [0-9]+ attempts: curl exit 7: curl: \(7\) Failed to connect to 127\.0\.0\.1 port 48080'
model_behavior_error_signal='(^|[^A-Za-z0-9_])(agents|pydantic_ai|strix)(\.[A-Za-z_][A-Za-z0-9_]*)*\.ModelBehaviorError([^A-Za-z0-9_]|$)'
# Any evidence that a vulnerability was actually reported. Its presence
# forces a hard failure so real findings are NEVER downgraded. Keep the
# severity branch anchored away from identifiers so environment lines
# such as STRIX_FAIL_ON_MIN_SEVERITY do not look like findings.
reported_vulnerability_signal='Vulnerabilities[[:space:]]+[1-9]|(^|[^A-Za-z0-9_])severity[[:space:]]*:'
# Capture the gate exit code plus its console output. The gate returns
# exit 1 both for genuine blocking vulnerabilities AND for
# LLM-backend-unavailable outcomes (GitHub Models "Too many requests"
# rate limits, OpenAI quota starvation, 413 tokens_limit_reached,
# connection/warm-up failures, and scanner ModelBehaviorError) that
# could not complete a scan. Provider failure is typed infrastructure
# evidence, but remains non-passing because no authoritative complete
# vulnerability result exists.
#
# The gateway owns provider discovery, repair, and failover. Invoke
# the trusted gate once so repository-side retries cannot multiply a
# single PR scan into hours of shared-runner occupancy.
strix_run_log="$RUNNER_TEMP/strix_gate_console.log"
: > "$strix_run_log"
strix_terminal_log="$strix_run_log"
strix_rc=0
set +e
bash "$TRUSTED_STRIX_GATE" 2>&1 | tee "$strix_terminal_log"
strix_rc="${PIPESTATUS[0]}"
set -e
if [ "$strix_rc" -eq 0 ]; then
exit 0
fi
# Preserve configuration failures (exit 2) and any unexpected exit
# code as hard failures — only the scan-failure code (1) can be an
# infrastructure/backend-unavailability outcome.
if [ "$strix_rc" -ne 1 ]; then
exit "$strix_rc"
fi
# An earlier out-of-scope/below-threshold finding may already have
# been exempted by the trusted gate. Classify a later provider
# outage from the tail after the last continuation marker, but keep
# that incomplete later scan non-passing.
strix_neutralization_scope_log="$strix_terminal_log"
if grep -Fq 'allowing pipeline continuation' "$strix_terminal_log"; then
strix_neutralization_scope_log="$RUNNER_TEMP/strix_gate_console_tail.log"
awk '/allowing pipeline continuation/{buf=""; next} {buf=buf $0 "\n"} END{printf "%s", buf}' \
"$strix_terminal_log" > "$strix_neutralization_scope_log"
fi
# Classify provider/backend exhaustion only when no vulnerability
# finding was emitted. Classification improves diagnosis; it never
# converts an incomplete scan into passing security evidence.
if ( grep -Eiq "$backend_unavailable_signal" "$strix_neutralization_scope_log" \
|| grep -Eq "$model_behavior_error_signal" "$strix_neutralization_scope_log" ) \
&& ! grep -Eiq "$reported_vulnerability_signal" "$strix_neutralization_scope_log"; then
echo "::error title=STRIX_PROVIDER_UNAVAILABLE::Strix could not complete authoritative vulnerability analysis because its provider/backend was unavailable (rate limit, token cap, connection, warm-up, or model-behavior failure). See the strix-reports artifact and run log."
exit "$strix_rc"
fi
echo "Strix reported security findings or failed for a non-backend reason; failing the required check (gate exit ${strix_rc})." >&2
exit "$strix_rc"
- name: Collect Strix reports for artifact upload
if: ${{ always() && steps.gate.outputs.enabled == 'true' }}
env:
PR_HEAD_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha }}
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/strix_runs"
copied_reports=0
for candidate_dir in "$TRUSTED_WORKSPACE/strix_runs" "$RUNNER_TEMP/strix_runs"; do
if [ -d "$candidate_dir" ] && [ -n "$(find "$candidate_dir" -mindepth 1 -print -quit)" ]; then
cp -R "$candidate_dir"/. "$GITHUB_WORKSPACE/strix_runs"/
copied_reports=1
fi
done
if [ -f "$RUNNER_TEMP/strix_gate_console.log" ]; then
cp "$RUNNER_TEMP/strix_gate_console.log" "$GITHUB_WORKSPACE/strix_runs/gate-console.log"
copied_reports=1
fi
if [ -n "$(find "$GITHUB_WORKSPACE/strix_runs" -mindepth 1 -print -quit)" ]; then
copied_reports=1
fi
if [ "$copied_reports" -eq 0 ]; then
summary_head_sha="${PR_HEAD_SHA:-$GITHUB_SHA}"
{
echo "Strix scan completed without structured report files."
echo "run_id=$GITHUB_RUN_ID"
echo "head_sha=$summary_head_sha"
} > "$GITHUB_WORKSPACE/strix_runs/scan-summary.txt"
fi
- name: Upload Strix reports artifact
if: ${{ always() && steps.gate.outputs.enabled == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: strix-reports
path: strix_runs/
if-no-files-found: error
retention-days: 5
- name: Publish same-head manual Strix status
if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }}
env:
TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }}
GITHUB_STATUS_TOKEN: ${{ (github.event.client_payload.target_repository == '' || github.event.client_payload.target_repository == github.repository) && github.token || '' }}
PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }}
OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }}
TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.repository }}
PR_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }}
STRIX_RESULT: ${{ job.status }}
run: |
set -euo pipefail
if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::PR head SHA must be a 40-character git SHA."
exit 1
fi
case "$STRIX_RESULT" in
success)
state="success"
description="Default-branch repository_dispatch Strix evidence passed"
;;
failure|cancelled|skipped)
state="failure"
description="Default-branch repository_dispatch Strix evidence failed"
;;
*)
state="error"
description="Default-branch repository_dispatch Strix evidence inconclusive"
;;
esac
post_strix_status() {
token_label="$1"
token="$2"
if [ -z "$token" ]; then
return 1
fi
status_response="$(mktemp)"
status_error="$(mktemp)"
if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}" \
-f state="$state" \
-f context="strix" \
-f description="$description" \
-f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
>"$status_response" 2>"$status_error"; then
rm -f "$status_response" "$status_error"
echo "Published manual Strix status to ${TARGET_REPOSITORY}@${PR_HEAD_SHA} using ${token_label}."
return 0
fi
error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)"
rm -f "$status_response" "$status_error"
if [ -n "$error_summary" ]; then
echo "::notice::Manual Strix status publish using ${token_label} did not succeed: ${error_summary}"
else
echo "::notice::Manual Strix status publish using ${token_label} did not succeed."
fi
return 1
}
if post_strix_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then
exit 0
fi
if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then
exit 0
fi
if post_strix_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then
exit 0
fi
if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then
exit 0
fi
if post_strix_status "github-token" "$GITHUB_STATUS_TOKEN"; then
exit 0
fi
echo "::warning::Could not publish manual Strix status from scan job; keeping scan evidence result authoritative in the workflow run."
publish-manual-pr-evidence-status:
name: publish-manual-pr-evidence-status
needs: strix
if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }}
runs-on: ubuntu-24.04
# Single-shot OIDC exchange plus a handful of curl/gh api calls, no loop
# or pagination -- same shape as the agent-mention-*-dispatch.yml
# validate-and-forward jobs, which bound at timeout-minutes: 5. Without
# this the job falls back to GitHub's 360-minute platform default on a
# hung network call.
timeout-minutes: 5
permissions:
id-token: write
statuses: write # Required for downscoped OIDC status publication.
steps:
- name: Exchange OpenCode app token for target repository status
id: target_app_token
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
run: |
set -euo pipefail
mark_unavailable() {
echo "available=false" >>"$GITHUB_OUTPUT"
}
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "OpenCode app token exchange unavailable: OIDC request environment is missing."
mark_unavailable
exit 0
fi
request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}"
separator="&"
case "$request_url" in
*\?*) ;;
*) separator="?" ;;
esac
if ! oidc_response="$(
curl -fsS \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${request_url}${separator}audience=${OIDC_AUDIENCE}"
)"; then
echo "OpenCode app token exchange unavailable: OIDC token request did not complete."
mark_unavailable
exit 0
fi
oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")"
if [ -z "$oidc_token" ]; then
echo "OpenCode app token exchange unavailable: OIDC token response was empty."
mark_unavailable
exit 0
fi
if ! token_response="$(
curl -fsS \
-X POST \
-H "Authorization: Bearer ${oidc_token}" \
"${OPENCODE_API_BASE_URL}/exchange_github_app_token"
)"; then
echo "OpenCode app token exchange unavailable: app token request did not complete."
mark_unavailable
exit 0
fi
app_token="$(jq -r '.token // empty' <<<"$token_response")"
if [ -z "$app_token" ]; then
echo "OpenCode app token exchange unavailable: app token response was empty."
mark_unavailable
exit 0
fi
echo "::add-mask::$app_token"
{
echo "available=true"
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"
- name: Publish same-head manual Strix status
env:
TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }}
GITHUB_STATUS_READ_TOKEN: ${{ github.token }}
PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }}
OPENCODE_APPROVE_STATUS_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }}
TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || github.repository }}
PR_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha }}
STRIX_RESULT: ${{ needs.strix.result }}
run: |
set -euo pipefail
if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::PR head SHA must be a 40-character git SHA."
exit 1
fi
case "$STRIX_RESULT" in
success)
state="success"
description="Default-branch repository_dispatch Strix evidence passed"
;;
failure|cancelled|skipped)
state="failure"
description="Default-branch repository_dispatch Strix evidence failed"
;;
*)
state="error"
description="Default-branch repository_dispatch Strix evidence inconclusive"
;;
esac
post_strix_status() {
token_label="$1"
token="$2"
if [ -z "$token" ]; then
return 1
fi
status_response="$(mktemp)"
status_error="$(mktemp)"
if GH_TOKEN="$token" gh api -X POST "repos/${TARGET_REPOSITORY}/statuses/${PR_HEAD_SHA}" \
-f state="$state" \
-f context="strix" \
-f description="$description" \
-f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
>"$status_response" 2>"$status_error"; then
rm -f "$status_response" "$status_error"
echo "Published manual Strix status to ${TARGET_REPOSITORY}@${PR_HEAD_SHA} using ${token_label}."
return 0
fi
error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)"
rm -f "$status_response" "$status_error"
if [ -n "$error_summary" ]; then
echo "::notice::Manual Strix status publish using ${token_label} did not succeed: ${error_summary}"
else
echo "::notice::Manual Strix status publish using ${token_label} did not succeed."
fi
return 1
}
existing_current_run_success_status() {
if [ "$state" != "success" ]; then
return 1
fi
target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
check_existing_status() {
token_label="$1"
token="$2"
if [ -z "$token" ]; then
return 1
fi
status_response="$(mktemp)"
status_error="$(mktemp)"
if GH_TOKEN="$token" gh api "repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses" \
>"$status_response" 2>"$status_error"; then
if jq -e --arg target_url "$target_url" \
'any(.[]; .context == "strix" and .state == "success" and ((.target_url // "") == $target_url))' \
"$status_response" >/dev/null; then
rm -f "$status_response" "$status_error"
echo "Existing current-run Strix success status is already present on ${TARGET_REPOSITORY}@${PR_HEAD_SHA}; follow-up status publication is complete."
return 0
fi
rm -f "$status_response" "$status_error"
echo "::notice::No current-run Strix success status was visible using ${token_label}."
return 1
fi
error_summary="$(head -n 1 "$status_error" | tr -d '\r' || true)"
rm -f "$status_response" "$status_error"
if [ -n "$error_summary" ]; then
echo "::notice::Could not inspect existing Strix status using ${token_label}: ${error_summary}"
else
echo "::notice::Could not inspect existing Strix status using ${token_label}."
fi
return 1
}
if check_existing_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then
return 0
fi
if check_existing_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then
return 0
fi
if check_existing_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then
return 0
fi
if check_existing_status "github-token" "$GITHUB_STATUS_READ_TOKEN"; then
return 0
fi
return 1
}
if post_strix_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then
exit 0
fi
if post_strix_status "pr-review-merge-token" "$PR_REVIEW_MERGE_STATUS_TOKEN"; then
exit 0
fi
if post_strix_status "opencode-approve-token" "$OPENCODE_APPROVE_STATUS_TOKEN"; then
exit 0
fi
if existing_current_run_success_status; then
exit 0
fi
# A successful scan remains authoritative evidence even when an
# external target repository does not grant any configured token
# permission to create commit statuses. Keep every credential-
# specific failure visible above, but do not turn a clean security
# scan into a failed workflow solely because of target settings.
if [ "$STRIX_RESULT" = "success" ]; then
echo "::warning title=Manual Strix status unavailable::Strix scan succeeded, but no configured credential could publish or read the target commit status. Preserving the successful scan result; the target repository's branch protection remains authoritative. See the preceding token-specific notices."
exit 0
fi
echo "::error::Could not publish manual Strix status from follow-up job after all configured credentials failed after a non-successful scan; the target PR head is missing required Strix status evidence. See the preceding notices for token-specific reasons."
exit 1