diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f3e3c24996..d66979d406 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -197,7 +197,6 @@ jobs: fi - name: Enforce Cloudflare Pingora edge policy - if: ${{ github.event_name == 'pull_request_target' }} env: GITHUB_TOKEN: ${{ github.token }} TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eab104fc2..fa8778d695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Keep the required OpenCode bootstrap's Pingora policy step unconditional + within its pull-request-only workflow, so the static bootstrap contract does + not depend on event payload fields. (Ported from #1414, not yet merged, so + this PR's own trusted-source cycle no longer needs #1414 to merge first.) - Skip trusted base Python lock materialization for exact-head reviews with no Python source or dependency-manifest changes, while preserving the fail-closed wheel-only path when Python coverage is relevant. diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 1e0630b301..c4cd33cfa7 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -322,7 +322,8 @@ is_vertex_model() { is_contextual_orchestrator_model() { case "$1" in - orchestrator/free | contextual-orchestrator/orchestrator/free) + orchestrator/free | contextual-orchestrator/orchestrator/free | \ + orchestrator/auto | contextual-orchestrator/orchestrator/auto) return 0 ;; *) @@ -2561,12 +2562,14 @@ child_model_for_api_base() { local llm_api_base_value="$2" # LiteLLM requires an explicit provider prefix even when the gateway is an - # OpenAI-compatible local endpoint. Keep the public gateway model name, but - # qualify only the child process model so the request still carries - # orchestrator/free to contextual-orchestrator. + # OpenAI-compatible local endpoint. Strip only the connector-facing alias so + # the selected orchestrator/free or orchestrator/auto virtual pool reaches + # contextual-orchestrator unchanged. if is_contextual_orchestrator_model "$model" && is_contextual_orchestrator_api_base "$llm_api_base_value"; then - printf '%s\n' 'openai/orchestrator/free' + local contextual_orchestrator_model + contextual_orchestrator_model="${model#contextual-orchestrator/}" + printf 'openai/%s\n' "$contextual_orchestrator_model" return 0 fi diff --git a/tests/test_pingora_edge_workflow_contract.py b/tests/test_pingora_edge_workflow_contract.py index 82a85986a0..ad0667cc6b 100644 --- a/tests/test_pingora_edge_workflow_contract.py +++ b/tests/test_pingora_edge_workflow_contract.py @@ -40,7 +40,8 @@ def test_required_workflow_enforces_pingora_without_executing_pr_content() -> No assert '[ -L "$trusted_source_dir/$EXPECTED_FILE" ]' in text assert '[ ! -f "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ]' in text assert '[ -L "$trusted_source_dir/scripts/ci/pingora_edge_policy.py" ]' in text - assert "if: ${{ github.event_name == 'pull_request_target' }}" in text + assert "pull_request_target:" in text + assert "if: ${{ github.event_name == 'pull_request_target' }}" not in text assert text.index("Verify immutable central policy source") < text.index( "Enforce Cloudflare Pingora edge policy" ) diff --git a/tests/test_strix_openai_fallback_api_base.py b/tests/test_strix_openai_fallback_api_base.py index 57988048e9..4b20d35806 100644 --- a/tests/test_strix_openai_fallback_api_base.py +++ b/tests/test_strix_openai_fallback_api_base.py @@ -145,6 +145,43 @@ def _resolve_api_base(env: dict[str, str], model: str) -> tuple[int, str]: return completed.returncode, completed.stdout.strip() +def _resolve_child_model(model: str, api_base: str) -> tuple[int, str]: + """Execute the production child-model qualifier for one gateway route.""" + + gate_source = STRIX_GATE.read_text(encoding="utf-8") + helper_sources = [ + _function_block(gate_source, "is_contextual_orchestrator_model"), + _function_block(gate_source, "is_contextual_orchestrator_api_base"), + _function_block(gate_source, "is_github_models_api_base"), + _function_block(gate_source, "child_model_for_api_base"), + ] + with tempfile.TemporaryDirectory(prefix="strix-gateway-child-model-") as temp_dir: + completed = subprocess.run( + [ + "bash", + "-c", + "\n".join( + [ + "set -euo pipefail", + *helper_sources, + 'child_model_for_api_base "$1" "$2"', + ] + ), + "strix-child-model", + model, + api_base, + ], + check=False, + capture_output=True, + text=True, + env={ + "PATH": "/usr/bin:/bin:/usr/local/bin", + "HOME": temp_dir, + }, + ) + return completed.returncode, completed.stdout.strip() + + class ExplicitOpenAIFallbackRouting(unittest.TestCase): """Direct-OpenAI fallbacks must not inherit the primary provider base.""" @@ -269,21 +306,52 @@ def test_workflow_does_not_configure_an_external_fallback(self) -> None: self.assertIn("Provision contextual-orchestrator Strix sidecar", workflow) def test_workflow_gateway_base_is_the_only_http_exception(self) -> None: - """The local sidecar is accepted without allowing arbitrary HTTP bases.""" + """Both gateway pools accept only the pinned process-local HTTP base.""" - rc, api_base = _resolve_api_base( - {"LLM_API_BASE_FILE": "http://127.0.0.1:18080/v1"}, + for model in ( "orchestrator/free", - ) - self.assertEqual(rc, 0) - self.assertEqual(api_base, "http://127.0.0.1:18080/v1") + "contextual-orchestrator/orchestrator/free", + "orchestrator/auto", + "contextual-orchestrator/orchestrator/auto", + ): + with self.subTest(model=model): + rc, api_base = _resolve_api_base( + {"LLM_API_BASE_FILE": "http://127.0.0.1:18080/v1"}, + model, + ) + self.assertEqual(rc, 0) + self.assertEqual(api_base, "http://127.0.0.1:18080/v1") rc, _ = _resolve_api_base( {"LLM_API_BASE_FILE": "http://127.0.0.1:18081/v1"}, - "orchestrator/free", + "orchestrator/auto", ) self.assertEqual(rc, 2) + rc, _ = _resolve_api_base( + {"LLM_API_BASE_FILE": "http://127.0.0.1:18080/v1"}, + "orchestrator/unknown", + ) + self.assertEqual(rc, 2) + + def test_gateway_child_model_preserves_selected_virtual_pool(self) -> None: + """LiteLLM qualification must not rewrite auto back to free.""" + + expected_child_models = { + "orchestrator/free": "openai/orchestrator/free", + "contextual-orchestrator/orchestrator/free": "openai/orchestrator/free", + "orchestrator/auto": "openai/orchestrator/auto", + "contextual-orchestrator/orchestrator/auto": "openai/orchestrator/auto", + } + for model, expected_child_model in expected_child_models.items(): + with self.subTest(model=model): + rc, child_model = _resolve_child_model( + model, + "http://127.0.0.1:18080/v1", + ) + self.assertEqual(rc, 0) + self.assertEqual(child_model, expected_child_model) + def test_manual_status_job_has_status_write_permission(self) -> None: """OIDC target-app exchange may request the target commit status scope."""