Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c9f56fcd5 | |||
| 8ca83e4c16 | |||
| 94c361cbb6 |
@@ -11,12 +11,13 @@ repos:
|
||||
|
||||
# MyPy for static type checking
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.16.0
|
||||
rev: v1.19.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [
|
||||
types-requests,
|
||||
types-python-dateutil,
|
||||
types-Pygments,
|
||||
pydantic,
|
||||
fastapi,
|
||||
pytest,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
# Strix
|
||||
|
||||
### The open-source AI pentesting tool. Autonomous AI hackers that find and fix your app’s vulnerabilities.
|
||||
### Open-source AI hackers to find and fix your app’s vulnerabilities.
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -40,15 +40,15 @@
|
||||
|
||||
## Strix Overview
|
||||
|
||||
Strix are autonomous AI penetration testing agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
|
||||
Strix are autonomous AI agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
|
||||
|
||||
**Key Capabilities:**
|
||||
|
||||
- **Full pentesting toolkit** - reconnaissance, exploitation, and validation out of the box
|
||||
- **Multi-agent orchestration** - teams of AI pentesters that collaborate and scale
|
||||
- **Real exploit validation** - working PoCs, not false positives like legacy vulnerability scanners
|
||||
- **Developer‑first CLI** - actionable findings with remediation guidance
|
||||
- **Auto‑fix & reporting** - generate patches and compliance-ready pentest reports
|
||||
- **Full hacker toolkit** out of the box
|
||||
- **Teams of agents** that collaborate and scale
|
||||
- **Real validation** with PoCs, not false positives
|
||||
- **Developer‑first** CLI with actionable reports
|
||||
- **Auto‑fix & reporting** to accelerate remediation
|
||||
|
||||
|
||||
<br>
|
||||
@@ -95,13 +95,13 @@ strix --target ./app-directory
|
||||
|
||||
## ☁️ Strix Platform
|
||||
|
||||
Try the Strix full-stack penetration testing platform at **[app.strix.ai](https://app.strix.ai)** - sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
Try the Strix full-stack security platform at **[app.strix.ai](https://app.strix.ai)** — sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
|
||||
- **Validated findings with PoCs** - every vulnerability includes a working proof-of-concept exploit and reproduction steps
|
||||
- **One-click autofix** - AI-generated security patches as ready-to-merge pull requests
|
||||
- **Continuous pentesting** - always-on vulnerability scanning that keeps pace with your deployments
|
||||
- **DevSecOps integrations** - GitHub, GitLab, Bitbucket, Slack, Jira, Linear, and CI/CD pipelines
|
||||
- **Continuous learning** - AI that builds on past findings, adapts to your codebase, and reduces false positives over time
|
||||
- **Validated findings with PoCs** and reproduction steps
|
||||
- **One-click autofix** as ready-to-merge pull requests
|
||||
- **Continuous monitoring** across code, cloud, and infrastructure
|
||||
- **Integrations** with GitHub, Slack, Jira, Linear, and CI/CD pipelines
|
||||
- **Continuous learning** that builds on past findings and remediations
|
||||
|
||||
[**Start your first pentest →**](https://app.strix.ai)
|
||||
|
||||
@@ -109,38 +109,37 @@ Try the Strix full-stack penetration testing platform at **[app.strix.ai](https:
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Agentic Pentesting Tools
|
||||
### Agentic Security Tools
|
||||
|
||||
Strix agents come equipped with a comprehensive offensive security toolkit - the same tools used by professional penetration testers and ethical hackers:
|
||||
Strix agents come equipped with a comprehensive security testing toolkit:
|
||||
|
||||
- **HTTP Interception Proxy** - Full request/response manipulation and analysis with Caido
|
||||
- **Browser Exploitation** - Automated browser for testing XSS, CSRF, clickjacking, and auth bypass flows
|
||||
- **Shell & Command Execution** - Interactive terminal for exploit development and post-exploitation
|
||||
- **Custom Exploit Runtime** - Python sandbox for writing and validating proof-of-concept exploits
|
||||
- **Reconnaissance & OSINT** - Automated attack surface mapping, subdomain enumeration, and fingerprinting
|
||||
- **Static & Dynamic Code Analysis** - SAST + DAST capabilities for comprehensive application security testing
|
||||
- **Vulnerability Knowledge Base** - Structured findings with CVSS scoring and OWASP classification
|
||||
- **Full HTTP Proxy** - Full request/response manipulation and analysis
|
||||
- **Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows
|
||||
- **Terminal Environments** - Interactive shells for command execution and testing
|
||||
- **Python Runtime** - Custom exploit development and validation
|
||||
- **Reconnaissance** - Automated OSINT and attack surface mapping
|
||||
- **Code Analysis** - Static and dynamic analysis capabilities
|
||||
- **Knowledge Management** - Structured findings and attack documentation
|
||||
|
||||
### Comprehensive Vulnerability Scanner
|
||||
### Comprehensive Vulnerability Detection
|
||||
|
||||
Strix identifies, validates, and exploits a wide range of security vulnerabilities across the OWASP Top 10 and beyond:
|
||||
Strix can identify and validate a wide range of security vulnerabilities:
|
||||
|
||||
- **Broken Access Control** - IDOR, privilege escalation, auth bypass
|
||||
- **Injection Attacks** - SQL injection, NoSQL injection, OS command injection, SSTI
|
||||
- **Server-Side Vulnerabilities** - SSRF, XXE, insecure deserialization, RCE
|
||||
- **Client-Side Attacks** - XSS (stored/reflected/DOM), prototype pollution, CSRF
|
||||
- **Business Logic Flaws** - Race conditions, payment manipulation, workflow bypass
|
||||
- **Authentication & Session** - JWT attacks, session fixation, credential stuffing vectors
|
||||
- **Infrastructure & Cloud** - Misconfigurations, exposed services, cloud security issues
|
||||
- **API Security** - Broken authentication, mass assignment, rate limiting bypass
|
||||
- **Access Control** - IDOR, privilege escalation, auth bypass
|
||||
- **Injection Attacks** - SQL, NoSQL, command injection
|
||||
- **Server-Side** - SSRF, XXE, deserialization flaws
|
||||
- **Client-Side** - XSS, prototype pollution, DOM vulnerabilities
|
||||
- **Business Logic** - Race conditions, workflow manipulation
|
||||
- **Authentication** - JWT vulnerabilities, session management
|
||||
- **Infrastructure** - Misconfigurations, exposed services
|
||||
|
||||
### Graph of Agents (Multi-Agent Pentesting)
|
||||
### Graph of Agents
|
||||
|
||||
Advanced multi-agent orchestration for comprehensive automated penetration testing:
|
||||
Advanced multi-agent orchestration for comprehensive security testing:
|
||||
|
||||
- **Distributed Pentesting** - Specialized AI agents for recon, exploitation, and post-exploitation
|
||||
- **Scalable Security Testing** - Parallel execution across multiple targets for fast, comprehensive coverage
|
||||
- **Dynamic Coordination** - Agents share discoveries, chain vulnerabilities, and collaborate like a red team
|
||||
- **Distributed Workflows** - Specialized agents for different attacks and assets
|
||||
- **Scalable Testing** - Parallel execution for fast comprehensive coverage
|
||||
- **Dynamic Coordination** - Agents collaborate and share discoveries
|
||||
|
||||
---
|
||||
|
||||
@@ -183,7 +182,7 @@ strix -n --target ./ --scan-mode quick --scope-mode diff --diff-base origin/main
|
||||
|
||||
### Headless Mode
|
||||
|
||||
Run Strix programmatically without interactive UI using the `-n/--non-interactive` flag - perfect for servers and automated jobs. The CLI prints real-time vulnerability findings, and the final report before exiting. Exits with non-zero code when vulnerabilities are found.
|
||||
Run Strix programmatically without interactive UI using the `-n/--non-interactive` flag—perfect for servers and automated jobs. The CLI prints real-time vulnerability findings, and the final report before exiting. Exits with non-zero code when vulnerabilities are found.
|
||||
|
||||
```bash
|
||||
strix -n --target https://your-app.com
|
||||
@@ -240,19 +239,19 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
|
||||
|
||||
**Recommended models for best results:**
|
||||
|
||||
- [OpenAI GPT-5.4](https://openai.com/api/) - `openai/gpt-5.4`
|
||||
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) - `anthropic/claude-sonnet-4-6`
|
||||
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) - `vertex_ai/gemini-3-pro-preview`
|
||||
- [OpenAI GPT-5.4](https://openai.com/api/) — `openai/gpt-5.4`
|
||||
- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) — `anthropic/claude-sonnet-4-6`
|
||||
- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) — `vertex_ai/gemini-3-pro-preview`
|
||||
|
||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||
|
||||
## Enterprise Pentesting
|
||||
## Enterprise
|
||||
|
||||
Get the same Strix experience with [enterprise-grade](https://strix.ai/demo) controls: SSO (SAML/OIDC), custom compliance-ready penetration testing reports (SOC 2, ISO 27001, PCI DSS), dedicated support & SLA, custom deployment options (VPC/self-hosted), BYOK model support, and tailored AI pentesting agents optimized for your environment. [Learn more](https://strix.ai/demo).
|
||||
Get the same Strix experience with [enterprise-grade](https://strix.ai/demo) controls: SSO (SAML/OIDC), custom compliance reports, dedicated support & SLA, custom deployment options (VPC/self-hosted), BYOK model support, and tailored agents optimized for your environment. [Learn more](https://strix.ai/demo).
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at **[docs.strix.ai](https://docs.strix.ai)** - including detailed guides for usage, CI/CD integrations, skills, and advanced configuration.
|
||||
Full documentation is available at **[docs.strix.ai](https://docs.strix.ai)** — including detailed guides for usage, CI/CD integrations, skills, and advanced configuration.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -275,5 +274,3 @@ Strix builds on the incredible work of open-source projects like [LiteLLM](https
|
||||
> Only test apps you own or have permission to test. You are responsible for using Strix ethically and legally.
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Configuration"
|
||||
description: "Environment variables for Strix"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Configure Strix using environment variables or a config file.
|
||||
|
||||
## LLM Configuration
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Skills"
|
||||
description: "Specialized knowledge packages that enhance agent capabilities"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Skills are structured knowledge packages that give Strix agents deep expertise in specific vulnerability types, technologies, and testing methodologies.
|
||||
|
||||
## The Idea
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Introduction"
|
||||
description: "Managed security testing without local setup"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Contributing"
|
||||
description: "Contribute to Strix development"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Introduction"
|
||||
description: "Open-source AI hackers to secure your apps"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix are autonomous AI agents that act like real hackers—they run your code dynamically, find vulnerabilities, and validate them with proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
|
||||
|
||||
<Frame>
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "CI/CD Integration"
|
||||
description: "Run Strix in any CI/CD pipeline"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix runs in headless mode for automated pipelines.
|
||||
|
||||
## Headless Mode
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "GitHub Actions"
|
||||
description: "Run Strix security scans on every pull request"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Integrate Strix into your GitHub workflow to catch vulnerabilities before they reach production.
|
||||
|
||||
## Basic Workflow
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Anthropic"
|
||||
description: "Configure Strix with Claude models"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Azure OpenAI"
|
||||
description: "Configure Strix with OpenAI models via Azure"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "AWS Bedrock"
|
||||
description: "Configure Strix with models via AWS Bedrock"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Local Models"
|
||||
description: "Run Strix with self-hosted LLMs for privacy and air-gapped testing"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Running Strix with local models allows for completely offline, privacy-first security assessments. Data never leaves your machine, making this ideal for sensitive internal networks or air-gapped environments.
|
||||
|
||||
## Privacy vs Performance
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Novita AI"
|
||||
description: "Configure Strix with Novita AI models"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
[Novita AI](https://novita.ai) provides fast, cost-efficient inference for open-source models via an OpenAI-compatible API.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "OpenAI"
|
||||
description: "Configure Strix with OpenAI models"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "OpenRouter"
|
||||
description: "Configure Strix with models via OpenRouter"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
[OpenRouter](https://openrouter.ai) provides access to 100+ models from multiple providers through a single API.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Overview"
|
||||
description: "Configure your AI model for Strix"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibility, supporting 100+ LLM providers.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Google Vertex AI"
|
||||
description: "Configure Strix with Gemini models via Google Cloud"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Installation
|
||||
|
||||
Vertex AI requires the Google Cloud dependency. Install Strix with the vertex extra:
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Quick Start"
|
||||
description: "Install Strix and run your first security scan"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker (running)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export const ScarfPixel = () => (
|
||||
<img
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
src="https://static.scarf.sh/a.png?x-pxid=a0ba15dd-a205-4a54-95d6-7814e9ae6b61"
|
||||
alt=""
|
||||
width="1"
|
||||
height="1"
|
||||
style={{ position: "absolute", width: 0, height: 0, opacity: 0, pointerEvents: "none" }}
|
||||
/>
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
agentic
|
||||
Caido
|
||||
deobfuscation
|
||||
deserialization
|
||||
Devstral
|
||||
[Dd]ocstrings
|
||||
exfiltration
|
||||
failover
|
||||
ffuf
|
||||
Firestore
|
||||
frontmatter
|
||||
fuzzer
|
||||
gcloud
|
||||
hardcoded
|
||||
Kimi
|
||||
Langfuse
|
||||
LLMs?
|
||||
[Mm]isconfigurations?
|
||||
Novita
|
||||
Ollama
|
||||
pentest(ers|ing)?
|
||||
pipx
|
||||
pull_request
|
||||
Pydantic
|
||||
spidering
|
||||
SQLi
|
||||
[Ss]trix
|
||||
Supabase
|
||||
traceback
|
||||
UIs
|
||||
untrusted
|
||||
uv
|
||||
vulns
|
||||
[Ww]ordlist
|
||||
@@ -3,10 +3,6 @@ title: "Browser"
|
||||
description: "Playwright-powered Chrome for web application testing"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix uses a headless Chrome browser via Playwright to interact with web applications exactly like a real user would.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Agent Tools"
|
||||
description: "How Strix agents interact with targets"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix agents use specialized tools to test your applications like a real penetration tester would.
|
||||
|
||||
## Core Tools
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "HTTP Proxy"
|
||||
description: "Caido-powered proxy for request interception and replay"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix includes [Caido](https://caido.io), a modern HTTP proxy built for security testing. All browser traffic flows through Caido, giving the agent full control over requests and responses.
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Sandbox Tools"
|
||||
description: "Pre-installed security tools in the Strix container"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix runs inside a Kali Linux-based Docker container with a comprehensive set of security tools pre-installed. The agent can use any of these tools through the [terminal](/tools/terminal).
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Terminal"
|
||||
description: "Bash shell for running commands and security tools"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix has access to a persistent bash terminal running inside the Docker sandbox. This gives the agent access to all [pre-installed security tools](/tools/sandbox).
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "CLI Reference"
|
||||
description: "Command-line options for Strix"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Custom Instructions"
|
||||
description: "Guide Strix with custom testing instructions"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Use instructions to provide context, credentials, or focus areas for your scan.
|
||||
|
||||
## Inline Instructions
|
||||
|
||||
@@ -3,10 +3,6 @@ title: "Scan Modes"
|
||||
description: "Choose the right scan depth for your use case"
|
||||
---
|
||||
|
||||
import { ScarfPixel } from "/snippets/scarf-pixel.mdx";
|
||||
|
||||
<ScarfPixel />
|
||||
|
||||
Strix offers three scan modes to balance speed and thoroughness.
|
||||
|
||||
## Quick
|
||||
|
||||
@@ -59,6 +59,42 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
||||
),
|
||||
)
|
||||
|
||||
RECOMMENDED_MODEL_NAMES = (
|
||||
"openai/gpt-5.5",
|
||||
"openai/gpt-5.5-pro",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.4-pro",
|
||||
"openai/gpt-5.3-codex",
|
||||
"anthropic/claude-opus-4-8",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"vertex_ai/gemini-3.1-pro-preview",
|
||||
"gemini/gemini-3.1-pro-preview",
|
||||
"xai/grok-4.3",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"deepseek/deepseek-reasoner",
|
||||
"dashscope/qwen3-max-2026-01-23",
|
||||
"moonshot/kimi-k2.7-code",
|
||||
"moonshot/kimi-k2.6",
|
||||
"mistral/mistral-medium-3-5",
|
||||
"mistral/magistral-medium-latest",
|
||||
)
|
||||
|
||||
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
|
||||
|
||||
FRONTIER_MODEL_FAMILIES = (
|
||||
(("azure", "azure_ai", "bedrock_mantle", "openai"), ("gpt-5",)),
|
||||
(
|
||||
("anthropic", "azure_ai", "bedrock", "claude", "databricks", "snowflake", "vertex_ai"),
|
||||
("claude-opus-4", "claude-sonnet-4"),
|
||||
),
|
||||
(("google", "gemini", "vertex_ai"), ("gemini-3",)),
|
||||
(("xai", "x-ai"), ("grok-4",)),
|
||||
(("deepseek",), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
|
||||
(("alibaba", "dashscope", "qwen"), ("qwen3.7", "qwen3.5", "qwen3-max")),
|
||||
(("moonshot", "moonshotai", "kimi"), ("kimi-k2.7", "kimi-k2.6", "kimi-k2.5")),
|
||||
(("mistral", "mistralai"), ("mistral-medium-3-5", "magistral-medium")),
|
||||
)
|
||||
|
||||
|
||||
def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
"""Apply Strix config to SDK-native defaults."""
|
||||
@@ -154,6 +190,78 @@ def model_supports_reasoning(model_name: str) -> bool:
|
||||
return bool(entry and entry.get("supports_reasoning"))
|
||||
|
||||
|
||||
def is_recommended_or_frontier_model(model_name: str) -> bool:
|
||||
"""Return whether a model is recommended or in a frontier model family."""
|
||||
name = _normalized_model_name(model_name)
|
||||
if not name:
|
||||
return False
|
||||
if name in _RECOMMENDED_MODEL_NAME_SET:
|
||||
return True
|
||||
provider_name, bare_model_name = _split_model_provider(name)
|
||||
return any(
|
||||
_matches_frontier_family(provider_name, bare_model_name, provider_markers, prefixes)
|
||||
for provider_markers, prefixes in FRONTIER_MODEL_FAMILIES
|
||||
)
|
||||
|
||||
|
||||
def _normalized_model_name(model_name: str) -> str:
|
||||
name = model_name.strip().lower()
|
||||
for prefix in ("litellm/", "any-llm/"):
|
||||
if name.startswith(prefix):
|
||||
name = name[len(prefix) :]
|
||||
break
|
||||
return name
|
||||
|
||||
|
||||
def _split_model_provider(model_name: str) -> tuple[str | None, str]:
|
||||
if "/" not in model_name:
|
||||
return None, model_name
|
||||
provider_name, bare_model_name = model_name.rsplit("/", 1)
|
||||
return provider_name, bare_model_name
|
||||
|
||||
|
||||
def _matches_frontier_family(
|
||||
provider_name: str | None,
|
||||
model_name: str,
|
||||
provider_markers: tuple[str, ...],
|
||||
model_prefixes: tuple[str, ...],
|
||||
) -> bool:
|
||||
if not _matches_model_prefix(model_name, model_prefixes):
|
||||
return False
|
||||
if provider_name is None:
|
||||
return True
|
||||
return _contains_provider_marker(
|
||||
provider_name, provider_markers, split_compound_names=True
|
||||
) or _contains_provider_marker(model_name, provider_markers)
|
||||
|
||||
|
||||
def _matches_model_prefix(model_name: str, model_prefixes: tuple[str, ...]) -> bool:
|
||||
return any(
|
||||
candidate.startswith(prefix)
|
||||
for candidate in _model_name_candidates(model_name)
|
||||
for prefix in model_prefixes
|
||||
)
|
||||
|
||||
|
||||
def _model_name_candidates(model_name: str) -> tuple[str, ...]:
|
||||
if "." not in model_name:
|
||||
return (model_name,)
|
||||
suffixes = tuple(
|
||||
model_name.split(".", index)[-1] for index in range(1, model_name.count(".") + 1)
|
||||
)
|
||||
return (model_name, *suffixes)
|
||||
|
||||
|
||||
def _contains_provider_marker(
|
||||
value: str, provider_markers: tuple[str, ...], *, split_compound_names: bool = False
|
||||
) -> bool:
|
||||
parts = set(value.replace(".", "/").split("/"))
|
||||
if split_compound_names:
|
||||
for separator in ("_", "-"):
|
||||
parts.update(piece for part in tuple(parts) for piece in part.split(separator))
|
||||
return any(marker in parts for marker in provider_markers)
|
||||
|
||||
|
||||
def is_known_openai_bare_model(model_name: str) -> bool:
|
||||
import litellm
|
||||
|
||||
|
||||
+4
-1
@@ -28,7 +28,10 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
|
||||
def __init__(self, *, model: str, max_budget_usd: float | None = None) -> None:
|
||||
import math
|
||||
if max_budget_usd is not None and (not math.isfinite(max_budget_usd) or max_budget_usd <= 0):
|
||||
|
||||
if max_budget_usd is not None and (
|
||||
not math.isfinite(max_budget_usd) or max_budget_usd <= 0
|
||||
):
|
||||
raise ValueError("max_budget_usd must be a finite number greater than 0")
|
||||
self._model = model
|
||||
self._max_budget_usd = max_budget_usd
|
||||
|
||||
+23
-20
@@ -136,27 +136,30 @@ def child_initial_input(
|
||||
task: str,
|
||||
parent_history: list[Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build the initial input for a child agent as a single user message.
|
||||
|
||||
Collapsing the inherited-context block, the identity line, and the task into
|
||||
one ``{"role": "user"}`` message keeps providers that require strictly
|
||||
alternating roles (e.g. Perplexity, llama.cpp) from rejecting consecutive
|
||||
user messages.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
initial_input: list[dict[str, Any]] = []
|
||||
if parent_history:
|
||||
rendered = json.dumps(parent_history, ensure_ascii=False, default=str)
|
||||
parts.append(
|
||||
"== Inherited context from parent (background only) ==\n"
|
||||
f"{rendered}\n"
|
||||
"== End of inherited context ==\n"
|
||||
"Use the above as background only; do not continue the "
|
||||
"parent's work. Your task follows.",
|
||||
initial_input.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"== Inherited context from parent (background only) ==\n"
|
||||
f"{rendered}\n"
|
||||
"== End of inherited context ==\n"
|
||||
"Use the above as background only; do not continue the "
|
||||
"parent's work. Your task follows."
|
||||
),
|
||||
},
|
||||
)
|
||||
parts.append(
|
||||
f"You are agent {name} ({child_id}); your parent is {parent_id}. "
|
||||
"Maintain your own identity. Call agent_finish when your task "
|
||||
"is complete.",
|
||||
initial_input.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"You are agent {name} ({child_id}); your parent is {parent_id}. "
|
||||
"Maintain your own identity. Call agent_finish when your task "
|
||||
"is complete."
|
||||
),
|
||||
}
|
||||
)
|
||||
parts.append(task)
|
||||
return [{"role": "user", "content": "\n\n".join(parts)}]
|
||||
initial_input.append({"role": "user", "content": task})
|
||||
return initial_input
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents import RunConfig
|
||||
from agents.sandbox import SandboxRunConfig
|
||||
from openai import RateLimitError
|
||||
|
||||
from strix.agents.factory import build_strix_agent, make_child_factory
|
||||
from strix.config import load_settings
|
||||
@@ -309,19 +308,6 @@ async def run_strix_scan(
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator.set_status(root_id, "stopped")
|
||||
return None
|
||||
except RateLimitError as exc:
|
||||
logger.warning(
|
||||
"Scan %s stopped: persistent rate limit from the LLM provider (%s). "
|
||||
"Resume with 'strix --resume %s' once the limit clears.",
|
||||
scan_id,
|
||||
exc,
|
||||
scan_id,
|
||||
)
|
||||
if root_id is not None:
|
||||
await coordinator.cancel_descendants(root_id)
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator.set_status(root_id, "stopped")
|
||||
return None
|
||||
except BaseException:
|
||||
logger.exception("Strix scan %s failed", scan_id)
|
||||
if root_id is not None:
|
||||
|
||||
+32
-3
@@ -23,9 +23,11 @@ from strix.config import (
|
||||
persist_current,
|
||||
)
|
||||
from strix.config.models import (
|
||||
RECOMMENDED_MODEL_NAMES,
|
||||
StrixProvider,
|
||||
configure_sdk_model_defaults,
|
||||
is_known_openai_bare_model,
|
||||
is_recommended_or_frontier_model,
|
||||
)
|
||||
from strix.core.paths import run_dir_for, runtime_state_dir
|
||||
from strix.interface.cli import run_cli
|
||||
@@ -254,6 +256,32 @@ async def warm_up_llm() -> None:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if raw_model and not is_recommended_or_frontier_model(raw_model):
|
||||
warn_text = Text()
|
||||
warn_text.append("MODEL QUALITY WARNING", style="bold yellow")
|
||||
warn_text.append("\n\n", style="white")
|
||||
warn_text.append(f"'{raw_model}'", style="bold cyan")
|
||||
warn_text.append(
|
||||
" is not a recommended frontier model for Strix.\nSecurity scans work best with:\n",
|
||||
style="white",
|
||||
)
|
||||
for recommended_model in RECOMMENDED_MODEL_NAMES:
|
||||
warn_text.append(f"• {recommended_model}\n", style="bold cyan")
|
||||
warn_text.append(
|
||||
"\nYou can continue, but weaker models may miss vulnerabilities "
|
||||
"or produce lower-quality findings.",
|
||||
style="white",
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
warn_text,
|
||||
title="[bold white]STRIX",
|
||||
title_align="left",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
),
|
||||
)
|
||||
|
||||
model = StrixProvider().get_model(raw_model)
|
||||
await asyncio.wait_for(
|
||||
model.get_response(
|
||||
@@ -310,6 +338,7 @@ def _positive_budget(value: str) -> float:
|
||||
except ValueError as exc:
|
||||
raise argparse.ArgumentTypeError(f"invalid float value: {value!r}") from exc
|
||||
import math
|
||||
|
||||
if not math.isfinite(budget) or budget <= 0:
|
||||
raise argparse.ArgumentTypeError("must be a finite number greater than 0")
|
||||
return budget
|
||||
@@ -820,10 +849,10 @@ def main() -> None:
|
||||
asyncio.run(run_tui(args))
|
||||
except KeyboardInterrupt:
|
||||
exit_reason = "interrupted"
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
exit_reason = "error"
|
||||
posthog.error("unhandled_exception")
|
||||
scarf.error("unhandled_exception")
|
||||
posthog.error("unhandled_exception", str(e))
|
||||
scarf.error("unhandled_exception", str(e))
|
||||
raise
|
||||
finally:
|
||||
report_state = get_global_report_state()
|
||||
|
||||
@@ -83,7 +83,7 @@ class ChatTextArea(TextArea): # type: ignore[misc]
|
||||
|
||||
super()._on_key(event)
|
||||
|
||||
@on(TextArea.Changed) # type: ignore[misc]
|
||||
@on(TextArea.Changed) # type: ignore[untyped-decorator]
|
||||
def _update_height(self, _event: TextArea.Changed | None = None) -> None:
|
||||
if not self.parent:
|
||||
return
|
||||
@@ -1549,7 +1549,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
|
||||
return AgentMessageRenderer.render_simple(content)
|
||||
|
||||
@on(Tree.NodeHighlighted) # type: ignore[misc]
|
||||
@on(Tree.NodeHighlighted) # type: ignore[untyped-decorator]
|
||||
def handle_tree_highlight(self, event: Tree.NodeHighlighted) -> None:
|
||||
if len(self.screen_stack) > 1 or self.show_splash:
|
||||
return
|
||||
@@ -1569,7 +1569,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
if agent_id:
|
||||
self.selected_agent_id = agent_id
|
||||
|
||||
@on(Tree.NodeSelected) # type: ignore[misc]
|
||||
@on(Tree.NodeSelected) # type: ignore[untyped-decorator]
|
||||
def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None:
|
||||
if len(self.screen_stack) > 1 or self.show_splash:
|
||||
return
|
||||
|
||||
@@ -48,7 +48,7 @@ logger = logging.getLogger(__name__)
|
||||
class StrixDockerSandboxClient(DockerSandboxClient):
|
||||
# Host directories to bind-mount into the container, set by the docker
|
||||
# backend before ``create()``. Each item is ``{source, target, read_only}``.
|
||||
strix_bind_mounts: list[dict[str, Any]] = [] # overridden per-instance in backends.py
|
||||
strix_bind_mounts: list[dict[str, Any]] | None = None
|
||||
|
||||
async def _create_container(
|
||||
self,
|
||||
|
||||
@@ -123,6 +123,8 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
|
||||
)
|
||||
|
||||
|
||||
def error(error_type: str) -> None:
|
||||
def error(error_type: str, error_msg: str | None = None) -> None:
|
||||
props = {**base_props(), "error_type": error_type}
|
||||
if error_msg:
|
||||
props["error_msg"] = error_msg
|
||||
_send("error", props)
|
||||
|
||||
@@ -129,10 +129,12 @@ def end(report_state: ReportState, exit_reason: str = "completed") -> None:
|
||||
)
|
||||
|
||||
|
||||
def error(error_type: str) -> None:
|
||||
def error(error_type: str, error_msg: str | None = None) -> None:
|
||||
props: dict[str, Any] = {
|
||||
**base_props(),
|
||||
"session": SESSION_ID,
|
||||
"error_type": error_type,
|
||||
}
|
||||
if error_msg:
|
||||
props["error_msg"] = error_msg
|
||||
_send("error", props)
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from pydantic import AliasChoices, Field
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
from strix.config import loader
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_LLM_ENV_KEYS = [
|
||||
"STRIX_LLM",
|
||||
"LLM_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"LLM_API_BASE",
|
||||
"OPENAI_API_BASE",
|
||||
"OPENAI_BASE_URL",
|
||||
"LITELLM_BASE_URL",
|
||||
"OLLAMA_API_BASE",
|
||||
"STRIX_REASONING_EFFORT",
|
||||
"LLM_TIMEOUT",
|
||||
"PERPLEXITY_API_KEY",
|
||||
# RuntimeSettings
|
||||
"STRIX_IMAGE",
|
||||
"STRIX_RUNTIME_BACKEND",
|
||||
"STRIX_MAX_LOCAL_COPY_MB",
|
||||
# TelemetrySettings
|
||||
"STRIX_TELEMETRY",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_loader_state(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Reset module globals and clear known env vars for deterministic runs."""
|
||||
for key in _LLM_ENV_KEYS:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
monkeypatch.setattr(loader, "_cached", None)
|
||||
monkeypatch.setattr(loader, "_override", None)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# _read_json_overrides
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_read_json_overrides_missing_file(tmp_path: Path) -> None:
|
||||
assert loader._read_json_overrides(tmp_path / "nope.json") == {}
|
||||
|
||||
|
||||
def test_read_json_overrides_corrupt_json(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text("{not valid json", encoding="utf-8")
|
||||
assert loader._read_json_overrides(path) == {}
|
||||
|
||||
|
||||
def test_read_json_overrides_non_dict_env(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(json.dumps({"env": ["not", "a", "dict"]}), encoding="utf-8")
|
||||
assert loader._read_json_overrides(path) == {}
|
||||
|
||||
|
||||
def test_read_json_overrides_maps_to_nested_settings(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "my-model", "PERPLEXITY_API_KEY": "pk"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert loader._read_json_overrides(path) == {
|
||||
"llm": {"model": "my-model"},
|
||||
"integrations": {"perplexity_api_key": "pk"},
|
||||
}
|
||||
|
||||
|
||||
def test_read_json_overrides_skips_keys_already_in_environ(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "from-env")
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(json.dumps({"env": {"STRIX_LLM": "from-file"}}), encoding="utf-8")
|
||||
# env wins -> the JSON value is not surfaced as an init kwarg.
|
||||
assert loader._read_json_overrides(path) == {}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# _aliases_for
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_aliases_for_simple_alias() -> None:
|
||||
finfo = FieldInfo(alias="SIMPLE_ALIAS")
|
||||
assert loader._aliases_for(finfo) == ["SIMPLE_ALIAS"]
|
||||
|
||||
|
||||
def test_aliases_for_alias_choices() -> None:
|
||||
finfo: FieldInfo = Field( # type: ignore[assignment]
|
||||
default=None,
|
||||
validation_alias=AliasChoices("FIRST", "SECOND"),
|
||||
)
|
||||
assert loader._aliases_for(finfo) == ["FIRST", "SECOND"]
|
||||
|
||||
|
||||
def test_aliases_for_string_validation_alias() -> None:
|
||||
finfo: FieldInfo = Field(default=None, validation_alias="STR_ALIAS") # type: ignore[assignment]
|
||||
assert loader._aliases_for(finfo) == ["STR_ALIAS"]
|
||||
|
||||
|
||||
def test_aliases_for_no_alias() -> None:
|
||||
assert loader._aliases_for(FieldInfo()) == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# apply_config_override + load_settings round-trip
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_apply_override_and_load_settings_round_trip(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "round-trip-model", "PERPLEXITY_API_KEY": "pk"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
loader.apply_config_override(path)
|
||||
settings = loader.load_settings()
|
||||
|
||||
assert settings.llm.model == "round-trip-model"
|
||||
assert settings.integrations.perplexity_api_key == "pk"
|
||||
# Second call is memoized -> same object.
|
||||
assert loader.load_settings() is settings
|
||||
|
||||
|
||||
def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None:
|
||||
first = tmp_path / "first.json"
|
||||
first.write_text(json.dumps({"env": {"STRIX_LLM": "first-model"}}), encoding="utf-8")
|
||||
second = tmp_path / "second.json"
|
||||
second.write_text(json.dumps({"env": {"STRIX_LLM": "second-model"}}), encoding="utf-8")
|
||||
|
||||
loader.apply_config_override(first)
|
||||
assert loader.load_settings().llm.model == "first-model"
|
||||
|
||||
loader.apply_config_override(second)
|
||||
assert loader.load_settings().llm.model == "second-model"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# persist_current
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_persist_current_writes_env_block(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "persisted-model")
|
||||
target = tmp_path / "sub" / "cli-config.json"
|
||||
loader.apply_config_override(target)
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert target.exists()
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "persisted-model"}
|
||||
}
|
||||
|
||||
|
||||
def test_persist_current_sets_0600_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "persisted-model")
|
||||
target = tmp_path / "cli-config.json"
|
||||
loader.apply_config_override(target)
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert target.stat().st_mode & 0o777 == 0o600
|
||||
@@ -1,114 +0,0 @@
|
||||
"""Tests for pure input builders in strix.core.inputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import pairwise
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.core.inputs import build_root_task, child_initial_input
|
||||
|
||||
|
||||
def _child_kwargs(parent_history: list[Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"name": "scout",
|
||||
"child_id": "agent-2",
|
||||
"parent_id": "agent-1",
|
||||
"task": "Audit the login flow.",
|
||||
"parent_history": parent_history,
|
||||
}
|
||||
|
||||
|
||||
def test_child_initial_input_single_message_without_history() -> None:
|
||||
result = child_initial_input(**_child_kwargs([]))
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
content = result[0]["content"]
|
||||
assert "agent scout (agent-2)" in content
|
||||
assert "Audit the login flow." in content
|
||||
assert "Inherited context" not in content
|
||||
|
||||
|
||||
def test_child_initial_input_single_message_with_history() -> None:
|
||||
history = [{"role": "assistant", "content": "previous work"}]
|
||||
result = child_initial_input(**_child_kwargs(history))
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
content = result[0]["content"]
|
||||
assert "Inherited context from parent" in content
|
||||
assert "previous work" in content
|
||||
assert "agent scout (agent-2)" in content
|
||||
assert "Audit the login flow." in content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"parent_history",
|
||||
[[], [{"role": "assistant", "content": "previous work"}]],
|
||||
)
|
||||
def test_child_initial_input_no_consecutive_same_role(parent_history: list[Any]) -> None:
|
||||
result = child_initial_input(**_child_kwargs(parent_history))
|
||||
|
||||
roles = [msg["role"] for msg in result]
|
||||
assert all(prev != nxt for prev, nxt in pairwise(roles))
|
||||
|
||||
|
||||
def test_build_root_task_empty_config() -> None:
|
||||
assert build_root_task({}) == ""
|
||||
|
||||
|
||||
def test_build_root_task_repository_target() -> None:
|
||||
config = {
|
||||
"targets": [
|
||||
{
|
||||
"type": "repository",
|
||||
"details": {
|
||||
"target_repo": "https://example.com/repo.git",
|
||||
"cloned_repo_path": "/workspace/repo",
|
||||
"workspace_subdir": "repo",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
task = build_root_task(config)
|
||||
|
||||
assert "Repositories:" in task
|
||||
assert "/workspace/repo" in task
|
||||
assert "https://example.com/repo.git" in task
|
||||
|
||||
|
||||
def test_build_root_task_web_application_with_instructions() -> None:
|
||||
config = {
|
||||
"targets": [
|
||||
{"type": "web_application", "details": {"target_url": "https://app.example.com"}},
|
||||
],
|
||||
"user_instructions": "Focus on auth.",
|
||||
}
|
||||
task = build_root_task(config)
|
||||
|
||||
assert "URLs:" in task
|
||||
assert "https://app.example.com" in task
|
||||
assert "Special instructions: Focus on auth." in task
|
||||
|
||||
|
||||
def test_build_root_task_diff_scope() -> None:
|
||||
config = {
|
||||
"targets": [],
|
||||
"diff_scope": {
|
||||
"active": True,
|
||||
"repos": [
|
||||
{
|
||||
"workspace_subdir": "repo",
|
||||
"analyzable_files_count": 3,
|
||||
"deleted_files_count": 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
task = build_root_task(config)
|
||||
|
||||
assert "Scope Constraints:" in task
|
||||
assert "3 changed file(s)" in task
|
||||
assert "2 deleted file(s)" in task
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for LLM model recommendation helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.config.models import RECOMMENDED_MODEL_NAMES, is_recommended_or_frontier_model
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES)
|
||||
def test_recommended_models_are_accepted(model_name: str) -> None:
|
||||
assert is_recommended_or_frontier_model(model_name)
|
||||
|
||||
|
||||
def test_recommended_models_are_matched_case_insensitively() -> None:
|
||||
assert is_recommended_or_frontier_model("Vertex_AI/Gemini-3-Pro-Preview")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"gpt-5.5",
|
||||
"litellm/openai/gpt-5.4-pro",
|
||||
"azure_ai/gpt-5.5-pro",
|
||||
"bedrock_mantle/openai.gpt-5.5",
|
||||
"anthropic/claude-opus-4-8",
|
||||
"anthropic.claude-opus-4-8",
|
||||
"vertex_ai/claude-sonnet-4-6@default",
|
||||
"any-llm/anthropic/claude-sonnet-4-6",
|
||||
"vertex_ai/gemini-3.1-pro-preview",
|
||||
"openrouter/google/gemini-3.1-pro-preview",
|
||||
"xai/grok-4.3",
|
||||
"openrouter/x-ai/grok-4",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"deepseek/deepseek-r1-0528",
|
||||
"deepseek/deepseek-reasoner",
|
||||
"dashscope/qwen3-max-2026-01-23",
|
||||
"qwen3.7-max",
|
||||
"moonshot/kimi-k2.6",
|
||||
"kimi-k2.7-code",
|
||||
"mistral/mistral-medium-3-5",
|
||||
"mistral/magistral-medium-latest",
|
||||
],
|
||||
)
|
||||
def test_frontier_model_families_are_accepted(model_name: str) -> None:
|
||||
assert is_recommended_or_frontier_model(model_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"",
|
||||
"openai/gpt-4.1",
|
||||
"anthropic/claude-3-5-sonnet-latest",
|
||||
"ollama/llama3.1",
|
||||
"deepseek/deepseek-chat",
|
||||
"custom-ollama/gpt-5-mini-local",
|
||||
"custom-provider/claude-opus-4-local",
|
||||
],
|
||||
)
|
||||
def test_non_frontier_models_are_rejected(model_name: str) -> None:
|
||||
assert not is_recommended_or_frontier_model(model_name)
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Tests for graceful handling of persistent RateLimitError in run_strix_scan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from openai import RateLimitError
|
||||
|
||||
import strix.tools.notes.tools as notes_tools
|
||||
import strix.tools.todo.tools as todo_tools
|
||||
from strix.core import runner
|
||||
from strix.core.agents import AgentCoordinator
|
||||
|
||||
|
||||
def _make_rate_limit_error() -> RateLimitError:
|
||||
request = httpx.Request("POST", "https://api.openai.com/v1/responses")
|
||||
response = httpx.Response(status_code=429, request=request)
|
||||
return RateLimitError("rate limited", response=response, body=None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_persistent_rate_limit_stops_gracefully(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Any, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A persistent RateLimitError stops the scan (root -> 'stopped') without raising."""
|
||||
monkeypatch.setattr(runner, "run_dir_for", lambda _scan_id: tmp_path)
|
||||
monkeypatch.setattr(runner, "runtime_state_dir", lambda _run_dir: tmp_path)
|
||||
monkeypatch.setattr(runner, "setup_scan_logging", lambda _run_dir: lambda: None)
|
||||
monkeypatch.setattr(runner, "set_scan_id", lambda _scan_id: None)
|
||||
|
||||
settings = types.SimpleNamespace(
|
||||
llm=types.SimpleNamespace(model="openai/gpt-4o", reasoning_effort="high")
|
||||
)
|
||||
monkeypatch.setattr(runner, "load_settings", lambda: settings)
|
||||
monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _settings: None)
|
||||
monkeypatch.setattr(
|
||||
runner, "uses_chat_completions_tool_schema", lambda _model, _settings: False
|
||||
)
|
||||
|
||||
monkeypatch.setattr(todo_tools, "hydrate_todos_from_disk", lambda _state_dir: None)
|
||||
monkeypatch.setattr(notes_tools, "hydrate_notes_from_disk", lambda _state_dir: None)
|
||||
|
||||
async def _create_or_reuse(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
|
||||
return {"client": object(), "session": object(), "caido_client": None}
|
||||
|
||||
async def _cleanup(*_args: Any, **_kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(runner.session_manager, "create_or_reuse", _create_or_reuse)
|
||||
monkeypatch.setattr(runner.session_manager, "cleanup", _cleanup)
|
||||
|
||||
monkeypatch.setattr(runner, "build_root_task", lambda _scan_config: "task")
|
||||
monkeypatch.setattr(runner, "build_scope_context", lambda _scan_config: "")
|
||||
monkeypatch.setattr(runner, "make_model_settings", lambda *_args, **_kwargs: object())
|
||||
monkeypatch.setattr(runner, "build_strix_agent", lambda **_kwargs: object())
|
||||
monkeypatch.setattr(runner, "make_child_factory", lambda **_kwargs: lambda **_k: object())
|
||||
monkeypatch.setattr(runner, "open_agent_session", lambda _root_id, _db: object())
|
||||
|
||||
async def _raise_rate_limit(*_args: Any, **_kwargs: Any) -> None:
|
||||
raise _make_rate_limit_error()
|
||||
|
||||
monkeypatch.setattr(runner, "run_agent_loop", _raise_rate_limit)
|
||||
|
||||
coordinator = AgentCoordinator()
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = await runner.run_strix_scan(
|
||||
scan_config={"targets": [], "scan_mode": "deep"},
|
||||
scan_id="scan-test",
|
||||
image="img",
|
||||
coordinator=coordinator,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
root_ids = [aid for aid, parent in coordinator.parent_of.items() if parent is None]
|
||||
assert len(root_ids) == 1
|
||||
assert coordinator.statuses[root_ids[0]] == "stopped"
|
||||
# the resume hint must carry the real scan id, not a literal placeholder
|
||||
assert "strix --resume scan-test" in caplog.text
|
||||
assert "<run_name>" not in caplog.text
|
||||
Reference in New Issue
Block a user