diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 66be927b56..b7fb59bfe4 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -2651,6 +2651,20 @@ run_strix_once() { if ! resolved_target_path="$(resolve_current_target_path "$TARGET_PATH")"; then return 1 fi + # contextual-orchestrator's gateway deliberately rejects any request that + # combines stream_options.include_usage=true with tools (a correctness + # guarantee against silently-incomplete usage accounting; out of scope to + # change here). Strix's agent loop always streams and always sends tools, + # so every call through that gateway hits the rejection immediately. + # Strix itself ships an opt-in for exactly this: LLM_DISABLE_STREAMING=true + # makes each turn a single non-streaming get_response (stream:false on the + # wire, so stream_options is never sent) replayed as one terminal stream + # event; nothing else about the run loop changes. Scope it narrowly to the + # contextual-orchestrator loopback so other providers keep real streaming. + local strix_disable_streaming="false" + if is_contextual_orchestrator_api_base "$llm_api_base_value"; then + strix_disable_streaming="true" + fi local start_epoch start_epoch="$(date +%s)" local child_llm_api_key="" @@ -2682,6 +2696,7 @@ run_strix_once() { STRIX_CHILD_EXECUTABLE_ROOT="$STRIX_EXECUTABLE_ROOT" \ STRIX_CHILD_EXECUTABLE_SHA256="$STRIX_EXECUTABLE_SHA256" \ STRIX_CHILD_REQUIRE_EXECUTABLE_INTEGRITY="${IS_PR_EVIDENCE_RUN:-false}" \ + STRIX_CHILD_DISABLE_STREAMING="$strix_disable_streaming" \ python3 - "$timeout_seconds" "$resolved_target_path" "$SCAN_MODE" "$STRIX_LOG" "$STRIX_SCAN_WORKING_DIR" <<'PY' import hashlib import hmac @@ -2736,6 +2751,12 @@ child_env["LLM_MODEL"] = os.environ["STRIX_CHILD_MODEL"] if os.environ.get("STRIX_CHILD_LLM_API_KEY"): child_env["LLM_API_KEY"] = os.environ["STRIX_CHILD_LLM_API_KEY"] child_env["STRIX_REPORTS_DIR"] = os.environ["STRIX_CHILD_REPORTS_DIR"] +if os.environ.get("STRIX_CHILD_DISABLE_STREAMING", "").strip().lower() == "true": + # See the comment above strix_disable_streaming's assignment in bash: + # this routes only the contextual-orchestrator gateway through Strix's + # own non-streaming fallback so stream_options is never sent alongside + # tools, without touching how Strix talks to any other provider. + child_env["LLM_DISABLE_STREAMING"] = "true" for key, value in os.environ.items(): if key.startswith("FAKE_STRIX_") and value: child_env[key] = value diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b528e8bafc..afd2546e8c 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -327,6 +327,9 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$GATE_SCRIPT" 'child_env["PNPM_CONFIG_IGNORE_SCRIPTS"] = "true"' "strix gate child process disables pnpm lifecycle scripts" assert_file_contains "$GATE_SCRIPT" 'child_env["YARN_ENABLE_SCRIPTS"] = "false"' "strix gate child process disables yarn lifecycle scripts" assert_file_contains "$GATE_SCRIPT" 'child_env["PYTHONWARNINGS"] = "ignore:Pydantic serializer warnings:UserWarning:pydantic.main"' "strix gate child env narrowly filters the known third-party Pydantic serializer warning" + assert_file_contains "$GATE_SCRIPT" 'if is_contextual_orchestrator_api_base "$llm_api_base_value"; then' "strix gate scopes the non-streaming opt-in to the contextual-orchestrator loopback gateway" + assert_file_contains "$GATE_SCRIPT" 'STRIX_CHILD_DISABLE_STREAMING="$strix_disable_streaming"' "strix gate threads the streaming opt-in through to the child process environment" + assert_file_contains "$GATE_SCRIPT" 'child_env["LLM_DISABLE_STREAMING"] = "true"' "strix gate disables Strix's own SDK streaming for the contextual-orchestrator gateway, which rejects stream_options.include_usage alongside tools" assert_file_contains "$GATE_SCRIPT" '[[ "$normalized_changed_file" =~ ^backend/.+\.py$ ]]' "strix gate detects nested backend Python files for PR-scoped import context" assert_file_contains "$GATE_SCRIPT" '[[ "$normalized_changed_file" == scripts/ci/test_*.sh || "$normalized_changed_file" == scripts/ci/*_test.sh ]]' "strix gate excludes large CI test harness scripts from model scan input" assert_file_contains "$GATE_SCRIPT" "Materialized PR-head changed-file scope for Strix scan" "strix gate avoids copying the full PR head tree into privileged scan targets by default" @@ -3289,7 +3292,7 @@ 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;PYTHONWARNINGS=%s;NPM_CONFIG_IGNORE_SCRIPTS=%s;PNPM_CONFIG_IGNORE_SCRIPTS=%s;YARN_ENABLE_SCRIPTS=%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;PYTHONWARNINGS=%s;NPM_CONFIG_IGNORE_SCRIPTS=%s;PNPM_CONFIG_IGNORE_SCRIPTS=%s;YARN_ENABLE_SCRIPTS=%s;UNRELATED_SECRET=%s;LLM_DISABLE_STREAMING=%s\n' \ "${LLM_TIMEOUT:-}" \ "${STRIX_MEMORY_COMPRESSOR_TIMEOUT:-}" \ "${STRIX_REASONING_EFFORT:-}" \ @@ -3299,7 +3302,8 @@ if [ -n "${FAKE_STRIX_RUNTIME_ENV_LOG:-}" ]; then "${NPM_CONFIG_IGNORE_SCRIPTS:-}" \ "${PNPM_CONFIG_IGNORE_SCRIPTS:-}" \ "${YARN_ENABLE_SCRIPTS:-}" \ - "${UNRELATED_SECRET:-}" >> "${FAKE_STRIX_RUNTIME_ENV_LOG:?}" + "${UNRELATED_SECRET:-}" \ + "${LLM_DISABLE_STREAMING:-}" >> "${FAKE_STRIX_RUNTIME_ENV_LOG:?}" fi target_path="" @@ -5929,6 +5933,13 @@ PY "$runtime_env_log" \ "LLM_TIMEOUT=90;STRIX_MEMORY_COMPRESSOR_TIMEOUT=10;STRIX_REASONING_EFFORT=minimal;STRIX_LLM_MAX_RETRIES=1;GEMINI_LOCATION=GLOBAL;PYTHONWARNINGS=ignore:Pydantic serializer warnings:UserWarning:pydantic.main;NPM_CONFIG_IGNORE_SCRIPTS=true;PNPM_CONFIG_IGNORE_SCRIPTS=true;YARN_ENABLE_SCRIPTS=false;UNRELATED_SECRET=" \ "scenario=$scenario runtime env forwarding" + # Non-contextual-orchestrator providers (gemini here) never see the + # stream-disabling opt-in: it is scoped narrowly to the gateway that + # rejects stream_options.include_usage alongside tools. + assert_file_contains \ + "$runtime_env_log" \ + "LLM_DISABLE_STREAMING=" \ + "scenario=$scenario non-gateway providers keep real streaming" fi if [ "$scenario" = "custom-openai-compatible-preserves-effort" ]; then assert_file_contains \ @@ -5936,6 +5947,16 @@ PY "STRIX_REASONING_EFFORT=minimal" \ "scenario=$scenario custom compatible endpoint effort" fi + if [ "$scenario" = "contextual-orchestrator-gateway-model-qualification" ]; then + # contextual-orchestrator rejects stream_options.include_usage=true + # alongside tools; Strix's agent loop always sends both, so the gate + # routes this gateway through Strix's own LLM_DISABLE_STREAMING opt-in + # (single non-streaming get_response per turn) instead of streaming. + assert_file_contains \ + "$runtime_env_log" \ + "LLM_DISABLE_STREAMING=true" \ + "scenario=$scenario contextual-orchestrator gateway disables SDK streaming to avoid the stream_options+tools rejection" + fi if [ "$scenario" = "report-known-internal-warning-sanitized" ]; then assert_file_not_contains \