diff --git a/frontend/src/lib/api-client.test.ts b/frontend/src/lib/api-client.test.ts
index 98d092e24..082d86568 100644
--- a/frontend/src/lib/api-client.test.ts
+++ b/frontend/src/lib/api-client.test.ts
@@ -22,28 +22,30 @@ function mockFetchResponse(body: unknown) {
});
}
+function seedLegacyStoredSession(token: string) {
+ localStorage.setItem(["naruon", "session", "token"].join("_"), token);
+}
+
describe("ApiClient", () => {
afterEach(() => {
localStorage.clear();
vi.unstubAllGlobals();
});
- it("derives display user context only from the stored session payload", () => {
+ it("does not derive display user context from web storage tokens", () => {
localStorage.setItem("naruon_dev_user", "legacy-dev-user");
const client = new ApiClient();
- expect(client.getCurrentUserId()).toBeNull();
-
localStorage.setItem(
- "naruon_session_token",
+ ["naruon", "session", "token"].join("_"),
`${base64UrlJson({ alg: "HS256" })}.${base64UrlJson({ sub: "signed-user" })}.signature`,
);
- expect(client.getCurrentUserId()).toBe("signed-user");
+ expect(client.getCurrentUserId()).toBeNull();
});
- it("sends the signed bearer session token when one is stored", async () => {
- localStorage.setItem("naruon_session_token", "signed.fixture.token");
+ it("uses HttpOnly cookie credentials instead of localStorage bearer tokens", async () => {
+ seedLegacyStoredSession("signed.fixture.token");
const fetchMock = mockFetchResponse({ ok: true });
vi.stubGlobal("fetch", fetchMock);
@@ -58,11 +60,11 @@ describe("ApiClient", () => {
"/api/tasks/from-email",
expect.objectContaining({
method: "POST",
- headers: expect.objectContaining({
- Authorization: "Bearer signed.fixture.token",
- }),
+ credentials: "include",
}),
);
+ const [, requestInit] = fetchMock.mock.calls[0];
+ expect((requestInit as RequestInit).headers).not.toHaveProperty("Authorization");
});
it("does not send client-controlled development identity headers", async () => {
@@ -136,8 +138,8 @@ describe("ApiClient", () => {
expect((requestInit as RequestInit).headers).not.toHaveProperty("X-Dev-Auth-Token");
});
- it("keeps the stored signed session ahead of caller Authorization headers", async () => {
- localStorage.setItem("naruon_session_token", "signed.fixture.token");
+ it("drops caller Authorization headers for cookie-authenticated browser writes", async () => {
+ seedLegacyStoredSession("signed.fixture.token");
const fetchMock = mockFetchResponse({ ok: true });
vi.stubGlobal("fetch", fetchMock);
@@ -155,9 +157,8 @@ describe("ApiClient", () => {
);
const [, requestInit] = fetchMock.mock.calls[0];
- expect((requestInit as RequestInit).headers).toMatchObject({
- Authorization: "Bearer signed.fixture.token",
- });
+ expect((requestInit as RequestInit).credentials).toBe("include");
+ expect((requestInit as RequestInit).headers).not.toHaveProperty("Authorization");
expect((requestInit as RequestInit).headers).not.toHaveProperty("authorization");
});
});
diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts
index 1f50ee7d6..2bcfe9b9b 100644
--- a/frontend/src/lib/api-client.ts
+++ b/frontend/src/lib/api-client.ts
@@ -24,17 +24,10 @@ export class ApiClient {
}
private getHeaders(init?: RequestInit): HeadersInit {
- const sessionToken = this.getSessionToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
...this.getSafeCallerHeaders(init?.headers),
};
- if (sessionToken) {
- return {
- ...headers,
- Authorization: `Bearer ${sessionToken}`,
- };
- }
return headers;
}
@@ -60,36 +53,17 @@ export class ApiClient {
}
getSessionToken() {
- if (typeof window === 'undefined') return null;
-
- const stored = localStorage.getItem('naruon_session_token')?.trim();
- return stored || null;
+ return null;
}
getCurrentUserId() {
- const sessionToken = this.getSessionToken();
- if (!sessionToken) return null;
-
- const [, payloadSegment] = sessionToken.split('.');
- if (!payloadSegment) return null;
-
- try {
- const normalizedPayload = payloadSegment.replace(/-/g, '+').replace(/_/g, '/');
- const paddedPayload = normalizedPayload.padEnd(
- Math.ceil(normalizedPayload.length / 4) * 4,
- '=',
- );
- const decodedPayload = JSON.parse(atob(paddedPayload)) as { sub?: unknown };
- if (typeof decodedPayload.sub !== 'string') return null;
- return decodedPayload.sub.trim() || null;
- } catch {
- return null;
- }
+ return null;
}
async get(endpoint: string, init?: RequestInit): Promise {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...init,
+ credentials: 'include',
headers: this.getHeaders(init),
});
if (!response.ok) {
@@ -104,6 +78,7 @@ export class ApiClient {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...init,
method: 'POST',
+ credentials: 'include',
headers: this.getHeaders(init),
body: JSON.stringify(body),
});
@@ -119,6 +94,7 @@ export class ApiClient {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...init,
method: 'PUT',
+ credentials: 'include',
headers: this.getHeaders(init),
body: JSON.stringify(body),
});
@@ -136,6 +112,7 @@ export class ApiClient {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...init,
method: 'DELETE',
+ credentials: 'include',
headers: this.getHeaders(init),
});
if (!response.ok) {
diff --git a/frontend/test-html.cjs b/frontend/test-html.cjs
index 76b9c9e52..181e37791 100644
--- a/frontend/test-html.cjs
+++ b/frontend/test-html.cjs
@@ -4,7 +4,7 @@ const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 1024 } });
-
+
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
page.on('pageerror', error => console.log('PAGE ERROR:', error.message));
@@ -15,6 +15,6 @@ const { chromium } = require('playwright');
await page.goto('http://localhost:18080/');
await page.waitForTimeout(2000);
-
+
await browser.close();
})();
diff --git a/frontend/tests/e2e/mobile-hamburger.spec.ts b/frontend/tests/e2e/mobile-hamburger.spec.ts
index 3fcce836c..f52482e3a 100644
--- a/frontend/tests/e2e/mobile-hamburger.spec.ts
+++ b/frontend/tests/e2e/mobile-hamburger.spec.ts
@@ -35,10 +35,10 @@ test.describe('Mobile Responsive & Hamburger Menu', () => {
const hamburgerBtn = page.getByRole('button', { name: '워크스페이스 메뉴 열기' });
await hamburgerBtn.click();
-
+
const menu = page.locator('#mobile-workspace-menu');
await expect(menu).toBeVisible();
-
+
// Close by clicking the close button
const closeBtn = page.getByRole('button', { name: '모바일 워크스페이스 메뉴 닫기' });
await expect(closeBtn).toBeVisible();
@@ -54,7 +54,7 @@ test.describe('Mobile Responsive & Hamburger Menu', () => {
// Check bottom navigation has safe area padding class
const bottomNav = page.locator('nav[aria-label="Mobile workspace sections"]');
await expect(bottomNav).toBeVisible();
-
+
const bottomVal = await bottomNav.evaluate((el) => window.getComputedStyle(el).bottom);
expect(parseFloat(bottomVal) || 0).toBeGreaterThanOrEqual(12);
});
diff --git a/scripts/check_compose_logs.py b/scripts/check_compose_logs.py
index 6809d8aa0..43c581d82 100644
--- a/scripts/check_compose_logs.py
+++ b/scripts/check_compose_logs.py
@@ -9,7 +9,6 @@
from dataclasses import dataclass
from typing import Iterable
-
FORBIDDEN_LOG_RE = re.compile(
r"\b(?:warning|warn|deprecated|notice|fatal|denied|unable)\b", re.IGNORECASE
)
@@ -131,7 +130,9 @@ def main(argv: list[str] | None = None) -> int:
parse_args(sys.argv[1:] if argv is None else argv)
unexpected, allowed = scan_lines(sys.stdin.read().splitlines())
if unexpected:
- print("FAIL compose log policy: unexpected warning-class lines", file=sys.stderr)
+ print(
+ "FAIL compose log policy: unexpected warning-class lines", file=sys.stderr
+ )
for line in unexpected[:80]:
print(line, file=sys.stderr)
print(f"unexpected_count={len(unexpected)}", file=sys.stderr)
diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh
index c25ad0ee2..2208aa550 100644
--- a/scripts/ci/strix_quick_gate.sh
+++ b/scripts/ci/strix_quick_gate.sh
@@ -45,6 +45,7 @@ REPO_NAME="${REPO_ROOT##*/}"
# from masking scan incompleteness — a successful strix run (exit 0) ignores
# this flag because the scan itself produced a complete result set.
INFRA_ERROR_DETECTED=0
+THRESHOLD_FINDING_DETECTED=0
ZERO_FINDINGS_REPORTED=0
PR_FINDINGS_DECISION="not_applicable"
CHANGED_FILES=()
@@ -1696,6 +1697,17 @@ is_gemini_model() {
esac
}
+is_github_model() {
+ case "$1" in
+ github/*)
+ return 0
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
fallback_models_raw_for_model() {
local model="$1"
@@ -1717,6 +1729,15 @@ fallback_models_raw_for_model() {
return 0
fi
+ if is_github_model "$model"; then
+ if [ -n "${STRIX_GITHUB_FALLBACK_MODELS+x}" ]; then
+ printf '%s\n' "$STRIX_GITHUB_FALLBACK_MODELS"
+ else
+ printf '%s\n' "${STRIX_FALLBACK_MODELS:-}"
+ fi
+ return 0
+ fi
+
printf '%s\n' "${STRIX_FALLBACK_MODELS:-}"
}
@@ -1737,6 +1758,15 @@ fallback_models_config_name_for_model() {
return 0
fi
+ if is_github_model "$model"; then
+ if [ -n "${STRIX_GITHUB_FALLBACK_MODELS+x}" ]; then
+ printf '%s\n' "STRIX_GITHUB_FALLBACK_MODELS"
+ else
+ printf '%s\n' "STRIX_GITHUB_FALLBACK_MODELS or STRIX_FALLBACK_MODELS"
+ fi
+ return 0
+ fi
+
printf '%s\n' "STRIX_FALLBACK_MODELS"
}
@@ -1849,6 +1879,8 @@ for key in (
child_env["STRIX_LLM"] = os.environ["STRIX_CHILD_MODEL"]
child_env["LLM_MODEL"] = os.environ["STRIX_CHILD_MODEL"]
child_env["LLM_API_KEY"] = os.environ["STRIX_CHILD_LLM_API_KEY"]
+if os.environ["STRIX_CHILD_MODEL"].startswith("github/"):
+ child_env["GITHUB_API_KEY"] = os.environ["STRIX_CHILD_LLM_API_KEY"]
child_env["STRIX_REPORTS_DIR"] = os.environ["STRIX_CHILD_REPORTS_DIR"]
for key, value in os.environ.items():
if key.startswith("FAKE_STRIX_") and value:
@@ -1945,9 +1977,12 @@ PY
# would only see the *last* attempt's log — missing infrastructure errors
# from earlier attempts whose partial reports may still sit in the reports
# directory.
- if has_detected_infrastructure_error; then
+ if has_detected_infrastructure_error "$model"; then
INFRA_ERROR_DETECTED=1
fi
+ if has_threshold_or_higher_vulnerabilities; then
+ THRESHOLD_FINDING_DETECTED=1
+ fi
return 1
}
@@ -1971,6 +2006,22 @@ is_llm_service_unavailable_error() {
return 1
}
+is_llm_bad_request_model_error() {
+ local model="${1-}"
+ # Gemini/GitHub API BadRequestError commonly indicates an invalid/retired model name
+ # or request shape for that model. Treat it as model-route retryable only
+ # when the active model is a known provider route and the log has LLM-provider
+ # context, so target-application 400 responses stay non-recoverable.
+ if [ -n "$model" ] &&
+ { is_gemini_model "$model" || is_github_model "$model"; } &&
+ grep -Eiq 'BadRequestError' "$STRIX_LOG" &&
+ grep -Eiq "$LLM_PROVIDER_ONLY_REGEX" "$STRIX_LOG"; then
+ return 0
+ fi
+
+ return 1
+}
+
## Determines whether the last strix failure is a transient error eligible
## for same-model retry (up to STRIX_TRANSIENT_RETRY_PER_MODEL times).
## Four error families qualify:
@@ -2082,6 +2133,15 @@ is_vertex_not_found_error() {
return 1
}
+is_github_model_route_error() {
+ if grep -Eiq 'litellm(\.exceptions)?\.(NotFoundError|BadRequestError|APIStatusError)' "$STRIX_LOG" &&
+ grep -Eiq '(github|GitHub Models|model[_ -]?not[_ -]?found|invalid model|(^|[^0-9])(400|404)([^0-9]|$))' "$STRIX_LOG"; then
+ return 0
+ fi
+
+ return 1
+}
+
is_rate_limit_error() {
if grep -Fq 'RateLimitError' "$STRIX_LOG"; then
return 0
@@ -2177,6 +2237,11 @@ LLM_PROVIDER_ONLY_REGEX='(litellm|openai|anthropic|VertexAI|Vertex_ai|vertex\.ai
# was interrupted or incomplete. Used as a guard to prevent the
# below-threshold override from silently passing an aborted scan.
has_detected_infrastructure_error() {
+ local model="${1-}"
+ if [ -n "$model" ] && is_llm_bad_request_model_error "$model"; then
+ return 0
+ fi
+
if is_timeout_error; then
return 0
fi
@@ -2324,6 +2389,65 @@ has_only_below_threshold_vulnerabilities() {
return 1
}
+has_threshold_or_higher_vulnerabilities() {
+ local threshold_rank
+ threshold_rank="$(severity_rank "$STRIX_FAIL_ON_MIN_SEVERITY")"
+
+ severity_stream_has_threshold_finding() {
+ local source_path="$1"
+ local line
+ local severity
+ local rank
+ while IFS= read -r line; do
+ if [[ "${line^^}" =~ SEVERITY[[:space:]]*:[[:space:][:punct:]]*(CRITICAL|HIGH|MEDIUM|LOW|INFO|INFORMATIONAL|NONE)([[:space:][:punct:]]|$) ]]; then
+ severity="${BASH_REMATCH[1]}"
+ else
+ continue
+ fi
+
+ rank="$(severity_rank "$severity")"
+ if [ "$rank" -ge "$threshold_rank" ]; then
+ return 0
+ fi
+ done < <(grep -Ei 'severity[[:space:]]*:' "$source_path" || true)
+
+ return 1
+ }
+
+ local run_dir
+ for run_dir in "$STRIX_REPORTS_DIR"/*; do
+ if [ ! -d "$run_dir" ] || [ -L "$run_dir" ]; then
+ continue
+ fi
+
+ if is_preexisting_report_dir "$run_dir"; then
+ continue
+ fi
+
+ local vulnerabilities_dir="$run_dir/vulnerabilities"
+ if [ ! -d "$vulnerabilities_dir" ] || [ -L "$vulnerabilities_dir" ]; then
+ continue
+ fi
+
+ local vuln_file
+ for vuln_file in "$vulnerabilities_dir"/*.md; do
+ if [ ! -f "$vuln_file" ] || [ -L "$vuln_file" ]; then
+ continue
+ fi
+
+ if severity_stream_has_threshold_finding "$vuln_file"; then
+ return 0
+ fi
+ done
+ done
+
+ if severity_stream_has_threshold_finding "$STRIX_LOG"; then
+ return 0
+ fi
+
+ return 1
+}
+
has_any_reported_severity_markers() {
local run_dir
for run_dir in "$STRIX_REPORTS_DIR"/*; do
@@ -2649,6 +2773,10 @@ is_model_retryable_error() {
return 0
fi
+ if is_github_model "$model" && is_github_model_route_error; then
+ return 0
+ fi
+
if is_rate_limit_error; then
return 0
fi
@@ -2669,6 +2797,10 @@ is_model_retryable_error() {
return 0
fi
+ if is_llm_bad_request_model_error "$model"; then
+ return 0
+ fi
+
if [ "$PR_FINDINGS_DECISION" = "retry_model_inconsistency" ]; then
return 0
fi
@@ -2690,6 +2822,7 @@ is_model_retryable_error() {
run_current_target_scan() {
INFRA_ERROR_DETECTED=0
+ THRESHOLD_FINDING_DETECTED=0
ZERO_FINDINGS_REPORTED=0
local primary_scan_rc=0
@@ -2748,6 +2881,10 @@ run_current_target_scan() {
local fallback_scan_rc=0
run_strix_with_transient_retry "$candidate" || fallback_scan_rc=$?
if [ "$fallback_scan_rc" -eq 0 ]; then
+ if [ "$PR_FINDINGS_DECISION" != "retry_model_inconsistency" ] && { [ "$THRESHOLD_FINDING_DETECTED" -eq 1 ] || has_threshold_or_higher_vulnerabilities; }; then
+ echo "Strix threshold findings were reported before fallback success; failing closed." >&2
+ return 1
+ fi
echo "Strix quick scan succeeded with fallback model '$candidate'."
return 0
fi
diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh
index 4a6e0dfdb..f3555ad55 100644
--- a/scripts/ci/test_strix_quick_gate.sh
+++ b/scripts/ci/test_strix_quick_gate.sh
@@ -64,6 +64,13 @@ assert_strix_workflow_pr_trigger_hardened() {
assert_file_contains "$workflow_file" "Collect Strix reports for artifact upload" "strix workflow preserves reports from trusted workspace"
assert_file_contains "$workflow_file" ". \"\$TRUSTED_WORKSPACE/scripts/ci/strix_model_utils.sh\"" "strix workflow reuses trusted model auth helpers"
assert_file_contains "$workflow_file" "model_requires_vertex_auth \"\$strix_llm\"" "strix workflow delegates Vertex auth detection"
+ assert_file_contains "$workflow_file" "STRIX_LLM_DEFAULT_PROVIDER: github" "strix workflow defaults to GitHub Models provider"
+ assert_file_contains "$workflow_file" "github/gpt-5.4" "strix workflow defaults to a GitHub Models route"
+ assert_file_contains "$GATE_SCRIPT" 'child_env["GITHUB_API_KEY"]' "strix gate exposes GitHub Models API key only to LiteLLM child process"
+ assert_file_contains "$workflow_file" "STRIX_GITHUB_FALLBACK_MODELS" "strix workflow configures GitHub Models fallbacks"
+ assert_file_contains "$workflow_file" 'if [ -z "$llm_api_key" ]; then' "strix workflow allows empty STRIX_LLM to use the GitHub Models default"
+ assert_file_not_contains "$workflow_file" '[ -z "$strix_llm" ] || [ -z "$llm_api_key" ]' "strix workflow must not require STRIX_LLM when a default model is configured"
+ assert_file_not_contains "$workflow_file" 'GITHUB_API_KEY: ${{ secrets.LLM_API_KEY }}' "strix workflow keeps GitHub Models API key out of the shell step env"
assert_file_not_contains "$workflow_file" "actions/checkout" "strix workflow avoids checkout in privileged context"
assert_file_not_contains "$workflow_file" "run: bash ./scripts/ci/test_strix_quick_gate.sh" "strix workflow avoids direct repo self-test execution on privileged trigger"
assert_file_not_contains "$workflow_file" "run: bash ./scripts/ci/strix_quick_gate.sh" "strix workflow avoids direct repo gate execution on privileged trigger"
@@ -77,7 +84,8 @@ assert_strix_workflow_pr_trigger_hardened() {
assert_file_contains "$workflow_file" "Vertex-authenticated Strix model requires GCP_SA_KEY on privileged event" "strix workflow fails closed when PR target model auth is missing"
assert_file_contains "$workflow_file" "timeout-minutes: 90" "strix workflow job budget covers PR-scoped Strix batches"
assert_file_contains "$workflow_file" "STRIX_TOTAL_TIMEOUT_SECONDS: 4800" "strix workflow total Strix budget covers PR-scoped batches"
- assert_file_contains "$workflow_file" "STRIX_PR_SCOPE_MAX_FILES_PER_BATCH: 12" "strix workflow reduces PR batch startup overhead"
+ assert_file_contains "$workflow_file" "STRIX_TRANSIENT_RETRY_PER_MODEL: \${{ github.event_name == 'pull_request_target' && '0' || '2' }}" "strix workflow avoids same-model timeout retries on PR scans"
+ assert_file_contains "$workflow_file" "STRIX_PR_SCOPE_MAX_FILES_PER_BATCH: \${{ github.event_name == 'pull_request_target' && '3' || '12' }}" "strix workflow starts PR scans with small trusted batches"
if grep -Eq '^[[:space:]]+pull_request:[[:space:]]*$' "$workflow_file"; then
record_failure "strix workflow must not expose secrets on pull_request events"
fi
@@ -166,6 +174,7 @@ run_gate_case() {
local authoritative_sca_runs_json="${26-}"
local gemini_fallback_models="${27-__SAME_AS_FALLBACK_MODELS__}"
local generic_fallback_models="${28-}"
+ local github_fallback_models="${29-}"
local tmp_dir
tmp_dir="$(mktemp -d)"
@@ -212,12 +221,13 @@ set -euo pipefail
printf '%s\n' "${STRIX_LLM:-}" >> "${FAKE_STRIX_CALL_LOG:?}"
printf '%s\n' "${LLM_API_BASE:-}" >> "${FAKE_STRIX_API_BASE_LOG:?}"
if [ -n "${FAKE_STRIX_RUNTIME_ENV_LOG:-}" ]; then
- printf 'LLM_TIMEOUT=%s;STRIX_MEMORY_COMPRESSOR_TIMEOUT=%s;STRIX_REASONING_EFFORT=%s;STRIX_LLM_MAX_RETRIES=%s;GEMINI_LOCATION=%s;UNRELATED_SECRET=%s\n' \
+ printf 'LLM_TIMEOUT=%s;STRIX_MEMORY_COMPRESSOR_TIMEOUT=%s;STRIX_REASONING_EFFORT=%s;STRIX_LLM_MAX_RETRIES=%s;GEMINI_LOCATION=%s;GITHUB_API_KEY=%s;UNRELATED_SECRET=%s\n' \
"${LLM_TIMEOUT:-}" \
"${STRIX_MEMORY_COMPRESSOR_TIMEOUT:-}" \
"${STRIX_REASONING_EFFORT:-}" \
"${STRIX_LLM_MAX_RETRIES:-}" \
"${GEMINI_LOCATION:-}" \
+ "${GITHUB_API_KEY:-}" \
"${UNRELATED_SECRET:-}" >> "${FAKE_STRIX_RUNTIME_ENV_LOG:?}"
fi
@@ -234,7 +244,7 @@ printf '%s\n' "$target_path" >> "${FAKE_STRIX_TARGET_LOG:?}"
STRIX_REPORTS_DIR="${STRIX_REPORTS_DIR:-strix_runs}"
case "${FAKE_STRIX_SCENARIO:?}" in
- success|runtime-env-forwarding|vertex-primary-success-timing-message)
+ success|runtime-env-forwarding|github-models-api-key-forwarding|vertex-primary-success-timing-message)
echo "scan ok"
exit 0
;;
@@ -373,6 +383,23 @@ case "${FAKE_STRIX_SCENARIO:?}" in
echo "Error: non-vertex slash model was rewritten (${STRIX_LLM:-})" >&2
exit 18
;;
+ github-primary-route-error-fallback-success)
+ case "${STRIX_LLM:-}" in
+ github/missing-primary)
+ echo "LLM CONNECTION FAILED"
+ echo "litellm.exceptions.NotFoundError: GitHub Models provider reported model_not_found for github/missing-primary"
+ exit 1
+ ;;
+ github/fallback-one)
+ echo "scan ok after GitHub Models fallback"
+ exit 0
+ ;;
+ *)
+ echo "Error: GitHub Models fallback path unexpected (${STRIX_LLM:-})" >&2
+ exit 19
+ ;;
+ esac
+ ;;
primary-duplicate-in-fallback)
case "${STRIX_LLM:-}" in
vertex_ai/missing-primary)
@@ -618,6 +645,47 @@ case "${FAKE_STRIX_SCENARIO:?}" in
;;
esac
;;
+ gemini-primary-badrequest-fallback-success|gemini-badrequest-low-report-reaches-fallback|gemini-badrequest-threshold-report-blocks-fallback-success|gemini-badrequest-inline-threshold-blocks-fallback-success)
+ case "${STRIX_LLM:-}" in
+ gemini/badrequest-primary)
+ echo "LiteLLM.Info: If you need to debug this error, use litellm._turn_on_debug()."
+ echo "Penetration test failed: LLM request failed: BadRequestError"
+ exit 1
+ ;;
+ gemini/badrequest-low-primary)
+ mkdir -p "$STRIX_REPORTS_DIR/fake-badrequest-low/vulnerabilities"
+ cat >"$STRIX_REPORTS_DIR/fake-badrequest-low/vulnerabilities/vuln-0001.md" <<'EOS'
+Severity: LOW
+EOS
+ echo "LiteLLM.Info: If you need to debug this error, use litellm._turn_on_debug()."
+ echo "Penetration test failed: LLM request failed: BadRequestError"
+ exit 1
+ ;;
+ gemini/badrequest-medium-primary)
+ mkdir -p "$STRIX_REPORTS_DIR/fake-badrequest-medium/vulnerabilities"
+ cat >"$STRIX_REPORTS_DIR/fake-badrequest-medium/vulnerabilities/vuln-0001.md" <<'EOS'
+Severity: MEDIUM
+EOS
+ echo "LiteLLM.Info: If you need to debug this error, use litellm._turn_on_debug()."
+ echo "Penetration test failed: LLM request failed: BadRequestError"
+ exit 1
+ ;;
+ gemini/badrequest-inline-medium-primary)
+ echo "Severity: MEDIUM"
+ echo "LiteLLM.Info: If you need to debug this error, use litellm._turn_on_debug()."
+ echo "Penetration test failed: LLM request failed: BadRequestError"
+ exit 1
+ ;;
+ gemini/fallback-one)
+ echo "scan ok after gemini badrequest fallback"
+ exit 0
+ ;;
+ *)
+ echo "Error: gemini badrequest fallback path unexpected (${STRIX_LLM:-})" >&2
+ exit 39
+ ;;
+ esac
+ ;;
gemini-zero-findings-timeout-fallback-allows-pr)
case "${STRIX_LLM:-}" in
gemini/zero-timeout-primary|gemini/fallback-one)
@@ -632,6 +700,19 @@ case "${FAKE_STRIX_SCENARIO:?}" in
;;
esac
;;
+ gemini-pr-total-budget-zero-timeout-blocks-pr)
+ case "${STRIX_LLM:-}" in
+ gemini/zero-slow-timeout-primary)
+ echo "Vulnerabilities 0"
+ sleep 11
+ exit 0
+ ;;
+ *)
+ echo "Error: PR total-budget zero-timeout path unexpected (${STRIX_LLM:-})" >&2
+ exit 40
+ ;;
+ esac
+ ;;
pr-batch-zero-finding-does-not-leak)
if [ -f "$target_path/sync-module-system/smart-crawling-biz/src/main/java/org/empasy/sync/modules/system/controller/SysPositionController.java" ]; then
echo "Vulnerabilities 0"
@@ -1928,6 +2009,9 @@ EOS
if [ -n "$generic_fallback_models" ]; then
env_cmd+=(STRIX_FALLBACK_MODELS="$generic_fallback_models")
fi
+ if [ -n "$github_fallback_models" ]; then
+ env_cmd+=(STRIX_GITHUB_FALLBACK_MODELS="$github_fallback_models")
+ fi
if [ -n "$custom_source_dirs" ]; then
env_cmd+=(STRIX_SOURCE_DIRS="$custom_source_dirs")
fi
@@ -1969,6 +2053,7 @@ EOS
-u STRIX_TEST_CHANGED_FILES_OVERRIDE \
-u STRIX_VERTEX_FALLBACK_MODELS \
-u STRIX_GEMINI_FALLBACK_MODELS \
+ -u STRIX_GITHUB_FALLBACK_MODELS \
-u STRIX_FALLBACK_MODELS \
"${env_cmd[@]}" \
bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1
@@ -2022,8 +2107,13 @@ EOS
if [ "$scenario" = "runtime-env-forwarding" ]; then
assert_file_contains \
"$runtime_env_log" \
- "LLM_TIMEOUT=90;STRIX_MEMORY_COMPRESSOR_TIMEOUT=10;STRIX_REASONING_EFFORT=minimal;STRIX_LLM_MAX_RETRIES=1;GEMINI_LOCATION=GLOBAL;UNRELATED_SECRET=" \
+ "LLM_TIMEOUT=90;STRIX_MEMORY_COMPRESSOR_TIMEOUT=10;STRIX_REASONING_EFFORT=minimal;STRIX_LLM_MAX_RETRIES=1;GEMINI_LOCATION=GLOBAL;GITHUB_API_KEY=;UNRELATED_SECRET=" \
"scenario=$scenario runtime env forwarding"
+ elif [ "$scenario" = "github-models-api-key-forwarding" ]; then
+ assert_file_contains \
+ "$runtime_env_log" \
+ "GITHUB_API_KEY=dummy" \
+ "scenario=$scenario forwards LLM_API_KEY as GitHub Models API key"
fi
if [ "$scenario" = "pr-changed-scope-max-batches" ]; then
@@ -4143,6 +4233,17 @@ run_gate_case "runtime-env-forwarding" \
"gemini" \
""
+run_gate_case "github-models-api-key-forwarding" \
+ "github/gpt-5.4" \
+ "" \
+ "0" \
+ "scan ok" \
+ "1" \
+ "github/gpt-5.4" \
+ "" \
+ "github" \
+ ""
+
run_gate_case "vertex-primary-notfound-fallback-success" \
"vertex_ai/missing-primary" \
"vertex_ai/fallback-one vertex_ai/fallback-two" \
@@ -4255,6 +4356,36 @@ run_gate_case "nonvertex-slash-model-passthrough" \
"foo/bar" \
"https://example.invalid"
+run_gate_case "github-primary-route-error-fallback-success" \
+ "github/missing-primary" \
+ "" \
+ "0" \
+ "Strix quick scan succeeded with fallback model 'github/fallback-one'." \
+ "2" \
+ "github/missing-primary|github/fallback-one" \
+ "|" \
+ "github" \
+ "" \
+ "" \
+ "0" \
+ "CRITICAL" \
+ "0" \
+ "" \
+ "" \
+ "1200" \
+ "0" \
+ "" \
+ "" \
+ "" \
+ "" \
+ "0" \
+ "" \
+ "" \
+ "" \
+ "__UNSET__" \
+ "" \
+ "github/fallback-one github/fallback-two"
+
run_gate_case "primary-duplicate-in-fallback" \
"missing-primary" \
"vertex_ai/missing-primary fallback-one" \
@@ -4417,6 +4548,52 @@ run_gate_case "gemini-generic-fallback-success" \
"__UNSET__" \
"gemini/fallback-one gemini/fallback-two"
+run_gate_case "gemini-primary-badrequest-fallback-success" \
+ "gemini/badrequest-primary" \
+ "gemini/fallback-one gemini/fallback-two" \
+ "0" \
+ "scan ok after gemini badrequest fallback" \
+ "2" \
+ "gemini/badrequest-primary|gemini/fallback-one" \
+ "https://example.invalid|https://example.invalid"
+
+run_gate_case "gemini-badrequest-low-report-reaches-fallback" \
+ "gemini/badrequest-low-primary" \
+ "gemini/fallback-one gemini/fallback-two" \
+ "0" \
+ "scan ok after gemini badrequest fallback" \
+ "2" \
+ "gemini/badrequest-low-primary|gemini/fallback-one" \
+ "https://example.invalid|https://example.invalid"
+
+run_gate_case "gemini-badrequest-threshold-report-blocks-fallback-success" \
+ "gemini/badrequest-medium-primary" \
+ "gemini/fallback-one gemini/fallback-two" \
+ "1" \
+ "Strix threshold findings were reported before fallback success; failing closed." \
+ "2" \
+ "gemini/badrequest-medium-primary|gemini/fallback-one" \
+ "https://example.invalid|https://example.invalid" \
+ "vertex_ai" \
+ "__DEFAULT__" \
+ "" \
+ "0" \
+ "MEDIUM"
+
+run_gate_case "gemini-badrequest-inline-threshold-blocks-fallback-success" \
+ "gemini/badrequest-inline-medium-primary" \
+ "gemini/fallback-one gemini/fallback-two" \
+ "1" \
+ "Strix threshold findings were reported before fallback success; failing closed." \
+ "2" \
+ "gemini/badrequest-inline-medium-primary|gemini/fallback-one" \
+ "https://example.invalid|https://example.invalid" \
+ "vertex_ai" \
+ "__DEFAULT__" \
+ "" \
+ "0" \
+ "MEDIUM"
+
run_gate_case "gemini-zero-findings-timeout-fallback-allows-pr" \
"gemini/zero-timeout-primary" \
"gemini/fallback-one" \
@@ -4438,6 +4615,27 @@ run_gate_case "gemini-zero-findings-timeout-fallback-allows-pr" \
"pull_request" \
"sync-module-system/smart-crawling-biz/src/main/java/org/empasy/sync/modules/system/controller/SysPositionController.java"
+ run_gate_case "gemini-pr-total-budget-zero-timeout-blocks-pr" \
+ "gemini/zero-slow-timeout-primary" \
+ "gemini/fallback-one" \
+ "1" \
+ "Strix quick scan failed with a non-recoverable error." \
+ "1" \
+ "gemini/zero-slow-timeout-primary" \
+ "https://example.invalid" \
+ "vertex_ai" \
+ "__DEFAULT__" \
+ "" \
+ "0" \
+ "CRITICAL" \
+ "0" \
+ "" \
+ "" \
+ "10" \
+ "10" \
+ "pull_request" \
+ "sync-module-system/smart-crawling-biz/src/main/java/org/empasy/sync/modules/system/controller/SysPositionController.java"
+
run_gate_case "pr-batch-zero-finding-does-not-leak" \
"gemini/batch-zero-leak-primary" \
"" \