-
Notifications
You must be signed in to change notification settings - Fork 0
fix(strix): classify ModelBehaviorError as backend-unavailable #1251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Strix ModelBehaviorError classifier: evidence and design record | ||
|
|
||
| ## Decision | ||
|
|
||
| Strix treats a trusted LiteLLM or OpenAI Agents SDK `ModelBehaviorError` as | ||
| provider/runtime abort evidence, not as a target-application vulnerability. | ||
| When the scan log also contains no `Vulnerabilities [1-9]` signal, the | ||
| required check may classify the run as a neutral skip so a malformed tool | ||
| call cannot block an otherwise empty scan. | ||
|
|
||
| This classifier exists because LineageWeave PR #74 job 95148793283 printed | ||
| `Vulnerabilities 0` and then failed closed on `ModelBehaviorError`. That | ||
| abort is an agents-SDK reaction to an invalid tool call, not a finding. | ||
|
|
||
| ## Trust boundary | ||
|
|
||
| The classifier accepts only a single bounded log line that contains both: | ||
|
|
||
| 1. a trusted SDK/provider marker (`litellm.exceptions.<Name>Error` or | ||
| `agents.exceptions.ModelBehaviorError`); and | ||
| 2. the `ModelBehaviorError` exception name. | ||
|
|
||
| It does not assemble those signals from different lines. A source literal | ||
| that merely mentions `ModelBehaviorError`, an application traceback without | ||
| the SDK marker, or any `Vulnerabilities [1-9]` / `severity:` finding remains | ||
| blocking. Real findings are never downgraded. | ||
|
|
||
| The gate also routes the same classifier into infrastructure detection, | ||
| cross-model fallback, and same-model retry. A catalog 404 is not retried on | ||
| the same model; a `ModelBehaviorError` may be, because the next sample from | ||
| the same model can emit a valid tool call. | ||
|
|
||
| ## Verification contract | ||
|
|
||
| Regression evidence proves that: | ||
|
|
||
| 1. the agents-SDK abort observed in required CI is recognized; | ||
| 2. a LiteLLM-wrapped `APIError: ModelBehaviorError` is recognized; | ||
| 3. a source literal without the SDK marker is not recognized; | ||
| 4. provider context and `ModelBehaviorError` on different lines are not | ||
| recognized; | ||
| 5. `Vulnerabilities [1-9]` prevents neutralization; | ||
| 6. the classifier is wired into infrastructure, retry, and same-model retry; | ||
| and | ||
| 7. the required-workflow smoke contract pins the workflow and gate strings. | ||
|
|
||
| ## Limitations | ||
|
|
||
| This change does not treat arbitrary model errors as success. It does not | ||
| weaken Strix severity, changed-file attribution, incomplete-scan fail-closed | ||
| behavior, or independent approval requirements. NVIDIA NIM catalog 404s stay | ||
| on their own same-line classifier. | ||
|
|
||
| ## References | ||
|
|
||
| Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC | ||
| 9110). Internet Engineering Task Force. https://doi.org/10.17487/RFC9110 | ||
|
|
||
| OpenAI. (2026). *Agents SDK exceptions*. OpenAI Agents SDK. | ||
| https://openai.github.io/openai-agents-python/ref/exceptions/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| """Regression contract for Strix ModelBehaviorError infrastructure flakes. | ||
|
|
||
| Strix runs through the OpenAI Agents SDK. When a model emits a malformed | ||
| tool call the SDK raises ``agents.exceptions.ModelBehaviorError`` and the | ||
| scan aborts. LiteLLM wraps the same abort as | ||
| ``litellm.exceptions.APIError: ... ModelBehaviorError``. Job 95148793283 | ||
| on LineageWeave PR #74 printed ``Vulnerabilities 0`` then failed the | ||
| required check because the outer workflow did not classify that abort as | ||
| backend-unavailable. | ||
|
|
||
| This contract keeps that flake as a neutral skip when -- and only when -- | ||
| a trusted LiteLLM or agents SDK marker shares a physical log line with | ||
| ``ModelBehaviorError`` and no ``Vulnerabilities [1-9]`` signal is present. | ||
| Target-source text, cross-line assembly, and real findings stay fail-closed. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| import subprocess | ||
| import tempfile | ||
| import unittest | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| REPOSITORY_ROOT = Path(__file__).resolve().parents[1] | ||
| STRIX_GATE = REPOSITORY_ROOT / "scripts" / "ci" / "strix_quick_gate.sh" | ||
| STRIX_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "strix.yml" | ||
|
|
||
| AGENTS_SDK_MODEL_BEHAVIOR_ERROR = ( | ||
| "Model nvidia_nim/nvidia/nemotron-3-super-120b-a12b\n" | ||
| "Vulnerabilities 0\n" | ||
| "agents.exceptions.ModelBehaviorError: Tool create_vulnerability_report " | ||
| "not found in agent strix\n" | ||
| "Strix run failed for model 'nvidia_nim/nvidia/nemotron-3-super-120b-a12b' " | ||
| "after 41s (exit code 1).\n" | ||
| "Strix scan failed after provider infrastructure or failure-signal " | ||
| "output; failing closed.\n" | ||
| ) | ||
|
|
||
| LITELLM_WRAPPED_MODEL_BEHAVIOR_ERROR = ( | ||
| "litellm.exceptions.APIError: ModelBehaviorError - invalid tool call\n" | ||
| "Vulnerabilities 0\n" | ||
| ) | ||
|
|
||
|
|
||
| def _function_block(source: str, function_name: str) -> str: | ||
| """Return one top-level Bash function, including its closing brace.""" | ||
|
|
||
| match = re.search( | ||
| rf"(?ms)^{re.escape(function_name)}\(\) {{\n.*?^}}\n", | ||
| source, | ||
| ) | ||
| if match is None: | ||
| raise AssertionError(f"missing Bash function: {function_name}") | ||
| return match.group(0) | ||
|
|
||
|
|
||
| def _classifies_as_model_behavior_error(log_text: str) -> bool: | ||
| """Execute the production classifier against a bounded synthetic log.""" | ||
|
|
||
| gate_source = STRIX_GATE.read_text(encoding="utf-8") | ||
| function_source = _function_block(gate_source, "is_model_behavior_error") | ||
| with tempfile.TemporaryDirectory(prefix="strix-model-behavior-") as temp_dir: | ||
| log_path = Path(temp_dir) / "strix.log" | ||
| log_path.write_text(log_text, encoding="utf-8") | ||
| script = "\n".join( | ||
| ( | ||
| "set -euo pipefail", | ||
| 'STRIX_LOG="$1"', | ||
| function_source, | ||
| "is_model_behavior_error", | ||
| ) | ||
| ) | ||
| completed = subprocess.run( | ||
| ["bash", "-c", script, "strix-classifier", str(log_path)], | ||
| check=False, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| if completed.returncode not in {0, 1}: | ||
| raise AssertionError(completed.stderr) | ||
| return completed.returncode == 0 | ||
|
|
||
|
|
||
| def _workflow_signal_pattern(workflow: str, variable_name: str) -> str: | ||
| """Extract one single-quoted POSIX ERE assigned in the Strix workflow.""" | ||
|
|
||
| match = re.search( | ||
| rf"(?m)^\s+{re.escape(variable_name)}='([^']+)'$", | ||
| workflow, | ||
| ) | ||
| if match is None: | ||
| raise AssertionError(f"missing workflow signal: {variable_name}") | ||
| return match.group(1) | ||
|
|
||
|
|
||
| def _workflow_neutralizes(log_text: str) -> bool: | ||
| """Execute the outer workflow's backend-neutralization condition.""" | ||
|
|
||
| workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") | ||
| backend_pattern = _workflow_signal_pattern(workflow, "backend_unavailable_signal") | ||
| vulnerability_pattern = _workflow_signal_pattern( | ||
| workflow, "reported_vulnerability_signal" | ||
| ) | ||
| with tempfile.TemporaryDirectory(prefix="strix-model-behavior-wf-") as temp_dir: | ||
| log_path = Path(temp_dir) / "strix.log" | ||
| log_path.write_text(log_text, encoding="utf-8") | ||
| backend = subprocess.run( | ||
| ["grep", "-Eiq", backend_pattern, str(log_path)], | ||
| check=False, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| vulnerability = subprocess.run( | ||
| ["grep", "-Eiq", vulnerability_pattern, str(log_path)], | ||
| check=False, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| if backend.returncode not in {0, 1}: | ||
| raise AssertionError(backend.stderr) | ||
| if vulnerability.returncode not in {0, 1}: | ||
| raise AssertionError(vulnerability.stderr) | ||
| return backend.returncode == 0 and vulnerability.returncode == 1 | ||
|
|
||
|
|
||
| class StrixModelBehaviorErrorIsNeutralTests(unittest.TestCase): | ||
| """Protect the PR #74-shaped ModelBehaviorError flake without weakening the gate.""" | ||
|
|
||
| def test_agents_sdk_model_behavior_error_is_classified(self) -> None: | ||
| self.assertTrue(_classifies_as_model_behavior_error(AGENTS_SDK_MODEL_BEHAVIOR_ERROR)) | ||
|
|
||
| def test_litellm_wrapped_model_behavior_error_is_classified(self) -> None: | ||
| self.assertTrue( | ||
| _classifies_as_model_behavior_error(LITELLM_WRAPPED_MODEL_BEHAVIOR_ERROR) | ||
| ) | ||
|
|
||
| def test_source_literal_without_trusted_sdk_context_is_not_classified(self) -> None: | ||
| log = "source literal: ModelBehaviorError\nVulnerabilities 0\n" | ||
| self.assertFalse(_classifies_as_model_behavior_error(log)) | ||
|
|
||
| def test_provider_and_exception_must_share_one_log_line(self) -> None: | ||
| log = ( | ||
| "litellm.exceptions.APIError: provider unavailable\n" | ||
| "ModelBehaviorError\n" | ||
| "Vulnerabilities 0\n" | ||
| ) | ||
| self.assertFalse(_classifies_as_model_behavior_error(log)) | ||
|
|
||
| def test_classifier_is_wired_into_infrastructure_retry_and_same_model_retry(self) -> None: | ||
| gate_source = STRIX_GATE.read_text(encoding="utf-8") | ||
| infrastructure = _function_block(gate_source, "has_detected_infrastructure_error") | ||
| retryable = _function_block(gate_source, "is_model_retryable_error") | ||
| same_model_retry = _function_block( | ||
| gate_source, "is_transient_same_model_retry_error" | ||
| ) | ||
|
|
||
| self.assertIn("is_model_behavior_error", infrastructure) | ||
| self.assertIn("is_model_behavior_error", retryable) | ||
| self.assertIn("is_model_behavior_error", same_model_retry) | ||
|
|
||
| def test_workflow_neutralizes_agents_sdk_abort_with_zero_findings(self) -> None: | ||
| self.assertTrue(_workflow_neutralizes(AGENTS_SDK_MODEL_BEHAVIOR_ERROR)) | ||
|
|
||
| def test_workflow_neutralizes_litellm_wrapped_abort_with_zero_findings(self) -> None: | ||
| self.assertTrue(_workflow_neutralizes(LITELLM_WRAPPED_MODEL_BEHAVIOR_ERROR)) | ||
|
|
||
| def test_workflow_rejects_source_literal_without_trusted_context(self) -> None: | ||
| self.assertFalse( | ||
| _workflow_neutralizes("source literal: ModelBehaviorError\nVulnerabilities 0\n") | ||
| ) | ||
|
|
||
| def test_workflow_rejects_cross_line_signal_assembly(self) -> None: | ||
| self.assertFalse( | ||
| _workflow_neutralizes( | ||
| "litellm.exceptions.APIError: provider unavailable\n" | ||
| "ModelBehaviorError\n" | ||
| "Vulnerabilities 0\n" | ||
| ) | ||
| ) | ||
|
|
||
| def test_still_fails_closed_when_a_real_vulnerability_is_also_reported(self) -> None: | ||
| log = AGENTS_SDK_MODEL_BEHAVIOR_ERROR + ( | ||
| "Vulnerability Report\nSeverity: CRITICAL\nVulnerabilities 1\n" | ||
| ) | ||
| self.assertFalse(_workflow_neutralizes(log)) | ||
| self.assertFalse( | ||
| _workflow_neutralizes( | ||
| "litellm.exceptions.APIError: ModelBehaviorError - invalid tool call\n" | ||
| "Vulnerabilities 1\n" | ||
| ) | ||
| ) | ||
|
|
||
| def test_workflow_keeps_fail_closed_vulnerability_evidence_contract(self) -> None: | ||
| workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") | ||
| self.assertIn("ModelBehaviorError", workflow) | ||
| self.assertIn(r"agents\.exceptions\.ModelBehaviorError", workflow) | ||
| self.assertIn("reported_vulnerability_signal", workflow) | ||
| self.assertIn("Vulnerabilities[[:space:]]+[1-9]", workflow) | ||
| self.assertIn( | ||
| '! grep -Eiq "$reported_vulnerability_signal"', | ||
| workflow, | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.