Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6749c78
fix(scheduler): require approved aggregate review state
Aug 11, 2026
dd8f59d
test(scheduler): cover missing aggregate review state
Aug 11, 2026
12e3d1f
test(scheduler): cover empty aggregate review state
Aug 11, 2026
2756cd3
fix(scheduler): block merge on running checks
Aug 11, 2026
5856151
fix(strix): fail closed on missing evidence
Aug 11, 2026
9a05f03
fix(scheduler): bind check evidence to current head
Aug 11, 2026
170b98e
fix(ci): make review model pool portable
Aug 11, 2026
ed666c7
test(security): pin Strix dependency floors
Aug 11, 2026
7750253
fix(ci): close coverage and Strix smoke gaps
Aug 11, 2026
249ba98
fix(ci): close scheduler evidence gaps
Aug 11, 2026
ab2a1ae
fix(ci): require trustworthy Strix evidence
seonghobae Aug 12, 2026
e6c6d12
fix(ci): bind scan reports and kill captured groups
seonghobae Aug 12, 2026
2c6f432
fix(ci): reject conflicting scan metadata
seonghobae Aug 12, 2026
8726df1
fix(ci): verify PR Strix workflow version
seonghobae Aug 12, 2026
67d834f
fix(ci): validate executable Strix workflow policy
seonghobae Aug 12, 2026
19ced89
fix(ci): bound review dispatch payloads and UX failures
seonghobae Aug 12, 2026
d6b9b3a
fix(ci): defer interpreter-incompatible lock candidates
seonghobae Aug 12, 2026
2fe2bba
fix(ci): retry transient trusted uv downloads
seonghobae Aug 12, 2026
9644f9f
docs(adr): record dispatch allowlist drift
seonghobae Aug 12, 2026
0cc98ac
docs(adr): record stale dependency alerts
seonghobae Aug 12, 2026
5aafbb2
docs(adr): record trusted Strix outage evidence
seonghobae Aug 12, 2026
65d4b08
test(ci): align Strix lock contract wording
seonghobae Aug 12, 2026
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
1 change: 0 additions & 1 deletion .github/workflows/agent-mention-noema-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ on:
concurrency:
group: agent-mention-noema-${{ github.event.client_payload.agent_invocation_key || github.run_id }}
cancel-in-progress: false
queue: max

permissions:
contents: read
Expand Down
21 changes: 6 additions & 15 deletions .github/workflows/agent-mention-opencode-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ on:
concurrency:
group: agent-mention-opencode-${{ github.event.client_payload.agent_invocation_key || github.run_id }}
cancel-in-progress: false
queue: max

permissions:
contents: read
Expand All @@ -36,11 +35,11 @@ jobs:
BASE_BRANCH: ${{ github.event.client_payload.base_branch || '' }}
REQUESTED_BY: ${{ github.event.client_payload.requested_by || '' }}
SOURCE_COMMENT_ID: ${{ github.event.client_payload.source_comment_id || '' }}
TRIGGER_REVIEWS: ${{ github.event.client_payload.trigger_reviews }}
REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || '' }}
ENABLE_AUTO_MERGE: ${{ github.event.client_payload.enable_auto_merge }}
UPDATE_BRANCHES: ${{ github.event.client_payload.update_branches }}
MERGE_MODE: ${{ github.event.client_payload.merge_mode || '' }}
TRIGGER_REVIEWS: ${{ github.event.client_payload.review_policy.trigger_reviews }}
REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_policy.review_dispatch_limit || '' }}
ENABLE_AUTO_MERGE: ${{ github.event.client_payload.review_policy.enable_auto_merge }}
UPDATE_BRANCHES: ${{ github.event.client_payload.review_policy.update_branches }}
MERGE_MODE: ${{ github.event.client_payload.review_policy.merge_mode || '' }}
steps:
- name: Validate exact invocation payload
run: |
Expand Down Expand Up @@ -195,10 +194,6 @@ jobs:
--arg pr_head_sha "$PR_HEAD_SHA" \
--arg pr_base_sha "$PR_BASE_SHA" \
--arg base_branch "$BASE_BRANCH" \
--arg requested_agent "$REQUESTED_AGENT" \
--arg agent_invocation_key "$INVOCATION_KEY" \
--arg requested_by "$REQUESTED_BY" \
--argjson source_comment_id "$SOURCE_COMMENT_ID" \
'{
event_type: "merge-scheduler",
client_payload: {
Expand All @@ -211,11 +206,7 @@ jobs:
review_dispatch_limit: "1",
enable_auto_merge: false,
update_branches: false,
merge_mode: "disabled",
requested_agent: $requested_agent,
agent_invocation_key: $agent_invocation_key,
requested_by: $requested_by,
source_comment_id: $source_comment_id
merge_mode: "disabled"
}
}' \
| gh api "repos/${GITHUB_REPOSITORY}/dispatches" -X POST --input -
119 changes: 119 additions & 0 deletions .github/workflows/strix-workflow-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Strix Workflow Contract

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read

