mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 05:19:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e10f2f9a |
No files matched your search
@@ -31,8 +31,6 @@ jobs:
|
||||
assert not any(name.startswith("strix/interface/tui/cmd/") for name in names)
|
||||
PY
|
||||
- name: Install into a clean environment and run the CLI
|
||||
env:
|
||||
UV_EXCLUDE_NEWER: 5 days
|
||||
run: |
|
||||
uv venv --python 3.12 /tmp/strix-smoke
|
||||
uv pip install --python /tmp/strix-smoke/bin/python dist/*.whl
|
||||
|
||||
@@ -28,7 +28,7 @@ Target-specific workflows built on the same engine:
|
||||
- **Open-source CLI (self-hosted):** free, fully local, BYO LLM key, needs Docker. Best for local dev loops, air-gapped/offline, and full control.
|
||||
```bash
|
||||
curl -sSL https://strix.ai/install | bash # install
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # any LiteLLM model id, or vercel_ai_gateway/zai/glm-5.3
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # any LiteLLM model id
|
||||
export LLM_API_KEY="<key>"
|
||||
strix -n -t ./ --scan-mode quick --max-budget 10 # headless scan; always use -n
|
||||
```
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ Thank you for your interest in contributing to Strix! This guide will help you g
|
||||
|
||||
3. **Configure your LLM provider**
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # or vercel_ai_gateway/zai/glm-5.3
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
|
||||
@@ -84,8 +84,7 @@ Strix are autonomous AI penetration testing agents that act just like real hacke
|
||||
curl -sSL https://strix.ai/install | bash
|
||||
|
||||
# Configure your AI provider
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3" # Vercel AI Gateway
|
||||
# export STRIX_LLM="openrouter/z-ai/glm-5.3" # OpenRouter
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Run your first security assessment
|
||||
@@ -297,8 +296,7 @@ jobs:
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3" # Vercel AI Gateway
|
||||
# export STRIX_LLM="openrouter/z-ai/glm-5.3" # OpenRouter
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Optional
|
||||
@@ -354,14 +352,12 @@ Each server's tools are namespaced by `name`, for example `github_list_issues`.
|
||||
|
||||
**Recommended models for best results:**
|
||||
|
||||
- [Z.ai GLM-5.3 on Vercel AI Gateway](https://vercel.com/ai-gateway/models/glm-5.3) - `vercel_ai_gateway/zai/glm-5.3` (recommended)
|
||||
- [Z.ai GLM-5.3 on OpenRouter](https://openrouter.ai/z-ai/glm-5.3) - `openrouter/z-ai/glm-5.3` (recommended)
|
||||
- [DeepSeek V4.1 Flash on OpenRouter](https://openrouter.ai/deepseek/deepseek-v4.1-flash-1017) - `openrouter/deepseek/deepseek-v4.1-flash-1017`
|
||||
- [OpenAI GPT-6.1 Sol](https://openai.com/api/) - `openai/gpt-6.1-sol` (needs [Daybreak](https://openai.com/daybreak/) access)
|
||||
- [Anthropic Claude Opus 5.5](https://claude.com/platform/api) - `anthropic/claude-opus-5-5` (needs [CVP](https://support.claude.com/en/articles/14604842-cyber-verification-program) access)
|
||||
- [Google Gemini 4 Argon](https://cloud.google.com/vertex-ai) - `vertex_ai/gemini-4-argon` (needs [Fairwind](https://deepmind.google/fairwind-program/) access)
|
||||
- [Z.ai GLM-5.3 on OpenRouter](https://openrouter.ai/z-ai/glm-5.3) - `openrouter/z-ai/glm-5.3` (the default pick)
|
||||
- [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`
|
||||
- [DeepSeek V4 Pro](https://platform.deepseek.com) - `deepseek/deepseek-v4-pro`
|
||||
- [Moonshot Kimi K3.1 Pro](https://platform.kimi.ai) - `moonshot/kimi-k3.1-pro`
|
||||
- [Moonshot Kimi K3](https://platform.kimi.ai) - `moonshot/kimi-k3`
|
||||
|
||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ USER root
|
||||
# overwrite a root-owned binary under /usr/local/bin, which otherwise fails with
|
||||
# "cannot move binary" and aborts the scan. Pin the initial version for
|
||||
# reproducible builds; self-update then pulls fresh detectors at runtime.
|
||||
ARG TRUFFLEHOG_VERSION=3.99.2
|
||||
ARG TRUFFLEHOG_VERSION=3.95.9
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /home/pentester/.local/bin "v${TRUFFLEHOG_VERSION}" && \
|
||||
chown -R pentester:pentester /home/pentester/.local
|
||||
RUN set -eux; \
|
||||
|
||||
@@ -8,7 +8,7 @@ Configure Strix using environment variables or a config file.
|
||||
## LLM Configuration
|
||||
|
||||
<ParamField path="STRIX_LLM" type="string" required>
|
||||
Model name in LiteLLM format (e.g., `openrouter/z-ai/glm-5.3`, `openai/gpt-6.1-sol`).
|
||||
Model name in LiteLLM format (e.g., `openrouter/z-ai/glm-5.3`, `openai/gpt-5.4`).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_API_KEY" type="string">
|
||||
@@ -33,18 +33,6 @@ Configure Strix using environment variables or a config file.
|
||||
the LiteLLM and native OpenAI routing paths.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_EXTRA_BODY" type="string">
|
||||
Extra fields merged into the JSON body of every request to the main model, as a JSON object
|
||||
(e.g. `{"service_tier":"flex","safety_identifier":"9f86d081..."}`). Use it
|
||||
for provider parameters Strix doesn't expose, such as OpenAI's `service_tier`
|
||||
or `safety_identifier`. Applies to both the LiteLLM and native OpenAI routing
|
||||
paths. Fields are not validated, so the provider or gateway must accept them;
|
||||
unknown fields are usually rejected. Extra fields can override fields Strix
|
||||
sends and break requests, so don't set request-shaping fields such as `model`, `messages`, `input`,
|
||||
`tools` or `stream`, and use `STRIX_REASONING_EFFORT` rather than
|
||||
`reasoning_effort`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_TIMEOUT" default="300" type="integer">
|
||||
Request timeout in seconds for LLM calls.
|
||||
</ParamField>
|
||||
@@ -100,12 +88,6 @@ affecting the agents that do the actual testing.
|
||||
inherits `LLM_EXTRA_HEADERS`; set this when its endpoint needs custom headers.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="DEDUPE_LLM_EXTRA_BODY" type="string">
|
||||
Optional JSON object of extra request-body fields sent on every
|
||||
deduplication-model request. A dedicated dedupe model never inherits
|
||||
`LLM_EXTRA_BODY`; set this when its endpoint needs them too.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_DEDUPE_REASONING_EFFORT" type="string">
|
||||
Reasoning effort for the deduplication model. Defaults to the model's own
|
||||
baseline when unset.
|
||||
@@ -141,10 +123,6 @@ affecting the agents that do the actual testing.
|
||||
Telemetry toggle. Set to `0`, `false`, `no`, or `off` to disable telemetry (PostHog, Scarf, OTEL).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_RUN_RECORD_TIMINGS" default="false" type="boolean">
|
||||
Record every LLM request's and tool call's timing in `run.json`, under `llm_requests` and `tool_calls`. Adds about 0.5 MB to a typical scan's `run.json`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="TRACELOOP_BASE_URL" type="string">
|
||||
OTLP/Traceloop base URL for remote OpenTelemetry export. If unset, Strix keeps traces local only.
|
||||
</ParamField>
|
||||
|
||||
+1
-1
@@ -35,8 +35,8 @@
|
||||
"llm-providers/overview",
|
||||
"llm-providers/openai",
|
||||
"llm-providers/anthropic",
|
||||
"llm-providers/vercel-ai-gateway",
|
||||
"llm-providers/openrouter",
|
||||
"llm-providers/vercel-ai-gateway",
|
||||
"llm-providers/vertex",
|
||||
"llm-providers/bedrock",
|
||||
"llm-providers/azure",
|
||||
|
||||
+1
-15
@@ -73,20 +73,7 @@ Strix uses a graph of specialized agents for comprehensive security testing:
|
||||
|
||||
## Quick Example
|
||||
|
||||
<CodeGroup>
|
||||
```bash Vercel AI Gateway
|
||||
# Install
|
||||
curl -sSL https://strix.ai/install | bash
|
||||
|
||||
# Configure
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Scan
|
||||
strix --target ./your-app
|
||||
```
|
||||
|
||||
```bash OpenRouter
|
||||
```bash
|
||||
# Install
|
||||
curl -sSL https://strix.ai/install | bash
|
||||
|
||||
@@ -97,7 +84,6 @@ export LLM_API_KEY="your-api-key"
|
||||
# Scan
|
||||
strix --target ./your-app
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: "Configure Strix with Claude models"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="anthropic/claude-sonnet-5-5"
|
||||
export STRIX_LLM="anthropic/claude-sonnet-4-6"
|
||||
export LLM_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
@@ -14,12 +14,8 @@ export LLM_API_KEY="sk-ant-..."
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `anthropic/claude-sonnet-5-5` | Best balance of intelligence and speed |
|
||||
| `anthropic/claude-opus-5-5` | Maximum capability for deep analysis |
|
||||
|
||||
<Note>
|
||||
Security testing with Claude needs the [Cyber Verification Program](https://support.claude.com/en/articles/14604842-cyber-verification-program) (CVP). Without it the safety classifiers block most offensive tasks. Red Team Access covers authorized penetration testing.
|
||||
</Note>
|
||||
| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed |
|
||||
| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis |
|
||||
|
||||
## Get API Key
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: "Configure Strix with OpenAI models via Azure"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="azure/your-gpt6-deployment"
|
||||
export STRIX_LLM="azure/your-gpt5-deployment"
|
||||
export AZURE_API_KEY="your-azure-api-key"
|
||||
export AZURE_API_BASE="https://your-resource.openai.azure.com"
|
||||
export AZURE_API_VERSION="2025-11-01-preview"
|
||||
@@ -24,7 +24,7 @@ export AZURE_API_VERSION="2025-11-01-preview"
|
||||
## Example
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="azure/gpt-6.1-sol-deployment"
|
||||
export STRIX_LLM="azure/gpt-5.4-deployment"
|
||||
export AZURE_API_KEY="abc123..."
|
||||
export AZURE_API_BASE="https://mycompany.openai.azure.com"
|
||||
export AZURE_API_VERSION="2025-11-01-preview"
|
||||
@@ -33,9 +33,5 @@ export AZURE_API_VERSION="2025-11-01-preview"
|
||||
## Prerequisites
|
||||
|
||||
1. Create an Azure OpenAI resource
|
||||
2. Deploy a model such as GPT-6.1 Sol
|
||||
2. Deploy a model (e.g., GPT-5.4)
|
||||
3. Get the endpoint URL and API key from the Azure portal
|
||||
|
||||
<Note>
|
||||
Security testing with OpenAI models needs [Daybreak](https://openai.com/daybreak/) access, also on Azure.
|
||||
</Note>
|
||||
@@ -6,7 +6,7 @@ description: "Configure Strix with models via AWS Bedrock"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="bedrock/us.anthropic.claude-sonnet-5-5"
|
||||
export STRIX_LLM="bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0"
|
||||
```
|
||||
|
||||
No API key required—uses AWS credentials from environment.
|
||||
@@ -36,13 +36,10 @@ Automatically uses instance role credentials.
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `bedrock/us.anthropic.claude-sonnet-5-5` | Claude Sonnet 5.5 |
|
||||
| `bedrock/us.anthropic.claude-opus-5-5` | Claude Opus 5.5 |
|
||||
| `bedrock/us.anthropic.claude-haiku-5-5` | Claude Haiku 5.5 |
|
||||
|
||||
<Note>
|
||||
Security testing with Claude needs the [Cyber Verification Program](https://support.claude.com/en/articles/14604842-cyber-verification-program) (CVP), also on Bedrock.
|
||||
</Note>
|
||||
| `bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0` | Claude 4.5 Sonnet |
|
||||
| `bedrock/anthropic.claude-4-5-opus-20251022-v1:0` | Claude 4.5 Opus |
|
||||
| `bedrock/anthropic.claude-4-5-haiku-20251022-v1:0` | Claude 4.5 Haiku |
|
||||
| `bedrock/amazon.titan-text-premier-v2:0` | Amazon Titan Premier v2 |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -68,21 +68,6 @@ export LLM_API_KEY="your-bearer-token" # sent as Authorization: Bearer
|
||||
export LLM_EXTRA_HEADERS='{"X-Feature-Key":"value","X-Tenant":"acme"}'
|
||||
```
|
||||
|
||||
### Extra request-body fields
|
||||
|
||||
Some provider parameters go in the request body rather than a header, and
|
||||
Strix has no dedicated setting for them. Set them with `LLM_EXTRA_BODY` as a
|
||||
JSON object — they are merged into the body of every request to the model. For example, to
|
||||
use OpenAI's cheaper flex processing and attribute requests to a user with a
|
||||
safety identifier (a hash, never a raw email):
|
||||
|
||||
```bash
|
||||
export LLM_EXTRA_BODY='{"service_tier":"flex","safety_identifier":"9f86d081884c7d65"}'
|
||||
```
|
||||
|
||||
Fields aren't validated, so use only fields your provider accepts, and don't
|
||||
set fields Strix already sends, such as `model`, `messages` or `stream`.
|
||||
|
||||
For endpoints behind a private CA, point Strix at your certificate bundle with
|
||||
the standard `SSL_CERT_FILE=/path/to/ca-bundle.pem` — never disable TLS
|
||||
verification against a real endpoint.
|
||||
|
||||
@@ -8,7 +8,7 @@ description: "Configure Strix with Novita AI models"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/zai-org/glm-5.3"
|
||||
export STRIX_LLM="openai/moonshotai/kimi-k2.5"
|
||||
export LLM_API_KEY="your-novita-api-key"
|
||||
export LLM_API_BASE="https://api.novita.ai/openai"
|
||||
```
|
||||
@@ -18,12 +18,11 @@ export LLM_API_BASE="https://api.novita.ai/openai"
|
||||
| Model | Configuration |
|
||||
|-------|---------------|
|
||||
| GLM-5.3 | `openai/zai-org/glm-5.3` |
|
||||
| Kimi K3.1 Pro | `openai/moonshotai/kimi-k3.1-pro` |
|
||||
| Kimi K3 | `openai/moonshotai/kimi-k3` |
|
||||
| DeepSeek V4.1 Flash | `openai/deepseek/deepseek-v4.1-flash-1017` |
|
||||
| DeepSeek V4 Pro | `openai/deepseek/deepseek-v4-pro` |
|
||||
| MiniMax M3.1 Flash | `openai/minimax/minimax-m3.1-flash` |
|
||||
| GLM-5.3 Flash | `openai/zai-org/glm-5.3-flash` |
|
||||
| Kimi K2.5 | `openai/moonshotai/kimi-k2.5` |
|
||||
| GLM-5 | `openai/zai-org/glm-5` |
|
||||
| MiniMax M2.5 | `openai/minimax/minimax-m2.5` |
|
||||
|
||||
## Get API Key
|
||||
|
||||
|
||||
@@ -6,23 +6,13 @@ description: "Configure Strix with OpenAI models"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/gpt-6.1-sol"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
## Available Models
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `openai/gpt-6.1-sol` | Best balance of capability and cost for agentic security work |
|
||||
| `openai/gpt-6-astra` | Flagship, maximum capability |
|
||||
| `openai/gpt-6-luna` | Fast and cheap |
|
||||
|
||||
See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for the full list.
|
||||
|
||||
<Note>
|
||||
Security testing with OpenAI models needs [Daybreak](https://openai.com/daybreak/) (Trusted Access for Cyber). Without it the models refuse most offensive tasks. [Request access](https://openai.com/form/enterprise-trusted-access-for-cyber/).
|
||||
</Note>
|
||||
See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for the full list of available models.
|
||||
|
||||
## Get API Key
|
||||
|
||||
@@ -35,7 +25,7 @@ Security testing with OpenAI models needs [Daybreak](https://openai.com/daybreak
|
||||
For OpenAI-compatible APIs:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/gpt-6.1-sol"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-key"
|
||||
export LLM_API_BASE="https://your-proxy.com/v1"
|
||||
```
|
||||
@@ -18,19 +18,13 @@ Access any model on OpenRouter using the format `openrouter/<provider>/<model>`:
|
||||
|
||||
| Model | Configuration |
|
||||
|-------|---------------|
|
||||
| GLM-5.3 (recommended) | `openrouter/z-ai/glm-5.3` |
|
||||
| GPT-6.1 Sol | `openrouter/openai/gpt-6.1-sol` |
|
||||
| GPT-6 Astra | `openrouter/openai/gpt-6-astra` |
|
||||
| Claude Opus 5.5 | `openrouter/anthropic/claude-opus-5.5` |
|
||||
| Claude Sonnet 5.5 | `openrouter/anthropic/claude-sonnet-5.5` |
|
||||
| Gemini 4 Argon | `openrouter/google/gemini-4-argon` |
|
||||
| Gemini 3.8 Flash | `openrouter/google/gemini-3.8-flash` |
|
||||
| DeepSeek V4.1 Flash | `openrouter/deepseek/deepseek-v4.1-flash-1017` |
|
||||
| GLM-5.3 (default) | `openrouter/z-ai/glm-5.3` |
|
||||
| GPT-5.4 | `openrouter/openai/gpt-5.4` |
|
||||
| Claude Sonnet 4.6 | `openrouter/anthropic/claude-sonnet-4.6` |
|
||||
| Gemini 3 Pro | `openrouter/google/gemini-3-pro-preview` |
|
||||
| DeepSeek V4 Pro | `openrouter/deepseek/deepseek-v4-pro` |
|
||||
| Kimi K3.1 Pro | `openrouter/moonshotai/kimi-k3.1-pro` |
|
||||
| GLM-5.3 Flash | `openrouter/z-ai/glm-5.3-flash` |
|
||||
|
||||
OpenAI, Anthropic and Gemini models keep their provider safeguards on OpenRouter. Security testing with them needs [Daybreak](https://openai.com/daybreak/), [CVP](https://support.claude.com/en/articles/14604842-cyber-verification-program) or [Fairwind](https://deepmind.google/fairwind-program/) access.
|
||||
| Kimi K3 | `openrouter/moonshotai/kimi-k3` |
|
||||
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
|
||||
|
||||
## Get API Key
|
||||
|
||||
|
||||
@@ -9,33 +9,19 @@ Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibi
|
||||
|
||||
Set your model and API key:
|
||||
|
||||
| Model | Provider | Configuration |
|
||||
| --------------------- | ------------------------- | -------------------------------- |
|
||||
| GLM-5.3 (recommended) | Vercel AI Gateway | `vercel_ai_gateway/zai/glm-5.3` |
|
||||
| GLM-5.3 (recommended) | OpenRouter | `openrouter/z-ai/glm-5.3` |
|
||||
| GPT-6.1 Sol | OpenAI | `openai/gpt-6.1-sol` |
|
||||
| Claude Opus 5.5 | Anthropic | `anthropic/claude-opus-5-5` |
|
||||
| Claude Sonnet 5.5 | Anthropic | `anthropic/claude-sonnet-5-5` |
|
||||
| Gemini 4 Argon | Google Vertex | `vertex_ai/gemini-4-argon` |
|
||||
| DeepSeek V4.1 Flash | DeepSeek | `deepseek/deepseek-flash` |
|
||||
| DeepSeek V4 Pro | DeepSeek | `deepseek/deepseek-v4-pro` |
|
||||
| Kimi K3.1 Pro | Moonshot | `moonshot/kimi-k3.1-pro` |
|
||||
| Model | Provider | Configuration |
|
||||
| -------------------- | ----------------- | -------------------------------- |
|
||||
| GLM-5.3 (default) | Z.ai (OpenRouter) | `openrouter/z-ai/glm-5.3` |
|
||||
| GPT-5.4 | OpenAI | `openai/gpt-5.4` |
|
||||
| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` |
|
||||
| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` |
|
||||
| DeepSeek V4 Pro | DeepSeek | `deepseek/deepseek-v4-pro` |
|
||||
| Kimi K3 | Moonshot | `moonshot/kimi-k3` |
|
||||
|
||||
<Note>
|
||||
Frontier OpenAI, Anthropic and Gemini models need a cyber access program for security testing: [Daybreak](https://openai.com/daybreak/) for OpenAI, the [Cyber Verification Program](https://support.claude.com/en/articles/14604842-cyber-verification-program) for Anthropic, and the [Fairwind Program](https://deepmind.google/fairwind-program/) for Gemini.
|
||||
</Note>
|
||||
|
||||
<CodeGroup>
|
||||
```bash Vercel AI Gateway
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
```bash OpenRouter
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Local Models
|
||||
|
||||
@@ -61,7 +47,7 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re
|
||||
Access 100+ models through a single API.
|
||||
</Card>
|
||||
<Card title="Vercel AI Gateway" href="/llm-providers/vercel-ai-gateway">
|
||||
Access 100+ models through one OpenAI-compatible endpoint.
|
||||
Access models from multiple providers through one endpoint.
|
||||
</Card>
|
||||
<Card title="Google Vertex AI" href="/llm-providers/vertex">
|
||||
Gemini 3 models via Google Cloud.
|
||||
@@ -83,9 +69,9 @@ Use LiteLLM's `provider/model-name` format:
|
||||
|
||||
```
|
||||
openrouter/z-ai/glm-5.3
|
||||
openai/gpt-6.1-sol
|
||||
anthropic/claude-sonnet-5-5
|
||||
vertex_ai/gemini-4-argon
|
||||
bedrock/us.anthropic.claude-sonnet-5-5
|
||||
openai/gpt-5.4
|
||||
anthropic/claude-sonnet-4-6
|
||||
vertex_ai/gemini-3-pro-preview
|
||||
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
|
||||
ollama/llama4
|
||||
```
|
||||
@@ -1,47 +1,32 @@
|
||||
---
|
||||
title: "Vercel AI Gateway"
|
||||
description: "Configure Strix with models through Vercel AI Gateway"
|
||||
description: "Configure Strix with models via Vercel AI Gateway"
|
||||
---
|
||||
|
||||
[Vercel AI Gateway](https://vercel.com/docs/ai-gateway) provides access to 100+ models from multiple providers through one OpenAI-compatible API.
|
||||
[Vercel AI Gateway](https://vercel.com/docs/ai-gateway) provides an OpenAI-compatible API for models from multiple providers.
|
||||
|
||||
## Setup
|
||||
|
||||
Create an [AI Gateway API key](https://vercel.com/docs/ai-gateway/authentication-and-byok), then configure Strix:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3"
|
||||
export STRIX_LLM="openai/anthropic/claude-opus-5"
|
||||
export LLM_API_KEY="your-ai-gateway-api-key"
|
||||
export LLM_API_BASE="https://ai-gateway.vercel.sh/v1"
|
||||
```
|
||||
|
||||
The `vercel_ai_gateway/` prefix selects the Gateway. The remaining value is the [AI Gateway model ID](https://vercel.com/docs/ai-gateway/models-and-providers).
|
||||
The first `openai/` segment tells Strix to use its OpenAI-compatible client. The remaining value is the [AI Gateway model ID](https://vercel.com/docs/ai-gateway/models-and-providers).
|
||||
|
||||
## Available Models
|
||||
|
||||
Access any model on the Gateway using the format `vercel_ai_gateway/<provider>/<model>`:
|
||||
|
||||
| Model | Configuration |
|
||||
|-------|---------------|
|
||||
| GLM-5.3 (recommended) | `vercel_ai_gateway/zai/glm-5.3` |
|
||||
| GPT-6.1 Sol | `vercel_ai_gateway/openai/gpt-6.1-sol` |
|
||||
| GPT-6 Astra | `vercel_ai_gateway/openai/gpt-6-astra` |
|
||||
| Claude Opus 5.5 | `vercel_ai_gateway/anthropic/claude-opus-5.5` |
|
||||
| Claude Sonnet 5.5 | `vercel_ai_gateway/anthropic/claude-sonnet-5.5` |
|
||||
| Gemini 4 Argon | `vercel_ai_gateway/google/gemini-4-argon` |
|
||||
| Gemini 3.8 Flash | `vercel_ai_gateway/google/gemini-3.8-flash` |
|
||||
| DeepSeek V4.1 Flash | `vercel_ai_gateway/deepseek/deepseek-v4.1-flash-1017` |
|
||||
| DeepSeek V4 Pro | `vercel_ai_gateway/deepseek/deepseek-v4-pro` |
|
||||
| Kimi K3.1 Pro | `vercel_ai_gateway/moonshotai/kimi-k3.1-pro` |
|
||||
| GLM-5.3 Flash | `vercel_ai_gateway/zai/glm-5.3-flash` |
|
||||
|
||||
OpenAI, Anthropic and Gemini models keep their provider safeguards on the Gateway. Security testing with them needs [Daybreak](https://openai.com/daybreak/), [CVP](https://support.claude.com/en/articles/14604842-cyber-verification-program) or [Fairwind](https://deepmind.google/fairwind-program/) access.
|
||||
|
||||
The full list is at the [AI Gateway models page](https://vercel.com/ai-gateway/models) or the models endpoint:
|
||||
Use any language model returned by the AI Gateway models endpoint:
|
||||
|
||||
```text
|
||||
https://ai-gateway.vercel.sh/v1/models
|
||||
```
|
||||
|
||||
Prefix its model ID with `openai/` when setting `STRIX_LLM`. For example, the Gateway model ID `anthropic/claude-opus-5` becomes `openai/anthropic/claude-opus-5` in Strix.
|
||||
|
||||
## Get API Key
|
||||
|
||||
1. Open the [AI Gateway API key settings](https://vercel.com/docs/ai-gateway/authentication-and-byok)
|
||||
@@ -50,6 +35,5 @@ https://ai-gateway.vercel.sh/v1/models
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Single API** — Access models from OpenAI, Anthropic, Google, Z.ai, and more
|
||||
- **Bring your own keys** — Route through your own provider accounts when you want
|
||||
- **Cost tracking** — Monitor Gateway usage and cost in Vercel
|
||||
- Access models from multiple providers through one endpoint
|
||||
- Track Gateway usage and cost in Vercel
|
||||
@@ -6,7 +6,7 @@ description: "Configure Strix with Gemini models via Google Cloud"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="vertex_ai/gemini-4-argon"
|
||||
export STRIX_LLM="vertex_ai/gemini-3-pro-preview"
|
||||
```
|
||||
|
||||
No API key required—uses Google Cloud Application Default Credentials.
|
||||
@@ -29,12 +29,8 @@ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `vertex_ai/gemini-4-argon` | Best overall performance for security testing |
|
||||
| `vertex_ai/gemini-3.8-flash` | Faster and cheaper |
|
||||
|
||||
<Note>
|
||||
Security testing with Gemini needs the [Fairwind Program](https://deepmind.google/fairwind-program/). Without it the cyber guardrails block most offensive tasks.
|
||||
</Note>
|
||||
| `vertex_ai/gemini-3-pro-preview` | Best overall performance for security testing |
|
||||
| `vertex_ai/gemini-3-flash-preview` | Faster and cheaper |
|
||||
|
||||
## Project Configuration
|
||||
|
||||
|
||||
+3
-10
@@ -25,22 +25,15 @@ description: "Install Strix and run your first security scan"
|
||||
|
||||
## Configuration
|
||||
|
||||
Set your LLM provider. We recommend GLM-5.3 through [Vercel AI Gateway](/llm-providers/vercel-ai-gateway) or [OpenRouter](/llm-providers/openrouter):
|
||||
Set your LLM provider:
|
||||
|
||||
<CodeGroup>
|
||||
```bash Vercel AI Gateway
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
```bash OpenRouter
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
For best results, use `zai/glm-5.3`, `deepseek/deepseek-v4.1-flash-1017`, `anthropic/claude-opus-5-5` (needs [CVP](https://support.claude.com/en/articles/14604842-cyber-verification-program) access), or `openai/gpt-6.1-sol` (needs [Daybreak](https://openai.com/daybreak/) access).
|
||||
For best results, use `openrouter/z-ai/glm-5.3` (the default pick), `openai/gpt-5.4`, `anthropic/claude-opus-4-6`, or `openai/gpt-5.2`.
|
||||
</Tip>
|
||||
|
||||
## Run Your First Scan
|
||||
|
||||
@@ -73,12 +73,6 @@ dev = [
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.uv]
|
||||
# Supply-chain cooldown: resolve only distributions that have been on the index for
|
||||
# at least 5 days, so a poisoned release (litellm 1.107.0, 2026-10-21) is never
|
||||
# picked up by `uv lock` / `uv add` before it can be caught and yanked.
|
||||
exclude-newer = "5 days"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
fi
|
||||
```
|
||||
|
||||
Then tell the user to add two repository secrets: `STRIX_LLM` (model id, for example `openrouter/z-ai/glm-5.3`) and `LLM_API_KEY` (the provider key). Do not create these values yourself.
|
||||
Then tell the user to add two repository secrets: `STRIX_LLM` (model id, for example `openai/gpt-5.4`) and `LLM_API_KEY` (the provider key). Do not create these values yourself.
|
||||
|
||||
Notes:
|
||||
- In CI/headless runs Strix automatically scopes to the PR's changed files (`--scope-mode auto`). If diff resolution fails, keep `fetch-depth: 0` or set `--diff-base` to the PR's actual base branch — use `origin/${{ github.base_ref }}` in GitHub Actions rather than a hard-coded `origin/main`, since repos use different default branches.
|
||||
|
||||
@@ -47,7 +47,7 @@ If unsure and the user has (or will create) an app.strix.ai account, prefer **Cl
|
||||
```
|
||||
3. **LLM configured** — two environment variables:
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # any LiteLLM model id (openai/..., anthropic/..., openrouter/...)
|
||||
export STRIX_LLM="openai/gpt-5.4" # any LiteLLM model id (openai/..., anthropic/..., openrouter/...)
|
||||
export LLM_API_KEY="<provider api key>"
|
||||
```
|
||||
Ask the user for these if unset. Never hardcode or commit keys.
|
||||
|
||||
+3
-83
@@ -13,13 +13,11 @@ from agents.agent import ToolsToFinalOutputResult
|
||||
from agents.sandbox import SandboxAgent
|
||||
from agents.sandbox.capabilities import Filesystem, Shell
|
||||
from agents.sandbox.errors import InvalidManifestPathError
|
||||
from agents.sandbox.session.pty_types import PTY_YIELD_TIME_MS_MAX
|
||||
from agents.tool import CustomTool, FunctionTool, Tool
|
||||
from pydantic import ValidationError
|
||||
|
||||
from strix.agents.prompt import render_system_prompt
|
||||
from strix.config import load_settings
|
||||
from strix.config.tool_call_arguments import describe_malformed_arguments
|
||||
from strix.tools.agents_graph.tools import (
|
||||
agent_finish,
|
||||
create_agent,
|
||||
@@ -269,10 +267,6 @@ def _with_coerced_arguments(tool: FunctionTool) -> FunctionTool:
|
||||
nullish = tool.name.startswith(_QUERY_TOOL_PREFIXES)
|
||||
|
||||
async def invoke(ctx: Any, raw_input: str) -> Any:
|
||||
malformed = describe_malformed_arguments(tool.name, raw_input)
|
||||
if malformed is not None:
|
||||
logger.debug("Tool %s got malformed arguments; asking the model to re-issue", tool.name)
|
||||
return malformed
|
||||
return await invoke_tool(ctx, _coerce_arguments(raw_input, schema, nullish=nullish))
|
||||
|
||||
tool.on_invoke_tool = invoke
|
||||
@@ -442,47 +436,8 @@ def _apply_shell_output_cap(parsed: dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
_EXEC_YIELD_CAP_S = PTY_YIELD_TIME_MS_MAX // 1000
|
||||
|
||||
_EXEC_COMMAND_DESCRIPTION_NOTE = (
|
||||
f"The wait for output is capped at {_EXEC_YIELD_CAP_S} s whatever yield_time_ms says. "
|
||||
"A non-TTY command still running at the yield cannot be read later (write_stdin fails "
|
||||
"on it and its output is lost): run anything longer in the background with its output "
|
||||
"redirected to a file (`cmd > /tmp/out.log 2>&1 &`, then read the log), or start it with "
|
||||
'tty=true and poll with write_stdin(session_id=<id>, chars="").'
|
||||
)
|
||||
|
||||
_RUNNING_NON_TTY_NOTICE = (
|
||||
"Note: this non-TTY process cannot be polled and its later output is lost. Rerun it in "
|
||||
"the background (`cmd > /tmp/out.log 2>&1 &`, then read the log) or with tty=true."
|
||||
)
|
||||
|
||||
_PROCESS_RUNNING_RE = re.compile(r"^Process running with session ID \d+$", re.MULTILINE)
|
||||
_OUTPUT_HEADER_RE = re.compile(r"^Output:\n", re.MULTILINE)
|
||||
|
||||
|
||||
def _annotate_running_non_tty(parsed: dict[str, Any], result: Any) -> Any:
|
||||
if parsed.get("tty") or not isinstance(result, str):
|
||||
return result
|
||||
header = _OUTPUT_HEADER_RE.search(result)
|
||||
head_end = len(result) if header is None else header.start()
|
||||
if _PROCESS_RUNNING_RE.search(result, 0, head_end) is None:
|
||||
return result
|
||||
notice = _RUNNING_NON_TTY_NOTICE
|
||||
requested = parsed.get("yield_time_ms")
|
||||
if isinstance(requested, int | float) and requested > PTY_YIELD_TIME_MS_MAX:
|
||||
notice = (
|
||||
f"Note: yield_time_ms={int(requested)} was capped at {PTY_YIELD_TIME_MS_MAX} "
|
||||
f"({_EXEC_YIELD_CAP_S} s). " + notice.removeprefix("Note: ")
|
||||
)
|
||||
if header is None:
|
||||
return f"{result}\n{notice}"
|
||||
return f"{result[: header.end()]}{notice}\n{result[header.end() :]}"
|
||||
|
||||
|
||||
def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
|
||||
invoke_tool = tool.on_invoke_tool
|
||||
tool.description = f"{tool.description.rstrip()} {_EXEC_COMMAND_DESCRIPTION_NOTE}"
|
||||
|
||||
async def invoke(ctx: Any, raw_input: str) -> Any:
|
||||
try:
|
||||
@@ -495,7 +450,7 @@ def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
|
||||
_apply_shell_output_cap(parsed)
|
||||
raw_input = json.dumps(parsed)
|
||||
try:
|
||||
result = await invoke_tool(ctx, raw_input)
|
||||
return await invoke_tool(ctx, raw_input)
|
||||
except ValidationError as exc:
|
||||
return _format_validation_error(tool.name, exc)
|
||||
except InvalidManifestPathError as exc:
|
||||
@@ -505,9 +460,6 @@ def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
|
||||
"(or omitted to use the turn's cwd). "
|
||||
f"Got: {rel!r}."
|
||||
)
|
||||
if isinstance(parsed, dict):
|
||||
return _annotate_running_non_tty(parsed, result)
|
||||
return result
|
||||
|
||||
tool.on_invoke_tool = invoke
|
||||
return tool
|
||||
@@ -619,17 +571,6 @@ def _finish_tool_use_behavior(
|
||||
return ToolsToFinalOutputResult(is_final_output=False, final_output=None)
|
||||
|
||||
|
||||
_COVERAGE_TOOLS: tuple[Tool, ...] = (record_coverage, update_coverage, list_coverage)
|
||||
_FINISH_SCAN_COVERAGE_STEP = re.compile(
|
||||
r"\n5\. \*\*Coverage reconciliation\.\*\*.*?(?=\n\n)", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def _without_coverage_step(tool: FunctionTool) -> FunctionTool:
|
||||
description = _FINISH_SCAN_COVERAGE_STEP.sub("", tool.description)
|
||||
return dataclasses.replace(tool, description=description)
|
||||
|
||||
|
||||
_BASE_TOOLS: tuple[Tool, ...] = (
|
||||
think,
|
||||
load_skill,
|
||||
@@ -728,7 +669,6 @@ def build_strix_agent(
|
||||
is_whitebox: bool = False,
|
||||
is_diff_scoped: bool = False,
|
||||
interactive: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
chat_completions_tools: bool = False,
|
||||
strict_tool_schemas: bool = True,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
@@ -736,8 +676,6 @@ def build_strix_agent(
|
||||
instructions_override: str | None = None,
|
||||
supports_images: bool = True,
|
||||
finish_tool: Tool = finish_scan,
|
||||
report_tool: Tool = create_vulnerability_report,
|
||||
coverage_tools: bool = True,
|
||||
) -> SandboxAgent[Any]:
|
||||
"""Build a SandboxAgent for either root or child use.
|
||||
|
||||
@@ -751,10 +689,6 @@ def build_strix_agent(
|
||||
instructions_override: Use this verbatim as the system prompt instead
|
||||
of rendering the built-in scan prompt.
|
||||
finish_tool: The tool that ends the run, given to the root agent.
|
||||
report_tool: The tool every agent files findings with, in place of
|
||||
``create_vulnerability_report``.
|
||||
coverage_tools: Give the agent the coverage ledger tools and the
|
||||
prompt rules for them.
|
||||
"""
|
||||
if instructions_override is not None:
|
||||
instructions = instructions_override
|
||||
@@ -766,26 +700,18 @@ def build_strix_agent(
|
||||
is_root=is_root,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
system_prompt_context=system_prompt_context,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool.name,
|
||||
coverage_tools=coverage_tools,
|
||||
)
|
||||
|
||||
agent_tools = [*_EXTRA_TOOLS, *(extra_tools or [])]
|
||||
if interactive:
|
||||
# Yielding to the user is only meaningful when one is attached.
|
||||
agent_tools.append(wait_for_user)
|
||||
base_tools = [report_tool if t is create_vulnerability_report else t for t in _BASE_TOOLS]
|
||||
if not coverage_tools:
|
||||
base_tools = [t for t in base_tools if t not in _COVERAGE_TOOLS]
|
||||
if finish_tool is finish_scan:
|
||||
finish_tool = _without_coverage_step(finish_scan)
|
||||
if is_root:
|
||||
tools: list[Tool] = [*base_tools, *agent_tools, finish_tool]
|
||||
tools: list[Tool] = [*_BASE_TOOLS, *agent_tools, finish_tool]
|
||||
else:
|
||||
tools = [*base_tools, *agent_tools, agent_finish]
|
||||
tools = [*_BASE_TOOLS, *agent_tools, agent_finish]
|
||||
_ensure_unique_tool_names(tools)
|
||||
tools = [
|
||||
_with_bounded_result(_with_strictness(_with_coerced_arguments(tool), strict_tool_schemas))
|
||||
@@ -834,13 +760,10 @@ def make_child_factory(
|
||||
is_whitebox: bool = False,
|
||||
is_diff_scoped: bool = False,
|
||||
interactive: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
chat_completions_tools: bool = False,
|
||||
strict_tool_schemas: bool = True,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
supports_images: bool = True,
|
||||
report_tool: Tool = create_vulnerability_report,
|
||||
coverage_tools: bool = True,
|
||||
) -> Any:
|
||||
"""Return the runner-owned builder used by ``spawn_child_agent``.
|
||||
|
||||
@@ -858,13 +781,10 @@ def make_child_factory(
|
||||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
chat_completions_tools=chat_completions_tools,
|
||||
strict_tool_schemas=strict_tool_schemas,
|
||||
system_prompt_context=system_prompt_context,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool,
|
||||
coverage_tools=coverage_tools,
|
||||
)
|
||||
|
||||
return _factory
|
||||
@@ -83,12 +83,9 @@ def render_system_prompt(
|
||||
is_root: bool = False,
|
||||
is_diff_scoped: bool = False,
|
||||
interactive: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
system_prompt_context: dict[str, Any] | None = None,
|
||||
include_scope: bool = True,
|
||||
supports_images: bool = True,
|
||||
report_tool: str = "create_vulnerability_report",
|
||||
coverage_tools: bool = True,
|
||||
) -> str:
|
||||
"""Render the system prompt. Returns empty string on template failure.
|
||||
|
||||
@@ -123,9 +120,7 @@ def render_system_prompt(
|
||||
is_root=is_root,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
)
|
||||
skill_content = load_skills(
|
||||
skills_to_load, supports_images=supports_images, coverage_tools=coverage_tools
|
||||
)
|
||||
skill_content = load_skills(skills_to_load, supports_images=supports_images)
|
||||
env.globals["get_skill"] = lambda name: skill_content.get(name, "")
|
||||
|
||||
# Skills every agent of this kind loads come first, so siblings share them
|
||||
@@ -135,12 +130,9 @@ def render_system_prompt(
|
||||
requested_skill_names=[name for name in skill_content if name not in shared],
|
||||
available_skills=get_available_skills(),
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
is_root=is_root,
|
||||
system_prompt_context=system_prompt_context or {},
|
||||
include_scope=include_scope,
|
||||
report_tool=report_tool,
|
||||
coverage_tools=coverage_tools,
|
||||
**skill_content,
|
||||
)
|
||||
except Exception:
|
||||
|
||||
@@ -44,11 +44,7 @@ INTERACTIVE BEHAVIOR:
|
||||
- If you end a turn on plain text and the nudge arrives, your words already reached the user. Do not repeat them: call wait_for_user.
|
||||
- You can narrate while you work — plain text is shown to the user as you go. Narrating is free; wait_for_user is specifically the act of WAITING for the user, so do not call it just to give a status update.
|
||||
- Respond naturally when the user asks questions or gives instructions.
|
||||
{%- if parallel_tool_calls %}
|
||||
- While actively working on a task, every turn should carry a tool call (independent calls can go in the same turn) — use think to plan, the appropriate tool to act, and wait_for_user only when you genuinely need the user.
|
||||
{%- else %}
|
||||
- While actively working on a task, every turn should carry exactly one tool call — use think to plan, the appropriate tool to act, and wait_for_user only when you genuinely need the user.
|
||||
{%- endif %}
|
||||
- Never loop through think or other tools just to prepare, polish, confirm, or announce an answer. Once you know the answer, write it and call wait_for_user.
|
||||
{% else %}
|
||||
AUTONOMOUS BEHAVIOR:
|
||||
@@ -59,9 +55,6 @@ AUTONOMOUS BEHAVIOR:
|
||||
- There is no user attached to this run, so there is nobody to ask and nothing to yield to. If there is nothing left to execute: do NOT send filler/repetitive text — either call wait_for_agents (only if you are genuinely expecting another agent to message you) or finish your work (subagents: agent_finish; root: finish_scan)
|
||||
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If waiting on another agent, use wait_for_agents; when done, use agent_finish (subagents) or finish_scan (root)
|
||||
- A text-only turn does nothing: it neither ends the run nor yields — it just wastes a turn and forces a retry. The lifecycle tools (``finish_scan`` for root, ``agent_finish`` for subagents) are the ONLY way to terminate, and the report flows through them. If you find yourself wanting to say "Done!" or "Scan complete" without a tool call, call the lifecycle tool instead.
|
||||
{%- if parallel_tool_calls %}
|
||||
- Read-only calls can go together in one turn: when several file reads or searches don't depend on each other's output, issue them as separate tool calls in the same turn rather than one per turn. Anything else — writing files, running commands that change state, sending requests, creating or messaging agents, reporting, or finishing — goes alone in its own turn.
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
</communication_rules>
|
||||
|
||||
@@ -126,7 +119,7 @@ WHITE-BOX TESTING (code provided):
|
||||
- Local execution, unit/integration testing, patch verification, and HTTP requests against locally started in-scope services are normal authorized white-box validation
|
||||
- If dynamically running the code proves impossible after exhaustive attempts, pivot to comprehensive static analysis.
|
||||
- Try to infer how to run the code based on its structure and content.
|
||||
- Derive the code fix as PART OF reporting, not as a separate later pass: {{ report_tool }} already requires the concrete patch inline (`code_locations` with verbatim `fix_before`/`fix_after` and `fix_pr_body`), so the reporting agent that analyzes the root cause is the one that produces the fix. Do NOT spawn a downstream agent afterwards to re-derive/re-apply the same patch.
|
||||
- Derive the code fix as PART OF reporting, not as a separate later pass: create_vulnerability_report already requires the concrete patch inline (`code_locations` with verbatim `fix_before`/`fix_after` and `fix_pr_body`), so the reporting agent that analyzes the root cause is the one that produces the fix. Do NOT spawn a downstream agent afterwards to re-derive/re-apply the same patch.
|
||||
- If you also apply and verify the patch in the repo (edit the file, re-test that the vulnerability is gone), do it in the same agent/turn while the analysis is fresh — right before or as part of filing the report — never as a second re-analysis pass.
|
||||
|
||||
COMBINED MODE (code + deployed target present):
|
||||
@@ -209,14 +202,12 @@ VALIDATION REQUIREMENTS:
|
||||
- Keep going until you find something that matters
|
||||
- CLOSURE DISCIPLINE: every candidate you open ends in exactly one explicit state — `confirmed` (working PoC, or a complete source→control→sink→impact trace that is reachable), `ruled_out` (you can name the SPECIFIC control, at a location, that runs on every attacker-reachable path before the sink), or `open_proof_gap` (plausible, unconfirmed, and you could NOT name such a control). "I moved on" is not a closure state. Silently dropping an uncertain candidate is mislabelling an `open_proof_gap` as `ruled_out` and is how real bugs get missed.
|
||||
- Missing information is NOT proof of safety: no caller found, can't tell if deployed/exposed, couldn't stand up the service, build failed — each is an `open_proof_gap`, never a reason to mark a candidate clean. Difficulty is a reason to defer, not to suppress.
|
||||
{% if coverage_tools -%}
|
||||
- COVERAGE: record every surface you assess with `record_coverage` (surface + risk area + outcome + evidence), including the ones that came back clean — a report that only lists findings cannot say what was reviewed and cleared. Use the `needs_follow_up` outcome for anything left in an `open_proof_gap` state, and carry the same items up in `agent_finish(open_items=[...])`. The ledger is shared and mutable: when you resolve a surface another agent left open — or find that a closed one is not — move that entry with `update_coverage` instead of recording a second one for the same surface. Before `finish_scan`, the root agent calls `list_coverage(outcome="needs_follow_up")` and follows up every row it returns: it assigns the outstanding work to a subagent that moves the existing entry with `update_coverage` to its real outcome, and a row stays `needs_follow_up` only when it is genuinely blocked, with the blocker stated in its evidence.
|
||||
{% endif -%}
|
||||
- COVERAGE: record every surface you assess with `record_coverage` (surface + risk area + outcome + evidence), including the ones that came back clean — a report that only lists findings cannot say what was reviewed and cleared. Use the `needs_follow_up` outcome for anything left in an `open_proof_gap` state, and carry the same items up in `agent_finish(open_items=[...])`. The ledger is shared and mutable: when you resolve a surface another agent left open — or find that a closed one is not — move that entry with `update_coverage` instead of recording a second one for the same surface. The root agent reconciles all of it via `list_coverage` before `finish_scan`.
|
||||
- THREAT MODEL: before you start testing, call `get_threat_model` on the target you were pointed at — it is the scan's shared answer to who the attacker is, where the trust boundaries sit, and what counts as critical here. It is scoped to this scan and nothing carries over from an earlier run, so `found: false` means no agent on this run has derived one yet. Read it instead of re-deriving trust boundaries yourself; where your testing disproves it — a boundary it calls trusted turns out to be attacker-reachable, a role it did not know about, a host or endpoint it never listed — record that with `amend_threat_model` so the agents after you inherit the correction. Amending is not optional politeness: a model nobody corrects turns the first agent's guesses into everyone's assumptions.
|
||||
- Before filing any report, run the counterevidence pass: argue the strongest case AGAINST the finding, record what you found in the `counterevidence` field, set `confidence` honestly (a static-only trace you couldn't execute is at best `medium`), and state what evidence would change the severity. See the counterevidence and severity-calibration knowledge above.
|
||||
- A vulnerability is ONLY considered reported when a reporting agent uses {{ report_tool }} (or create_dependency_report for known-CVE dependency/supply-chain findings) with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
|
||||
- Reporting and fixing are ONE step, not two: when source is available, the reporting agent derives the concrete fix and files it INLINE via {{ report_tool }} (`code_locations` with `fix_before`/`fix_after` + `fix_pr_body`) — the report is not complete without it. Do NOT report first and then spawn a separate downstream agent to re-derive and re-apply the same patch; that just re-does the analysis and wastes tokens. (Do not silently patch a finding WITHOUT filing a report — the report, with its embedded fix, is the deliverable.)
|
||||
- DEDUPLICATION: The {{ report_tool }} tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent. If your evidence proves more than the finding it matched (a working exploit where that one had only a static trace, a chain that raises the impact), revise that finding with update_vulnerability_report using the duplicate_of id — never re-file it.
|
||||
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report (or create_dependency_report for known-CVE dependency/supply-chain findings) with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
|
||||
- Reporting and fixing are ONE step, not two: when source is available, the reporting agent derives the concrete fix and files it INLINE via create_vulnerability_report (`code_locations` with `fix_before`/`fix_after` + `fix_pr_body`) — the report is not complete without it. Do NOT report first and then spawn a separate downstream agent to re-derive and re-apply the same patch; that just re-does the analysis and wastes tokens. (Do not silently patch a finding WITHOUT filing a report — the report, with its embedded fix, is the deliverable.)
|
||||
- DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent. If your evidence proves more than the finding it matched (a working exploit where that one had only a static trace, a chain that raises the impact), revise that finding with update_vulnerability_report using the duplicate_of id — never re-file it.
|
||||
- HTTP EVIDENCE: a finding you validated through the proxy is not fully filed until `http_exchange_ids` carries the proxy request ids of the exchanges that prove it — the request that triggers the vulnerability plus the baseline/control request it differs from (an unauthenticated success next to the authenticated one, the payload response next to the benign one). Copy the ids exactly as `list_requests`/`view_request` show them, never invent or guess one, and never omit the field to bypass validation. Leave it out only when there is no captured HTTP exchange at all (static-only code findings, dependency CVEs). If you filed before the proving exchanges existed, attach them afterwards with update_vulnerability_report. Without the ids, the finding ships as prose nobody can replay.
|
||||
- REVISING A FINDING: use update_vulnerability_report (report id + the fields you want to replace + update_reason) when you learn something a finding already on file does not carry — you built the PoC after filing it, a chain raised its impact, further testing weakened it, or its counterevidence/remediation/code locations were wrong. Editing a finding needs no duplicate verdict, and it is always better than filing a second report for the same issue. Read the finding first with get_report, and pass only the fields that change.
|
||||
- WITHDRAWING A FINDING: when re-testing proves a finding you filed is not a vulnerability at all — the exploit only worked because of your own test setup (a mixed-up session, a self-inflicted state change, a misread response), the behaviour is intended, or the control is in fact enforced — delete it with delete_vulnerability_report (report id + delete_reason). Never leave a disproved finding on file as a zero-CVSS, "retracted", or "false positive" report, and never rewrite its text into a note saying it was wrong: a report that exists is read as a finding. Delete is for a finding that does not exist; a finding that is real but weaker than filed is revised, not deleted.
|
||||
@@ -229,16 +220,14 @@ Every one of these tools writes to state the rest of the scan reads. Reaching fo
|
||||
- TODOS — `create_todo` / `list_todos` / `update_todo` / `mark_todo_done` / `mark_todo_pending` / `delete_todo`: your own working checklist for a multi-step task. Create todos when your task has several distinct steps so nothing is dropped across a long run; mark them done as you finish. This is private working memory — use `notes` for anything another agent needs.
|
||||
- NOTES — `create_note` / `list_notes` / `get_note` / `update_note` / `delete_note`: the scan's shared scratchpad, visible to every agent. Write a note for a durable cross-agent fact that is not a finding and not coverage — a working credential set, a discovered endpoint inventory, an enumerated tenant list, a rate-limit quirk the next agent needs. `update_note` to keep a living inventory current; `delete_note` only for something now wrong or superseded. Check `list_notes`/`get_note` before recon work so you build on what is already mapped instead of redoing it.
|
||||
- THREAT MODEL — `get_threat_model` / `amend_threat_model` / `save_threat_model`: covered above. `save_threat_model` REPLACES the whole document and clears amendments, so it is for establishing the baseline or folding amendments in (normally root) — to correct part of an existing model, `amend_threat_model` instead.
|
||||
{% if coverage_tools -%}
|
||||
- COVERAGE — `record_coverage` / `update_coverage` / `list_coverage`: covered above. One row per surface+risk; correct an existing row with `update_coverage`, never a second `record_coverage`.
|
||||
{% endif -%}
|
||||
- RESEARCH — `web_search`: pull fresh, target-specific external knowledge — latest bypasses, WAF evasions, DB-/framework-specific syntax, CVE and advisory detail — before falling back to memorized payloads, and refresh payload corpora mid-spray.
|
||||
- SPAWN WORK — `create_agent`: delegate a focused subtask to a specialist child (see the multi-agent rules below for when to spawn and how to scope it). Give it the target to model against and what is already known.
|
||||
- TRACK CHILDREN — `view_agent_graph`: your live map of every agent and its status. Call it before spawning (to confirm no existing agent already covers the scope) and before finishing (to confirm no child is still running).
|
||||
- STEER CHILDREN — `send_message_to_agent`: send a running child new information, a course correction, or a request to wrap up, without killing it. Use it to answer a child's question or narrow its scope mid-run.
|
||||
- BLOCK ON CHILDREN — `wait_for_agents`: block until named children report back when your next move genuinely depends on their results. If you can keep making progress in parallel, keep working instead of waiting.
|
||||
- CANCEL CHILDREN — `stop_agent`: gracefully cancel a child whose work is redundant, misdirected, or no longer needed. Prefer `send_message_to_agent` to redirect a child that is merely off-track; reserve `stop_agent` for work that should not continue at all.
|
||||
- FINISH — subagents call `agent_finish` (with `open_items=[...]` for anything left unresolved); the root agent calls `finish_scan` exactly once, only after every child is wrapped up and coverage is reconciled. `agent_finish`/`finish_scan` are handoffs, not reporting channels — a vulnerability is reported only via `{{ report_tool }}`/`create_dependency_report`.
|
||||
- FINISH — subagents call `agent_finish` (with `open_items=[...]` for anything left unresolved); the root agent calls `finish_scan` exactly once, only after every child is wrapped up and coverage is reconciled. `agent_finish`/`finish_scan` are handoffs, not reporting channels — a vulnerability is reported only via `create_vulnerability_report`/`create_dependency_report`.
|
||||
</execution_guidelines>
|
||||
|
||||
<vulnerability_focus>
|
||||
@@ -391,7 +380,7 @@ CRITICAL RULES:
|
||||
- **REALISTIC OUTCOMES** - Some tests find nothing, some validations fail
|
||||
- **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
|
||||
- **SPAWN REACTIVELY** - Create new agents based on what you discover
|
||||
- **ONLY REPORTING AGENTS** can use {{ report_tool }} tool
|
||||
- **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
|
||||
- **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 skills, up to 5 for complex contexts
|
||||
- **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
|
||||
|
||||
|
||||
+2
-26
@@ -6,8 +6,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import UnionType
|
||||
from typing import TYPE_CHECKING, Any, Union, get_args, get_origin
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import AliasChoices, BaseModel
|
||||
|
||||
@@ -127,36 +126,13 @@ def _read_json_overrides(path: Path) -> dict[str, dict[str, Any]]:
|
||||
continue # env wins under some alias; skip the JSON file for this field
|
||||
for alias in aliases:
|
||||
if alias in env_block_upper:
|
||||
sub_data[fname] = _decode_saved_value(finfo, env_block_upper[alias])
|
||||
sub_data[fname] = env_block_upper[alias]
|
||||
break
|
||||
if sub_data:
|
||||
nested[sub_name] = sub_data
|
||||
return nested
|
||||
|
||||
|
||||
def _decode_saved_value(finfo: FieldInfo, value: Any) -> Any:
|
||||
"""Decode a JSON string saved for a dict field, as pydantic does for env vars.
|
||||
|
||||
``persist_current`` saves env values verbatim, so a dict setting such as
|
||||
``LLM_EXTRA_HEADERS`` lands in the file as JSON text. Values that are
|
||||
already objects, or aren't valid JSON, are left for validation to judge.
|
||||
"""
|
||||
if not isinstance(value, str) or not _is_dict_field(finfo):
|
||||
return value
|
||||
try:
|
||||
return json.loads(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
def _is_dict_field(finfo: FieldInfo) -> bool:
|
||||
annotation = finfo.annotation
|
||||
candidates = (
|
||||
get_args(annotation) if get_origin(annotation) in (Union, UnionType) else (annotation,)
|
||||
)
|
||||
return any(c is dict or get_origin(c) is dict for c in candidates)
|
||||
|
||||
|
||||
def _first_alias_value(aliases: list[str], source: Mapping[str, Any]) -> Any | None:
|
||||
return next((source[alias] for alias in aliases if alias in source), None)
|
||||
|
||||
|
||||
+2
-17
@@ -41,7 +41,6 @@ from openai.types.shared import Reasoning
|
||||
from strix.agents.prompt import CACHE_POINT
|
||||
from strix.config import codex
|
||||
from strix.config.loader import load_settings
|
||||
from strix.config.tool_call_arguments import repair_input
|
||||
from strix.config.tool_call_ids import TurnCallIdRewriter, dedupe_input
|
||||
from strix.config.tool_call_limits import TurnToolCallLimiter
|
||||
from strix.llm import request_log
|
||||
@@ -256,12 +255,6 @@ class _TurnGuardModel(Model):
|
||||
Ids that collide with the history are rewritten before the turn is
|
||||
recorded, and already-corrupted histories are repaired on the way out.
|
||||
|
||||
Tool-call arguments: a turn whose ``arguments`` are not valid JSON fails
|
||||
that one call with a parse error, but the raw string is recorded and
|
||||
strict providers then reject every request that replays it. Such
|
||||
arguments are rewritten to a valid JSON object on the way out (see
|
||||
:mod:`strix.config.tool_call_arguments`).
|
||||
|
||||
Tool-call volume: a degenerate response can queue hundreds of calls that
|
||||
the run loop then honours one by one. Only the first
|
||||
``LLM_MAX_TOOL_CALLS_PER_TURN`` calls of a response are kept.
|
||||
@@ -320,7 +313,7 @@ class _TurnGuardModel(Model):
|
||||
conversation_id: str | None,
|
||||
prompt: ResponsePromptParam | None,
|
||||
) -> ModelResponse:
|
||||
sanitized = _sanitize_input(input)
|
||||
sanitized = dedupe_input(input)
|
||||
rewriter = TurnCallIdRewriter(sanitized)
|
||||
system_instructions, sanitized = _split_cached_prefix(
|
||||
system_instructions, sanitized, model_settings
|
||||
@@ -356,7 +349,7 @@ class _TurnGuardModel(Model):
|
||||
conversation_id: str | None,
|
||||
prompt: ResponsePromptParam | None,
|
||||
) -> AsyncIterator[TResponseStreamEvent]:
|
||||
sanitized = _sanitize_input(input)
|
||||
sanitized = dedupe_input(input)
|
||||
rewriter = TurnCallIdRewriter(sanitized)
|
||||
system_instructions, sanitized = _split_cached_prefix(
|
||||
system_instructions, sanitized, model_settings
|
||||
@@ -386,10 +379,6 @@ class _TurnGuardModel(Model):
|
||||
self._log_dropped(limiter)
|
||||
|
||||
|
||||
def _sanitize_input(model_input: str | list[TResponseInputItem]) -> str | list[Any]:
|
||||
return repair_input(dedupe_input(model_input))
|
||||
|
||||
|
||||
def _split_cached_prefix(
|
||||
system_instructions: str | None,
|
||||
model_input: str | list[Any],
|
||||
@@ -705,11 +694,7 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
"""Apply Strix config to SDK-native defaults."""
|
||||
llm = settings.llm
|
||||
set_tracing_disabled(True)
|
||||
from strix.report.state import record_llm_request
|
||||
|
||||
request_log.install()
|
||||
if settings.telemetry.run_record_timings:
|
||||
request_log.register_sink(record_llm_request)
|
||||
if codex.subscription_model(llm.model):
|
||||
return
|
||||
_configure_litellm_compatibility()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import AliasChoices, Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
@@ -55,20 +55,11 @@ class LlmSettings(BaseSettings):
|
||||
alias="LLM_EXTRA_HEADERS",
|
||||
repr=False,
|
||||
)
|
||||
extra_body: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
alias="LLM_EXTRA_BODY",
|
||||
repr=False,
|
||||
)
|
||||
reasoning_effort: ReasoningEffort = Field(default="high", alias="STRIX_REASONING_EFFORT")
|
||||
force_required_tool_choice: bool = Field(
|
||||
default=False,
|
||||
alias="STRIX_FORCE_REQUIRED_TOOL_CHOICE",
|
||||
)
|
||||
parallel_tool_calls: bool = Field(
|
||||
default=False,
|
||||
alias="STRIX_PARALLEL_TOOL_CALLS",
|
||||
)
|
||||
prompt_cache: bool = Field(
|
||||
default=True,
|
||||
alias="STRIX_PROMPT_CACHE",
|
||||
@@ -119,11 +110,6 @@ class DedupeSettings(BaseSettings):
|
||||
alias="DEDUPE_LLM_EXTRA_HEADERS",
|
||||
repr=False,
|
||||
)
|
||||
extra_body: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
alias="DEDUPE_LLM_EXTRA_BODY",
|
||||
repr=False,
|
||||
)
|
||||
|
||||
@field_validator("reasoning_effort", mode="before")
|
||||
@classmethod
|
||||
@@ -167,7 +153,6 @@ class TelemetrySettings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
enabled: bool = Field(default=True, alias="STRIX_TELEMETRY")
|
||||
run_record_timings: bool = Field(default=False, alias="STRIX_RUN_RECORD_TIMINGS")
|
||||
|
||||
|
||||
WebSearchProvider = Literal["auto", "perplexity", "exa"]
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
"""Keep replayed tool-call arguments valid JSON.
|
||||
|
||||
A model occasionally emits a tool call whose ``arguments`` string is not a
|
||||
JSON object (truncated, unbalanced, empty). The call itself fails safely: the
|
||||
tool reports the parse error and the run continues. But the raw string is
|
||||
recorded in the session as-is, and strict OpenAI-compatible servers (vLLM,
|
||||
SGLang, ...) validate every assistant tool call in the request, so from then
|
||||
on each turn is rejected with ``Assistant tool call function.arguments must
|
||||
be valid JSON`` and the agent can never recover. Rewriting the replayed
|
||||
arguments to a JSON object that carries the original text keeps the history
|
||||
valid while the model still sees what it sent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from openai.types.responses import ResponseFunctionToolCall
|
||||
|
||||
|
||||
MALFORMED_ARGUMENTS_KEY = "malformed_arguments"
|
||||
|
||||
|
||||
def _reject_constant(name: str) -> Any:
|
||||
raise ValueError(f"non-standard JSON constant {name}")
|
||||
|
||||
|
||||
def _loads_strict(text: str) -> Any:
|
||||
try:
|
||||
return json.loads(text, parse_constant=_reject_constant)
|
||||
except RecursionError as exc:
|
||||
raise ValueError("arguments are nested too deeply to parse") from exc
|
||||
|
||||
|
||||
def describe_malformed_arguments(tool_name: str, arguments: str) -> str | None:
|
||||
"""Return a model-facing recovery message if ``arguments`` is not a JSON object.
|
||||
|
||||
A tool call whose arguments do not parse is almost always one the stream cut
|
||||
off (the server flushed a prefix of the JSON, or the client closed early),
|
||||
so instead of the SDK's generic parse-error result the model is told the
|
||||
call never ran and must be re-issued whole.
|
||||
"""
|
||||
if not arguments.strip():
|
||||
return None
|
||||
try:
|
||||
parsed = _loads_strict(arguments)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
else:
|
||||
if isinstance(parsed, dict):
|
||||
return None
|
||||
detail = f"expected a JSON object, got {type(parsed).__name__}"
|
||||
return (
|
||||
f"{tool_name}: the tool call was not executed because its arguments were "
|
||||
f"truncated or otherwise not valid JSON ({detail}). The response was likely "
|
||||
"cut off mid-call. Re-issue the call with complete, valid JSON arguments."
|
||||
)
|
||||
|
||||
|
||||
def repair_arguments(arguments: object) -> str | None:
|
||||
"""Return replacement arguments a strict server accepts, or ``None`` if already valid."""
|
||||
if not isinstance(arguments, str) or not arguments.strip():
|
||||
return "{}"
|
||||
try:
|
||||
parsed = _loads_strict(arguments)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
if isinstance(parsed, dict):
|
||||
return None
|
||||
return json.dumps({MALFORMED_ARGUMENTS_KEY: arguments}, ensure_ascii=False)
|
||||
|
||||
|
||||
def repair_history_arguments(items: list[Any]) -> tuple[list[Any], bool]:
|
||||
"""Rewrite function calls in a conversation history whose arguments are not a JSON object."""
|
||||
rebuilt: list[Any] = []
|
||||
changed = False
|
||||
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
if item.get("type") == "function_call":
|
||||
repaired = repair_arguments(item.get("arguments"))
|
||||
if repaired is not None:
|
||||
item = {**item, "arguments": repaired} # noqa: PLW2901
|
||||
changed = True
|
||||
elif isinstance(item, ResponseFunctionToolCall):
|
||||
repaired = repair_arguments(item.arguments)
|
||||
if repaired is not None:
|
||||
item = item.model_copy(update={"arguments": repaired}) # noqa: PLW2901
|
||||
changed = True
|
||||
rebuilt.append(item)
|
||||
|
||||
return rebuilt, changed
|
||||
|
||||
|
||||
def repair_input(model_input: str | list[Any]) -> str | list[Any]:
|
||||
if isinstance(model_input, str):
|
||||
return model_input
|
||||
rebuilt, changed = repair_history_arguments(model_input)
|
||||
return rebuilt if changed else model_input
|
||||
+3
-19
@@ -75,7 +75,6 @@ class AgentCoordinator:
|
||||
self._budget_stopped = False
|
||||
self._reserve_stopped = False
|
||||
self._budget_paused = False
|
||||
self._operator_paused = False
|
||||
self._resume_epoch = 0
|
||||
self._budget_policy: BudgetPolicy = "stop"
|
||||
self._extend_budget: Callable[[], None] | None = None
|
||||
@@ -106,10 +105,6 @@ class AgentCoordinator:
|
||||
def budget_paused(self) -> bool:
|
||||
return self._budget_paused
|
||||
|
||||
@property
|
||||
def operator_paused(self) -> bool:
|
||||
return self._operator_paused
|
||||
|
||||
@property
|
||||
def resume_epoch(self) -> int:
|
||||
"""Bumped by every ``resume_budget``; an agent parks against the value it read."""
|
||||
@@ -155,7 +150,6 @@ class AgentCoordinator:
|
||||
"""
|
||||
async with self._lock:
|
||||
self._budget_paused = True
|
||||
self._operator_paused = True
|
||||
logger.info("scan paused by the operator")
|
||||
await self._maybe_snapshot()
|
||||
|
||||
@@ -171,7 +165,6 @@ class AgentCoordinator:
|
||||
self._set_budget_limit(max_budget_usd)
|
||||
async with self._lock:
|
||||
self._budget_paused = False
|
||||
self._operator_paused = False
|
||||
self._resume_epoch += 1
|
||||
woken = [aid for aid, status in self.statuses.items() if status == "budget_paused"]
|
||||
for aid in woken:
|
||||
@@ -239,7 +232,7 @@ class AgentCoordinator:
|
||||
self._budget_stopped = budget_stopped
|
||||
self._reserve_stopped = reserve_stopped
|
||||
if not budget_paused:
|
||||
self._budget_paused = self._operator_paused
|
||||
self._budget_paused = False
|
||||
for aid, status in self.statuses.items():
|
||||
if status == "budget_paused":
|
||||
self.statuses[aid] = "waiting"
|
||||
@@ -431,12 +424,7 @@ class AgentCoordinator:
|
||||
unknown, or it is terminal and its loop does not park for wake-ups.
|
||||
"""
|
||||
from_user = message.get("from") == "user"
|
||||
if (
|
||||
from_user
|
||||
and self._budget_paused
|
||||
and not self._operator_paused
|
||||
and self._budget_policy != "pause"
|
||||
):
|
||||
if from_user and self._budget_paused and self._budget_policy != "pause":
|
||||
await self.resume_from_budget_pause(exclude=target_agent_id)
|
||||
async with self._lock:
|
||||
if target_agent_id not in self.statuses:
|
||||
@@ -459,9 +447,7 @@ class AgentCoordinator:
|
||||
self.recovery_counts.pop(target_agent_id, None)
|
||||
self.idle_resume_counts.pop(target_agent_id, None)
|
||||
self._parent_notified.discard(target_agent_id)
|
||||
held = self._operator_paused and self.statuses[target_agent_id] == "budget_paused"
|
||||
if not held:
|
||||
self.statuses[target_agent_id] = "waiting"
|
||||
self.statuses[target_agent_id] = "waiting"
|
||||
runtime.wake.set()
|
||||
stream = runtime.stream
|
||||
interrupt_on_message = runtime.interrupt_on_message
|
||||
@@ -654,7 +640,6 @@ class AgentCoordinator:
|
||||
"budget_stopped": self._budget_stopped,
|
||||
"reserve_stopped": self._reserve_stopped,
|
||||
"budget_paused": self._budget_paused,
|
||||
"operator_paused": self._operator_paused,
|
||||
}
|
||||
|
||||
async def restore(self, snap: dict[str, Any]) -> None:
|
||||
@@ -677,7 +662,6 @@ class AgentCoordinator:
|
||||
self._budget_stopped = bool(snap.get("budget_stopped", False))
|
||||
self._reserve_stopped = bool(snap.get("reserve_stopped", False))
|
||||
self._budget_paused = bool(snap.get("budget_paused", False))
|
||||
self._operator_paused = bool(snap.get("operator_paused", False))
|
||||
for aid in self.statuses:
|
||||
self.runtimes.setdefault(aid, AgentRuntime())
|
||||
|
||||
|
||||
+4
-14
@@ -24,8 +24,6 @@ from openai import (
|
||||
|
||||
from strix.config import codex
|
||||
from strix.core.hooks import (
|
||||
SUBAGENT_TURN_LIMITS_KEY,
|
||||
TURN_LIMIT_KEY,
|
||||
BudgetExceededError,
|
||||
BudgetPausedError,
|
||||
SubagentBudgetReservedError,
|
||||
@@ -41,7 +39,6 @@ from strix.core.sessions import (
|
||||
)
|
||||
from strix.llm import request_log
|
||||
from strix.llm.compaction import is_context_overflow, maybe_compact
|
||||
from strix.skills import skill_turn_limit
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -430,7 +427,6 @@ async def spawn_child_agent(
|
||||
name=name,
|
||||
parent_id=parent_id,
|
||||
task=task,
|
||||
skills=skills,
|
||||
initial_input=child_initial_input(
|
||||
name=name,
|
||||
child_id=child_id,
|
||||
@@ -514,7 +510,6 @@ async def respawn_subagents(
|
||||
name=name,
|
||||
parent_id=parent_id,
|
||||
task=str(md.get("task", "")),
|
||||
skills=child_skills,
|
||||
initial_input=[],
|
||||
start_parked=start_parked,
|
||||
event_sink=event_sink,
|
||||
@@ -604,7 +599,7 @@ async def _run_until_lifecycle(
|
||||
hooks=hooks,
|
||||
)
|
||||
except BudgetPausedError as exc:
|
||||
if coordinator.budget_policy != "pause" and not exc.operator_pause:
|
||||
if coordinator.budget_policy != "pause":
|
||||
raise
|
||||
# The agent parked right before an LLM call; everything up to that
|
||||
# point is already in its session. Once resumed, the same call goes
|
||||
@@ -617,7 +612,6 @@ async def _run_until_lifecycle(
|
||||
# Stopped while parked (operator stop or a parent's stop_agent).
|
||||
await coordinator.reset_recovery(agent_id)
|
||||
return result
|
||||
await coordinator.consume_pending(agent_id)
|
||||
if session is not None:
|
||||
input_data = []
|
||||
continue
|
||||
@@ -837,7 +831,7 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
await coordinator.detach_stream(agent_id, stream)
|
||||
except BudgetPausedError as exc:
|
||||
logger.info("agent %s paused at the scan budget limit: %s", agent_id, exc)
|
||||
if coordinator.budget_policy == "pause" or exc.operator_pause:
|
||||
if coordinator.budget_policy == "pause":
|
||||
await coordinator.park_for_budget(agent_id)
|
||||
else:
|
||||
await coordinator.pause_for_budget(agent_id)
|
||||
@@ -1009,7 +1003,7 @@ async def _append_tool_required_message(
|
||||
message = (
|
||||
"Your previous message ended a turn without a tool call. Plain text never ends "
|
||||
"execution and never hands control to the user: it is shown to the user, and the "
|
||||
"run continues. Continue immediately and call a tool. "
|
||||
"run continues. Continue immediately and call exactly one tool. "
|
||||
"If you have nothing to do until the user replies, call wait_for_user; your "
|
||||
"text already reached them, so do not repeat it. "
|
||||
"If you are blocked waiting for another agent, call wait_for_agents. "
|
||||
@@ -1021,7 +1015,7 @@ async def _append_tool_required_message(
|
||||
message = (
|
||||
"Your previous response ended the autonomous run without a lifecycle tool "
|
||||
"call. That is invalid in non-interactive mode; plain text final answers are "
|
||||
"ignored. Continue immediately and call a tool. "
|
||||
"ignored. Continue immediately and call exactly one tool. "
|
||||
f"If your work is complete, call {finish_tool}. "
|
||||
"If you are blocked waiting for another agent, call wait_for_agents. "
|
||||
"Otherwise use the appropriate execution or planning tool. "
|
||||
@@ -1166,7 +1160,6 @@ async def _start_child_runner(
|
||||
name: str,
|
||||
parent_id: str | None,
|
||||
task: str,
|
||||
skills: list[str],
|
||||
initial_input: Any,
|
||||
start_parked: bool = False,
|
||||
event_sink: StreamEventSink | None = None,
|
||||
@@ -1180,9 +1173,6 @@ async def _start_child_runner(
|
||||
child_ctx["agent_id"] = child_id
|
||||
child_ctx["parent_id"] = parent_id
|
||||
child_ctx["task"] = task
|
||||
child_ctx[TURN_LIMIT_KEY] = skill_turn_limit(
|
||||
skills, parent_ctx.get(SUBAGENT_TURN_LIMITS_KEY) or {}
|
||||
)
|
||||
|
||||
async def _child_loop() -> None:
|
||||
# A budget stop is a clean scan-wide shutdown, not a child failure: the
|
||||
|
||||
+2
-94
@@ -4,32 +4,24 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents.lifecycle import RunHooks
|
||||
|
||||
from strix.core.agents import BudgetPolicy, coordinator_from_context
|
||||
from strix.report.state import get_global_report_state, record_tool_call
|
||||
from strix.report.state import get_global_report_state
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents import RunContextWrapper
|
||||
from agents.agent import Agent
|
||||
from agents.items import ModelResponse, TResponseInputItem
|
||||
from agents.tool import Tool
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
LLM_TURN_KEY = "llm_turn"
|
||||
# ``run_strix_scan(subagent_turn_limits=...)``, and the limit it gives one sub-agent.
|
||||
SUBAGENT_TURN_LIMITS_KEY = "subagent_turn_limits"
|
||||
TURN_LIMIT_KEY = "turn_limit"
|
||||
_TURN_LIMIT_WARN_BEFORE = 10
|
||||
_TURN_LIMIT_REPEAT = 5
|
||||
|
||||
# ``BudgetPolicy`` decides what happens when the accumulated LLM cost reaches
|
||||
# ``max_budget_usd``.
|
||||
@@ -43,8 +35,6 @@ _TURN_LIMIT_REPEAT = 5
|
||||
# same call when the operator raises the limit or resumes.
|
||||
__all__ = [
|
||||
"LLM_TURN_KEY",
|
||||
"SUBAGENT_TURN_LIMITS_KEY",
|
||||
"TURN_LIMIT_KEY",
|
||||
"BudgetExceededError",
|
||||
"BudgetPausedError",
|
||||
"BudgetPolicy",
|
||||
@@ -75,12 +65,9 @@ class BudgetPausedError(RuntimeError):
|
||||
decided to park; the agent waits for a resume newer than that.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, message: str, *, resume_epoch: int = 0, operator_pause: bool = False
|
||||
) -> None:
|
||||
def __init__(self, message: str, *, resume_epoch: int = 0) -> None:
|
||||
super().__init__(message)
|
||||
self.resume_epoch = resume_epoch
|
||||
self.operator_pause = operator_pause
|
||||
|
||||
|
||||
def _validate_budget(max_budget_usd: float | None) -> None:
|
||||
@@ -173,7 +160,6 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
max_turns: int | None = None,
|
||||
interactive: bool = False,
|
||||
budget_policy: BudgetPolicy = "stop",
|
||||
record_tool_timings: bool = False,
|
||||
) -> None:
|
||||
_validate_budget(max_budget_usd)
|
||||
if max_turns is not None and max_turns <= 0:
|
||||
@@ -186,8 +172,6 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
self._max_turns = max_turns
|
||||
self._interactive = interactive
|
||||
self._budget_policy: BudgetPolicy = budget_policy
|
||||
self._record_tool_timings = record_tool_timings
|
||||
self._tool_starts: dict[str, tuple[datetime, float]] = {}
|
||||
|
||||
@property
|
||||
def max_budget_usd(self) -> float | None:
|
||||
@@ -214,26 +198,15 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
system_prompt: str | None, # noqa: ARG002
|
||||
input_items: list[TResponseInputItem],
|
||||
) -> None:
|
||||
self._pause_if_operator_paused(context)
|
||||
if self._budget_policy == "pause":
|
||||
self._pause_if_limited(context)
|
||||
context.context[LLM_TURN_KEY] = int(context.context.get(LLM_TURN_KEY, 0)) + 1
|
||||
try:
|
||||
self._maybe_warn_turns(context, input_items)
|
||||
self._maybe_warn_soft_turn_limit(context, input_items)
|
||||
self._maybe_warn_budget(context, input_items)
|
||||
except Exception:
|
||||
logger.exception("budget/turn warning injection failed")
|
||||
|
||||
def _pause_if_operator_paused(self, context: RunContextWrapper[dict[str, Any]]) -> None:
|
||||
coordinator = coordinator_from_context(context.context)
|
||||
if coordinator is not None and coordinator.operator_paused:
|
||||
raise BudgetPausedError(
|
||||
"scan paused; waiting for the operator to resume",
|
||||
resume_epoch=coordinator.resume_epoch,
|
||||
operator_pause=True,
|
||||
)
|
||||
|
||||
def _pause_if_limited(self, context: RunContextWrapper[dict[str, Any]]) -> None:
|
||||
"""Park the agent before a paid call when the scan is at its limit or paused.
|
||||
|
||||
@@ -284,40 +257,6 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
)
|
||||
input_items.append({"role": "user", "content": content})
|
||||
|
||||
def _maybe_warn_soft_turn_limit(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
input_items: list[TResponseInputItem],
|
||||
) -> None:
|
||||
"""A soft limit, never a stop: warn 10 turns before it, then every few turns past it."""
|
||||
limit = context.context.get(TURN_LIMIT_KEY)
|
||||
requests = getattr(getattr(context, "usage", None), "requests", None)
|
||||
if not isinstance(limit, int) or not isinstance(requests, int):
|
||||
return
|
||||
turns_used = requests + 1
|
||||
if turns_used == limit - _TURN_LIMIT_WARN_BEFORE:
|
||||
content = (
|
||||
f"[NOTICE] You have used {turns_used} of your {limit}-turn limit. Start "
|
||||
"wrapping up: take only the steps you still need to answer your task, then "
|
||||
"call agent_finish. You may go past the limit only while you are still "
|
||||
"making real progress."
|
||||
)
|
||||
elif turns_used >= limit and (turns_used - limit) % _TURN_LIMIT_REPEAT == 0:
|
||||
content = (
|
||||
f"[NOTICE] You have used {turns_used} turns, past your {limit}-turn limit. "
|
||||
"Call agent_finish now with what you have, listing anything unresolved in "
|
||||
"open_items, unless your next steps are clearly making progress on your task."
|
||||
)
|
||||
else:
|
||||
return
|
||||
logger.info(
|
||||
"Turn-limit notice for agent %s: turn %d, limit %d",
|
||||
context.context.get("agent_id"),
|
||||
turns_used,
|
||||
limit,
|
||||
)
|
||||
input_items.append({"role": "user", "content": content})
|
||||
|
||||
def _maybe_warn_budget(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
@@ -363,37 +302,6 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
)
|
||||
input_items.append({"role": "user", "content": content})
|
||||
|
||||
async def on_tool_start(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
agent: Agent[dict[str, Any]], # noqa: ARG002
|
||||
tool: Tool, # noqa: ARG002
|
||||
) -> None:
|
||||
call_id = getattr(context, "tool_call_id", None)
|
||||
if self._record_tool_timings and isinstance(call_id, str):
|
||||
self._tool_starts[call_id] = (datetime.now(UTC), time.monotonic())
|
||||
|
||||
async def on_tool_end(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
agent: Agent[dict[str, Any]], # noqa: ARG002
|
||||
tool: Tool,
|
||||
result: object, # noqa: ARG002
|
||||
) -> None:
|
||||
call_id = getattr(context, "tool_call_id", None)
|
||||
start = self._tool_starts.pop(call_id, None) if isinstance(call_id, str) else None
|
||||
if start is None:
|
||||
return
|
||||
started_at, started = start
|
||||
ctx = context.context if isinstance(context.context, dict) else {}
|
||||
agent_id = ctx.get("agent_id")
|
||||
record_tool_call(
|
||||
started_at=started_at,
|
||||
agent_id=agent_id if isinstance(agent_id, str) else None,
|
||||
tool=tool.name,
|
||||
duration_ms=round((time.monotonic() - started) * 1000),
|
||||
)
|
||||
|
||||
async def on_llm_end(
|
||||
self,
|
||||
context: RunContextWrapper[dict[str, Any]],
|
||||
|
||||
+5
-14
@@ -247,30 +247,25 @@ def make_model_settings(
|
||||
*,
|
||||
model_name: str,
|
||||
force_required_tool_choice: bool = False,
|
||||
parallel_tool_calls: bool = False,
|
||||
request_timeout: float | None = None,
|
||||
prompt_cache: bool = True,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
has_tools: bool = True,
|
||||
) -> ModelSettings:
|
||||
headers = _request_headers(model_name, extra_headers)
|
||||
model_settings = ModelSettings(
|
||||
parallel_tool_calls=parallel_tool_calls if has_tools else None,
|
||||
parallel_tool_calls=False if has_tools else None,
|
||||
retry=DEFAULT_MODEL_RETRY,
|
||||
include_usage=True,
|
||||
extra_args=request_timeout_extra_args(request_timeout),
|
||||
extra_headers=headers,
|
||||
extra_body=dict(extra_body) if extra_body else None,
|
||||
)
|
||||
if (
|
||||
reasoning_effort is not None
|
||||
and reasoning_effort != "none"
|
||||
and model_supports_reasoning(model_name)
|
||||
):
|
||||
model_settings = model_settings.resolve(
|
||||
_reasoning_settings(reasoning_effort, extra_body),
|
||||
)
|
||||
model_settings = model_settings.resolve(_reasoning_settings(reasoning_effort))
|
||||
if force_required_tool_choice and _accepts_required_tool_choice(model_name):
|
||||
model_settings = model_settings.resolve(ModelSettings(tool_choice="required"))
|
||||
|
||||
@@ -295,9 +290,7 @@ def _request_headers(
|
||||
return headers or None
|
||||
|
||||
|
||||
def _reasoning_settings(
|
||||
effort: ReasoningEffort, extra_body: dict[str, Any] | None = None
|
||||
) -> ModelSettings:
|
||||
def _reasoning_settings(effort: ReasoningEffort) -> ModelSettings:
|
||||
"""``max`` is not in the OpenAI SDK's ``Reasoning.effort`` enum, so send it as
|
||||
a raw body field instead — also keeping it clear of LiteLLM's DeepSeek mapping,
|
||||
which collapses every ``reasoning_effort`` level to plain thinking-enabled.
|
||||
@@ -305,13 +298,11 @@ def _reasoning_settings(
|
||||
|
||||
It goes in ``extra_body``, the field every model implementation forwards as the
|
||||
request's ``extra_body``; the same value under ``extra_args`` collides with that
|
||||
keyword and raises before a request is ever sent. ``resolve`` replaces
|
||||
``extra_body`` wholesale, so the user's ``LLM_EXTRA_BODY`` fields are carried
|
||||
over, with ``reasoning_effort`` taking precedence.
|
||||
keyword and raises before a request is ever sent.
|
||||
"""
|
||||
if effort != "max":
|
||||
return ModelSettings(reasoning=Reasoning(effort=effort))
|
||||
return ModelSettings(extra_body={**(extra_body or {}), "reasoning_effort": "max"})
|
||||
return ModelSettings(extra_body={"reasoning_effort": "max"})
|
||||
|
||||
|
||||
def _prompt_cache_extra_args(model_name: str) -> dict[str, Any] | None:
|
||||
|
||||
+3
-46
@@ -8,7 +8,7 @@ import io
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -36,12 +36,7 @@ from strix.core.execution import (
|
||||
from strix.core.execution import (
|
||||
spawn_child_agent as start_child_agent,
|
||||
)
|
||||
from strix.core.hooks import (
|
||||
SUBAGENT_TURN_LIMITS_KEY,
|
||||
BudgetExceededError,
|
||||
ReportUsageHooks,
|
||||
recomputed_budget_flags,
|
||||
)
|
||||
from strix.core.hooks import BudgetExceededError, ReportUsageHooks, recomputed_budget_flags
|
||||
from strix.core.inputs import (
|
||||
build_root_task,
|
||||
build_scan_targets,
|
||||
@@ -59,7 +54,6 @@ from strix.tools.output_store import (
|
||||
WORKSPACE_SPILL_DIR,
|
||||
configure_spill_writer,
|
||||
)
|
||||
from strix.tools.reporting.tool import create_vulnerability_report
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -176,17 +170,11 @@ def _compose_root_instructions_override(
|
||||
is_whitebox: bool,
|
||||
is_diff_scoped: bool,
|
||||
interactive: bool,
|
||||
parallel_tool_calls: bool,
|
||||
system_prompt_context: dict[str, Any],
|
||||
supports_images: bool,
|
||||
report_tool: str,
|
||||
coverage_tools: bool,
|
||||
replace_scan_prompt: bool = False,
|
||||
) -> str | None:
|
||||
if root_instructions_override is None:
|
||||
return None
|
||||
if replace_scan_prompt:
|
||||
return f"{root_instructions_override}\n\n{render_scope_prompt(system_prompt_context)}"
|
||||
|
||||
base_instructions = render_system_prompt(
|
||||
skills=skills,
|
||||
@@ -195,12 +183,9 @@ def _compose_root_instructions_override(
|
||||
is_root=True,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
system_prompt_context=system_prompt_context,
|
||||
include_scope=False,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool,
|
||||
coverage_tools=coverage_tools,
|
||||
)
|
||||
return (
|
||||
f"{base_instructions}\n\n"
|
||||
@@ -218,7 +203,7 @@ async def run_strix_scan(
|
||||
scan_id: str | None = None,
|
||||
image: str,
|
||||
local_sources: list[dict[str, Any]] | None = None,
|
||||
extra_files: list[dict[str, Any]] | Awaitable[list[dict[str, Any]] | None] | None = None,
|
||||
extra_files: list[dict[str, Any]] | None = None,
|
||||
coordinator: AgentCoordinator | None = None,
|
||||
interactive: bool = False,
|
||||
max_turns: int = DEFAULT_MAX_TURNS,
|
||||
@@ -228,22 +213,16 @@ async def run_strix_scan(
|
||||
cleanup_on_exit: bool = True,
|
||||
event_sink: StreamEventSink | None = None,
|
||||
root_instructions_override: str | None = None,
|
||||
replace_root_scan_prompt: bool = False,
|
||||
extra_system_prompt_context: dict[str, Any] | None = None,
|
||||
status_sink: StatusSink | None = None,
|
||||
mcp_connection_requests: list[McpConnectionRequest] | None = None,
|
||||
mcp_status_sink: McpStatusSink | None = None,
|
||||
root_finish_tool: Tool = finish_scan,
|
||||
report_tool: Tool = create_vulnerability_report,
|
||||
coverage_tools: bool = True,
|
||||
subagent_turn_limits: Mapping[str, int] | None = None,
|
||||
) -> RunResultBase | None:
|
||||
"""Run or resume one Strix scan against a sandbox.
|
||||
|
||||
``root_instructions_override`` adds root scan instructions to the rendered
|
||||
root prompt without replacing the system-verified scope block.
|
||||
``replace_root_scan_prompt=True`` makes it the root's whole prompt, followed
|
||||
by the scope, instead of adding it to the rendered scan prompt.
|
||||
``extra_files`` entries (``{"workspace_path", "content"}``) are placed into
|
||||
the sandbox workspace at session bring-up; see
|
||||
:func:`strix.runtime.session_manager.create_or_reuse`.
|
||||
@@ -262,13 +241,6 @@ async def run_strix_scan(
|
||||
way the engine does the connecting, so the caller passes inert configs plus
|
||||
metadata and never live sessions.
|
||||
``root_finish_tool`` is the tool the root agent ends the run with.
|
||||
``report_tool`` is the tool every agent files findings with, in place of
|
||||
``create_vulnerability_report``.
|
||||
``coverage_tools=False`` drops the coverage ledger tools and their prompt
|
||||
rules from every agent.
|
||||
``subagent_turn_limits`` maps a skill name to a soft turn limit for sub-agents
|
||||
spawned with that skill (the largest applies). Agents are warned 10 turns
|
||||
before it and reminded past it, but never stopped.
|
||||
"""
|
||||
|
||||
def report(phase: str) -> None:
|
||||
@@ -426,11 +398,9 @@ async def run_strix_scan(
|
||||
settings.llm.reasoning_effort,
|
||||
model_name=resolved_model,
|
||||
force_required_tool_choice=settings.llm.force_required_tool_choice,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
request_timeout=settings.llm.timeout,
|
||||
prompt_cache=settings.llm.prompt_cache,
|
||||
extra_headers=settings.llm.extra_headers,
|
||||
extra_body=settings.llm.extra_body,
|
||||
)
|
||||
run_config = RunConfig(
|
||||
model=resolved_model,
|
||||
@@ -448,7 +418,6 @@ async def run_strix_scan(
|
||||
max_turns=max_turns,
|
||||
interactive=interactive,
|
||||
budget_policy=budget_policy,
|
||||
record_tool_timings=settings.telemetry.run_record_timings,
|
||||
)
|
||||
coordinator.set_budget_limit_setter(hooks.set_max_budget_usd)
|
||||
if interactive and budget_policy != "pause":
|
||||
@@ -522,12 +491,8 @@ async def run_strix_scan(
|
||||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
system_prompt_context=root_context,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool.name,
|
||||
coverage_tools=coverage_tools,
|
||||
replace_scan_prompt=replace_root_scan_prompt,
|
||||
)
|
||||
|
||||
root_agent = build_strix_agent(
|
||||
@@ -538,15 +503,12 @@ async def run_strix_scan(
|
||||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
chat_completions_tools=chat_completions_tools,
|
||||
strict_tool_schemas=strict_tool_schemas,
|
||||
system_prompt_context=root_context,
|
||||
instructions_override=root_instructions,
|
||||
supports_images=supports_images,
|
||||
finish_tool=root_finish_tool,
|
||||
report_tool=report_tool,
|
||||
coverage_tools=coverage_tools,
|
||||
)
|
||||
|
||||
if not is_resume:
|
||||
@@ -563,13 +525,10 @@ async def run_strix_scan(
|
||||
is_whitebox=is_whitebox,
|
||||
is_diff_scoped=is_diff_scoped,
|
||||
interactive=interactive,
|
||||
parallel_tool_calls=settings.llm.parallel_tool_calls,
|
||||
chat_completions_tools=chat_completions_tools,
|
||||
strict_tool_schemas=strict_tool_schemas,
|
||||
system_prompt_context=scope_context,
|
||||
supports_images=supports_images,
|
||||
report_tool=report_tool,
|
||||
coverage_tools=coverage_tools,
|
||||
)
|
||||
|
||||
async def spawn_child_agent(**kwargs: Any) -> dict[str, Any]:
|
||||
@@ -598,8 +557,6 @@ async def run_strix_scan(
|
||||
"scan_targets": build_scan_targets(scan_config),
|
||||
"max_context_images": settings.runtime.max_context_images,
|
||||
"supports_images": supports_images,
|
||||
"coverage_tools": coverage_tools,
|
||||
SUBAGENT_TURN_LIMITS_KEY: dict(subagent_turn_limits or {}),
|
||||
}
|
||||
|
||||
root_session = open_agent_session(root_id, agents_db)
|
||||
|
||||
@@ -164,13 +164,12 @@ async def warm_up_llm() -> None:
|
||||
raw_model = dedupe_model
|
||||
deduper = resolve_dedupe_model(settings.dedupe, dedupe_model)
|
||||
# A dedicated dedupe model may route to another provider, which must
|
||||
# never receive the main endpoint's headers or body fields; it has
|
||||
# its own DEDUPE_LLM_EXTRA_HEADERS and DEDUPE_LLM_EXTRA_BODY.
|
||||
# never receive the main endpoint's headers; it has its own
|
||||
# DEDUPE_LLM_EXTRA_HEADERS.
|
||||
await preflight_request(
|
||||
deduper,
|
||||
model_name=dedupe_model,
|
||||
extra_headers=settings.dedupe.extra_headers,
|
||||
extra_body=settings.dedupe.extra_body,
|
||||
timeout=llm.preflight_timeout,
|
||||
api_base_setting="DEDUPE_LLM_API_BASE",
|
||||
)
|
||||
|
||||
@@ -116,7 +116,6 @@ async def preflight_model_connection(
|
||||
model,
|
||||
model_name=model_name,
|
||||
extra_headers=resolved_settings.llm.extra_headers,
|
||||
extra_body=resolved_settings.llm.extra_body,
|
||||
timeout=resolved_settings.llm.preflight_timeout,
|
||||
api_base_setting="LLM_API_BASE",
|
||||
)
|
||||
@@ -127,7 +126,6 @@ async def preflight_request(
|
||||
*,
|
||||
model_name: str,
|
||||
extra_headers: dict[str, str] | None,
|
||||
extra_body: dict[str, Any] | None,
|
||||
timeout: int,
|
||||
api_base_setting: str,
|
||||
) -> None:
|
||||
@@ -146,7 +144,6 @@ async def preflight_request(
|
||||
request_timeout=timeout,
|
||||
prompt_cache=False,
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
has_tools=False,
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -66,7 +66,7 @@ func panelFocus(panel sidebarPanel) (focusMode, bool) {
|
||||
|
||||
func (m Model) sidebarPanels() []panelRect {
|
||||
statsHeight, vulnHeight, mcpHeight, agentHeight := m.sidebarHeights()
|
||||
top := m.sidebarTopHeight()
|
||||
top := m.viewerHeight()
|
||||
rects := []panelRect{{panelAgents, top, agentHeight}}
|
||||
top += agentHeight
|
||||
if vulnHeight > 0 {
|
||||
@@ -83,7 +83,7 @@ func (m Model) sidebarPanels() []panelRect {
|
||||
|
||||
func (m Model) sidebarGap() int {
|
||||
statsHeight, vulnHeight, mcpHeight, agentHeight := m.sidebarHeights()
|
||||
return max(0, m.height-m.sidebarTopHeight()-agentHeight-vulnHeight-mcpHeight-statsHeight)
|
||||
return max(0, m.height-m.viewerHeight()-agentHeight-vulnHeight-mcpHeight-statsHeight)
|
||||
}
|
||||
|
||||
func (m Model) panelAt(y int) (panelRect, bool) {
|
||||
|
||||
@@ -355,77 +355,3 @@ func TestLonePanelHasNoControls(t *testing.T) {
|
||||
t.Fatalf("controls should return once a second panel exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetsBoxSitsUnderViewer(t *testing.T) {
|
||||
m := panelsModel(t)
|
||||
m.snapshot.Targets = []string{"https://example.com", "./app"}
|
||||
m.snapshot.TargetCount = 2
|
||||
m.resizeViewport()
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
rows := strings.Split(ansi.Strip(m.sidebarView(sidebarWidth, m.height)), "\n")
|
||||
if len(rows) != m.height {
|
||||
t.Fatalf("sidebar renders %d rows, want %d", len(rows), m.height)
|
||||
}
|
||||
top := m.viewerHeight()
|
||||
for i, want := range []string{"Targets 2", "▸ https://example.com", "▸ ./app"} {
|
||||
if !strings.Contains(rows[top+1+i], want) {
|
||||
t.Fatalf("row %d missing %q:\n%s", top+1+i, want, strings.Join(rows, "\n"))
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rows[panelRectOf(t, m, panelAgents).top+1], "Agents") {
|
||||
t.Fatalf("agents panel not where its rect says:\n%s", strings.Join(rows, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetsBoxDropsOnShortTerminal(t *testing.T) {
|
||||
m := panelsModel(t)
|
||||
m.height = 19
|
||||
m.snapshot.Targets = []string{"a", "b", "c", "d", "e"}
|
||||
m.snapshot.TargetCount = 5
|
||||
m.resizeViewport()
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
if got := lipgloss.Height(m.sidebarView(sidebarWidth, m.height)); got > m.height {
|
||||
t.Fatalf("sidebar renders %d rows on a %d-row terminal", got, m.height)
|
||||
}
|
||||
}
|
||||
|
||||
// The targets box only takes rows the agent tree would otherwise grow into, so
|
||||
// the stats panel keeps the Caido URL and the other panels keep their heights.
|
||||
func TestTargetsBoxNeverSqueezesPanels(t *testing.T) {
|
||||
for height := 19; height <= 40; height++ {
|
||||
m := panelsModel(t)
|
||||
m.height = height
|
||||
m.snapshot.Usage = map[string]any{"total_tokens": 1234, "cost": 0.5}
|
||||
m.snapshot.CaidoURL = "http://127.0.0.1:8080"
|
||||
m.resizeViewport()
|
||||
stats, vuln, mcp, _ := m.sidebarHeights()
|
||||
m.snapshot.Targets = []string{"a", "b", "c", "d", "e"}
|
||||
m.snapshot.TargetCount = 5
|
||||
m.resizeViewport()
|
||||
gotStats, gotVuln, gotMcp, _ := m.sidebarHeights()
|
||||
if gotStats != stats || gotVuln != vuln || gotMcp != mcp {
|
||||
t.Fatalf("height %d: targets squeezed panels: stats %d->%d findings %d->%d mcp %d->%d",
|
||||
height, stats, gotStats, vuln, gotVuln, mcp, gotMcp)
|
||||
}
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
if got := lipgloss.Height(m.sidebarView(sidebarWidth, m.height)); got != m.height {
|
||||
t.Fatalf("height %d: sidebar renders %d rows", height, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetsViewCountsTheTargetsItCannotShow(t *testing.T) {
|
||||
m := panelsModel(t)
|
||||
m.snapshot.Targets = []string{"a", "b", "c", "d", "e"}
|
||||
m.snapshot.TargetCount = 5
|
||||
for rows, want := range map[int]string{
|
||||
2: "Targets 5\n+5 more",
|
||||
3: "Targets 5\n▸ a\n+4 more",
|
||||
6: "Targets 5\n▸ a\n▸ b\n▸ c\n▸ d\n+1 more",
|
||||
9: "Targets 5\n▸ a\n▸ b\n▸ c\n▸ d\n+1 more",
|
||||
} {
|
||||
if got := ansi.Strip(m.targetsView(20, rows)); got != want {
|
||||
t.Fatalf("%d rows:\n%s\nwant:\n%s", rows, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +421,7 @@ func (m Model) setupComposer(width int) string {
|
||||
inner := max(1, width-4)
|
||||
body := m.highlightInputSelection(m.input.View())
|
||||
body += "\n\n" + m.setupSummaryView(inner)
|
||||
if targets := m.targetsView(inner, maxTargetRows); targets != "" {
|
||||
if targets := m.setupTargetsView(inner); targets != "" {
|
||||
body += "\n" + targets
|
||||
}
|
||||
return lipgloss.NewStyle().Width(max(1, width-2)).Padding(0, 1).
|
||||
@@ -454,22 +454,15 @@ func (m Model) setupSummaryView(width int) string {
|
||||
return truncate(strings.Join(chips, render.Dim().Render(" · ")), max(1, width))
|
||||
}
|
||||
|
||||
// maxTargetRows fits the header, four targets and the count of the rest.
|
||||
const maxTargetRows = 6
|
||||
|
||||
// targetsView lists what the scan is pointed at, once anything is queued, in at
|
||||
// most maxRows rows. Targets that do not fit are counted on the last row.
|
||||
func (m Model) targetsView(width, maxRows int) string {
|
||||
if len(m.snapshot.Targets) == 0 || maxRows < 2 {
|
||||
// setupTargetsView lists what the scan is pointed at, once anything is queued.
|
||||
func (m Model) setupTargetsView(width int) string {
|
||||
if len(m.snapshot.Targets) == 0 {
|
||||
return ""
|
||||
}
|
||||
const visible = 4
|
||||
total := max(m.snapshot.TargetCount, len(m.snapshot.Targets))
|
||||
visible := min(len(m.snapshot.Targets), maxTargetRows-2, maxRows-1)
|
||||
if visible < total {
|
||||
visible = min(visible, maxRows-2)
|
||||
}
|
||||
rows := []string{render.Bold(green).Render("Targets") + render.Dim().Render(fmt.Sprintf(" %d", total))}
|
||||
for _, target := range m.snapshot.Targets[:visible] {
|
||||
for _, target := range m.snapshot.Targets[:min(visible, len(m.snapshot.Targets))] {
|
||||
rows = append(rows, render.Col(dim).Render("▸ ")+render.Col(white).Render(truncate(target, max(1, width-2))))
|
||||
}
|
||||
if hidden := total - visible; hidden > 0 {
|
||||
|
||||
@@ -542,15 +542,6 @@ func (m Model) sidebarView(width, height int) string {
|
||||
BorderForeground(dark).
|
||||
Padding(0, 1).
|
||||
Render(m.viewerBox(width - 4))}
|
||||
if targetsHeight := m.targetsBoxHeight(); targetsHeight > 0 {
|
||||
parts = append(parts, lipgloss.NewStyle().
|
||||
Width(width-2).
|
||||
Height(targetsHeight-2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(dark).
|
||||
Padding(0, 1).
|
||||
Render(m.targetsView(width-4, targetsHeight-2)))
|
||||
}
|
||||
agents := ""
|
||||
if agentHeight > 1 {
|
||||
agentRows := max(1, agentHeight-4)
|
||||
@@ -601,13 +592,6 @@ func (m Model) sidebarView(width, height int) string {
|
||||
}
|
||||
|
||||
func (m Model) sidebarHeights() (statsHeight, vulnHeight, mcpHeight, agentHeight int) {
|
||||
_, statsHeight, vulnHeight, mcpHeight, agentHeight = m.sidebarLayout()
|
||||
return
|
||||
}
|
||||
|
||||
// sidebarLayout sizes the targets box and the panels. The targets box only
|
||||
// takes rows no panel needs, so it never squeezes the panels below it.
|
||||
func (m Model) sidebarLayout() (targetsHeight, statsHeight, vulnHeight, mcpHeight, agentHeight int) {
|
||||
// Measure the stats panel the way its box will render it: a long model name
|
||||
// wraps inside the sidebar, and counting only its newlines would size the
|
||||
// box short and push the whole frame past the bottom of the terminal.
|
||||
@@ -644,14 +628,6 @@ func (m Model) sidebarLayout() (targetsHeight, statsHeight, vulnHeight, mcpHeigh
|
||||
}
|
||||
}
|
||||
spare := max(0, m.height-m.viewerHeight()-statsHeight-vulnHeight-mcpHeight-agentHeight)
|
||||
// The box needs its border and two rows: the header and a target or the
|
||||
// count of hidden ones. A zoomed panel keeps the whole sidebar.
|
||||
if targets := m.targetsView(m.sidebarInnerWidth(), maxTargetRows); targets != "" && m.zoomedPanel == panelNone {
|
||||
if fit := min(lipgloss.Height(targets)+2, spare); fit >= 4 {
|
||||
targetsHeight = fit
|
||||
spare -= fit
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case m.zoomedPanel == panelFindings && vulnHeight > 1:
|
||||
vulnHeight += spare
|
||||
@@ -667,17 +643,6 @@ func (m Model) viewerHeight() int {
|
||||
return strings.Count(m.viewerView(m.viewerContentWidth()), "\n") + 3
|
||||
}
|
||||
|
||||
// sidebarTopHeight is the rows above the panels: the viewer box and, when the
|
||||
// run has targets, the targets box under it.
|
||||
func (m Model) sidebarTopHeight() int {
|
||||
return m.viewerHeight() + m.targetsBoxHeight()
|
||||
}
|
||||
|
||||
func (m Model) targetsBoxHeight() int {
|
||||
targetsHeight, _, _, _, _ := m.sidebarLayout()
|
||||
return targetsHeight
|
||||
}
|
||||
|
||||
func (m Model) sidebarInnerWidth() int {
|
||||
_, sidebarWidth, _, _ := m.layout()
|
||||
if sidebarWidth == 0 {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Supply-chain cooldown: only install versions published at least 5 days ago (npm >= 11.10).
|
||||
min-release-age=5
|
||||
+4
-70
@@ -29,7 +29,7 @@
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.4.4"
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1401,72 +1401,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
|
||||
@@ -4162,9 +4096,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz",
|
||||
"integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==",
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -30,6 +30,6 @@
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.4.4"
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,6 @@ async def _summarize(model: str, prompt: str, max_tokens: int) -> str | None:
|
||||
request_timeout=llm.timeout,
|
||||
prompt_cache=False,
|
||||
extra_headers=llm.extra_headers,
|
||||
extra_body=llm.extra_body,
|
||||
has_tools=False,
|
||||
).resolve(ModelSettings(max_tokens=max_tokens))
|
||||
try:
|
||||
|
||||
@@ -39,13 +39,11 @@ def _dedupe_model_settings(
|
||||
model_name=model_name,
|
||||
force_required_tool_choice=False,
|
||||
request_timeout=request_timeout,
|
||||
# The main model's headers and body fields apply only when dedupe falls
|
||||
# back to the main model; a dedicated dedupe model may route to another
|
||||
# provider, which must never receive the main endpoint's credentials or
|
||||
# identifiers. A dedicated model gets its own DEDUPE_LLM_EXTRA_HEADERS
|
||||
# and DEDUPE_LLM_EXTRA_BODY instead.
|
||||
# The main model's headers apply only when dedupe falls back to the main
|
||||
# model; a dedicated dedupe model may route to another provider, which
|
||||
# must never receive the main endpoint's credentials. A dedicated model
|
||||
# gets its own DEDUPE_LLM_EXTRA_HEADERS instead.
|
||||
extra_headers=dedupe.extra_headers if dedupe.model else llm.extra_headers,
|
||||
extra_body=dedupe.extra_body if dedupe.model else llm.extra_body,
|
||||
has_tools=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ from strix.telemetry import posthog, scarf
|
||||
if TYPE_CHECKING:
|
||||
from agents.usage import Usage
|
||||
|
||||
from strix.llm.request_log import LlmRequestEvent
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1021,47 +1019,6 @@ class StreamedOpenRouterCosts:
|
||||
streamed_openrouter_costs = StreamedOpenRouterCosts()
|
||||
|
||||
|
||||
def record_llm_request(event: "LlmRequestEvent") -> None:
|
||||
"""Request-log sink: each provider attempt's timing, kept in run.json's ``llm_requests``."""
|
||||
report_state = get_global_report_state()
|
||||
if report_state is None:
|
||||
return
|
||||
details = event.details or {}
|
||||
report_state.run_record.setdefault("llm_requests", []).append(
|
||||
{
|
||||
"started_at": event.started_at.isoformat(),
|
||||
"agent_id": event.agent_id,
|
||||
"model": event.model,
|
||||
"provider": details.get("upstream_provider") or event.provider,
|
||||
"outcome": event.outcome,
|
||||
"status": event.status_code,
|
||||
"duration_ms": event.duration_ms,
|
||||
"ttft_ms": event.time_to_first_token_ms,
|
||||
"input_tokens": event.input_tokens,
|
||||
"cached_tokens": event.cached_input_tokens,
|
||||
"output_tokens": event.output_tokens,
|
||||
"retry_attempt": event.retry_attempt,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def record_tool_call(
|
||||
*, started_at: datetime, agent_id: str | None, tool: str, duration_ms: int
|
||||
) -> None:
|
||||
"""Each tool call's timing, kept in run.json's ``tool_calls``."""
|
||||
report_state = get_global_report_state()
|
||||
if report_state is None:
|
||||
return
|
||||
report_state.run_record.setdefault("tool_calls", []).append(
|
||||
{
|
||||
"started_at": started_at.isoformat(),
|
||||
"agent_id": agent_id,
|
||||
"tool": tool,
|
||||
"duration_ms": duration_ms,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def record_openrouter_provider(provider: Any, usage: Any) -> None:
|
||||
"""Tally which upstream provider served a stream, from its final usage chunk.
|
||||
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents.sandbox.session.manager import Instrumentation
|
||||
from agents.sandbox.session.sandbox_session import SandboxSession
|
||||
from agents.sandbox.session.sandbox_session_state import SandboxSessionState
|
||||
from agents.sandbox.snapshot import NoopSnapshot
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import io
|
||||
from collections.abc import Sequence
|
||||
|
||||
from agents.sandbox.entries import BaseEntry
|
||||
from agents.sandbox.manifest import Manifest
|
||||
from agents.sandbox.materialization import MaterializedFile
|
||||
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
|
||||
from agents.sandbox.types import ExecResult, ExposedPortEndpoint, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _PendingSessionState(SandboxSessionState):
|
||||
type: str = "pending"
|
||||
|
||||
|
||||
class DeferredSandboxSession(SandboxSession):
|
||||
def __init__(
|
||||
self,
|
||||
bring_up: asyncio.Task[tuple[Any, BaseSandboxSession]],
|
||||
*,
|
||||
manifest: Manifest,
|
||||
exposed_ports: tuple[int, ...] = (),
|
||||
) -> None:
|
||||
self._bring_up = bring_up
|
||||
self._pending_state: SandboxSessionState = _PendingSessionState(
|
||||
snapshot=NoopSnapshot(id="pending"),
|
||||
manifest=manifest,
|
||||
exposed_ports=exposed_ports,
|
||||
)
|
||||
self._pending_limits: Any = None
|
||||
self._pending_archive_limits: Any = None
|
||||
self._resolved: BaseSandboxSession | None = None
|
||||
self._client: Any = None
|
||||
self._instrumentation: Instrumentation = Instrumentation()
|
||||
self._seq = 0
|
||||
|
||||
@property
|
||||
def pending(self) -> bool:
|
||||
return self._resolved is None
|
||||
|
||||
@property
|
||||
def bring_up(self) -> asyncio.Task[tuple[Any, BaseSandboxSession]]:
|
||||
return self._bring_up
|
||||
|
||||
async def resolve(self) -> BaseSandboxSession:
|
||||
if self._resolved is not None:
|
||||
return self._resolved
|
||||
client, session = await asyncio.shield(self._bring_up)
|
||||
self._adopt(client, session)
|
||||
return session
|
||||
|
||||
def _adopt(self, client: Any, session: BaseSandboxSession) -> None:
|
||||
if self._resolved is not None:
|
||||
return
|
||||
self._client = client
|
||||
self._resolved = session
|
||||
self._inner = session
|
||||
if self._pending_limits is not None:
|
||||
session._set_concurrency_limits(self._pending_limits)
|
||||
if self._pending_archive_limits is not None:
|
||||
session._set_archive_limits(self._pending_archive_limits)
|
||||
if self._pending_state.manifest != session.state.manifest:
|
||||
session.state = session.state.model_copy(
|
||||
update={"manifest": self._pending_state.manifest}
|
||||
)
|
||||
|
||||
@property
|
||||
def client(self) -> Any:
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def state(self) -> SandboxSessionState:
|
||||
if self._resolved is None:
|
||||
return self._pending_state
|
||||
return self._resolved.state
|
||||
|
||||
@state.setter
|
||||
def state(self, value: SandboxSessionState) -> None:
|
||||
if self._resolved is None:
|
||||
self._pending_state = value
|
||||
else:
|
||||
self._resolved.state = value
|
||||
|
||||
def _set_concurrency_limits(self, limits: Any) -> None:
|
||||
if self._resolved is None:
|
||||
self._pending_limits = limits
|
||||
else:
|
||||
self._resolved._set_concurrency_limits(limits)
|
||||
|
||||
def _set_archive_limits(self, limits: Any) -> None:
|
||||
if self._resolved is None:
|
||||
self._pending_archive_limits = limits
|
||||
else:
|
||||
self._resolved._set_archive_limits(limits)
|
||||
|
||||
def _manifest_base_dir(self) -> Path:
|
||||
if self._resolved is None:
|
||||
return Path.cwd()
|
||||
return self._resolved._manifest_base_dir()
|
||||
|
||||
def normalize_path(self, path: Path | str, *, for_write: bool = False) -> Path:
|
||||
if self._resolved is None:
|
||||
return Path(path)
|
||||
return self._resolved.normalize_path(path, for_write=for_write)
|
||||
|
||||
def register_persist_workspace_skip_path(self, path: Path | str) -> Path:
|
||||
if self._resolved is None:
|
||||
return Path(path)
|
||||
return self._resolved.register_persist_workspace_skip_path(path)
|
||||
|
||||
def supports_pty(self) -> bool:
|
||||
return self._resolved.supports_pty() if self._resolved is not None else False
|
||||
|
||||
async def running(self) -> bool:
|
||||
if self._resolved is None:
|
||||
if not self._bring_up.done():
|
||||
return True
|
||||
return not self._bring_up.cancelled() and self._bring_up.exception() is None
|
||||
return await self._resolved.running()
|
||||
|
||||
async def start(self) -> None:
|
||||
if self._resolved is None:
|
||||
return
|
||||
await self._resolved.start()
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._resolved is None:
|
||||
return
|
||||
await self._resolved.stop()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
if self._resolved is None:
|
||||
return
|
||||
await self._resolved.shutdown()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self._resolved is None:
|
||||
return
|
||||
await self._resolved.aclose()
|
||||
|
||||
async def _validate_manifest_application(self, *, only_ephemeral: bool = False) -> None:
|
||||
session = await self.resolve()
|
||||
await session._validate_manifest_application(only_ephemeral=only_ephemeral)
|
||||
|
||||
async def apply_manifest(self, *, only_ephemeral: bool = False) -> Any:
|
||||
session = await self.resolve()
|
||||
return await session.apply_manifest(only_ephemeral=only_ephemeral)
|
||||
|
||||
async def _apply_entry_batch(
|
||||
self,
|
||||
entries: Sequence[tuple[Path, BaseEntry]],
|
||||
*,
|
||||
base_dir: Path,
|
||||
) -> list[MaterializedFile]:
|
||||
del base_dir
|
||||
session = await self.resolve()
|
||||
return await session._apply_entry_batch(entries, base_dir=session._manifest_base_dir())
|
||||
|
||||
async def exec(self, *command: str | Path, **kwargs: Any) -> ExecResult:
|
||||
await self.resolve()
|
||||
return await super().exec(*command, **kwargs)
|
||||
|
||||
async def ls(self, *args: Any, **kwargs: Any) -> Any:
|
||||
await self.resolve()
|
||||
return await super().ls(*args, **kwargs)
|
||||
|
||||
async def rm(self, *args: Any, **kwargs: Any) -> Any:
|
||||
await self.resolve()
|
||||
return await super().rm(*args, **kwargs)
|
||||
|
||||
async def mkdir(self, *args: Any, **kwargs: Any) -> Any:
|
||||
await self.resolve()
|
||||
return await super().mkdir(*args, **kwargs)
|
||||
|
||||
async def read(self, path: Path, *, user: str | User | None = None) -> io.IOBase:
|
||||
await self.resolve()
|
||||
return await super().read(path, user=user)
|
||||
|
||||
async def write(self, *args: Any, **kwargs: Any) -> Any:
|
||||
await self.resolve()
|
||||
return await super().write(*args, **kwargs)
|
||||
|
||||
async def resolve_exposed_port(self, port: int) -> ExposedPortEndpoint:
|
||||
await self.resolve()
|
||||
return await super().resolve_exposed_port(port)
|
||||
|
||||
async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint:
|
||||
session = await self.resolve()
|
||||
return await session._resolve_exposed_port(port)
|
||||
|
||||
async def pty_exec_start(self, *args: Any, **kwargs: Any) -> Any:
|
||||
await self.resolve()
|
||||
return await super().pty_exec_start(*args, **kwargs)
|
||||
|
||||
async def pty_write_stdin(self, *args: Any, **kwargs: Any) -> Any:
|
||||
await self.resolve()
|
||||
return await super().pty_write_stdin(*args, **kwargs)
|
||||
|
||||
async def pty_terminate_all(self) -> None:
|
||||
if self._resolved is None:
|
||||
return
|
||||
await super().pty_terminate_all()
|
||||
|
||||
async def persist_workspace(self) -> io.IOBase:
|
||||
await self.resolve()
|
||||
return await super().persist_workspace()
|
||||
|
||||
async def hydrate_workspace(self, data: io.IOBase) -> None:
|
||||
await self.resolve()
|
||||
await super().hydrate_workspace(data)
|
||||
|
||||
async def teardown(self) -> None:
|
||||
if self._resolved is None:
|
||||
self._bring_up.cancel()
|
||||
with contextlib.suppress(BaseException):
|
||||
client, session = await self._bring_up
|
||||
self._adopt(client, session)
|
||||
if self._resolved is None:
|
||||
return
|
||||
await self._client.delete(self._resolved)
|
||||
|
||||
|
||||
class DeferredSandboxClient:
|
||||
def __init__(self, session: DeferredSandboxSession) -> None:
|
||||
self._session = session
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
client = self._session.client
|
||||
if client is None:
|
||||
raise AttributeError(f"sandbox client unavailable before bring-up finishes ({name})")
|
||||
return getattr(client, name)
|
||||
|
||||
async def delete(self, session: Any) -> None:
|
||||
if isinstance(session, DeferredSandboxSession):
|
||||
await session.teardown()
|
||||
return
|
||||
real = await self._session.resolve()
|
||||
await self._session.client.delete(real if session is self._session else session)
|
||||
@@ -66,33 +66,6 @@ def _apply_sandbox_network(create_kwargs: dict[str, Any]) -> None:
|
||||
create_kwargs.pop("ports", None)
|
||||
|
||||
|
||||
_SANDBOX_DEVICES_ENV = "STRIX_DOCKER_SANDBOX_DEVICES"
|
||||
|
||||
|
||||
def _sandbox_devices() -> list[str]:
|
||||
raw = os.environ.get(_SANDBOX_DEVICES_ENV, "")
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
def _apply_sandbox_devices(create_kwargs: dict[str, Any]) -> None:
|
||||
"""Pass the host devices listed in ``STRIX_DOCKER_SANDBOX_DEVICES`` through.
|
||||
|
||||
Comma-separated device paths, each mapped to the same path in the container
|
||||
with ``rwm`` access. A tunnel brought up inside the sandbox (OpenVPN,
|
||||
Tailscale) needs ``/dev/net/tun``: Docker's default device cgroup denies it
|
||||
even with ``NET_ADMIN``. Unset means no extra devices.
|
||||
"""
|
||||
devices = _sandbox_devices()
|
||||
if not devices:
|
||||
return
|
||||
existing = [str(item) for item in (create_kwargs.get("devices") or [])]
|
||||
for path in devices:
|
||||
spec = f"{path}:{path}:rwm"
|
||||
if spec not in existing and path not in existing:
|
||||
existing.append(spec)
|
||||
create_kwargs["devices"] = existing
|
||||
|
||||
|
||||
def _apply_resource_limits(create_kwargs: dict[str, Any]) -> None:
|
||||
"""Apply optional cgroup resource caps from the environment. Unset/blank
|
||||
values leave docker's default (unbounded), so this is opt-in per host."""
|
||||
@@ -260,7 +233,6 @@ class StrixDockerSandboxClient(DockerSandboxClient):
|
||||
extra_hosts["host.docker.internal"] = "host-gateway"
|
||||
|
||||
_apply_sandbox_network(create_kwargs)
|
||||
_apply_sandbox_devices(create_kwargs)
|
||||
_apply_resource_limits(create_kwargs)
|
||||
_apply_log_limits(create_kwargs)
|
||||
_apply_run_labels(create_kwargs)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import inspect
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
@@ -21,12 +19,9 @@ from strix.config import load_settings
|
||||
from strix.runtime.backends import backend_supports_bind_mounts, get_backend
|
||||
from strix.runtime.caido_bootstrap import bootstrap_caido
|
||||
from strix.runtime.caido_handle import CaidoBootstrapHandle
|
||||
from strix.runtime.deferred_session import DeferredSandboxClient, DeferredSandboxSession
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable
|
||||
|
||||
from agents.sandbox.session import BaseSandboxSession
|
||||
|
||||
from strix.runtime.status import StatusSink
|
||||
@@ -260,22 +255,13 @@ def _gitdir_from_pointer(git_file: Path) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _resolve_extra_file_archive(
|
||||
extra_files: list[dict[str, Any]] | Awaitable[list[dict[str, Any]] | None] | None,
|
||||
local_sources: list[dict[str, Any]],
|
||||
) -> bytes | None:
|
||||
files = await extra_files if inspect.isawaitable(extra_files) else extra_files
|
||||
return build_extra_file_archive(files, local_sources) if files else None
|
||||
|
||||
|
||||
async def create_or_reuse(
|
||||
scan_id: str,
|
||||
*,
|
||||
image: str,
|
||||
local_sources: list[dict[str, Any]],
|
||||
extra_files: list[dict[str, Any]] | Awaitable[list[dict[str, Any]] | None] | None = None,
|
||||
extra_files: list[dict[str, Any]] | None = None,
|
||||
status_sink: StatusSink | None = None,
|
||||
defer: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Return the existing session bundle for ``scan_id`` or create a new one.
|
||||
|
||||
@@ -285,9 +271,7 @@ async def create_or_reuse(
|
||||
Each ``extra_files`` entry (``{"workspace_path": "/workspace/<rel>",
|
||||
"content": bytes | str}``) lands as a regular, agent-writable file at its
|
||||
``workspace_path`` on every backend: the files are uploaded as one archive
|
||||
and unpacked inside the sandbox right after bring-up. ``extra_files`` may
|
||||
also be an awaitable resolving to that list: provisioning starts at once and
|
||||
the files are awaited only when the sandbox is ready to receive them.
|
||||
and unpacked inside the sandbox right after bring-up.
|
||||
"""
|
||||
|
||||
def report(phase: str) -> None:
|
||||
@@ -308,6 +292,9 @@ async def create_or_reuse(
|
||||
else:
|
||||
bind_mounts = []
|
||||
entries = build_manifest_entries(local_sources)
|
||||
extra_file_archive = (
|
||||
build_extra_file_archive(extra_files, local_sources) if extra_files else None
|
||||
)
|
||||
|
||||
# Caido runs as an in-container sidecar; HTTP(S) traffic from any
|
||||
# process started via ``session.exec`` (the SDK's Shell tool, etc.)
|
||||
@@ -336,69 +323,41 @@ async def create_or_reuse(
|
||||
backend_name,
|
||||
image,
|
||||
)
|
||||
report("Starting sandbox container")
|
||||
client, session = await backend(
|
||||
image=image,
|
||||
manifest=manifest,
|
||||
exposed_ports=(_CONTAINER_CAIDO_PORT,),
|
||||
bind_mounts=bind_mounts,
|
||||
)
|
||||
|
||||
async def _bring_up() -> tuple[Any, Any]:
|
||||
report("Starting sandbox container")
|
||||
client, session = await _create_or_discard(
|
||||
backend(
|
||||
image=image,
|
||||
manifest=manifest,
|
||||
exposed_ports=(_CONTAINER_CAIDO_PORT,),
|
||||
bind_mounts=bind_mounts,
|
||||
)
|
||||
)
|
||||
if extra_file_archive is not None:
|
||||
report("Placing workspace files")
|
||||
try:
|
||||
extra_file_archive = await _resolve_extra_file_archive(extra_files, local_sources)
|
||||
if extra_file_archive is not None:
|
||||
report("Placing workspace files")
|
||||
await stage_extra_files(session, extra_file_archive)
|
||||
await stage_extra_files(session, extra_file_archive)
|
||||
except BaseException:
|
||||
await _discard_session(client, session)
|
||||
raise
|
||||
return client, session
|
||||
|
||||
async def _caido_url(session: Any) -> str:
|
||||
report("Setting up the proxy")
|
||||
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
|
||||
scheme = "https" if caido_endpoint.tls else "http"
|
||||
host_caido_url = f"{scheme}://{caido_endpoint.host}:{caido_endpoint.port}"
|
||||
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
|
||||
return host_caido_url
|
||||
report("Setting up the proxy")
|
||||
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
|
||||
scheme = "https" if caido_endpoint.tls else "http"
|
||||
host_caido_url = f"{scheme}://{caido_endpoint.host}:{caido_endpoint.port}"
|
||||
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
|
||||
|
||||
def _caido(session: Any, host_caido_url: str) -> Any:
|
||||
return bootstrap_caido(
|
||||
session,
|
||||
host_url=host_caido_url,
|
||||
container_url=container_caido_url,
|
||||
)
|
||||
|
||||
if defer:
|
||||
bring_up = asyncio.create_task(_bring_up(), name=f"sandbox-bring-up-{scan_id}")
|
||||
deferred = DeferredSandboxSession(
|
||||
bring_up,
|
||||
manifest=manifest,
|
||||
exposed_ports=(_CONTAINER_CAIDO_PORT,),
|
||||
)
|
||||
|
||||
async def _caido_when_up() -> Any:
|
||||
session = await deferred.resolve()
|
||||
return await _caido(session, await _caido_url(session))
|
||||
|
||||
bundle = {
|
||||
"client": DeferredSandboxClient(deferred),
|
||||
"session": deferred,
|
||||
"caido_client": CaidoBootstrapHandle(
|
||||
asyncio.create_task(_caido_when_up(), name=f"caido-bootstrap-{scan_id}")
|
||||
),
|
||||
}
|
||||
_SESSION_CACHE[scan_id] = bundle
|
||||
logger.info("Sandbox session for scan %s deferred and cached", scan_id)
|
||||
return bundle
|
||||
|
||||
client, session = await _bring_up()
|
||||
host_caido_url = await _caido_url(session)
|
||||
# The Caido login + project setup polls the guest for a couple of seconds
|
||||
# and nothing needs the client before the first proxy tool call, so it
|
||||
# runs concurrently with the rest of scan start; consumers resolve the
|
||||
# handle at first use (see CaidoBootstrapHandle).
|
||||
caido_client = CaidoBootstrapHandle(
|
||||
asyncio.create_task(_caido(session, host_caido_url), name=f"caido-bootstrap-{scan_id}")
|
||||
asyncio.create_task(
|
||||
bootstrap_caido(
|
||||
session,
|
||||
host_url=host_caido_url,
|
||||
container_url=container_caido_url,
|
||||
),
|
||||
name=f"caido-bootstrap-{scan_id}",
|
||||
)
|
||||
)
|
||||
|
||||
bundle = {
|
||||
@@ -411,26 +370,6 @@ async def create_or_reuse(
|
||||
return bundle
|
||||
|
||||
|
||||
_DISCARD_TASKS: set[asyncio.Task[None]] = set()
|
||||
|
||||
|
||||
async def _create_or_discard(create: Awaitable[tuple[Any, Any]]) -> tuple[Any, Any]:
|
||||
pending = asyncio.ensure_future(create)
|
||||
try:
|
||||
return await asyncio.shield(pending)
|
||||
except asyncio.CancelledError:
|
||||
discard = asyncio.create_task(_discard_when_created(pending))
|
||||
_DISCARD_TASKS.add(discard)
|
||||
discard.add_done_callback(_DISCARD_TASKS.discard)
|
||||
raise
|
||||
|
||||
|
||||
async def _discard_when_created(create: asyncio.Future[tuple[Any, Any]]) -> None:
|
||||
with contextlib.suppress(BaseException):
|
||||
client, session = await create
|
||||
await _discard_session(client, session)
|
||||
|
||||
|
||||
async def _discard_session(client: Any, session: Any) -> None:
|
||||
"""Best-effort teardown of a session that never made it into the cache."""
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
from collections import Counter
|
||||
from collections.abc import Iterator, Mapping
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import TypeGuard
|
||||
|
||||
@@ -21,7 +21,6 @@ _SKILL_TEMPLATES = Environment(
|
||||
undefined=StrictUndefined,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
keep_trailing_newline=True,
|
||||
)
|
||||
|
||||
_INTERNAL_SKILL_CATEGORIES: frozenset[str] = frozenset({"scan_modes", "coordination", "analysis"})
|
||||
@@ -271,22 +270,7 @@ def _candidate_skill_files(skill_name: str) -> list[Path]:
|
||||
return _bare_skill_files(skill_name)
|
||||
|
||||
|
||||
def skill_turn_limit(skill_names: list[str], turn_limits: Mapping[str, int]) -> int | None:
|
||||
"""Largest of ``turn_limits`` (keyed by skill name) among these skills, if any."""
|
||||
files = {path for name in skill_names for path in _candidate_skill_files(name)}
|
||||
return max(
|
||||
(
|
||||
limit
|
||||
for name, limit in turn_limits.items()
|
||||
if files.intersection(_candidate_skill_files(name))
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
def load_skills(
|
||||
skill_names: list[str], *, supports_images: bool = True, coverage_tools: bool = True
|
||||
) -> dict[str, str]:
|
||||
def load_skills(skill_names: list[str], *, supports_images: bool = True) -> dict[str, str]:
|
||||
"""Load skill markdown bodies (frontmatter stripped) by name.
|
||||
|
||||
Skill files live at ``strix/skills/<category>/<name>.md`` (or any
|
||||
@@ -319,7 +303,7 @@ def load_skills(
|
||||
metadata, skill_body = _parse_skill_content(content, file_path)
|
||||
if metadata.get("template") == "jinja":
|
||||
skill_body = _SKILL_TEMPLATES.from_string(skill_body).render(
|
||||
supports_images=supports_images, coverage_tools=coverage_tools
|
||||
supports_images=supports_images
|
||||
)
|
||||
skill_content[var_name] = skill_body
|
||||
logger.debug("Loaded skill: %s -> %s", skill_name, var_name)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: counterevidence
|
||||
description: Closure discipline for security findings — what counts as proof of safety, what does not, and how to record an unresolved candidate instead of silently dropping it
|
||||
template: jinja
|
||||
---
|
||||
|
||||
# Counterevidence and Closure Discipline
|
||||
@@ -21,7 +20,7 @@ state, and "I moved on" is not one of them.
|
||||
|
||||
**1. `confirmed`** — you have a working PoC or, in white-box, a complete
|
||||
source → control → sink → impact trace plus evidence the path is
|
||||
reachable. File it as a vulnerability report.
|
||||
reachable. File it with `create_vulnerability_report`.
|
||||
|
||||
**2. `ruled_out`** — you can name the **specific control** that makes the
|
||||
code safe, at a specific location, and you have checked that the control
|
||||
@@ -33,12 +32,8 @@ that sentence, you are not in `ruled_out`.
|
||||
|
||||
**3. `open_proof_gap`** — the candidate is plausible, you could not
|
||||
confirm it, and you also could not name a control that rules it out. This
|
||||
{% if coverage_tools %}
|
||||
is a legitimate, expected outcome. Record it with
|
||||
`record_coverage(outcome="needs_follow_up")`, carry it up in
|
||||
{% else %}
|
||||
is a legitimate, expected outcome. Carry it up in
|
||||
{% endif %}
|
||||
`agent_finish(open_items=[...])`, and reflect it in `counterevidence` /
|
||||
`confidence_rationale` if you file a related report. Do **not** convert
|
||||
it to `ruled_out` to tidy up your worklist.
|
||||
@@ -102,7 +97,6 @@ What ships and what is reachable is what matters.
|
||||
reduces severity — it does not make the finding unreal. Downgrade it;
|
||||
do not delete it.
|
||||
|
||||
{% if coverage_tools %}
|
||||
## Recording Closure
|
||||
|
||||
Closure is only useful if it is written down. Every surface you assess
|
||||
@@ -131,7 +125,6 @@ named control does not cover the path you just found goes back to
|
||||
previous state is kept as history, so correcting the record costs
|
||||
nothing and leaving it wrong costs a finding.
|
||||
|
||||
{% endif %}
|
||||
## What DOES Rule Out a Candidate
|
||||
|
||||
- You executed the attack and it demonstrably failed, and you understand
|
||||
@@ -151,7 +144,8 @@ endpoint is broken/unreachable for unrelated reasons".
|
||||
|
||||
## Before You File a Report
|
||||
|
||||
Run this pass on every finding before filing it:
|
||||
Run this pass on every finding before calling
|
||||
`create_vulnerability_report`:
|
||||
|
||||
1. **Argue the other side.** Spend real effort building the strongest
|
||||
case that this is *not* exploitable, or not as severe as you think.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: source_aware_discovery
|
||||
description: Enumeration discipline for reading code — which locations to keep as separate candidates, which safe siblings prove nothing, and the per-family sweeps that are routinely missed
|
||||
template: jinja
|
||||
---
|
||||
|
||||
# Source-Aware Discovery
|
||||
@@ -207,10 +206,6 @@ Not worth it: "this could use more validation" with no path, style and
|
||||
maintainability complaints, and cosmetic variants of a candidate you
|
||||
already opened.
|
||||
|
||||
{% if coverage_tools %}
|
||||
Keep reading until no distinct plausible candidate remains — then record
|
||||
what you swept with `record_coverage`, including the families that came
|
||||
back clean.
|
||||
{% else %}
|
||||
Keep reading until no distinct plausible candidate remains.
|
||||
{% endif %}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: root-agent
|
||||
description: Orchestration layer that coordinates specialized subagents for security assessments
|
||||
template: jinja
|
||||
---
|
||||
|
||||
# Root Agent
|
||||
@@ -36,12 +35,10 @@ Every scan needs one shared answer to "who is the attacker here, and what are th
|
||||
|
||||
Either way you write it with the least information anyone on this scan will ever have, so expect it to be wrong somewhere. Subagents correct it with `amend_threat_model`, which appends an attributed addendum instead of overwriting — expect many of these on a black-box run, as authenticating, pivoting between roles, and reaching internal surfaces is exactly what turns inference into fact. Read the amendments back before you write the final report: an agent telling you a boundary you called trusted is attacker-reachable is a finding about your model, not a note. Only call `save_threat_model` again to fold accumulated amendments into the body; it replaces the document and clears them.
|
||||
|
||||
{% if coverage_tools %}
|
||||
## Reconcile Coverage Before Finishing
|
||||
|
||||
Coverage entries are shared and mutable. Before `finish_scan`, list the `needs_follow_up` rows: each one is either work you still owe or a row somebody already resolved without updating. Assign the former to a subagent and have it call `update_coverage` on the existing entry rather than recording a second one — a stale open item sitting next to its own resolution is worse than either alone.
|
||||
|
||||
{% endif %}
|
||||
## Agent Architecture
|
||||
|
||||
Structure agents by function:
|
||||
|
||||
@@ -115,8 +115,7 @@ not just "upgrade <vulnerable pkg> to <fixed>".
|
||||
### Usage / reachability analysis (required for every dependency CVE)
|
||||
|
||||
For every CVE you are about to report, run a static usage analysis and record
|
||||
the result in the structured `reachability` + `reachability_evidence` fields,
|
||||
and report every usage site you find as a `code_locations` entry (see Reporting).
|
||||
the result in the structured `reachability` + `reachability_evidence` fields.
|
||||
The level is an **evidence ladder, never an exploitability verdict** — claim
|
||||
only what you proved, and cite the proof. It never changes severity (that is
|
||||
`advisory_cvss` alone); it exists so the reader can prioritize.
|
||||
@@ -221,18 +220,8 @@ findings and rejects empty PoC fields):
|
||||
GHSA, look up the mapped CVE; if there is genuinely no CVE, do not report it
|
||||
with this tool.
|
||||
- There are no PoC fields — `create_dependency_report` does not take
|
||||
`poc_description` / `poc_script_code`. The proof lives in `description` and
|
||||
`technical_analysis` (scanner output + advisory).
|
||||
- **You MUST report usage sites in `code_locations`.** Add one entry for each
|
||||
import of the package and each call of an affected symbol that your usage
|
||||
analysis found. Every `file:line` you cite in `reachability_evidence` must
|
||||
also be a `code_locations` entry. Also add the line in `manifest_path` that
|
||||
pins the vulnerable version. Each entry needs the repo-relative `file`, the
|
||||
exact `start_line` / `end_line`, the verbatim `snippet` at those lines, and a
|
||||
short `label` (e.g. "imports js-yaml", "calls yaml.load on CLI input"). Read
|
||||
the real file to confirm the lines; never guess. When the package is
|
||||
`not_imported`, the manifest line is the only entry. If an entry carries
|
||||
`fix_before` / `fix_after`, also fill `fix_verification`.
|
||||
`poc_description` / `poc_script_code` / `code_locations`. The proof lives in
|
||||
`description` and `technical_analysis` (scanner output + advisory).
|
||||
- **Always fill the structured dependency fields** (they power the dedicated
|
||||
dependency-report card; do not leave them only in free-text):
|
||||
- `package_name` — `PkgName` (required).
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: diff
|
||||
description: Methodology for diff-scoped review of a pull request, commit, or branch — what counts as in scope, how far to follow a change, and what not to report
|
||||
template: jinja
|
||||
---
|
||||
|
||||
# Diff-Scoped Review
|
||||
@@ -70,14 +69,9 @@ When it is not, the closure rules apply unchanged: a complete
|
||||
source → control → sink → impact trace through the changed code is
|
||||
reportable at reduced confidence, with the missing runtime proof named in
|
||||
`confidence_rationale`. A candidate you can neither confirm nor rule out
|
||||
{% if coverage_tools %}
|
||||
with a named control is an `open_proof_gap` — record it as
|
||||
`needs_follow_up` coverage rather than dropping it because the
|
||||
environment was inconvenient.
|
||||
{% else %}
|
||||
with a named control is an `open_proof_gap` — carry it up as an open
|
||||
item rather than dropping it because the environment was inconvenient.
|
||||
{% endif %}
|
||||
|
||||
## Reporting
|
||||
|
||||
@@ -86,9 +80,7 @@ plainly which part of the diff introduced or exposed it. A reviewer
|
||||
reading your report next to the diff should be able to see the connection
|
||||
without re-deriving your analysis.
|
||||
|
||||
{% if coverage_tools %}
|
||||
Record coverage per changed component, not per changed file — a
|
||||
formatting-only file and a rewritten auth module are not equal rows.
|
||||
{% endif %}
|
||||
State which changed areas you reviewed and cleared, so the author knows
|
||||
what a clean result actually covered.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: xss
|
||||
description: XSS and HTML injection testing covering reflected, stored, DOM-based, and normalization-dependent vectors
|
||||
description: XSS testing covering reflected, stored, and DOM-based vectors with CSP bypass techniques
|
||||
---
|
||||
|
||||
# XSS
|
||||
@@ -11,7 +11,6 @@ Cross-site scripting persists because context, parser, and framework edges are c
|
||||
|
||||
**Types**
|
||||
- Reflected, stored, and DOM-based XSS across web/mobile/desktop shells
|
||||
- Scriptless HTML injection and second-order render paths
|
||||
|
||||
**Contexts**
|
||||
- HTML, attribute, URL, JS, CSS, SVG/MathML, Markdown, PDF
|
||||
@@ -21,7 +20,6 @@ Cross-site scripting persists because context, parser, and framework edges are c
|
||||
|
||||
**Defenses to Bypass**
|
||||
- CSP/Trusted Types, DOMPurify, framework auto-escaping
|
||||
- Decode, normalization, sanitization, and encoding order
|
||||
|
||||
## Injection Points
|
||||
|
||||
@@ -59,24 +57,6 @@ Cross-site scripting persists because context, parser, and framework edges are c
|
||||
|
||||
Distinguish HTML-string input from live DOM objects sanitized with `IN_PLACE`. Resolve the deployed DOMPurify build and check relevant upstream advisories; a controlled observable `nodeName` is one live-node bypass mechanism. Trace same-origin foreign nodes or `adoptNode()` into this mode before selecting the bypass ([example advisory](https://github.com/cure53/DOMPurify/security/advisories/GHSA-x4vx-rjvf-j5p4)).
|
||||
|
||||
### Scriptless HTML Injection
|
||||
|
||||
Do not require JavaScript execution before you report HTML injection. Prove that the browser creates attacker-controlled markup and a concrete security effect.
|
||||
|
||||
Test base or form target changes, external requests, deceptive content, DOM clobbering through `id` or `name`, and dangling markup. Report XSS only when you prove a script-capable execution path. Do not dismiss HTML injection only because CSP blocks scripts.
|
||||
|
||||
### Unicode Normalization
|
||||
|
||||
Test normalization only when the target performs it. NFKC can fold fullwidth delimiters such as `<` and `>` into HTML syntax.
|
||||
|
||||
Map validation, normalization, sanitization, encoding, and parsing in order. Start with the harmless canary `<h1 id="strix-nfkc">marker</h1>`.
|
||||
|
||||
Report a bypass only when normalization creates dangerous syntax that reaches an unprotected sink. Load `semantic_confusion` when components interpret the value differently.
|
||||
|
||||
### Second-Order Rendering
|
||||
|
||||
Follow stored values into previews, administrative pages, email, exports, and hydration. Record which stage transforms and parses the value.
|
||||
|
||||
### DOM XSS
|
||||
|
||||
**Sources**
|
||||
@@ -186,7 +166,7 @@ Keep a compact set tuned per context:
|
||||
## Testing Methodology
|
||||
|
||||
1. **Identify sources** - URL/query/hash/referrer, postMessage, storage, WebSocket, server JSON
|
||||
2. **Trace transformations and sinks** - Record decoding, normalization, sanitization, reparsing, and the final sink
|
||||
2. **Trace to sinks** - Map data flow from source to sink
|
||||
3. **Classify context** - HTML node, attribute, URL, script block, event handler, JS eval-like, CSS, SVG
|
||||
4. **Assess defenses** - Output encoding, sanitizer, CSP, Trusted Types, DOMPurify config
|
||||
5. **Craft payloads** - Minimal payloads per context with encoding/whitespace/casing variants
|
||||
@@ -195,11 +175,9 @@ Keep a compact set tuned per context:
|
||||
## Validation
|
||||
|
||||
1. Provide minimal payload and context (sink type) with before/after DOM or network evidence
|
||||
2. Record the original and transformed value when decoding or normalization affects the result
|
||||
3. Demonstrate cross-browser execution where relevant or explain parser-specific behavior
|
||||
4. Show bypass of stated defenses (sanitizer settings, CSP/Trusted Types) with proof
|
||||
5. Separate HTML injection from XSS and state the proven capability
|
||||
6. Quantify impact beyond alert: data accessed, action performed, persistence achieved
|
||||
2. Demonstrate cross-browser execution where relevant or explain parser-specific behavior
|
||||
3. Show bypass of stated defenses (sanitizer settings, CSP/Trusted Types) with proof
|
||||
4. Quantify impact beyond alert: data accessed, action performed, persistence achieved
|
||||
|
||||
## False Positives
|
||||
|
||||
@@ -207,8 +185,6 @@ Keep a compact set tuned per context:
|
||||
- CSP with nonces/hashes and no inline/event handlers
|
||||
- Trusted Types enforced on sinks; DOMPurify in strict mode with URI allowlists
|
||||
- Scriptable contexts disabled (no HTML pass-through, safe URL schemes enforced)
|
||||
- The target does not perform the claimed normalization or decode
|
||||
- A normalized value is safely encoded before the final sink
|
||||
|
||||
## Impact
|
||||
|
||||
@@ -231,4 +207,4 @@ Keep a compact set tuned per context:
|
||||
|
||||
## Summary
|
||||
|
||||
Context, transformation order, and sink decide whether input becomes text, markup, or executable code. Prove HTML injection separately from XSS, and validate every render path. Encode for the exact context and verify at runtime with CSP/Trusted Types. Small payloads with strong evidence beat payload catalogs.
|
||||
Context + sink decide execution. Encode for the exact context, verify at runtime with CSP/Trusted Types, and validate every alternative render path. Small payloads with strong evidence beat payload catalogs.
|
||||
@@ -535,8 +535,9 @@ async def create_agent(
|
||||
success looks like, any constraints. Name the target the
|
||||
child should call ``get_threat_model`` on, and any shared
|
||||
state it should build on rather than rediscover — what
|
||||
recon already mapped, which surfaces are already covered.
|
||||
A child that is not told what is already known repeats it.
|
||||
recon already mapped, which surfaces are already covered,
|
||||
which coverage entry it is picking up. A child that is not
|
||||
told what is already known repeats it.
|
||||
inherit_context: Default ``True``. The child receives the
|
||||
parent's input history as background; only set ``False``
|
||||
when starting a clean-slate task.
|
||||
@@ -636,8 +637,9 @@ async def agent_finish(
|
||||
doing: what did you test, what did you find/confirm/rule out,
|
||||
what's still open.
|
||||
|
||||
**Close out honestly.** Before calling this, anything you could
|
||||
neither confirm nor rule out belongs in ``open_items`` — an
|
||||
**Close out honestly.** Before calling this, every surface you
|
||||
assessed should have a ``record_coverage`` entry, and anything you
|
||||
could neither confirm nor rule out belongs in ``open_items`` — an
|
||||
unresolved candidate handed up to the parent is useful, a silently
|
||||
dropped one is a missed vulnerability. Reporting nothing and
|
||||
listing no open items asserts the area is clean; only say that if
|
||||
|
||||
@@ -29,11 +29,7 @@ async def load_skill(ctx: RunContextWrapper, skills: list[str]) -> str:
|
||||
if err:
|
||||
return f"load_skill: {err}"
|
||||
context = ctx.context if isinstance(ctx.context, dict) else {}
|
||||
contents = load_skills(
|
||||
requested,
|
||||
supports_images=context.get("supports_images", True),
|
||||
coverage_tools=context.get("coverage_tools", True),
|
||||
)
|
||||
contents = load_skills(requested, supports_images=context.get("supports_images", True))
|
||||
if not contents:
|
||||
return "load_skill: no content loaded for requested skills."
|
||||
sections = [f"## Skill: {name}\n\n{body}" for name, body in contents.items()]
|
||||
|
||||
@@ -13,7 +13,6 @@ from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from agents import RunContextWrapper, function_tool
|
||||
from caido_sdk_client import BaseError
|
||||
|
||||
from strix.runtime.caido_handle import CaidoBootstrapHandle
|
||||
from strix.tools.nullish import clean_optional
|
||||
@@ -69,15 +68,6 @@ async def _call[T](client: Client, fn: Callable[[Client], Awaitable[T]]) -> T:
|
||||
return await fn(client)
|
||||
|
||||
|
||||
_REQUEST_EXISTS_QUERY = "query RequestExists($id: ID!) { request(id: $id) { id } }"
|
||||
_REQUEST_EXISTS_ATTEMPTS = 2
|
||||
|
||||
|
||||
async def _request_exists(client: Client, request_id: str) -> bool:
|
||||
data = await client.graphql.query(_REQUEST_EXISTS_QUERY, {"id": request_id})
|
||||
return bool(data) and data.get("request") is not None
|
||||
|
||||
|
||||
async def existing_request_ids(
|
||||
ctx: RunContextWrapper,
|
||||
request_ids: list[str],
|
||||
@@ -90,24 +80,16 @@ async def existing_request_ids(
|
||||
if client is None:
|
||||
raise RuntimeError("Caido client is not available")
|
||||
|
||||
# Request IDs are not an HTTPQL field, so each one is resolved by id. Only
|
||||
# the id is selected: pulling the raw request and response bodies just to
|
||||
# learn whether a row exists is what made this time out on busy projects.
|
||||
# Request IDs are not an HTTPQL field. Resolve each ID through the same
|
||||
# project-bound lookup as view_request rather than constructing a filter.
|
||||
existing: set[str] = set()
|
||||
for request_id in request_ids:
|
||||
for attempt in range(1, _REQUEST_EXISTS_ATTEMPTS + 1):
|
||||
try:
|
||||
exists = await _call(
|
||||
client,
|
||||
functools.partial(_request_exists, request_id=request_id),
|
||||
)
|
||||
except Exception:
|
||||
if attempt == _REQUEST_EXISTS_ATTEMPTS:
|
||||
raise
|
||||
continue
|
||||
if exists:
|
||||
existing.add(request_id)
|
||||
break
|
||||
result = await _call(
|
||||
client,
|
||||
functools.partial(caido_api.get_request_with_client, request_id=request_id),
|
||||
)
|
||||
if result is not None:
|
||||
existing.add(str(result.request.id))
|
||||
return existing
|
||||
|
||||
|
||||
@@ -142,11 +124,7 @@ def _no_client() -> str:
|
||||
|
||||
|
||||
def _err(name: str, exc: Exception) -> str:
|
||||
# Caido-side failures (timeouts, bad filters, ...) are returned to the model, not our bugs.
|
||||
if isinstance(exc, BaseError):
|
||||
logger.warning("%s failed", name, exc_info=True)
|
||||
else:
|
||||
logger.exception("%s failed", name)
|
||||
logger.exception("%s failed", name)
|
||||
return json.dumps(
|
||||
{"success": False, "error": f"{name} failed: {exc}"},
|
||||
ensure_ascii=False,
|
||||
|
||||
@@ -222,9 +222,9 @@ def _normalize_http_exchange_ids(raw: Any) -> tuple[list[str] | None, list[str]]
|
||||
return normalized, errors
|
||||
|
||||
|
||||
_HTTP_EXCHANGE_UNVERIFIED_WARNING = (
|
||||
"http_exchange_ids were stored without verification because the proxy project did "
|
||||
"not respond in time. Ids that do not exist in the project will not render as evidence."
|
||||
_HTTP_EXCHANGE_DROPPED_WARNING = (
|
||||
"http_exchange_ids were not stored: the proxy project could not be reached to verify "
|
||||
"them. Attach them with update_vulnerability_report when the proxy responds again."
|
||||
)
|
||||
|
||||
|
||||
@@ -235,9 +235,9 @@ async def _verify_http_exchange_ids(
|
||||
"""Verify proxy exchange IDs against the current Caido project.
|
||||
|
||||
IDs the project does not know are rejected. When the proxy itself cannot be
|
||||
queried the IDs are kept and a warning is returned instead: a slow or
|
||||
unreachable proxy must never strip evidence from a finding, and consumers
|
||||
resolve each id against the recorded traffic before rendering it.
|
||||
queried the IDs are dropped and a warning is returned instead, so a proxy
|
||||
outage never blocks a finding and unverified IDs are never recorded as
|
||||
evidence.
|
||||
"""
|
||||
request_ids, errors = _normalize_http_exchange_ids(raw)
|
||||
if request_ids is None or errors or not request_ids:
|
||||
@@ -250,7 +250,7 @@ async def _verify_http_exchange_ids(
|
||||
"Could not verify HTTP exchange IDs against the current Caido project",
|
||||
exc_info=True,
|
||||
)
|
||||
return request_ids, [], _HTTP_EXCHANGE_UNVERIFIED_WARNING
|
||||
return None, [], _HTTP_EXCHANGE_DROPPED_WARNING
|
||||
|
||||
missing_ids = [request_id for request_id in request_ids if request_id not in existing_ids]
|
||||
if missing_ids:
|
||||
@@ -1876,10 +1876,14 @@ def _build_dependency_evidence(
|
||||
evidence += f"\n\n**Usage analysis:** {label}."
|
||||
if reachability_evidence and reachability_evidence.strip():
|
||||
evidence += f" {reachability_evidence.strip()}"
|
||||
evidence += (
|
||||
" This is a prioritization signal from static analysis, not a"
|
||||
" proof of exploitability or of safety."
|
||||
)
|
||||
return evidence
|
||||
|
||||
|
||||
async def _do_create_dependency( # noqa: PLR0912, PLR0915
|
||||
async def _do_create_dependency( # noqa: PLR0912
|
||||
*,
|
||||
title: str,
|
||||
description: str,
|
||||
@@ -1901,8 +1905,6 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
|
||||
manifest_path: str | None = None,
|
||||
reachability: str = "unknown",
|
||||
reachability_evidence: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
fix_verification: str | None = None,
|
||||
contextual_cvss_breakdown: dict[str, str] | None = None,
|
||||
contextual_cvss_reasoning: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
@@ -1945,16 +1947,6 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
|
||||
if manifest_err:
|
||||
errors.append(manifest_err)
|
||||
|
||||
parsed_locations = _normalize_code_locations(code_locations)
|
||||
if parsed_locations:
|
||||
errors.extend(_validate_code_locations(parsed_locations))
|
||||
elif code_locations:
|
||||
errors.append(
|
||||
"code_locations were dropped as unusable - every location needs a relative "
|
||||
"'file' and an integer 'start_line'"
|
||||
)
|
||||
errors.extend(_validate_fix_verification(parsed_locations, fix_verification))
|
||||
|
||||
reachability = (reachability or "unknown").strip().lower()
|
||||
if reachability not in _VALID_REACHABILITY:
|
||||
errors.append(
|
||||
@@ -2061,8 +2053,6 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
|
||||
cvss=cvss_score if advisory_cvss is not None else None,
|
||||
cve=parsed_cve,
|
||||
cwe=cwe,
|
||||
code_locations=parsed_locations,
|
||||
fix_verification=fix_verification,
|
||||
finding_class="dependency_cve",
|
||||
dependency_metadata=dependency_metadata,
|
||||
agent_id=agent_id if isinstance(agent_id, str) else None,
|
||||
@@ -2117,8 +2107,6 @@ async def create_dependency_report(
|
||||
dependency_path: str | None = None,
|
||||
reachability: str = "unknown",
|
||||
reachability_evidence: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
fix_verification: str | None = None,
|
||||
contextual_cvss_breakdown: dict[str, str] | None = None,
|
||||
contextual_cvss_reasoning: str | None = None,
|
||||
) -> str:
|
||||
@@ -2229,23 +2217,6 @@ async def create_dependency_report(
|
||||
is off in production), and say who controls the input. State
|
||||
it plainly when no entry point reaches the sink — that is the
|
||||
most useful result a reader can get.
|
||||
code_locations: Where application code imports or calls the package —
|
||||
the evidence behind your ``reachability`` claim. List of dicts,
|
||||
same shape as ``create_vulnerability_report``::
|
||||
|
||||
{"file": "src/api/client.ts", "start_line": 14, "end_line": 14,
|
||||
"snippet": "const x = require('pkg')", "label": "imports it"}
|
||||
|
||||
Cite the repo-relative ``file`` and the exact 1-based
|
||||
``start_line`` where ``snippet`` actually sits — verify against
|
||||
the real file, never guess. A location may also propose
|
||||
the fix itself: ``fix_before`` (verbatim current code, e.g. the
|
||||
manifest pin line) + ``fix_after`` (same lines with the fixed
|
||||
version) — that combination requires ``fix_verification``.
|
||||
fix_verification: Required whenever any ``code_locations`` entry
|
||||
carries ``fix_after``. Same bar as a normal finding: security
|
||||
closure, bypass review, preserved behavior, and how each was
|
||||
checked.
|
||||
contextual_cvss_breakdown: **Required.** Full CVSS v3.1 rating of this
|
||||
CVE **in this codebase** — the same 8-metric object as
|
||||
``create_vulnerability_report``'s ``cvss_breakdown``:
|
||||
@@ -2306,8 +2277,6 @@ async def create_dependency_report(
|
||||
manifest_path=manifest_path,
|
||||
reachability=reachability,
|
||||
reachability_evidence=reachability_evidence,
|
||||
code_locations=code_locations,
|
||||
fix_verification=fix_verification,
|
||||
contextual_cvss_breakdown=contextual_cvss_breakdown,
|
||||
contextual_cvss_reasoning=contextual_cvss_reasoning,
|
||||
agent_id=agent_id,
|
||||
|
||||
@@ -80,94 +80,6 @@ async def test_wrap_exec_command_preserves_explicit_shell(shell: str) -> None:
|
||||
assert json.loads(captured["raw_input"])["shell"] == shell
|
||||
|
||||
|
||||
def _running_exec_tool(result: str) -> FunctionTool:
|
||||
async def invoke(_ctx: Any, _raw_input: str) -> str:
|
||||
return result
|
||||
|
||||
return FunctionTool(
|
||||
name="exec_command",
|
||||
description="Runs a command.",
|
||||
params_json_schema={"type": "object", "properties": {}},
|
||||
on_invoke_tool=invoke,
|
||||
)
|
||||
|
||||
|
||||
_RUNNING_RESULT = (
|
||||
"Chunk ID: abc123\nWall time: 30.0012 seconds\nProcess running with session ID 7\nOutput:\n"
|
||||
)
|
||||
|
||||
|
||||
def test_wrap_exec_command_describes_the_yield_cap() -> None:
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool("ok"))
|
||||
|
||||
assert wrapped.description.startswith("Runs a command. The wait for output is capped at 30 s")
|
||||
assert "cannot be read later" in wrapped.description
|
||||
assert "tty=true" in wrapped.description
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_flags_a_running_non_tty_process() -> None:
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(_RUNNING_RESULT))
|
||||
|
||||
result = await wrapped.on_invoke_tool(
|
||||
cast("Any", None), json.dumps({"cmd": "sleep 60; curl x", "yield_time_ms": 90000})
|
||||
)
|
||||
|
||||
assert result == (
|
||||
"Chunk ID: abc123\nWall time: 30.0012 seconds\n"
|
||||
"Process running with session ID 7\nOutput:\n"
|
||||
"Note: yield_time_ms=90000 was capped at 30000 (30 s). this non-TTY process cannot be "
|
||||
"polled and its later output is lost. Rerun it in the background "
|
||||
"(`cmd > /tmp/out.log 2>&1 &`, then read the log) or with tty=true.\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_flags_a_running_non_tty_process_within_the_cap() -> None:
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(_RUNNING_RESULT))
|
||||
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), json.dumps({"cmd": "sleep 60"}))
|
||||
|
||||
assert "Note: this non-TTY process cannot be polled" in result
|
||||
assert "was capped" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_leaves_a_running_tty_process_alone() -> None:
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(_RUNNING_RESULT))
|
||||
|
||||
result = await wrapped.on_invoke_tool(
|
||||
cast("Any", None), json.dumps({"cmd": "python3 -m http.server", "tty": True})
|
||||
)
|
||||
|
||||
assert result == _RUNNING_RESULT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_leaves_an_exited_process_alone() -> None:
|
||||
exited = "Chunk ID: abc123\nWall time: 0.1 seconds\nProcess exited with code 0\nOutput:\nhi"
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(exited))
|
||||
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), json.dumps({"cmd": "echo hi"}))
|
||||
|
||||
assert result == exited
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_exec_command_ignores_the_running_marker_inside_command_output() -> None:
|
||||
exited = (
|
||||
"Chunk ID: abc123\nWall time: 0.2 seconds\nProcess exited with code 0\nOutput:\n"
|
||||
"Process running with session ID 7\nOutput:\n"
|
||||
)
|
||||
wrapped = factory._wrap_exec_command(_running_exec_tool(exited))
|
||||
|
||||
result = await wrapped.on_invoke_tool(
|
||||
cast("Any", None), json.dumps({"cmd": "cat transcript.txt"})
|
||||
)
|
||||
|
||||
assert result == exited
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_responses_filesystem_custom_tool_output_is_bounded() -> None:
|
||||
async def invoke(_ctx: Any, _inp: str) -> str:
|
||||
|
||||
@@ -38,26 +38,11 @@ async def _roundtrip(
|
||||
|
||||
|
||||
_STRING = {"todos": {"type": "string"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_truncated_arguments_short_circuit_with_a_reissue_message() -> None:
|
||||
captured: dict[str, str] = {}
|
||||
wrapped = factory._with_coerced_arguments(_capturing_tool(captured, _STRING))
|
||||
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), '{"todos": "a, b')
|
||||
|
||||
assert "not executed" in result
|
||||
assert "Re-issue the call" in result
|
||||
assert "raw_input" not in captured
|
||||
|
||||
|
||||
_ARRAY = {"tags": {"type": "array", "items": {"type": "string"}}}
|
||||
_NULLABLE_ARRAY = {
|
||||
"tags": {"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}]}
|
||||
}
|
||||
_OBJECT = {"modifications": {"type": "object"}}
|
||||
OVER_NESTED = '{"a":' + "[" * 100_000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -151,15 +136,12 @@ async def test_unknown_and_null_arguments_are_untouched() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("arguments", ["not json", '{"n": NaN}', OVER_NESTED])
|
||||
async def test_non_object_payloads_are_reported_instead_of_invoked(arguments: str) -> None:
|
||||
async def test_non_object_payloads_pass_through_unchanged() -> None:
|
||||
captured: dict[str, str] = {}
|
||||
wrapped = factory._with_coerced_arguments(_capturing_tool(captured, _ARRAY))
|
||||
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), arguments)
|
||||
|
||||
assert result.startswith("probe: the tool call was not executed")
|
||||
assert "raw_input" not in captured
|
||||
assert await wrapped.on_invoke_tool(cast("Any", None), "not json") == "ok"
|
||||
assert captured["raw_input"] == "not json"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -93,51 +93,6 @@ def test_per_call_extra_tools_reject_duplicate_registered_names() -> None:
|
||||
factory.build_strix_agent(is_root=True, extra_tools=[_tool("same_name")])
|
||||
|
||||
|
||||
def test_report_tool_replaces_create_vulnerability_report() -> None:
|
||||
for is_root in (True, False):
|
||||
agent = factory.build_strix_agent(is_root=is_root, report_tool=_tool("file_pr_finding"))
|
||||
names = [t.name for t in agent.tools]
|
||||
assert "file_pr_finding" in names
|
||||
assert "create_vulnerability_report" not in names
|
||||
assert isinstance(agent.instructions, str)
|
||||
assert "file_pr_finding" in agent.instructions
|
||||
assert "create_vulnerability_report" not in agent.instructions
|
||||
|
||||
|
||||
def _descriptions(tools: list[Any]) -> list[str]:
|
||||
return [t.description for t in tools if isinstance(t, FunctionTool)]
|
||||
|
||||
|
||||
def test_coverage_tools_can_be_turned_off() -> None:
|
||||
coverage = {"record_coverage", "update_coverage", "list_coverage"}
|
||||
for is_root in (True, False):
|
||||
on = factory.build_strix_agent(is_root=is_root, is_whitebox=True)
|
||||
off = factory.build_strix_agent(is_root=is_root, is_whitebox=True, coverage_tools=False)
|
||||
assert coverage <= {t.name for t in on.tools}
|
||||
assert not coverage & {t.name for t in off.tools}
|
||||
assert isinstance(on.instructions, str)
|
||||
assert isinstance(off.instructions, str)
|
||||
assert "- COVERAGE" in on.instructions
|
||||
assert "## Recording Closure" in on.instructions
|
||||
assert not any(name in off.instructions for name in coverage)
|
||||
on_descriptions = "\n".join(_descriptions(on.tools))
|
||||
off_descriptions = "\n".join(_descriptions(off.tools))
|
||||
if is_root:
|
||||
assert "Coverage reconciliation" in on_descriptions
|
||||
assert "Coverage reconciliation" not in off_descriptions
|
||||
assert not any(name in off_descriptions for name in coverage)
|
||||
|
||||
diff_on = factory.build_strix_agent(is_root=True, is_diff_scoped=True)
|
||||
diff_off = factory.build_strix_agent(is_root=True, is_diff_scoped=True, coverage_tools=False)
|
||||
assert isinstance(diff_on.instructions, str)
|
||||
assert isinstance(diff_off.instructions, str)
|
||||
assert "Record coverage per changed component" in diff_on.instructions
|
||||
assert "`needs_follow_up` coverage" in diff_on.instructions
|
||||
assert "Record coverage per changed component" not in diff_off.instructions
|
||||
assert "`needs_follow_up` coverage" not in diff_off.instructions
|
||||
assert not any(name in diff_off.instructions for name in coverage)
|
||||
|
||||
|
||||
def test_instructions_override_is_used_verbatim() -> None:
|
||||
custom = "You are a scan agent. Follow the provided scope."
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from strix.core.sessions import open_agent_session
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ _CALL_LATENCY_S = 0.005
|
||||
|
||||
class _FakeLedger:
|
||||
def __init__(self) -> None:
|
||||
self.on_pause: Callable[[], Awaitable[None]] | None = None
|
||||
self.cost = 0.0
|
||||
self.calls: list[str] = []
|
||||
self.remaining: dict[str, int] = {}
|
||||
@@ -81,8 +80,6 @@ class _FakeStream:
|
||||
await self._hooks.on_llm_start(ctx_wrapper, self._agent, None, input_items)
|
||||
except BudgetPausedError as exc:
|
||||
self.run_loop_exception = exc
|
||||
if self._ledger.on_pause is not None:
|
||||
await self._ledger.on_pause()
|
||||
return
|
||||
self._ledger.warned_inputs.append(input_items)
|
||||
if self._ledger.gate is not None:
|
||||
@@ -153,16 +150,13 @@ class _Scan:
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
*,
|
||||
max_budget_usd: float | None,
|
||||
budget_policy: str = "pause",
|
||||
) -> None:
|
||||
self.ledger = _FakeLedger()
|
||||
self.hooks = ReportUsageHooks(
|
||||
model="test-model",
|
||||
max_budget_usd=max_budget_usd,
|
||||
budget_policy=budget_policy, # type: ignore[arg-type]
|
||||
model="test-model", max_budget_usd=max_budget_usd, budget_policy="pause"
|
||||
)
|
||||
self.coordinator = AgentCoordinator()
|
||||
self.coordinator.set_budget_policy(budget_policy) # type: ignore[arg-type]
|
||||
self.coordinator.set_budget_policy("pause")
|
||||
self.coordinator.set_budget_limit_setter(self.hooks.set_max_budget_usd)
|
||||
monkeypatch.setattr(execution, "Runner", _fake_runner(self.ledger, self.coordinator))
|
||||
monkeypatch.setattr(execution, "_compact_session", _noop_compact)
|
||||
@@ -544,140 +538,3 @@ async def test_stop_policy_is_unchanged() -> None:
|
||||
def test_budget_policy_is_validated() -> None:
|
||||
with pytest.raises(ValueError, match="budget_policy"):
|
||||
ReportUsageHooks(model="m", budget_policy="later") # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _pause_flags(coordinator: AgentCoordinator) -> tuple[bool, bool]:
|
||||
return coordinator.budget_paused, coordinator.operator_paused
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_operator_pause_holds_under_the_stop_policy(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
scan = _Scan(tmp_path, monkeypatch, max_budget_usd=None, budget_policy="stop")
|
||||
agents = ("root", "child-a", "child-b")
|
||||
|
||||
with patch("strix.core.hooks.get_global_report_state", return_value=scan.ledger):
|
||||
await scan.start_root(calls=6)
|
||||
await scan.start_child("child-a", calls=6)
|
||||
await scan.start_child("child-b", calls=6)
|
||||
|
||||
await _wait_until(lambda: scan.ledger.cost >= 3.0)
|
||||
await scan.coordinator.pause_budget()
|
||||
spent_at_pause = scan.ledger.cost
|
||||
await _wait_until(lambda: _all_parked(scan.coordinator, *agents))
|
||||
assert _pause_flags(scan.coordinator) == (True, True)
|
||||
assert all(not task.done() for task in scan.tasks())
|
||||
|
||||
await scan.coordinator.send("child-a", {"from": "root", "content": "status?"})
|
||||
await scan.coordinator.send("root", {"from": "user", "content": "keep going"})
|
||||
await asyncio.sleep(0.05)
|
||||
assert _pause_flags(scan.coordinator) == (True, True)
|
||||
assert scan.ledger.cost == pytest.approx(spent_at_pause)
|
||||
assert _all_parked(scan.coordinator, *agents)
|
||||
|
||||
woken = await scan.coordinator.resume_budget()
|
||||
assert sorted(woken) == sorted(agents)
|
||||
assert _pause_flags(scan.coordinator) == (False, False)
|
||||
await asyncio.wait_for(asyncio.gather(*scan.tasks(), return_exceptions=True), timeout=5.0)
|
||||
assert scan.ledger.cost == pytest.approx(18.0)
|
||||
assert {aid: str(s) for aid, s in scan.coordinator.statuses.items()} == {
|
||||
"root": "completed",
|
||||
"child-a": "completed",
|
||||
"child-b": "completed",
|
||||
}
|
||||
root_items = await scan.sessions[0].get_items()
|
||||
assert any("keep going" in str(item) for item in root_items)
|
||||
|
||||
for session in scan.sessions:
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_resume_that_lands_before_the_park_still_continues(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
scan = _Scan(tmp_path, monkeypatch, max_budget_usd=None, budget_policy="stop")
|
||||
resumed = 0
|
||||
|
||||
async def resume_at_once() -> None:
|
||||
nonlocal resumed
|
||||
resumed += 1
|
||||
await scan.coordinator.resume_budget()
|
||||
|
||||
scan.ledger.on_pause = resume_at_once
|
||||
with patch("strix.core.hooks.get_global_report_state", return_value=scan.ledger):
|
||||
await scan.start_root(calls=6)
|
||||
await scan.start_child("child-a", calls=6)
|
||||
await _wait_until(lambda: scan.ledger.cost >= 2.0)
|
||||
await scan.coordinator.pause_budget()
|
||||
await asyncio.wait_for(asyncio.gather(*scan.tasks(), return_exceptions=True), timeout=5.0)
|
||||
assert resumed >= 1
|
||||
assert _pause_flags(scan.coordinator) == (False, False)
|
||||
assert scan.ledger.cost == pytest.approx(12.0)
|
||||
assert {aid: str(s) for aid, s in scan.coordinator.statuses.items()} == {
|
||||
"root": "completed",
|
||||
"child-a": "completed",
|
||||
}
|
||||
|
||||
for session in scan.sessions:
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_operator_pause_gates_the_stop_policy_hooks() -> None:
|
||||
coordinator = AgentCoordinator()
|
||||
hooks = ReportUsageHooks(model="m", max_budget_usd=None)
|
||||
assert hooks.budget_policy == "stop"
|
||||
|
||||
await coordinator.pause_budget()
|
||||
with pytest.raises(BudgetPausedError):
|
||||
await hooks.on_llm_start(_ctx(coordinator), MagicMock(), None, [])
|
||||
|
||||
await coordinator.resume_budget()
|
||||
await hooks.on_llm_start(_ctx(coordinator), MagicMock(), None, [])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_user_message_lifts_only_the_limit_pause(tmp_path: Path) -> None:
|
||||
coordinator = AgentCoordinator()
|
||||
await coordinator.register("root", "strix", parent_id=None)
|
||||
session = open_agent_session("root", tmp_path / "agents.db")
|
||||
await coordinator.attach_runtime("root", session=session)
|
||||
extensions: list[int] = []
|
||||
coordinator.set_budget_extender(lambda: extensions.append(1))
|
||||
|
||||
await coordinator.pause_budget()
|
||||
await coordinator.park_for_budget("root")
|
||||
assert coordinator.statuses["root"] == "budget_paused"
|
||||
await coordinator.send("root", {"from": "user", "content": "keep going"})
|
||||
assert _pause_flags(coordinator) == (True, True)
|
||||
assert extensions == []
|
||||
assert coordinator.pending_counts["root"] == 1
|
||||
|
||||
await coordinator.resume_budget()
|
||||
await coordinator.pause_for_budget("root")
|
||||
await coordinator.send("root", {"from": "user", "content": "keep going"})
|
||||
assert _pause_flags(coordinator) == (False, False)
|
||||
assert extensions == [1]
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_and_reset_keep_an_operator_pause() -> None:
|
||||
coordinator = AgentCoordinator()
|
||||
await coordinator.register("root", "strix", parent_id=None)
|
||||
await coordinator.pause_budget()
|
||||
snap = await coordinator.snapshot()
|
||||
|
||||
restored = AgentCoordinator()
|
||||
await restored.restore(snap)
|
||||
assert _pause_flags(restored) == (True, True)
|
||||
await restored.reset_budget_stops(budget_stopped=False, reserve_stopped=False)
|
||||
assert _pause_flags(restored) == (True, True)
|
||||
|
||||
await restored.resume_budget()
|
||||
await restored.pause_for_budget("root")
|
||||
await restored.reset_budget_stops(budget_stopped=False, reserve_stopped=False)
|
||||
assert _pause_flags(restored) == (False, False)
|
||||
assert restored.statuses["root"] == "waiting"
|
||||
@@ -147,9 +147,7 @@ def _patch_budget(monkeypatch: pytest.MonkeyPatch, *, keep_tokens: int, window:
|
||||
context.auto_compact = True
|
||||
settings = SimpleNamespace(
|
||||
context=context,
|
||||
llm=SimpleNamespace(
|
||||
api_key=None, api_base=None, timeout=1, extra_headers=None, extra_body=None
|
||||
),
|
||||
llm=SimpleNamespace(api_key=None, api_base=None, timeout=1, extra_headers=None),
|
||||
)
|
||||
monkeypatch.setattr(compaction, "load_settings", lambda: settings)
|
||||
|
||||
@@ -230,11 +228,7 @@ async def test_summarize_routes_through_provider_with_settings(
|
||||
"load_settings",
|
||||
lambda: SimpleNamespace(
|
||||
llm=SimpleNamespace(
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
timeout=1,
|
||||
extra_headers={"X-Feature-Key": "svc"},
|
||||
extra_body={"service_tier": "flex"},
|
||||
api_key=None, api_base=None, timeout=1, extra_headers={"X-Feature-Key": "svc"}
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -245,7 +239,6 @@ async def test_summarize_routes_through_provider_with_settings(
|
||||
assert captured["model"] == "litellm/openai/some-model"
|
||||
settings = captured["model_settings"]
|
||||
assert settings.extra_headers == {"X-Feature-Key": "svc"}
|
||||
assert settings.extra_body == {"service_tier": "flex"}
|
||||
assert settings.max_tokens == 64
|
||||
|
||||
|
||||
|
||||
@@ -37,12 +37,6 @@ _LLM_ENV_KEYS = [
|
||||
"STRIX_RUNTIME_BACKEND",
|
||||
# TelemetrySettings
|
||||
"STRIX_TELEMETRY",
|
||||
# JSON-object settings
|
||||
"LLM_EXTRA_HEADERS",
|
||||
"LLM_EXTRA_BODY",
|
||||
"STRIX_DEDUPE_MODEL",
|
||||
"DEDUPE_LLM_EXTRA_HEADERS",
|
||||
"DEDUPE_LLM_EXTRA_BODY",
|
||||
]
|
||||
|
||||
|
||||
@@ -412,57 +406,6 @@ def test_persist_current_replaces_corrupt_file(
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {"env": {"STRIX_LLM": "env-model"}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("env_var", "section", "field"),
|
||||
[
|
||||
("LLM_EXTRA_HEADERS", "llm", "extra_headers"),
|
||||
("LLM_EXTRA_BODY", "llm", "extra_body"),
|
||||
("DEDUPE_LLM_EXTRA_HEADERS", "dedupe", "extra_headers"),
|
||||
("DEDUPE_LLM_EXTRA_BODY", "dedupe", "extra_body"),
|
||||
],
|
||||
)
|
||||
def test_persisted_json_object_setting_reloads_without_env(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, env_var: str, section: str, field: str
|
||||
) -> None:
|
||||
loader.apply_config_override(tmp_path / "cli-config.json")
|
||||
monkeypatch.setenv(env_var, '{"X-Tenant": "acme"}')
|
||||
loader.load_settings()
|
||||
loader.persist_current()
|
||||
monkeypatch.delenv(env_var)
|
||||
loader.apply_config_override(tmp_path / "cli-config.json")
|
||||
|
||||
settings = loader.load_settings()
|
||||
|
||||
assert getattr(getattr(settings, section), field) == {"X-Tenant": "acme"}
|
||||
|
||||
|
||||
def test_json_object_setting_saved_as_object_still_loads(tmp_path: Path) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"LLM_EXTRA_BODY": {"service_tier": "flex"}}}), encoding="utf-8"
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
|
||||
assert loader.load_settings().llm.extra_body == {"service_tier": "flex"}
|
||||
|
||||
|
||||
def test_invalid_saved_json_object_setting_is_rejected(tmp_path: Path) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(json.dumps({"env": {"LLM_EXTRA_BODY": "not json"}}), encoding="utf-8")
|
||||
loader.apply_config_override(target)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
loader.load_settings()
|
||||
|
||||
|
||||
def test_saved_string_setting_is_not_json_decoded(tmp_path: Path) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(json.dumps({"env": {"STRIX_LLM": '{"not": "decoded"}'}}), encoding="utf-8")
|
||||
loader.apply_config_override(target)
|
||||
|
||||
assert loader.load_settings().llm.model == '{"not": "decoded"}'
|
||||
|
||||
|
||||
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"
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
@@ -19,13 +18,11 @@ from strix.config.models import (
|
||||
_configure_litellm_compatibility,
|
||||
_install_openrouter_stream_cost_capture,
|
||||
)
|
||||
from strix.core.hooks import ReportUsageHooks
|
||||
from strix.llm import request_log
|
||||
from strix.report.state import (
|
||||
ReportState,
|
||||
litellm_cost_callback,
|
||||
openrouter_stream_cost,
|
||||
record_llm_request,
|
||||
set_global_report_state,
|
||||
streamed_openrouter_costs,
|
||||
)
|
||||
@@ -139,7 +136,6 @@ def test_cost_callback_estimates_cost_with_provider_prefixed_model() -> None:
|
||||
|
||||
with (
|
||||
patch("strix.report.state.get_global_report_state", return_value=report_state),
|
||||
patch("strix.report.state.resolve_litellm_model", side_effect=lambda model: model),
|
||||
patch("litellm.completion_cost", side_effect=fake_completion_cost),
|
||||
):
|
||||
litellm_cost_callback(kwargs, response)
|
||||
@@ -458,73 +454,3 @@ def test_openrouter_records_upstream_of_pre_stream_error() -> None:
|
||||
request_log._http_reply.reset(token)
|
||||
assert reply.upstream_provider == "InferenceNet"
|
||||
assert reply.upstream_error_type is None
|
||||
|
||||
|
||||
def test_request_timings_land_in_run_record() -> None:
|
||||
started = datetime(2026, 10, 19, 12, 0, tzinfo=UTC)
|
||||
event = request_log.LlmRequestEvent(
|
||||
call_id="call-1",
|
||||
route="litellm",
|
||||
provider="openrouter",
|
||||
model="openrouter/deepseek/deepseek-v4.1-flash",
|
||||
api_host="openrouter.ai",
|
||||
streaming=True,
|
||||
outcome="success",
|
||||
status_code=200,
|
||||
provider_request_id=None,
|
||||
response_id="gen-1",
|
||||
error_type=None,
|
||||
error_message=None,
|
||||
started_at=started,
|
||||
finished_at=started + timedelta(milliseconds=4200),
|
||||
duration_ms=4200,
|
||||
input_tokens=90_000,
|
||||
output_tokens=600,
|
||||
cached_input_tokens=85_000,
|
||||
agent_id="agent-1",
|
||||
time_to_first_token_ms=900,
|
||||
details={"upstream_provider": "Together"},
|
||||
)
|
||||
state = ReportState.__new__(ReportState)
|
||||
state.run_record = {}
|
||||
try:
|
||||
set_global_report_state(state)
|
||||
record_llm_request(event)
|
||||
finally:
|
||||
set_global_report_state(None)
|
||||
|
||||
assert state.run_record["llm_requests"] == [
|
||||
{
|
||||
"started_at": "2026-10-19T12:00:00+00:00",
|
||||
"agent_id": "agent-1",
|
||||
"model": "openrouter/deepseek/deepseek-v4.1-flash",
|
||||
"provider": "Together",
|
||||
"outcome": "success",
|
||||
"status": 200,
|
||||
"duration_ms": 4200,
|
||||
"ttft_ms": 900,
|
||||
"input_tokens": 90_000,
|
||||
"cached_tokens": 85_000,
|
||||
"output_tokens": 600,
|
||||
"retry_attempt": 0,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def test_tool_call_timings_land_in_run_record() -> None:
|
||||
hooks = ReportUsageHooks(model="m", record_tool_timings=True)
|
||||
context = SimpleNamespace(tool_call_id="call-1", context={"agent_id": "agent-1"})
|
||||
tool = SimpleNamespace(name="terminal_execute")
|
||||
state = ReportState.__new__(ReportState)
|
||||
state.run_record = {}
|
||||
try:
|
||||
set_global_report_state(state)
|
||||
await hooks.on_tool_start(context, MagicMock(), tool) # type: ignore[arg-type]
|
||||
await hooks.on_tool_end(context, MagicMock(), tool, "ok") # type: ignore[arg-type]
|
||||
finally:
|
||||
set_global_report_state(None)
|
||||
|
||||
[entry] = state.run_record["tool_calls"]
|
||||
assert entry["agent_id"] == "agent-1"
|
||||
assert entry["tool"] == "terminal_execute"
|
||||
assert entry["duration_ms"] >= 0
|
||||
@@ -1,22 +0,0 @@
|
||||
"""The root agent must follow up open coverage rows before finishing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.agents.prompt import render_system_prompt
|
||||
|
||||
|
||||
_FOLLOW_UP = 'calls `list_coverage(outcome="needs_follow_up")` and follows up every row it returns'
|
||||
_KEEP_GENUINE_GAPS = "stays `needs_follow_up` only when it is genuinely blocked"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scan_mode", ["quick", "standard", "deep"])
|
||||
@pytest.mark.parametrize("is_whitebox", [False, True])
|
||||
@pytest.mark.parametrize("is_root", [False, True])
|
||||
def test_prompt_follows_up_open_coverage(scan_mode: str, is_whitebox: bool, is_root: bool) -> None:
|
||||
prompt = render_system_prompt(scan_mode=scan_mode, is_whitebox=is_whitebox, is_root=is_root)
|
||||
|
||||
assert prompt
|
||||
assert _FOLLOW_UP in prompt
|
||||
assert _KEEP_GENUINE_GAPS in prompt
|
||||
@@ -148,42 +148,3 @@ def test_config_file_loads_dedupe_model(
|
||||
assert settings.dedupe.reasoning_effort == "minimal"
|
||||
# Main model stays independent of the dedupe override.
|
||||
assert settings.llm.model == "openai/root"
|
||||
|
||||
|
||||
def test_dedicated_dedupe_model_uses_own_body_fields_not_main(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"safety_identifier": "main"}))
|
||||
loader._cached = None
|
||||
try:
|
||||
dedupe = DedupeSettings(
|
||||
STRIX_DEDUPE_MODEL="deepseek/cheap",
|
||||
DEDUPE_LLM_EXTRA_BODY={"service_tier": "flex"},
|
||||
)
|
||||
settings = _dedupe_model_settings(dedupe, "deepseek/cheap", 300)
|
||||
assert settings.extra_body == {"service_tier": "flex"}
|
||||
finally:
|
||||
loader._cached = None
|
||||
|
||||
|
||||
def test_dedicated_dedupe_model_gets_no_main_body_fields_by_default(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"safety_identifier": "main"}))
|
||||
loader._cached = None
|
||||
try:
|
||||
dedupe = DedupeSettings(STRIX_DEDUPE_MODEL="deepseek/cheap")
|
||||
settings = _dedupe_model_settings(dedupe, "deepseek/cheap", 300)
|
||||
assert settings.extra_body is None
|
||||
finally:
|
||||
loader._cached = None
|
||||
|
||||
|
||||
def test_fallback_dedupe_inherits_main_body_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"safety_identifier": "main"}))
|
||||
loader._cached = None
|
||||
try:
|
||||
settings = _dedupe_model_settings(DedupeSettings(), "openai/main-model", 300)
|
||||
assert settings.extra_body == {"safety_identifier": "main"}
|
||||
finally:
|
||||
loader._cached = None
|
||||
@@ -1,316 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from agents.run_config import SandboxRunConfig
|
||||
from agents.sandbox import SandboxAgent
|
||||
from agents.sandbox.capabilities import Filesystem, Shell
|
||||
from agents.sandbox.runtime_session_manager import SandboxRuntimeSessionManager
|
||||
|
||||
from strix.runtime import session_manager
|
||||
from strix.runtime.backends import _BACKENDS, _BIND_MOUNT_BACKENDS, register_backend
|
||||
from strix.runtime.deferred_session import DeferredSandboxClient, DeferredSandboxSession
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
@dataclass
|
||||
class _RuntimeSettings:
|
||||
backend: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Settings:
|
||||
runtime: _RuntimeSettings
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Endpoint:
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8080
|
||||
tls: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ExecResult:
|
||||
exit_code: int = 0
|
||||
stdout: bytes = b""
|
||||
stderr: bytes = b""
|
||||
|
||||
def ok(self) -> bool:
|
||||
return self.exit_code == 0
|
||||
|
||||
|
||||
class _State:
|
||||
def __init__(self) -> None:
|
||||
self.manifest: Any = None
|
||||
self.session_id = "00000000-0000-4000-8000-000000000001"
|
||||
|
||||
def model_copy(self, *, update: dict[str, Any]) -> _State:
|
||||
copy = _State()
|
||||
copy.manifest = update.get("manifest", self.manifest)
|
||||
return copy
|
||||
|
||||
|
||||
class _Session:
|
||||
def __init__(self) -> None:
|
||||
self.state = _State()
|
||||
self.execs: list[tuple[str, ...]] = []
|
||||
self.writes: list[tuple[Path, bytes]] = []
|
||||
self.limits: list[Any] = []
|
||||
|
||||
def _set_concurrency_limits(self, limits: Any) -> None:
|
||||
self.limits.append(limits)
|
||||
|
||||
def _set_archive_limits(self, limits: Any) -> None:
|
||||
self.limits.append(limits)
|
||||
|
||||
async def running(self) -> bool:
|
||||
return True
|
||||
|
||||
async def resolve_exposed_port(self, _port: int) -> _Endpoint:
|
||||
return _Endpoint()
|
||||
|
||||
async def exec(self, *argv: str, timeout: float | None = None, **_kw: Any) -> _ExecResult:
|
||||
del timeout
|
||||
self.execs.append(argv)
|
||||
return _ExecResult()
|
||||
|
||||
async def write(self, path: Path, data: io.IOBase, **_kw: Any) -> None:
|
||||
self.writes.append((path, data.read()))
|
||||
|
||||
|
||||
class _Client:
|
||||
def __init__(self) -> None:
|
||||
self.deleted: list[Any] = []
|
||||
|
||||
async def delete(self, session: Any) -> None:
|
||||
self.deleted.append(session)
|
||||
|
||||
|
||||
async def _no_caido(*_args: Any, **_kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class _Backend:
|
||||
def __init__(self, *, fail: bool = False) -> None:
|
||||
self.gate = asyncio.Event()
|
||||
self.calls = 0
|
||||
self.fail = fail
|
||||
self.client = _Client()
|
||||
self.session = _Session()
|
||||
|
||||
async def __call__(self, **_kwargs: Any) -> tuple[Any, Any]:
|
||||
self.calls += 1
|
||||
await self.gate.wait()
|
||||
if self.fail:
|
||||
raise RuntimeError("bring-up exploded")
|
||||
return self.client, self.session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -> Iterator[_Backend]:
|
||||
name = f"deferred-{request.node.name}"
|
||||
fake = _Backend(fail="fail" in request.node.name)
|
||||
register_backend(name, fake, supports_bind_mounts=True)
|
||||
monkeypatch.setattr(session_manager, "bootstrap_caido", _no_caido)
|
||||
monkeypatch.setattr(
|
||||
session_manager, "load_settings", lambda: _Settings(runtime=_RuntimeSettings(backend=name))
|
||||
)
|
||||
yield fake
|
||||
_BACKENDS.pop(name, None)
|
||||
_BIND_MOUNT_BACKENDS.discard(name)
|
||||
|
||||
|
||||
async def _settle() -> None:
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
async def _create(scan_id: str, **kwargs: Any) -> dict[str, Any]:
|
||||
return await session_manager.create_or_reuse(
|
||||
scan_id, image="img", local_sources=[], defer=True, **kwargs
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deferred_bundle_returns_before_the_backend_finishes(backend: _Backend) -> None:
|
||||
scan_id = "deferred-returns"
|
||||
try:
|
||||
bundle = await asyncio.wait_for(_create(scan_id), timeout=1)
|
||||
session = bundle["session"]
|
||||
assert isinstance(session, DeferredSandboxSession)
|
||||
assert isinstance(bundle["client"], DeferredSandboxClient)
|
||||
assert session.pending
|
||||
await _settle()
|
||||
assert backend.calls == 1
|
||||
assert await session.running()
|
||||
assert session.state.manifest.environment.value["PYTHONUNBUFFERED"] == "1"
|
||||
again = await session_manager.create_or_reuse(scan_id, image="img", local_sources=[])
|
||||
assert again is bundle
|
||||
assert backend.calls == 1
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_first_io_resolves_the_sandbox_then_delegates(backend: _Backend) -> None:
|
||||
scan_id = "deferred-io"
|
||||
try:
|
||||
bundle = await _create(scan_id)
|
||||
session = bundle["session"]
|
||||
session._set_concurrency_limits("limits")
|
||||
exec_task = asyncio.create_task(session.exec("echo", "hi"))
|
||||
await asyncio.sleep(0.05)
|
||||
assert not exec_task.done()
|
||||
backend.gate.set()
|
||||
result = await asyncio.wait_for(exec_task, timeout=1)
|
||||
assert result.ok()
|
||||
assert backend.session.execs == [("echo", "hi")]
|
||||
assert not session.pending
|
||||
assert backend.session.limits == ["limits"]
|
||||
assert backend.session.state.manifest is session.state.manifest
|
||||
await session.write(Path("/workspace/x"), io.BytesIO(b"data"))
|
||||
assert backend.session.writes == [(Path("/workspace/x"), b"data")]
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
assert backend.client.deleted == [backend.session]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failed_bring_up_surfaces_on_first_io(backend: _Backend) -> None:
|
||||
scan_id = "deferred-fail"
|
||||
try:
|
||||
bundle = await _create(scan_id)
|
||||
session = bundle["session"]
|
||||
backend.gate.set()
|
||||
with pytest.raises(RuntimeError, match="bring-up exploded"):
|
||||
await asyncio.wait_for(session.exec("true"), timeout=1)
|
||||
assert not await session.running()
|
||||
with pytest.raises(RuntimeError, match="bring-up exploded"):
|
||||
await bundle["caido_client"].get()
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
assert backend.client.deleted == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_while_pending_cancels_the_bring_up(backend: _Backend) -> None:
|
||||
scan_id = "deferred-cancel"
|
||||
bundle = await _create(scan_id)
|
||||
session = bundle["session"]
|
||||
await asyncio.wait_for(session_manager.cleanup(scan_id), timeout=1)
|
||||
assert session.bring_up.cancelled()
|
||||
assert backend.client.deleted == []
|
||||
assert session_manager._SESSION_CACHE.get(scan_id) is None
|
||||
backend.gate.set()
|
||||
await _settle()
|
||||
assert backend.client.deleted == [backend.session]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_while_pending_ignores_a_failed_late_bring_up(backend: _Backend) -> None:
|
||||
scan_id = "deferred-cancel-fail"
|
||||
await _create(scan_id)
|
||||
await asyncio.wait_for(session_manager.cleanup(scan_id), timeout=1)
|
||||
backend.gate.set()
|
||||
await _settle()
|
||||
assert backend.client.deleted == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_files_are_staged_inside_the_deferred_bring_up(backend: _Backend) -> None:
|
||||
scan_id = "deferred-extra"
|
||||
try:
|
||||
bundle = await _create(
|
||||
scan_id,
|
||||
extra_files=[{"workspace_path": "/workspace/.strix/notes.md", "content": "hi"}],
|
||||
)
|
||||
assert backend.session.writes == []
|
||||
backend.gate.set()
|
||||
await asyncio.wait_for(bundle["session"].resolve(), timeout=1)
|
||||
assert len(backend.session.writes) == 1
|
||||
assert backend.session.execs
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_awaitable_extra_files_do_not_delay_the_backend_call(backend: _Backend) -> None:
|
||||
scan_id = "deferred-extra-awaitable"
|
||||
files: asyncio.Future[list[dict[str, Any]] | None] = asyncio.get_running_loop().create_future()
|
||||
try:
|
||||
bundle = await _create(scan_id, extra_files=files)
|
||||
backend.gate.set()
|
||||
await _settle()
|
||||
assert backend.calls == 1
|
||||
assert backend.session.writes == []
|
||||
files.set_result([{"workspace_path": "/workspace/.strix/notes.md", "content": "hi"}])
|
||||
await asyncio.wait_for(bundle["session"].resolve(), timeout=1)
|
||||
assert len(backend.session.writes) == 1
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_awaitable_extra_files_resolving_to_none_stage_nothing(backend: _Backend) -> None:
|
||||
scan_id = "deferred-extra-none"
|
||||
|
||||
async def _nothing() -> list[dict[str, Any]] | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
bundle = await _create(scan_id, extra_files=_nothing())
|
||||
backend.gate.set()
|
||||
await asyncio.wait_for(bundle["session"].resolve(), timeout=1)
|
||||
assert backend.session.writes == []
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_path_still_blocks_until_the_sandbox_is_up(backend: _Backend) -> None:
|
||||
scan_id = "deferred-off"
|
||||
backend.gate.set()
|
||||
try:
|
||||
bundle = await session_manager.create_or_reuse(scan_id, image="img", local_sources=[])
|
||||
assert bundle["session"] is backend.session
|
||||
assert bundle["client"] is backend.client
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
assert backend.client.deleted == [backend.session]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sdk_session_prep_does_not_wait_for_the_bring_up(backend: _Backend) -> None:
|
||||
scan_id = "deferred-sdk-prep"
|
||||
try:
|
||||
bundle = await _create(scan_id)
|
||||
agent = SandboxAgent(
|
||||
name="root", instructions="x", tools=[], capabilities=[Filesystem(), Shell()]
|
||||
)
|
||||
manager = SandboxRuntimeSessionManager(
|
||||
starting_agent=agent,
|
||||
sandbox_config=SandboxRunConfig(client=bundle["client"], session=bundle["session"]),
|
||||
run_state=None,
|
||||
)
|
||||
session = await asyncio.wait_for(
|
||||
manager.ensure_session(
|
||||
agent=agent, capabilities=list(agent.capabilities), is_resumed_state=False
|
||||
),
|
||||
timeout=1,
|
||||
)
|
||||
assert session is bundle["session"]
|
||||
assert session.pending
|
||||
backend.gate.set()
|
||||
await asyncio.wait_for(session.resolve(), timeout=1)
|
||||
assert not session.pending
|
||||
finally:
|
||||
await session_manager.cleanup(scan_id)
|
||||
@@ -1,32 +0,0 @@
|
||||
"""``STRIX_DOCKER_SANDBOX_DEVICES`` passes host devices through to the sandbox container."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from strix.runtime import docker_client
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pytest
|
||||
|
||||
|
||||
def test_unset_adds_no_devices(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv(docker_client._SANDBOX_DEVICES_ENV, raising=False)
|
||||
kwargs: dict[str, object] = {}
|
||||
docker_client._apply_sandbox_devices(kwargs)
|
||||
assert kwargs == {}
|
||||
|
||||
|
||||
def test_devices_are_mapped_to_the_same_path_rwm(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv(docker_client._SANDBOX_DEVICES_ENV, " /dev/net/tun, /dev/kvm ,,")
|
||||
kwargs: dict[str, object] = {}
|
||||
docker_client._apply_sandbox_devices(kwargs)
|
||||
assert kwargs["devices"] == ["/dev/net/tun:/dev/net/tun:rwm", "/dev/kvm:/dev/kvm:rwm"]
|
||||
|
||||
|
||||
def test_existing_devices_are_kept_and_not_duplicated(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv(docker_client._SANDBOX_DEVICES_ENV, "/dev/fuse,/dev/net/tun")
|
||||
kwargs: dict[str, object] = {"devices": ["/dev/fuse"]}
|
||||
docker_client._apply_sandbox_devices(kwargs)
|
||||
assert kwargs["devices"] == ["/dev/fuse", "/dev/net/tun:/dev/net/tun:rwm"]
|
||||
@@ -507,30 +507,3 @@ def test_recomputed_budget_flags(
|
||||
expected: tuple[bool, bool],
|
||||
) -> None:
|
||||
assert recomputed_budget_flags(cost, max_budget, interactive=interactive) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_turn_limit_warns_ten_before_and_repeats_past_it() -> None:
|
||||
hooks = ReportUsageHooks(model="test-model")
|
||||
noticed = []
|
||||
for requests in range(40):
|
||||
ctx = _make_warn_context(requests=requests, parent_id="root-1")
|
||||
ctx.context["turn_limit"] = 25
|
||||
items: list[Any] = []
|
||||
await hooks.on_llm_start(ctx, MagicMock(), None, items)
|
||||
if items:
|
||||
noticed.append((requests + 1, items[0]["content"]))
|
||||
|
||||
assert [turn for turn, _ in noticed] == [15, 25, 30, 35, 40]
|
||||
assert "15 of your 25-turn limit" in noticed[0][1]
|
||||
assert "past your 25-turn limit" in noticed[1][1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_turn_limit_no_notice() -> None:
|
||||
hooks = ReportUsageHooks(model="test-model")
|
||||
items: list[Any] = []
|
||||
await hooks.on_llm_start(
|
||||
_make_warn_context(requests=14, parent_id="root-1"), MagicMock(), None, items
|
||||
)
|
||||
assert items == []
|
||||
@@ -314,12 +314,6 @@ def test_make_model_settings_disables_parallel_tool_calls_by_default() -> None:
|
||||
assert make_model_settings("none", model_name="gpt-4o").parallel_tool_calls is False
|
||||
|
||||
|
||||
def test_make_model_settings_enables_parallel_tool_calls_when_requested() -> None:
|
||||
settings = make_model_settings("none", model_name="gpt-4o", parallel_tool_calls=True)
|
||||
|
||||
assert settings.parallel_tool_calls is True
|
||||
|
||||
|
||||
def test_make_model_settings_omits_parallel_tool_calls_without_tools() -> None:
|
||||
settings = make_model_settings("none", model_name="gpt-4o", has_tools=False)
|
||||
|
||||
@@ -367,58 +361,6 @@ def test_make_model_settings_extra_headers_survive_reasoning_resolve() -> None:
|
||||
assert settings.extra_headers == {"X-Feature-Key": "svc"}
|
||||
|
||||
|
||||
def test_make_model_settings_sets_extra_body() -> None:
|
||||
settings = make_model_settings(
|
||||
"none",
|
||||
model_name="openai/gpt-5",
|
||||
extra_body={"service_tier": "flex", "safety_identifier": "user-hash"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"service_tier": "flex", "safety_identifier": "user-hash"}
|
||||
|
||||
|
||||
def test_make_model_settings_omits_extra_body_when_unset() -> None:
|
||||
assert make_model_settings("none", model_name="gpt-4o").extra_body is None
|
||||
|
||||
|
||||
def test_make_model_settings_extra_body_survives_reasoning_resolve() -> None:
|
||||
settings = make_model_settings(
|
||||
"high",
|
||||
model_name="openai/o3",
|
||||
extra_body={"service_tier": "flex"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"service_tier": "flex"}
|
||||
|
||||
|
||||
def test_make_model_settings_extra_body_merges_with_max_reasoning_field() -> None:
|
||||
settings = make_model_settings(
|
||||
"max",
|
||||
model_name="deepseek/deepseek-v4-flash",
|
||||
extra_body={"safety_identifier": "user-hash"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"safety_identifier": "user-hash", "reasoning_effort": "max"}
|
||||
|
||||
|
||||
def test_make_model_settings_max_reasoning_overrides_extra_body_reasoning_field() -> None:
|
||||
settings = make_model_settings(
|
||||
"max",
|
||||
model_name="deepseek/deepseek-v4-flash",
|
||||
extra_body={"reasoning_effort": "low"},
|
||||
)
|
||||
|
||||
assert settings.extra_body == {"reasoning_effort": "max"}
|
||||
|
||||
|
||||
def test_make_model_settings_does_not_mutate_caller_extra_body() -> None:
|
||||
extra_body = {"safety_identifier": "user-hash"}
|
||||
|
||||
make_model_settings("max", model_name="deepseek/deepseek-v4-flash", extra_body=extra_body)
|
||||
|
||||
assert extra_body == {"safety_identifier": "user-hash"}
|
||||
|
||||
|
||||
def test_make_model_settings_timeout_survives_reasoning_resolve() -> None:
|
||||
# Reasoning is resolved via ModelSettings.resolve(); the timeout in extra_args
|
||||
# must not be dropped when a reasoning override is merged in.
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""Tests for LLM_EXTRA_BODY: extra fields merged into every LLM request body."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
import litellm
|
||||
import openai
|
||||
import pytest
|
||||
from agents.extensions.models.litellm_model import LitellmModel
|
||||
from agents.models.interface import ModelTracing
|
||||
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
||||
from agents.models.openai_responses import OpenAIResponsesModel
|
||||
|
||||
from strix.config import loader
|
||||
from strix.config.loader import load_settings
|
||||
from strix.core.inputs import make_model_settings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
from agents.models.interface import Model
|
||||
|
||||
|
||||
_EXTRA_BODY = {"service_tier": "flex", "safety_identifier": "user-hash"}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||||
monkeypatch.delenv("LLM_EXTRA_BODY", raising=False)
|
||||
monkeypatch.setattr(loader, "_cached", None)
|
||||
monkeypatch.setattr(loader, "_override", None)
|
||||
yield
|
||||
|
||||
|
||||
def test_extra_body_parsed_from_json_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", json.dumps({"service_tier": "flex", "n": 1}))
|
||||
|
||||
assert load_settings().llm.extra_body == {"service_tier": "flex", "n": 1}
|
||||
|
||||
|
||||
async def _send(model: Model) -> None:
|
||||
await model.get_response(
|
||||
system_instructions=None,
|
||||
input="Reply with just 'OK'.",
|
||||
model_settings=make_model_settings(None, model_name="gpt-5", extra_body=_EXTRA_BODY),
|
||||
tools=[],
|
||||
output_schema=None,
|
||||
handoffs=[],
|
||||
tracing=ModelTracing.DISABLED,
|
||||
previous_response_id=None,
|
||||
conversation_id=None,
|
||||
prompt=None,
|
||||
)
|
||||
|
||||
|
||||
def _capturing_client(captured: dict[str, Any]) -> openai.AsyncOpenAI:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
captured.update(json.loads(request.content))
|
||||
return httpx.Response(400, json={"error": {"message": "captured"}})
|
||||
|
||||
return openai.AsyncOpenAI(
|
||||
api_key="test",
|
||||
base_url="https://gateway.example/v1",
|
||||
max_retries=0,
|
||||
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("model_cls", [OpenAIChatCompletionsModel, OpenAIResponsesModel])
|
||||
async def test_extra_body_reaches_native_openai_request_body(model_cls: type) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
with pytest.raises(openai.BadRequestError):
|
||||
await _send(model_cls(model="gpt-5", openai_client=_capturing_client(captured)))
|
||||
|
||||
assert {k: captured.get(k) for k in _EXTRA_BODY} == _EXTRA_BODY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_body_passed_to_litellm(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def fake_acompletion(**kwargs: Any) -> Any:
|
||||
captured.update(kwargs)
|
||||
raise RuntimeError("captured")
|
||||
|
||||
monkeypatch.setattr(litellm, "acompletion", fake_acompletion)
|
||||
|
||||
with pytest.raises(RuntimeError, match="captured"):
|
||||
await _send(LitellmModel(model="openai/gpt-5", base_url="https://gateway.example/v1"))
|
||||
|
||||
assert captured["extra_body"] == _EXTRA_BODY
|
||||
@@ -60,7 +60,6 @@ def test_preflight_request_fails_after_its_own_timeout_with_a_clear_message() ->
|
||||
model, # type: ignore[arg-type]
|
||||
model_name="openai/gpt-4o",
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
timeout=1,
|
||||
api_base_setting="DEDUPE_LLM_API_BASE",
|
||||
)
|
||||
@@ -131,52 +130,3 @@ def test_warm_up_checks_the_dedupe_model_with_its_own_headers_and_the_preflight_
|
||||
"DEDUPE_LLM_API_BASE",
|
||||
]
|
||||
assert calls[1][0] is dedupe_model
|
||||
|
||||
|
||||
def test_warm_up_checks_each_model_with_its_own_body_fields(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-4o")
|
||||
monkeypatch.setenv("LLM_API_KEY", "sk-test")
|
||||
monkeypatch.setenv("LLM_EXTRA_BODY", '{"safety_identifier": "main"}')
|
||||
monkeypatch.setenv("STRIX_DEDUPE_MODEL", "anthropic/claude-sonnet-4-5")
|
||||
monkeypatch.setenv("DEDUPE_LLM_EXTRA_BODY", '{"service_tier": "flex"}')
|
||||
_fresh_settings(monkeypatch, tmp_path)
|
||||
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
async def record(_model: Any, **kwargs: Any) -> None:
|
||||
calls.append(kwargs)
|
||||
|
||||
monkeypatch.setattr(scan_setup, "preflight_request", record)
|
||||
monkeypatch.setattr(cli_main, "preflight_request", record)
|
||||
monkeypatch.setattr("strix.config.models.configure_sdk_model_defaults", lambda _settings: None)
|
||||
monkeypatch.setattr("strix.report.dedupe.resolve_dedupe_model", lambda _dedupe, _name: object())
|
||||
|
||||
asyncio.run(cli_main.warm_up_llm())
|
||||
|
||||
assert [kwargs["extra_body"] for kwargs in calls] == [
|
||||
{"safety_identifier": "main"},
|
||||
{"service_tier": "flex"},
|
||||
]
|
||||
|
||||
|
||||
def test_preflight_request_sends_the_extra_body() -> None:
|
||||
seen: list[Any] = []
|
||||
|
||||
class _Recorder:
|
||||
async def get_response(self, *, model_settings: Any, **_: Any) -> None:
|
||||
seen.append(model_settings.extra_body)
|
||||
|
||||
asyncio.run(
|
||||
preflight_request(
|
||||
_Recorder(), # type: ignore[arg-type]
|
||||
model_name="openai/gpt-4o",
|
||||
extra_headers=None,
|
||||
extra_body={"safety_identifier": "main"},
|
||||
timeout=5,
|
||||
api_base_setting="LLM_API_BASE",
|
||||
)
|
||||
)
|
||||
|
||||
assert seen == [{"safety_identifier": "main"}]
|
||||
+15
-54
@@ -9,10 +9,10 @@ call at a time against the shared client.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import pytest
|
||||
from caido_sdk_client.errors import OperationUserError
|
||||
|
||||
from strix.runtime.caido_handle import CaidoBootstrapHandle
|
||||
from strix.tools.proxy import caido_api, tools
|
||||
@@ -26,7 +26,6 @@ class _FakeClient:
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.closed = False
|
||||
self.graphql: Any = None
|
||||
|
||||
async def aclose(self) -> None:
|
||||
self.closed = True
|
||||
@@ -231,23 +230,20 @@ async def test_ctx_client_degrades_when_bootstrap_failed() -> None:
|
||||
assert await tools._ctx_client(cast("Any", _Ctx({"caido_client": handle}))) is None
|
||||
|
||||
|
||||
class _FakeGraphQL:
|
||||
def __init__(self, answers: dict[str, list[Any]]) -> None:
|
||||
self.answers = answers
|
||||
self.queries: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
async def query(self, document: str, variables: dict[str, Any]) -> dict[str, Any]:
|
||||
self.queries.append((document, dict(variables)))
|
||||
answer = self.answers[variables["id"]].pop(0)
|
||||
if isinstance(answer, Exception):
|
||||
raise answer
|
||||
return {"request": answer}
|
||||
|
||||
|
||||
async def test_existing_request_ids_select_only_the_id() -> None:
|
||||
async def test_existing_request_ids_queries_current_project(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
client = _FakeClient("host")
|
||||
graphql = _FakeGraphQL({"1042": [{"id": "1042"}], "1088": [None]})
|
||||
client.graphql = graphql
|
||||
looked_up: list[str] = []
|
||||
|
||||
async def get_request_with_client(passed_client: Any, request_id: str) -> Any:
|
||||
assert passed_client is client
|
||||
looked_up.append(request_id)
|
||||
if request_id == "1042":
|
||||
return SimpleNamespace(request=SimpleNamespace(id="1042"))
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(caido_api, "get_request_with_client", get_request_with_client)
|
||||
|
||||
existing = await tools.existing_request_ids(
|
||||
cast("Any", _Ctx({"caido_client": client})),
|
||||
@@ -255,39 +251,4 @@ async def test_existing_request_ids_select_only_the_id() -> None:
|
||||
)
|
||||
|
||||
assert existing == {"1042"}
|
||||
assert [variables["id"] for _, variables in graphql.queries] == ["1042", "1088"]
|
||||
for document, _ in graphql.queries:
|
||||
assert "raw" not in document
|
||||
assert "{ id }" in document
|
||||
|
||||
|
||||
async def test_existing_request_ids_retry_once_before_giving_up() -> None:
|
||||
client = _FakeClient("host")
|
||||
graphql = _FakeGraphQL(
|
||||
{
|
||||
"1042": [TimeoutError("deadline exceeded"), {"id": "1042"}],
|
||||
"1088": [TimeoutError("deadline exceeded"), TimeoutError("deadline exceeded")],
|
||||
}
|
||||
)
|
||||
client.graphql = graphql
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
await tools.existing_request_ids(
|
||||
cast("Any", _Ctx({"caido_client": client})),
|
||||
["1042", "1088"],
|
||||
)
|
||||
|
||||
assert [variables["id"] for _, variables in graphql.queries] == [
|
||||
"1042",
|
||||
"1042",
|
||||
"1088",
|
||||
"1088",
|
||||
]
|
||||
|
||||
|
||||
def test_err_logs_caido_errors_as_warnings(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level("WARNING", logger="strix.tools.proxy.tools"):
|
||||
tools._err("list_requests", OperationUserError(""))
|
||||
|
||||
[record] = caplog.records
|
||||
assert record.levelname == "WARNING"
|
||||
assert looked_up == ["1042", "1088"]
|
||||
+10
-167
@@ -408,165 +408,6 @@ async def test_dependency_report_records_transitive_chain(report_state: ReportSt
|
||||
)
|
||||
|
||||
|
||||
async def test_dependency_report_persists_code_locations(report_state: ReportState) -> None:
|
||||
"""Usage-site locations file onto a dependency finding like any other."""
|
||||
result = await _do_create_dependency(
|
||||
title="CVE-2021-23337 in lodash 4.17.20",
|
||||
description="Command injection via template.",
|
||||
target="repo/package.json",
|
||||
cve="CVE-2021-23337",
|
||||
package_name="lodash",
|
||||
installed_version="4.17.20",
|
||||
impact="Arbitrary command execution.",
|
||||
remediation_steps="Upgrade to 4.17.21.",
|
||||
assumptions="Assumes the template sink is reachable.",
|
||||
package_ecosystem="npm",
|
||||
manifest_path="package-lock.json",
|
||||
fixed_version="4.17.21",
|
||||
cwe="CWE-94",
|
||||
advisory_cvss=7.2,
|
||||
technical_analysis=None,
|
||||
fix_effort="trivial",
|
||||
reachability="vulnerable_symbol_used",
|
||||
reachability_evidence=_DEP_EVIDENCE,
|
||||
code_locations=[
|
||||
{
|
||||
"file": "src/render.ts",
|
||||
"start_line": 14,
|
||||
"end_line": 14,
|
||||
"snippet": "const merge = require('lodash').merge",
|
||||
"label": "imports the vulnerable package",
|
||||
}
|
||||
],
|
||||
contextual_cvss_breakdown=_DEP_CONTEXT,
|
||||
contextual_cvss_reasoning=_DEP_REASONING,
|
||||
)
|
||||
assert result["success"] is True
|
||||
locs = report_state.vulnerability_reports[0]["code_locations"]
|
||||
assert locs[0]["file"] == "src/render.ts"
|
||||
assert locs[0]["label"] == "imports the vulnerable package"
|
||||
|
||||
|
||||
async def test_dependency_report_fix_locations_require_verification(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
"""A location proposing a fix still requires fix_verification."""
|
||||
result = await _do_create_dependency(
|
||||
title="CVE-2021-23337 in lodash 4.17.20",
|
||||
description="Command injection via template.",
|
||||
target="repo/package.json",
|
||||
cve="CVE-2021-23337",
|
||||
package_name="lodash",
|
||||
installed_version="4.17.20",
|
||||
impact="Arbitrary command execution.",
|
||||
remediation_steps="Upgrade to 4.17.21.",
|
||||
assumptions="Assumes the template sink is reachable.",
|
||||
package_ecosystem="npm",
|
||||
manifest_path="package-lock.json",
|
||||
fixed_version="4.17.21",
|
||||
cwe="CWE-94",
|
||||
advisory_cvss=7.2,
|
||||
technical_analysis=None,
|
||||
fix_effort="trivial",
|
||||
reachability="imported",
|
||||
reachability_evidence=_DEP_EVIDENCE,
|
||||
code_locations=[
|
||||
{
|
||||
"file": "package-lock.json",
|
||||
"start_line": 10,
|
||||
"end_line": 10,
|
||||
"fix_before": '"lodash": "4.17.20"',
|
||||
"fix_after": '"lodash": "4.17.21"',
|
||||
}
|
||||
],
|
||||
contextual_cvss_breakdown=_DEP_CONTEXT,
|
||||
contextual_cvss_reasoning=_DEP_REASONING,
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert any("fix_verification" in e for e in result["errors"])
|
||||
assert not report_state.vulnerability_reports
|
||||
|
||||
|
||||
async def test_dependency_report_fix_locations_persist_with_verification(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
"""A fix-pair location files when fix_verification is supplied, and both
|
||||
fields persist on the stored report."""
|
||||
result = await _do_create_dependency(
|
||||
title="CVE-2021-23337 in lodash 4.17.20",
|
||||
description="Command injection via template.",
|
||||
target="repo/package.json",
|
||||
cve="CVE-2021-23337",
|
||||
package_name="lodash",
|
||||
installed_version="4.17.20",
|
||||
impact="Arbitrary command execution.",
|
||||
remediation_steps="Upgrade to 4.17.21.",
|
||||
assumptions="Assumes the template sink is reachable.",
|
||||
package_ecosystem="npm",
|
||||
manifest_path="package-lock.json",
|
||||
fixed_version="4.17.21",
|
||||
cwe="CWE-94",
|
||||
advisory_cvss=7.2,
|
||||
technical_analysis=None,
|
||||
fix_effort="trivial",
|
||||
reachability="imported",
|
||||
reachability_evidence=_DEP_EVIDENCE,
|
||||
code_locations=[
|
||||
{
|
||||
"file": "package-lock.json",
|
||||
"start_line": 10,
|
||||
"end_line": 10,
|
||||
"fix_before": '"lodash": "4.17.20"',
|
||||
"fix_after": '"lodash": "4.17.21"',
|
||||
}
|
||||
],
|
||||
fix_verification="Bump verified: lockfile parses and the sink is unreachable.",
|
||||
contextual_cvss_breakdown=_DEP_CONTEXT,
|
||||
contextual_cvss_reasoning=_DEP_REASONING,
|
||||
)
|
||||
assert result["success"] is True
|
||||
report = report_state.vulnerability_reports[0]
|
||||
loc = report["code_locations"][0]
|
||||
assert loc["fix_before"] == '"lodash": "4.17.20"'
|
||||
assert loc["fix_after"] == '"lodash": "4.17.21"'
|
||||
assert report["fix_verification"] == (
|
||||
"Bump verified: lockfile parses and the sink is unreachable."
|
||||
)
|
||||
|
||||
|
||||
async def test_dependency_report_rejects_unusable_locations(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
"""A supplied list that normalizes to nothing errors instead of filing
|
||||
silently without the requested usage evidence."""
|
||||
result = await _do_create_dependency(
|
||||
title="CVE-2021-23337 in lodash 4.17.20",
|
||||
description="Command injection via template.",
|
||||
target="repo/package.json",
|
||||
cve="CVE-2021-23337",
|
||||
package_name="lodash",
|
||||
installed_version="4.17.20",
|
||||
impact="Arbitrary command execution.",
|
||||
remediation_steps="Upgrade to 4.17.21.",
|
||||
assumptions="Assumes the template sink is reachable.",
|
||||
package_ecosystem="npm",
|
||||
manifest_path="package-lock.json",
|
||||
fixed_version="4.17.21",
|
||||
cwe="CWE-94",
|
||||
advisory_cvss=7.2,
|
||||
technical_analysis=None,
|
||||
fix_effort="trivial",
|
||||
reachability="imported",
|
||||
reachability_evidence=_DEP_EVIDENCE,
|
||||
code_locations=[{"file": "/abs/path.ts", "snippet": "require('lodash')"}],
|
||||
contextual_cvss_breakdown=_DEP_CONTEXT,
|
||||
contextual_cvss_reasoning=_DEP_REASONING,
|
||||
)
|
||||
assert result["success"] is False
|
||||
assert any("dropped as unusable" in e for e in result["errors"])
|
||||
assert not report_state.vulnerability_reports
|
||||
|
||||
|
||||
async def test_dependency_report_omits_blank_chain_fields(report_state: ReportState) -> None:
|
||||
result = await _do_create_dependency(
|
||||
title="CVE-2024-0001 in sample 1.0.0",
|
||||
@@ -663,6 +504,7 @@ async def test_dependency_report_records_reachability(report_state: ReportState)
|
||||
== "src/render.ts:14 calls `_.template()`."
|
||||
)
|
||||
assert "**Usage analysis:**" in report["evidence"]
|
||||
assert "not a proof of exploitability or of safety" in report["evidence"]
|
||||
# The level must never influence the rating — that comes from the contextual
|
||||
# breakdown, or from advisory_cvss when no breakdown applies.
|
||||
assert report["severity"] == "high"
|
||||
@@ -1640,7 +1482,7 @@ async def test_http_exchange_ids_must_exist_in_current_proxy_project(
|
||||
assert warning is None
|
||||
|
||||
|
||||
async def test_http_exchange_ids_are_kept_when_proxy_cannot_be_queried(
|
||||
async def test_http_exchange_ids_are_dropped_when_proxy_cannot_be_queried(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def existing_request_ids(
|
||||
@@ -1656,10 +1498,11 @@ async def test_http_exchange_ids_are_kept_when_proxy_cannot_be_queried(
|
||||
["1042", "1042", "1088"],
|
||||
)
|
||||
|
||||
assert request_ids == ["1042", "1088"]
|
||||
assert request_ids is None
|
||||
assert errors == []
|
||||
assert warning is not None
|
||||
assert "without verification" in warning
|
||||
assert "not stored" in warning
|
||||
assert "update_vulnerability_report" in warning
|
||||
|
||||
|
||||
async def test_create_reports_persistence_failure_as_tool_error(
|
||||
@@ -1678,7 +1521,7 @@ async def test_create_reports_persistence_failure_as_tool_error(
|
||||
assert report_state.vulnerability_reports == []
|
||||
|
||||
|
||||
async def test_evidence_only_update_keeps_ids_when_proxy_cannot_be_queried(
|
||||
async def test_evidence_only_update_reports_proxy_outage_as_retryable(
|
||||
report_state: ReportState,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -1711,11 +1554,11 @@ async def test_evidence_only_update_keeps_ids_when_proxy_cannot_be_queried(
|
||||
)
|
||||
result = json.loads(raw)
|
||||
|
||||
assert result["success"] is True
|
||||
assert "without verification" in result["warning"]
|
||||
assert result["success"] is False
|
||||
assert "No fields to update" not in result["error"]
|
||||
assert "update_vulnerability_report" in result["error"]
|
||||
assert result["report_id"] == "vuln-0009"
|
||||
assert report_state.vulnerability_reports[0]["http_exchange_ids"] == ["204"]
|
||||
assert report_state.vulnerability_reports[0] != original
|
||||
assert report_state.vulnerability_reports[0] == original
|
||||
|
||||
|
||||
def test_update_reports_persistence_failure_as_tool_error(report_state: ReportState) -> None:
|
||||
|
||||
@@ -263,11 +263,7 @@ def test_narrow_terminals_never_wrap_a_row() -> None:
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX key reader")
|
||||
def test_posix_key_reader_handles_multibyte_and_arrows() -> None:
|
||||
tty = pytest.importorskip("tty")
|
||||
master, slave = os.openpty()
|
||||
# Without this the pty echoes the writes into an output queue nobody reads,
|
||||
# and read_raw_key's TCSADRAIN restore blocks forever on macOS.
|
||||
tty.setcbreak(slave)
|
||||
try:
|
||||
with os.fdopen(slave, "r+b", buffering=0) as stream:
|
||||
os.write(master, "\u00e9".encode())
|
||||
|
||||
@@ -25,11 +25,9 @@ def _wire_runner(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
||||
@@ -41,11 +41,9 @@ def _settings() -> Any:
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
||||
@@ -39,11 +39,9 @@ async def test_persistent_rate_limit_stops_gracefully(
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
||||
@@ -61,14 +61,11 @@ def _patch_engine_scaffold(
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
telemetry=types.SimpleNamespace(run_record_timings=False),
|
||||
)
|
||||
monkeypatch.setattr(runner, "load_settings", lambda: settings)
|
||||
monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _settings: None)
|
||||
@@ -162,36 +159,6 @@ async def test_root_prompt_options_flow_into_root_agent(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replace_root_scan_prompt_drops_the_scan_prompt(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Any,
|
||||
) -> None:
|
||||
scope_context = {
|
||||
"scope_source": "user_scan_config",
|
||||
"authorized_targets": [
|
||||
{"type": "web_application", "value": "https://example.com", "workspace_path": ""},
|
||||
],
|
||||
}
|
||||
captured = _patch_engine_scaffold(monkeypatch, tmp_path, scope_context)
|
||||
|
||||
await runner.run_strix_scan(
|
||||
scan_config={"targets": [], "scan_mode": "deep"},
|
||||
scan_id="scan-replace",
|
||||
image="img",
|
||||
coordinator=AgentCoordinator(),
|
||||
root_instructions_override="CUSTOM SCAN PROMPT",
|
||||
replace_root_scan_prompt=True,
|
||||
)
|
||||
|
||||
instructions_override = captured["kwargs"]["instructions_override"]
|
||||
assert instructions_override.startswith("CUSTOM SCAN PROMPT")
|
||||
assert instructions_override.count("SCOPE:") == 1
|
||||
assert "https://example.com" in instructions_override
|
||||
assert "<root_scan_instructions_override>" not in instructions_override
|
||||
assert "application security validation agent" not in instructions_override
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extra_system_prompt_context_cannot_override_scope_context(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -317,19 +284,6 @@ async def test_unknown_tool_calls_are_returned_to_the_model(
|
||||
assert captured["run_config"].tool_not_found_behavior == "return_error_to_model"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("interactive", [False, True])
|
||||
def test_parallel_tool_calls_prompt_is_opt_in(interactive: bool) -> None:
|
||||
default = render_system_prompt(include_scope=False, interactive=interactive)
|
||||
parallel = render_system_prompt(
|
||||
include_scope=False, interactive=interactive, parallel_tool_calls=True
|
||||
)
|
||||
|
||||
assert "Read-only calls can go together" not in default
|
||||
assert ("Read-only calls can go together" in parallel) is not interactive
|
||||
assert ("exactly one tool call" in default) is interactive
|
||||
assert "exactly one tool call" not in parallel
|
||||
|
||||
|
||||
def test_scope_is_rendered_once_at_the_end_of_the_prompt() -> None:
|
||||
prompt = render_system_prompt(
|
||||
system_prompt_context={
|
||||
|
||||
@@ -49,11 +49,9 @@ def _settings() -> Any:
|
||||
model="openai/gpt-4o",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
parallel_tool_calls=False,
|
||||
timeout=300,
|
||||
prompt_cache=True,
|
||||
extra_headers=None,
|
||||
extra_body=None,
|
||||
),
|
||||
runtime=types.SimpleNamespace(max_context_images=3),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ from strix.skills import (
|
||||
register_skill_dir,
|
||||
registered_skill_dirs,
|
||||
skill_search_dirs,
|
||||
skill_turn_limit,
|
||||
validate_requested_skills,
|
||||
)
|
||||
from strix.utils.resource_paths import get_strix_resource_path
|
||||
@@ -283,16 +282,3 @@ def test_new_skill_files_load() -> None:
|
||||
for name in names:
|
||||
key = name.split("/")[-1]
|
||||
assert loaded.get(key), f"{name} failed to load"
|
||||
|
||||
|
||||
def test_skill_turn_limit_matches_bare_and_qualified_names(tmp_path: Path) -> None:
|
||||
_write_skill(tmp_path, "pr_review", "investigator", "---\nname: investigator\n---\nx")
|
||||
_write_skill(tmp_path, "pr_review", "validator", "---\nname: validator\n---\nx")
|
||||
register_skill_dir(tmp_path)
|
||||
limits = {"pr_review/investigator": 25, "pr_review/validator": 40}
|
||||
|
||||
assert skill_turn_limit(["investigator"], limits) == 25
|
||||
assert skill_turn_limit(["pr_review/validator", "idor"], limits) == 40
|
||||
assert skill_turn_limit(["investigator", "validator"], limits) == 40
|
||||
assert skill_turn_limit(["idor"], limits) is None
|
||||
assert skill_turn_limit(["investigator"], {}) is None
|
||||
@@ -1,300 +0,0 @@
|
||||
"""Tests for keeping replayed tool-call arguments valid JSON.
|
||||
|
||||
A model that emits a tool call with malformed ``arguments`` fails that one
|
||||
call, but the raw string is recorded in the session. Strict OpenAI-compatible
|
||||
servers validate every assistant tool call in the request, so each later turn
|
||||
is rejected and the agent can never recover. A gateway that validates
|
||||
arguments the way those servers do proves both the failure and the fix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from agents import Agent, Runner, function_tool
|
||||
from agents.models.interface import Model, ModelProvider
|
||||
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
||||
from agents.run import RunConfig
|
||||
from openai import AsyncOpenAI, BadRequestError
|
||||
from openai.types.responses import ResponseFunctionToolCall
|
||||
|
||||
from strix.config.models import _NonStreamingModel, _TurnGuardModel
|
||||
from strix.config.tool_call_arguments import (
|
||||
MALFORMED_ARGUMENTS_KEY,
|
||||
_loads_strict,
|
||||
describe_malformed_arguments,
|
||||
repair_arguments,
|
||||
repair_history_arguments,
|
||||
repair_input,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
TRUNCATED = '{"n": 1'
|
||||
OVER_NESTED = '{"a":' + "[" * 100_000
|
||||
|
||||
|
||||
def _tool_call_completion(arguments: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": "chatcmpl-1",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gw-model",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"finish_reason": "tool_calls",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "do_thing", "arguments": arguments},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7},
|
||||
}
|
||||
|
||||
|
||||
def _text_completion(text: str) -> dict[str, Any]:
|
||||
return {
|
||||
"id": "chatcmpl-2",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gw-model",
|
||||
"choices": [
|
||||
{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": text}}
|
||||
],
|
||||
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
|
||||
}
|
||||
|
||||
|
||||
_REQUESTS: list[list[dict[str, Any]]] = []
|
||||
|
||||
|
||||
def _assistant_arguments(messages: list[dict[str, Any]]) -> list[str]:
|
||||
return [
|
||||
str(call["function"]["arguments"])
|
||||
for message in messages
|
||||
for call in message.get("tool_calls") or []
|
||||
]
|
||||
|
||||
|
||||
def _tool_results(messages: list[dict[str, Any]]) -> list[str]:
|
||||
return [str(m.get("content")) for m in messages if m.get("role") == "tool"]
|
||||
|
||||
|
||||
class _StrictHandler(BaseHTTPRequestHandler):
|
||||
"""Gateway that rejects malformed assistant tool-call arguments, like vLLM/SGLang do."""
|
||||
|
||||
def log_message(self, *args: Any) -> None:
|
||||
pass
|
||||
|
||||
def do_POST(self) -> None:
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length) or b"{}")
|
||||
messages = body.get("messages", [])
|
||||
_REQUESTS.append(messages)
|
||||
|
||||
for arguments in _assistant_arguments(messages):
|
||||
try:
|
||||
json.loads(arguments)
|
||||
except ValueError:
|
||||
self._respond(
|
||||
400,
|
||||
{
|
||||
"object": "error",
|
||||
"message": "Assistant tool call function.arguments must be valid JSON.",
|
||||
"type": "BadRequest",
|
||||
"param": None,
|
||||
"code": 400,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if len(_REQUESTS) == 1:
|
||||
self._respond(200, _tool_call_completion(TRUNCATED))
|
||||
else:
|
||||
self._respond(200, _text_completion("all done"))
|
||||
|
||||
def _respond(self, status: int, payload: dict[str, Any]) -> None:
|
||||
encoded = json.dumps(payload).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(encoded)))
|
||||
self.end_headers()
|
||||
self.wfile.write(encoded)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def strict_gateway() -> Iterator[str]:
|
||||
_REQUESTS.clear()
|
||||
server = HTTPServer(("127.0.0.1", 0), _StrictHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
yield f"http://127.0.0.1:{server.server_address[1]}/v1"
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def _model(base_url: str) -> Model:
|
||||
client = AsyncOpenAI(api_key="tok", base_url=base_url, max_retries=0)
|
||||
return _NonStreamingModel(OpenAIChatCompletionsModel(model="gw-model", openai_client=client))
|
||||
|
||||
|
||||
async def _run_agent(base_url: str, *, wrap: bool) -> Any:
|
||||
@function_tool
|
||||
def do_thing(n: int) -> str:
|
||||
return f"did {n}"
|
||||
|
||||
class _Provider(ModelProvider):
|
||||
def get_model(self, model_name: str | None) -> Model: # noqa: ARG002
|
||||
model = _model(base_url)
|
||||
return _TurnGuardModel(model) if wrap else model
|
||||
|
||||
agent = Agent(name="t", instructions="use the tool", tools=[do_thing], model="gw-model")
|
||||
result = Runner.run_streamed(
|
||||
agent, input="please", run_config=RunConfig(model_provider=_Provider())
|
||||
)
|
||||
async for _ in result.stream_events():
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_malformed_arguments_poison_every_later_turn_without_the_wrapper(
|
||||
strict_gateway: str,
|
||||
) -> None:
|
||||
# Repro: the model truncates a tool call's arguments once. The tool fails
|
||||
# that call gracefully, but the next request replays the raw string and
|
||||
# the provider rejects the whole conversation from then on.
|
||||
with pytest.raises(BadRequestError, match="must be valid JSON"):
|
||||
await _run_agent(strict_gateway, wrap=False)
|
||||
|
||||
assert _assistant_arguments(_REQUESTS[-1]) == [TRUNCATED]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_malformed_arguments_are_replayed_as_valid_json(strict_gateway: str) -> None:
|
||||
result = await _run_agent(strict_gateway, wrap=True)
|
||||
|
||||
assert result.final_output == "all done"
|
||||
(arguments,) = _assistant_arguments(_REQUESTS[-1])
|
||||
assert json.loads(arguments) == {MALFORMED_ARGUMENTS_KEY: TRUNCATED}
|
||||
(tool_result,) = _tool_results(_REQUESTS[-1])
|
||||
assert "JSON" in tool_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("arguments", "expected"),
|
||||
[
|
||||
('{"cmd": "ls', json.dumps({MALFORMED_ARGUMENTS_KEY: '{"cmd": "ls'})),
|
||||
("[1, 2]", json.dumps({MALFORMED_ARGUMENTS_KEY: "[1, 2]"})),
|
||||
("", "{}"),
|
||||
(" ", "{}"),
|
||||
(None, "{}"),
|
||||
],
|
||||
)
|
||||
def test_repair_arguments_rewrites_anything_but_a_json_object(
|
||||
arguments: str | None, expected: str
|
||||
) -> None:
|
||||
assert repair_arguments(arguments) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arguments", ["{}", '{"cmd": "ls -la"}', '{"nested": {"a": [1]}}', '{"n": "NaN"}']
|
||||
)
|
||||
def test_repair_arguments_leaves_json_objects_alone(arguments: str) -> None:
|
||||
assert repair_arguments(arguments) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arguments", ['{"n": NaN}', '{"n": Infinity}', '{"n": -Infinity}'])
|
||||
def test_repair_arguments_wraps_non_standard_json_constants(arguments: str) -> None:
|
||||
repaired = repair_arguments(arguments)
|
||||
|
||||
assert repaired is not None
|
||||
assert _loads_strict(repaired) == {MALFORMED_ARGUMENTS_KEY: arguments}
|
||||
|
||||
|
||||
def test_repair_arguments_wraps_over_nested_json() -> None:
|
||||
repaired = repair_arguments(OVER_NESTED)
|
||||
|
||||
assert repaired is not None
|
||||
assert _loads_strict(repaired) == {MALFORMED_ARGUMENTS_KEY: OVER_NESTED}
|
||||
|
||||
|
||||
def test_history_repair_rewrites_only_malformed_calls() -> None:
|
||||
items = [
|
||||
{"role": "user", "content": "go"},
|
||||
{"type": "function_call", "call_id": "a", "name": "x", "arguments": '{"cmd": "ls'},
|
||||
{"type": "function_call_output", "call_id": "a", "output": "invalid JSON"},
|
||||
{"type": "function_call", "call_id": "b", "name": "y", "arguments": '{"ok": true}'},
|
||||
ResponseFunctionToolCall(call_id="c", name="z", arguments="", type="function_call"),
|
||||
]
|
||||
|
||||
rebuilt, changed = repair_history_arguments(items)
|
||||
|
||||
assert changed
|
||||
assert rebuilt[0] is items[0]
|
||||
assert json.loads(rebuilt[1]["arguments"]) == {MALFORMED_ARGUMENTS_KEY: '{"cmd": "ls'}
|
||||
assert rebuilt[1]["call_id"] == "a"
|
||||
assert rebuilt[2] is items[2]
|
||||
assert rebuilt[3] is items[3]
|
||||
assert isinstance(rebuilt[4], ResponseFunctionToolCall)
|
||||
assert rebuilt[4].arguments == "{}"
|
||||
assert isinstance(items[1], dict)
|
||||
assert items[1]["arguments"] == '{"cmd": "ls'
|
||||
|
||||
|
||||
def test_repair_input_returns_same_object_when_nothing_changes() -> None:
|
||||
items = [{"type": "function_call", "call_id": "a", "name": "x", "arguments": "{}"}]
|
||||
|
||||
assert repair_input(items) is items
|
||||
assert repair_input("plain prompt") == "plain prompt"
|
||||
|
||||
|
||||
def test_repair_input_wraps_over_nested_function_call_arguments() -> None:
|
||||
items = [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "a",
|
||||
"name": "x",
|
||||
"arguments": OVER_NESTED,
|
||||
}
|
||||
]
|
||||
|
||||
repaired = repair_input(items)
|
||||
|
||||
assert isinstance(repaired, list)
|
||||
assert _loads_strict(repaired[0]["arguments"]) == {MALFORMED_ARGUMENTS_KEY: OVER_NESTED}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
['{"cmd": "ls -la', "[1, 2]", "null", "not json", '{"n": NaN}', OVER_NESTED],
|
||||
)
|
||||
def test_describe_malformed_arguments_tells_the_model_to_reissue(arguments: str) -> None:
|
||||
message = describe_malformed_arguments("exec_command", arguments)
|
||||
|
||||
assert message is not None
|
||||
assert message.startswith("exec_command: the tool call was not executed")
|
||||
assert "Re-issue the call" in message
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arguments", ["", " ", "{}", '{"cmd": "ls"}'])
|
||||
def test_describe_malformed_arguments_accepts_objects_and_empty_input(arguments: str) -> None:
|
||||
assert describe_malformed_arguments("exec_command", arguments) is None
|
||||
@@ -9,10 +9,6 @@ resolution-markers = [
|
||||
"python_full_version < '3.14' and sys_platform != 'win32'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.2"
|
||||
@@ -24,7 +20,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.14.5"
|
||||
version = "3.14.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -36,132 +32,90 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/4c/bdccd81e9ee225b69c60e7766c9a5b05364f118f4d383713b89a682d772d/aiohttp-3.14.5.tar.gz", hash = "sha256:5558a7f5a05af9ecf744af91e5baefc436f93c9333e656c27ec253f9a6bbe178", size = 8078052, upload-time = "2026-10-11T01:05:12.408Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/94/6ba86efddcb616c811b40e6a0dfdd862738f647e4e3860a961075d5e9ed8/aiohttp-3.14.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450", size = 817499, upload-time = "2026-10-11T01:00:18.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/e1/7bca6d84dabd228aa8eb4b7f9feac2586aaa9be5505d7d65bf287a615c43/aiohttp-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8", size = 549467, upload-time = "2026-10-11T01:00:20.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/11b89f45ca486252dd67dd5f3231fec04bf5518da39a95cb3997619f17fb/aiohttp-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2", size = 548067, upload-time = "2026-10-11T01:00:22.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ff/c6615806c14aab34f82b9424ccde8ce6e417315fd57ce1c5b4d4747888e1/aiohttp-3.14.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764", size = 1896245, upload-time = "2026-10-11T01:00:24.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b2/25a8c971ae6a92c8394d77e42422d7f38989e05cf41a5ceb92d73d67ab7e/aiohttp-3.14.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820", size = 1872180, upload-time = "2026-10-11T01:00:26.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/d5/99f93ea36cc5205e47c1e5a803e087f2ad21b5430b5db2e942cb6e988a37/aiohttp-3.14.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835", size = 1937261, upload-time = "2026-10-11T01:00:28.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/a6/9ac9c9e6695040bd73d2584a1b59a9388f6433c76d5294a7bf591e21ffe5/aiohttp-3.14.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02", size = 2064994, upload-time = "2026-10-11T01:00:30.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e4/aa172eb534b7f02f1f8ff1c3213347eaf3cf218db91a727c6863c22f1035/aiohttp-3.14.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584", size = 1919630, upload-time = "2026-10-11T01:00:32.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/7d/4eedafc5bababa8932636c141e346806966eade12c0b7e5946d43bf8218b/aiohttp-3.14.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d", size = 1710016, upload-time = "2026-10-11T01:00:34.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/94/eee018537ba19da0ceb2ac79cab83faed4ac49568e08376e2799043f2538/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30", size = 1858112, upload-time = "2026-10-11T01:00:36.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/76/354653a306547238f3427283905972d796ba7c292ba9977abec9f2b6f260/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073", size = 1896371, upload-time = "2026-10-11T01:00:38.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ec/63e8c7136b570e356345ad3174e3820fdc973ea10712cb6c649bf875755a/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794", size = 1905836, upload-time = "2026-10-11T01:00:40.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d8/11365bda144b127928cd42533d0eff78a55613c9e28f81941bd6630ea887/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd", size = 1696534, upload-time = "2026-10-11T01:00:42.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3d/82df0461b18e00b2998f205c03e0d3010222478c43640aceb8e03dcd7e8f/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d", size = 1933970, upload-time = "2026-10-11T01:00:44.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/ad/6ddfe0aacd931c17b53533336d97e9d11a98b96d6ae815a9da0b19f82ccf/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748", size = 1899501, upload-time = "2026-10-11T01:00:46.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8e/189bdd9ae4793059bb09f6dc880f211a6c6c7859cebcbf33912dbfab7dd7/aiohttp-3.14.5-cp312-cp312-win32.whl", hash = "sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1", size = 480370, upload-time = "2026-10-11T01:00:48.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/ce/1f08114679d49655b30a6e0a29858375c94b82c1de1a0bd0a20c2fee8b02/aiohttp-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111", size = 506141, upload-time = "2026-10-11T01:00:50.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d4/c7b4f60b16a1b7e43249fa9031ae05e7e8c341ba4b7d1866f914dafeaa0e/aiohttp-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac", size = 488108, upload-time = "2026-10-11T01:00:52.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/e1/2841e020ebb7aefae5513586193e011e06313d9a6bdbd296622afbbce204/aiohttp-3.14.5-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:9ad7e6aa38c20da1be697874349c4c273c8a03b7887169665081706398d0439a", size = 538932, upload-time = "2026-10-11T01:00:54.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a6/7fb8ea8fe96bcc7b7c7a36d10f021d99d01a8dc8a4b3f0ddacecfad9a80e/aiohttp-3.14.5-cp313-cp313-android_24_x86_64.whl", hash = "sha256:f59c7673465908cbe506117176156c127f29f917677afceada34957179221d91", size = 553329, upload-time = "2026-10-11T01:00:56.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/64/d056e3c27647dc25af1a592cf356245382ea7c808171b9dac7677afedfc8/aiohttp-3.14.5-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1b5416552740edf07234cc9437d0706f2acb67b93c198670b1a68e1b2b587dec", size = 521367, upload-time = "2026-10-11T01:00:58.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/e4/95226147e11d4db916fd1d495dcf85af8e3816e38333e42718241196e848/aiohttp-3.14.5-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:43351bdb5e4c3cb7d1772368e988534e869a74db7778079a83782c11c69535c7", size = 528322, upload-time = "2026-10-11T01:01:00.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/cd/1d3c9192cafdb51cad62b2d3ded96cff9cc8af51893210aadd448a325389/aiohttp-3.14.5-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c8c4478bef6d57fcfda15dae461ea3c9f06aa7b257c58df3f2300174ccbb185a", size = 539874, upload-time = "2026-10-11T01:01:02.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/0c/dfa33aecc7d4d1dc75e05248f5eac5a0edf4d09e7b44d93ab62529b0c1db/aiohttp-3.14.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c2c30484dd1417ef98b51021ffa2cc0d7f3c78918adaaaab7e70817335ab3e02", size = 813836, upload-time = "2026-10-11T01:01:04.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/17/4a63738052d20567d55529d6daa1b9480d906fd52930fbcf6d3fbed618f0/aiohttp-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dab9ac5a67c8d1f070c00fa8fccb7cbd1b8dcc1a8d6b42f37540df9b3d4cc603", size = 548070, upload-time = "2026-10-11T01:01:06.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/e5/b57e58695a757fd4c02497c033fced96a69c631b13866c43d336530c9670/aiohttp-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e1cc2bfaee8c214f06080a7c7d5772419b8a1108e8e5349236189811823fb02a", size = 545786, upload-time = "2026-10-11T01:01:07.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/68/8c2c67a3aedf46e00f3c42f04fbc6983de80d4ed5786151e33681ba45883/aiohttp-3.14.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74efb69332b85675b1eabd760a8cfc2e2cf42c60607c66f88014c1bdfb40942d", size = 1886492, upload-time = "2026-10-11T01:01:09.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/4b/74aab5e8d28c62e8f795b4fe8f38cf5586fd264a9a27bd2141ef6490333d/aiohttp-3.14.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42b5e616946dbaf505e2bff18c9af2cd4ef9e7ef300ee58a6e951a5b7cf147ae", size = 1870057, upload-time = "2026-10-11T01:01:12.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/f7/eafc3b1988302b1815d9fd4a21071be5c360d616c0a430d02fd92dc97688/aiohttp-3.14.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f9033b43f511f27547c557dcaba0177649e10a3725336ccd2cce0fdc1dc4850d", size = 1925119, upload-time = "2026-10-11T01:01:14.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/04/78d8f294f74dd570f3898ff20402349fce524176045df98ba727d6846a68/aiohttp-3.14.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15a310d3c71398e3d7bfc93a1a73fbe664315cd9e9016b8efc1cff85eeab7155", size = 2053137, upload-time = "2026-10-11T01:01:16.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/51/395d225ef36f5a50d8e548dcd3141bfdbcd31fb6eed859022c573d2c4d66/aiohttp-3.14.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ffa3a523a36d8628f98c06492ae16a31a23d14c0b4ec721757b477319f656d6", size = 1911038, upload-time = "2026-10-11T01:01:18.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/a4/2aec1aa06d82e8a244843b5dae31d78061e5e76744270a86dd0ee051c889/aiohttp-3.14.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fb6a6e919bfb703227bc1ce6579281b84b1a2ba57deb9794dfdbec7dcd1e40c", size = 1702205, upload-time = "2026-10-11T01:01:20.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/27/6051bfde7b6f418f70edd60d655fa426abb3355fa0981764739d87ecf160/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43e1b7994a8b038125f722bff07492ef501110722c2727c408995d9fb864c421", size = 1848588, upload-time = "2026-10-11T01:01:22.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/44/55efc06fc26c4e6e1c095f231b4c222bf2d86d64a8eebbc24b2bb5958ea8/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5f3e96071686755d9cd3600c3880183eb94b012178e92746d68101800f0ed8a3", size = 1887540, upload-time = "2026-10-11T01:01:25.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/dc/1502bfdc2a65760d386ac6a00090b0addaa8a3c9c60b3f8127fad3a9afb2/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cd88b01f3d37b7a2a34f91d98f14720206f1ea3d540843fab2d649dd5fb91fec", size = 1895030, upload-time = "2026-10-11T01:01:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/7e/44174bb6288264418c9eec07a5e35180969c0d5a796c7af544db3cb8a33a/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f2ed8b64dc0c651c0f5a9c926777719770021251b8f336d97c4b80b660836ce1", size = 1693244, upload-time = "2026-10-11T01:01:29.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/dd/b507d64e50db23888582fff08eda13998b12f9dea70c072edaac19218380/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a9918e58faf62ba2c7147927d06057aec78f42475aff5048047ec47e7265a600", size = 1926308, upload-time = "2026-10-11T01:01:31.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4b/5b51b4f63e3f2793151f4aea49c48fe1e00baeb7cec9c7a206de499f8de0/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:42f320d4a5b00b9af0bddcfec5407dc6f2d9816f006b2f79ebbaa31f16895df3", size = 1888406, upload-time = "2026-10-11T01:01:33.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/13/d5e818a5eaba9f822016727299f41c0e1075799f82a6433ec508a93ab867/aiohttp-3.14.5-cp313-cp313-win32.whl", hash = "sha256:3ae800a20947e2c2e53088047d021e6bf7d51560cc49f6a0737a1f79d2e3a13c", size = 479970, upload-time = "2026-10-11T01:01:35.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/d0/8eca2c65aa467320990d78fb2005f38ed3588944280c39ff9deb6423fef1/aiohttp-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:d05e94cdfe0d15d0206f970722d2554780ce562787b21b218b275447f8751319", size = 505985, upload-time = "2026-10-11T01:01:37.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/f8/4cdd65305d2fca14b886bea9ed2abb1fe726287872524e56e3d26692b47d/aiohttp-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:f001b571ead90ca1770f1e616db255351a1703317f20374c361ef22f12c06d09", size = 487629, upload-time = "2026-10-11T01:01:39.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/be/3184a1d34a8be665569eadb7e9e764b4629e4f3413e241cb2e4d6fecf3b3/aiohttp-3.14.5-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5", size = 540673, upload-time = "2026-10-11T01:01:41.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/2a/d35f3ba4cf157b072e3b674bf9983047ca5ea5173c995d32d877e1191d36/aiohttp-3.14.5-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343", size = 554087, upload-time = "2026-10-11T01:01:43.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d2/61a33880ca4eaca95a9c60ca3f6beed15555af1028652dfaac601627787b/aiohttp-3.14.5-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738", size = 522613, upload-time = "2026-10-11T01:01:46.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/de579b299d2225dc2c6fd99d579d91f16c02a913fb5af9a3cf2fe9bd88ba/aiohttp-3.14.5-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c", size = 529682, upload-time = "2026-10-11T01:01:48.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/4a/ddb923564e15e053b6e060b0036e1694dcadcb13aa476c5a87dcad20e336/aiohttp-3.14.5-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3", size = 540120, upload-time = "2026-10-11T01:01:50.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/36/a640fbecaa53727a5900b892bdbe17b5f3e8cc88903e22864fe41b654def/aiohttp-3.14.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9", size = 818795, upload-time = "2026-10-11T01:01:52.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/b6/d52ca608859e271b5fa7944074802dc45f60e52c318a4ddc34edbf73586e/aiohttp-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db", size = 549667, upload-time = "2026-10-11T01:01:54.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b5/05b8ac39a76ff4bca89f42a4c2471560c71f894ff6e4bc16158c951874e3/aiohttp-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba", size = 548922, upload-time = "2026-10-11T01:01:56.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/b0/5aa186d56ce2334dabe29b70bd99dc8ae926ee44184c0de64a53d415a4f3/aiohttp-3.14.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382", size = 1882422, upload-time = "2026-10-11T01:01:59.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/f7/7d5c91bb9620db300c8ddb05337a9014301acb626223faff3abcb8ea47d7/aiohttp-3.14.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4", size = 1843621, upload-time = "2026-10-11T01:02:01.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/0a/b208953b96d8f24b75f6da704f508e6c5cf3022f52b60c61933df082e89c/aiohttp-3.14.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1", size = 1927915, upload-time = "2026-10-11T01:02:03.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/79/90ebcccb55e2d1e11a1fed581d83bb966e38fb35fb4b2577fdc980f8707a/aiohttp-3.14.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677", size = 2036339, upload-time = "2026-10-11T01:02:06.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/66/55a8904b3a129fafdf94f9cc0a2e4ca09a3c914650be52355db7ad0bbdb6/aiohttp-3.14.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c", size = 1888041, upload-time = "2026-10-11T01:02:08.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b8/96b25da7329a52e42c812b1e8b076386039ec4fc312afa043d173d8147fc/aiohttp-3.14.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622", size = 1703849, upload-time = "2026-10-11T01:02:10.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/43/fbf976e3ae4c038d6f5c84945ab2150298c2d71201674d4e53d158063e75/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3", size = 1849703, upload-time = "2026-10-11T01:02:13.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/7d/218e912f4c1d89bde7ac551409be57ad2f6121638e56942d395a7cb1fa58/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0", size = 1852695, upload-time = "2026-10-11T01:02:15.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/42/252a1b9287e3b6e393a1c3bd1776f36af30f5f25f071bbf2b0cb7eba9116/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480", size = 1893493, upload-time = "2026-10-11T01:02:17.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/97/71cae83d5100556fad1521684f7cd1e3e578432644f850245ed3bd969310/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f", size = 1693468, upload-time = "2026-10-11T01:02:20.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/69/73d88e97a8b5f0ca7a946d0011c0de99fb188b1687c7948ecd0553dc5bf0/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115", size = 1915398, upload-time = "2026-10-11T01:02:23.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f0/881644bcb15d4b258daea9b720a0af9dc4330496cc8d6ade9090cdd0cffc/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a", size = 1871470, upload-time = "2026-10-11T01:02:25.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/de/19d9ebbcce5aedaa3242d8a99ff8816a60bdb7629fb0084bf4a45bfd1f62/aiohttp-3.14.5-cp314-cp314-win32.whl", hash = "sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2", size = 485517, upload-time = "2026-10-11T01:02:27.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/74/8cdaf0e58c2588371670d5a9a8215bbb971d36940b6e5d051967dd05c07d/aiohttp-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67", size = 511755, upload-time = "2026-10-11T01:02:29.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/6b/e0100e25502430a531c7cf1482a378d0b65bf728ab60c01ee270e56bc469/aiohttp-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7", size = 495151, upload-time = "2026-10-11T01:02:32.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/c2/ca2ead7b655688c53c03aeeb6e96e6851c9ff08d6be7b13802f53a6ae8fd/aiohttp-3.14.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed", size = 855396, upload-time = "2026-10-11T01:02:34.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/70/22206fea409255a240c926ce11de48de354ae2bb90ca44f709c05497585d/aiohttp-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781", size = 567734, upload-time = "2026-10-11T01:02:36.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e5/79a36c118308b56f8667d67e05d2fb6dc638ab45985704cdb199631bedaa/aiohttp-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550", size = 567379, upload-time = "2026-10-11T01:02:38.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/a3/2ebec7dece3b1f02c30d2e484647f6f7b13952b1bb40a4cb285b849e8432/aiohttp-3.14.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d", size = 2050686, upload-time = "2026-10-11T01:02:41.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/d2/7e4d093db2f4450482652e7ef19a9e19919028f5135aa52bc4078c3beb80/aiohttp-3.14.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863", size = 1915077, upload-time = "2026-10-11T01:02:43.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/88/bd40d09958442a0a1df67da81de496361d6e2afc04f9db2950d835da750b/aiohttp-3.14.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99", size = 2027204, upload-time = "2026-10-11T01:02:46.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/eb/3a601c1f8d3103c1a60ea981f20924855da9f575d2007fc38f8898b792fb/aiohttp-3.14.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5", size = 2149520, upload-time = "2026-10-11T01:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/d0/4e41bfe1b1ce1cb6f6d2e59fa7a88ef5cf92c402b2d07e9018778ba9edbc/aiohttp-3.14.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f", size = 2002039, upload-time = "2026-10-11T01:02:51.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/72067019545c502b881b031153c437752ecef48d7d213bc0218ccebb4bfb/aiohttp-3.14.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0", size = 1786164, upload-time = "2026-10-11T01:02:53.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/fe/7741efd6119bfb7a00827fe6f7b84b4409de58d888ae21adc5a9a6824992/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d", size = 1970689, upload-time = "2026-10-11T01:02:55.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e7/342a13bf67f34d269bf2f7e870ecd72b99c832cc6f0a271a9210c0ebfb84/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658", size = 1951967, upload-time = "2026-10-11T01:02:58.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/d4/fdb3b27340617e5e64df18a70fa89097778652ea5c7c7e2f79def76999c3/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84", size = 1968511, upload-time = "2026-10-11T01:03:00.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/b2/e8f88298de78d1a951f36f9f966d38ec6ed1d4721303ba02064a545e8aa6/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441", size = 1778497, upload-time = "2026-10-11T01:03:03.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/68/9ccdb93d664345c546be7f34480b921774d8c0d98f47e70d7e03b115d475/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210", size = 1999676, upload-time = "2026-10-11T01:03:05.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/4a/a33cfa6dcb00e94194ae4fe710432ca4ed111e016356b4ba4d2f4c3a124c/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d", size = 1963437, upload-time = "2026-10-11T01:03:08.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/20/eacbecfea3b5c3dcbfc9b023e3a5460f43e5dda16d06a2877ebe7184c3f3/aiohttp-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab", size = 503973, upload-time = "2026-10-11T01:03:10.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/78/18eec294f6c8c5dc845dcf6d730a0147d8d0f17e86138a7bdb85e43a30fa/aiohttp-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3", size = 531190, upload-time = "2026-10-11T01:03:12.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/39/e53f8169acc85271ebd12b5b32ad7f1541b35639ccbe0f49034c64785d10/aiohttp-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e", size = 510360, upload-time = "2026-10-11T01:03:15.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/1c/06d89f58b2db3ee92dd377217659d587e06f973e57bf0a97a0d8a4586c0c/aiohttp-3.14.5-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da", size = 540674, upload-time = "2026-10-11T01:03:17.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/39/5e822e038f496f0540ada91e27099d48f9e7f919b6c6deb4cf6db36cc706/aiohttp-3.14.5-cp315-cp315-android_24_x86_64.whl", hash = "sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29", size = 554183, upload-time = "2026-10-11T01:03:19.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/ff/0cf2619d902b5b160762422a7e5e02091295766a7fe4fcf5b9a655e386c1/aiohttp-3.14.5-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833", size = 523160, upload-time = "2026-10-11T01:03:22.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/99/3553abfc53a40849dbaacc3f54c730ec58410809ffa2d05885ae56108f2d/aiohttp-3.14.5-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf", size = 530311, upload-time = "2026-10-11T01:03:24.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/a4/5d25f73754bc1e8f983ba704e86d641aea290c967f195f2aeebdaed2bd84/aiohttp-3.14.5-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789", size = 540274, upload-time = "2026-10-11T01:03:26.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a4/07eda5db2e3ee017d9590f36c12a94ec6f1f50516e8df78672373dfc7185/aiohttp-3.14.5-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f", size = 819239, upload-time = "2026-10-11T01:03:29.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/aa/a8723dd987a696dd48d4cf2f0088e589ce77caebff0b96f2a78c20424380/aiohttp-3.14.5-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c", size = 549832, upload-time = "2026-10-11T01:03:32.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/5c/969a1b72692055fd2a419590c41847ec9144fefb97b75ff1cde5b6372891/aiohttp-3.14.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14", size = 549234, upload-time = "2026-10-11T01:03:34.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/05/8e3e07fd8a0d33d06955ff4e54a1cb92f4bce347ff55e441dc3e25a7b5e5/aiohttp-3.14.5-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11", size = 1891314, upload-time = "2026-10-11T01:03:36.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/b3/05a79ce2e25f024e93de30c94f39dc6aa6e2bc1e9c531a5c4b18dc61b7a4/aiohttp-3.14.5-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946", size = 1884372, upload-time = "2026-10-11T01:03:39.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/52/0fd8af0717db109eea258191b326b5cb5847fb88928bfa6c862fd78b9ad3/aiohttp-3.14.5-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1", size = 1930957, upload-time = "2026-10-11T01:03:42.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b3/fa78733da88812bf9fb193913fb0ce1548f8b6912047633fdf88a758ff8c/aiohttp-3.14.5-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8", size = 2038886, upload-time = "2026-10-11T01:03:44.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/f5/2fcc5e30053a938286f17d0edf3f0850b8061b984256fa7c26850b9c8809/aiohttp-3.14.5-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c", size = 1894470, upload-time = "2026-10-11T01:03:47.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/2a/f87feb42abe8e6a7c03814dbcb711540849a1e90aa392055f3183c643610/aiohttp-3.14.5-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016", size = 1716273, upload-time = "2026-10-11T01:03:50.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b2/adf1f960dd977722ed1347d33da512a1624807114f57a3b91f5cc828e081/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd", size = 1857513, upload-time = "2026-10-11T01:03:52.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/fd/ef8d910e641de4160026a513ace5888b7f92826bbc3fd90ced05d55a828e/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f", size = 1887704, upload-time = "2026-10-11T01:03:55.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/db/6c9142f941cba8d35be8e1fae6ea2bd390e754fc14076b8147aee1a4592f/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412", size = 1895684, upload-time = "2026-10-11T01:03:58.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/7c/6a6bd9a72e576d376c668333b00c51c6147aba4fb863ed6c94996d497eca/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4", size = 1706299, upload-time = "2026-10-11T01:04:00.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ec/d2cc494242f8c4d3d1cd70baf591742818a74386dc3b84af3195c5c4fced/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee", size = 1916645, upload-time = "2026-10-11T01:04:03.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/6e/e852c53647e815db09a1b6b5ab634f54bd736412397e11940feef4d2c89a/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d", size = 1875408, upload-time = "2026-10-11T01:04:06.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f6/72ab6ef20c332399be593bac543d2c24a6d241e39d3540ce9f95a63e4bd2/aiohttp-3.14.5-cp315-cp315-win32.whl", hash = "sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534", size = 485590, upload-time = "2026-10-11T01:04:09.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/eb/e9de75b8c6d2170c42c08ff303abf857ea8a6d9d9b6e99b5aba40f15e962/aiohttp-3.14.5-cp315-cp315-win_amd64.whl", hash = "sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d", size = 511740, upload-time = "2026-10-11T01:04:12.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/25/455f3c2785eb0d50748cffd0abd07500815f419a9495b14610b7622d8d2d/aiohttp-3.14.5-cp315-cp315-win_arm64.whl", hash = "sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a", size = 495171, upload-time = "2026-10-11T01:04:14.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/6c/497f0a98782eebfcf0f02a7fbdef5428148bd426027140cbad494cf842b5/aiohttp-3.14.5-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4", size = 855638, upload-time = "2026-10-11T01:04:17.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/c5/55c0cef2572af9b1ee81608f7c0a1bf74e6c9151a73b04915d933f192335/aiohttp-3.14.5-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51", size = 567455, upload-time = "2026-10-11T01:04:19.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/47/e1a0e39f4a2b881f6071225afaf94a6547001b5aefc1566734d73c685934/aiohttp-3.14.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905", size = 567836, upload-time = "2026-10-11T01:04:22.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1d/817d85836f52b687160064a326e62e037dc42f1ad5b6b5188a70e5c134b5/aiohttp-3.14.5-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59", size = 2051409, upload-time = "2026-10-11T01:04:24.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/25/e8ea6fc212a9aabce982917346ce8ecab0929c74b60232a056dd11c99da0/aiohttp-3.14.5-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2", size = 1936493, upload-time = "2026-10-11T01:04:27.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/33/de0517f71f1a19feea4aff78a2ec4ec2d98634129ec30ead78fce2f8f81b/aiohttp-3.14.5-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef", size = 2021405, upload-time = "2026-10-11T01:04:30.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/11/ddaf2e7930543e9f0cad3a1c54e1af0c1bd2fae3973d568c4263d3b010e9/aiohttp-3.14.5-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e", size = 2142017, upload-time = "2026-10-11T01:04:33.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/7b/58784353c06de8adc20f426daad3d85dd86fd55331c713a3f4b638c94573/aiohttp-3.14.5-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84", size = 2002482, upload-time = "2026-10-11T01:04:35.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f0/417d9535caa9e165ffe6a53e2347a78107e7c87dcb0e10aca315c3af0344/aiohttp-3.14.5-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0", size = 1791774, upload-time = "2026-10-11T01:04:38.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/01/25e49c2e8a01b9f0e19ca0a8448ad50aa2bdf96c8cd41e92bd45af044784/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3", size = 1970358, upload-time = "2026-10-11T01:04:41.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/fc/c132fd3465b6c7e4ce0193154f602c3e6c46b680e4da7eb3bd0d8a40d1c7/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc", size = 1969192, upload-time = "2026-10-11T01:04:44.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/4e/d58b45e7dba4eb607eca11fc0a4aa77ae0f39c11afa804635a61ce18cff4/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57", size = 1963618, upload-time = "2026-10-11T01:04:47.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d9/f6ac50946efb3490428ef52b56e62c1c6b7f9c6ff3ec6083535526c83e60/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9", size = 1784817, upload-time = "2026-10-11T01:04:50.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/2f/f255eb63da788cd8a452fe350869c03266ccad7d884925f6963c87808f24/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2", size = 1995956, upload-time = "2026-10-11T01:04:53.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9b/241aa3393eaafda0034470add1625f82a4c252901108723b283a9b0b32ca/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79", size = 1967794, upload-time = "2026-10-11T01:04:55.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/7f/a68e689288c9e4bfcf8d0979f2b12b774bf01861985420df6150eba5448e/aiohttp-3.14.5-cp315-cp315t-win32.whl", hash = "sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e", size = 503876, upload-time = "2026-10-11T01:04:58.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/78/49b0299da6d54de19fc6fdc6889d50233ac47d192a461624f4fc010fde83/aiohttp-3.14.5-cp315-cp315t-win_amd64.whl", hash = "sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb", size = 530957, upload-time = "2026-10-11T01:05:02.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/d4/b0afc936aeb6d2f93157e3408b069ec5d7934429ae7d023de5e0953b1887/aiohttp-3.14.5-cp315-cp315t-win_arm64.whl", hash = "sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b", size = 510408, upload-time = "2026-10-11T01:05:05.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/30/173960c42b05a6c59f7558e4b12a4b0d9ba376cf6aa9bde7f9e08a30ca8d/aiohttp-3.14.5-py3-none-any.whl", hash = "sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b", size = 279517, upload-time = "2026-10-11T01:05:08.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -793,11 +747,11 @@ websockets = [
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.13"
|
||||
version = "3.2.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/5e/aa0d4e701b50db0bab71b125dd19ddb0f98c638d008d6f7e3d8ce9cbc92e/graphql_core-3.2.13.tar.gz", hash = "sha256:bb81dd266d4ab7b591bd976f1b23639d97776cb9ac1a896b4a93c271e11ed618", size = 577471, upload-time = "2026-09-27T12:22:57.136Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b4/b52a324b0b297ddc49b8ea31a51d39fae63132f694a267b9381d94e2e0af/graphql_core-3.2.13-py3-none-any.whl", hash = "sha256:b0eb04f2c31556b2310a77c8fb53c74e8b56570f8fea594d89c6ef7827dbb497", size = 268191, upload-time = "2026-09-27T12:22:55.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2705,118 +2659,84 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.25.1"
|
||||
version = "1.24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user