jobs:
workflow-contract:
runs-on: ubuntu-latest
steps:
- name: Read PR Strix workflow as data
env:
GH_TOKEN: ${{ github.token }}
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if ! [[ "$HEAD_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::PR workflow contract metadata is malformed."
exit 1
fi
workflow_json="$(gh api "repos/${HEAD_REPOSITORY}/contents/.github/workflows/strix.yml?ref=${HEAD_SHA}")"
printf '%s' "$workflow_json" |
jq -r '.content // empty' |
tr -d '\n' |
base64 --decode > "$RUNNER_TEMP/strix-pr-workflow.yml"
workflow_file="$RUNNER_TEMP/strix-pr-workflow.yml"
test -s "$workflow_file"
ruby - "$workflow_file" <<'RUBY'
require "psych"

def reject!(message)
warn "::error::PR Strix workflow contract rejected: #{message}"
exit 1
end

def executable_source(run)
run.lines.reject { |line| line.lstrip.start_with?("#") }.join
end

def statically_reachable?(node)
condition = node["if"]
return true if condition.nil?
return false if condition == false

value = condition.to_s.strip
!value.match?(/\b(?:false|0\s*==\s*1|1\s*==\s*0)\b/i)
end

workflow_path = ARGV.fetch(0)
begin
workflow = Psych.safe_load(File.read(workflow_path), aliases: false)
rescue Psych::Exception => error
reject!("workflow is not valid YAML: #{error.message.lines.first.strip}")
end
reject!("top-level YAML value is not a mapping") unless workflow.is_a?(Hash)

jobs = workflow["jobs"]
strix = jobs.is_a?(Hash) && jobs["strix"]
steps = strix.is_a?(Hash) && strix["steps"]
reject!("jobs.strix.steps is not a sequence") unless steps.is_a?(Array)
reject!("jobs.strix is statically unreachable") unless statically_reachable?(strix)

reachable_steps = steps.select do |step|
step.is_a?(Hash) && statically_reachable?(step)
end
named_step = lambda do |name|
reachable_steps.find { |step| step["name"] == name }
end
run_source = lambda do |step|
step && step["run"].is_a?(String) ? executable_source(step["run"]) : ""
end

gate_step = reachable_steps.find do |step|
source = run_source.call(step)
source.include?("if [ \"$strix_rc\" -eq 0 ]; then") &&
source.include?("echo \"::error title=Strix evidence incomplete::") &&
source.match?(/exit\s+[\"']?\$strix_rc/)
end
reject!("fail-closed gate is missing from a reachable run step") unless gate_step

collect_step = named_step.call("Collect Strix reports for artifact upload")
validate_step = named_step.call("Validate Strix report provenance")
upload_step = named_step.call("Upload Strix reports artifact")
reject!("structured report collection step is missing or unreachable") unless collect_step
reject!("structured provenance validation step is missing or unreachable") unless validate_step
reject!("report upload step is missing or unreachable") unless upload_step

gate_index = reachable_steps.index(gate_step)
collect_index = reachable_steps.index(collect_step)
validate_index = reachable_steps.index(validate_step)
upload_index = reachable_steps.index(upload_step)
unless gate_index < collect_index && collect_index < validate_index && validate_index < upload_index
reject!("fail-closed gate, collection, provenance validation, and upload are out of order")
end

validation_source = run_source.call(validate_step)
required_fragments = {
"scan-stage head binding" => "scan_stage_head_sha",
"candidate metadata conflict rejection" => "if [ \"$candidate_metadata_matches\" -ne 1 ]; then",
"completed successful run selection" => "if ! jq -e '(.status == \"completed\") and (.scan_results.scan_completed == true) and (.scan_results.success == true)'",
"non-empty report requirement" => "if [ -s \"$candidate_report\" ]; then",
"artifact evidence binding" => "> \"$GITHUB_WORKSPACE/strix_runs/evidence-binding.json\"",
"report digest" => "report_sha256=\"$(sha256sum \"$report_file\" | awk '{print $1}')\"",
"completed binding flag" => "{head_sha:$head_sha, run_id:$run_id, run_json:$run_json, report:$report, report_sha256:$report_sha256, scan_completed:true}",
"provider fail-closed guard" => "if [ -f \"$gate_console\" ] && grep -Eiq",
"candidate report path" => "candidate_report=\"$(dirname -- \"$candidate_run\")/penetration_test_report.md\""
}
required_fragments.each do |label, fragment|
reject!("#{label} is absent from executable provenance validation") unless validation_source.include?(fragment)
end

neutral_marker = "neutral skip so an infrastructure outage does not block merges"
reject!("Strix workflow still neutralizes missing security evidence") if executable_source(File.read(workflow_path)).include?(neutral_marker)
RUBY
145 changes: 113 additions & 32 deletions .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -834,14 +834,16 @@ jobs:
export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=300"
export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds"
export "STRIX_TOTAL_${budget_suffix}_SECONDS=5700"
printf '%s\n' "${PR_HEAD_SHA:-$GITHUB_SHA}" > "$RUNNER_TEMP/strix_scan_head_sha"

# 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
# token-cap, connection/warm-up failures) that could not complete a scan. A backend outage is CI
# infrastructure noise, not a security finding, so it must not fail
# the required check and block merges.
# exit 1 for genuine blocking vulnerabilities and for
# LLM-backend-unavailable outcomes (rate limits, quota starvation,
# token caps, connection failures, or warm-up failures) that could
# not complete a scan. Both cases fail closed: no security evidence
# means the required check must not be treated as a pass.
# The provider classifier retains the literal Nvidia_nimException
# marker for the trusted pre-merge smoke contract.
strix_run_log="$RUNNER_TEMP/strix_gate_console.log"
strix_rc=0
set +e
Expand All @@ -853,32 +855,7 @@ jobs:
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

# Recognized signals that the LLM backend was unavailable / starved.
backend_unavailable_signal='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:]]*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'
# 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:]]*:'

# Neutral skip only when ALL hold: a backend-unavailability signal is
# present and no vulnerability was reported anywhere. This preserves
# real security gating while keeping uncontrollable provider outages
# from blocking current-head merge progress.
if grep -Eiq "$backend_unavailable_signal" "$strix_run_log" \
&& ! grep -Eiq "$reported_vulnerability_signal" "$strix_run_log"; then
echo "::warning title=Strix backend unavailable::Strix could not complete because its LLM backend was unavailable (rate limit / token cap / connection or warm-up failure) before producing a vulnerability report. Treating as a neutral skip so an infrastructure outage does not block merges; genuine findings still fail the check. See the strix-reports artifact and the run log."
exit 0
fi

echo "Strix reported security findings or failed for a non-backend reason; failing the required check (gate exit ${strix_rc})." >&2
echo "::error title=Strix evidence incomplete::Strix did not complete with a successful security result; failing closed (gate exit ${strix_rc})." >&2
exit "$strix_rc"

- name: Collect Strix reports for artifact upload
Expand All @@ -899,6 +876,10 @@ jobs:
cp "$RUNNER_TEMP/strix_gate_console.log" "$GITHUB_WORKSPACE/strix_runs/gate-console.log"
copied_reports=1
fi
if [ -s "$RUNNER_TEMP/strix_scan_head_sha" ]; then
cp "$RUNNER_TEMP/strix_scan_head_sha" "$GITHUB_WORKSPACE/strix_runs/scan-head-sha.txt"
copied_reports=1
fi
if [ -n "$(find "$GITHUB_WORKSPACE/strix_runs" -mindepth 1 -print -quit)" ]; then
copied_reports=1
fi
Expand All @@ -911,6 +892,106 @@ jobs:
} > "$GITHUB_WORKSPACE/strix_runs/scan-summary.txt"
fi

- name: Validate Strix report provenance
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
evidence_head_sha="${PR_HEAD_SHA:-$GITHUB_SHA}"
if ! [[ "$evidence_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Strix evidence head SHA must be a 40-character git SHA."
exit 1
fi

scan_stage_head_sha=""
if [ -s "$GITHUB_WORKSPACE/strix_runs/scan-head-sha.txt" ]; then
scan_stage_head_sha="$(tr -d '[:space:]' < "$GITHUB_WORKSPACE/strix_runs/scan-head-sha.txt")"
fi
if ! [[ "$scan_stage_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Strix evidence must contain the exact head SHA recorded at scan start."
exit 1
fi

successful_run_file=""
report_file=""
while IFS= read -r -d '' candidate_run; do
if ! jq -e '(.status == "completed") and (.scan_results.scan_completed == true) and (.scan_results.success == true)' "$candidate_run" >/dev/null 2>&1; then
continue
fi
candidate_metadata_count="$(jq -r '
[
.head_sha,
.commit_sha,
((.scan_results // {}).head_sha),
((.scan_results // {}).commit_sha)
]
| map(select(. != null))
| length
' "$candidate_run")"
if [ "$candidate_metadata_count" -eq 0 ]; then
candidate_head_sha="$scan_stage_head_sha"
else
candidate_metadata_matches=1
candidate_head_sha=""
while IFS= read -r candidate_metadata_value; do
if [ -z "$candidate_head_sha" ]; then
candidate_head_sha="$candidate_metadata_value"
fi
if ! [[ "$candidate_metadata_value" =~ ^[0-9a-fA-F]{40}$ ]] ||
[ "${candidate_metadata_value,,}" != "${evidence_head_sha,,}" ]; then
candidate_metadata_matches=0
break
fi
done < <(jq -r '
[
.head_sha,
.commit_sha,
((.scan_results // {}).head_sha),
((.scan_results // {}).commit_sha)
]
| map(select(. != null))
| .[]
| if type == "string" then . else "__invalid_metadata_type__" end
' "$candidate_run")
if [ "$candidate_metadata_matches" -ne 1 ]; then
continue
fi
fi
candidate_report="$(dirname -- "$candidate_run")/penetration_test_report.md"
if [ -s "$candidate_report" ]; then
successful_run_file="$candidate_run"
report_file="$candidate_report"
break
fi
done < <(find "$GITHUB_WORKSPACE/strix_runs" -type f -name run.json -print0)

if [ -z "$successful_run_file" ] || [ -z "$report_file" ]; then
echo "::error::Strix evidence must contain a completed successful run.json and a non-empty penetration_test_report.md."
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

gate_console="$GITHUB_WORKSPACE/strix_runs/gate-console.log"
if [ -f "$gate_console" ] && grep -Eiq 'Strix (report artifacts emitted|run emitted|scan failed|reported zero vulnerabilities).*failing closed|No Strix vulnerability report artifact was produced' "$gate_console"; then
echo "::error::Strix evidence contains a fail-closed/provider-infrastructure marker; it cannot be published as a successful scan."
exit 1
fi

run_id="$(jq -r '.run_id // empty' "$successful_run_file")"
if [ -z "$run_id" ]; then
echo "::error::Strix evidence run.json must contain a run_id."
exit 1
fi
report_sha256="$(sha256sum "$report_file" | awk '{print $1}')"
jq -n \
--arg head_sha "$evidence_head_sha" \
--arg run_id "$run_id" \
--arg run_json "${successful_run_file#"$GITHUB_WORKSPACE/strix_runs/"}" \
--arg report "${report_file#"$GITHUB_WORKSPACE/strix_runs/"}" \
--arg report_sha256 "$report_sha256" \
'{head_sha:$head_sha, run_id:$run_id, run_json:$run_json, report:$report, report_sha256:$report_sha256, scan_completed:true}' \
> "$GITHUB_WORKSPACE/strix_runs/evidence-binding.json"

- name: Upload Strix reports artifact
if: ${{ always() && steps.gate.outputs.enabled == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
Loading
Loading