mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 05:19:39 -10:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215482eb22 | ||
|
|
0601550662 | ||
|
|
851743adcf | ||
|
|
09536cf148 | ||
|
|
cb581bf93d | ||
|
|
6b7ae2ebe4 | ||
|
|
92b30bcad5 | ||
|
|
dfdbe4b2ab | ||
|
|
98e3df1425 | ||
|
|
ffa35e22d3 | ||
|
|
ef5d810047 | ||
|
|
d6f4bc5ced | ||
|
|
4ac5dc65e4 | ||
|
|
4bf353fb7b | ||
|
|
f121ab03fb | ||
|
|
2c9e9f3444 | ||
|
|
200a501f15 | ||
|
|
dc40345e26 | ||
|
|
fa3539cb75 | ||
|
|
aaf1f8c823 | ||
|
|
bac386e509 | ||
|
|
898bbb4741 | ||
|
|
5418c767a2 | ||
|
|
fff22045ae | ||
|
|
dca9482c82 | ||
|
|
f46bed6740 | ||
|
|
b8d586a39b | ||
|
|
b17c0275a9 | ||
|
|
14cc948c36 | ||
|
|
594398876f | ||
|
|
0f81f40765 | ||
|
|
a3c31236bd | ||
|
|
fde37a149f | ||
|
|
3a3855fc4f |
No files matched your search
@@ -1,46 +1,39 @@
|
||||
name: Package
|
||||
name: Binary
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
paths:
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- strix.spec
|
||||
- scripts/**
|
||||
- strix/interface/tui/**
|
||||
- .github/actions/**
|
||||
- .github/workflows/binary.yml
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- strix.spec
|
||||
- scripts/**
|
||||
- strix/interface/tui/**
|
||||
- .github/actions/**
|
||||
- .github/workflows/binary.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
wheel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version-file: strix/interface/tui/go.mod
|
||||
cache-dependency-path: strix/interface/tui/go.sum
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
- run: uv build --wheel
|
||||
- name: Wheel ships the TUI sidecar and the viewer bundle
|
||||
run: |
|
||||
uv run --frozen python - <<'PY'
|
||||
import glob, zipfile
|
||||
(wheel,) = glob.glob("dist/*.whl")
|
||||
names = zipfile.ZipFile(wheel).namelist()
|
||||
assert "strix/bin/strix-tui" in names, names
|
||||
assert "strix/interface/viewer/static/index.html" in names, names
|
||||
assert not any(name.startswith("strix/interface/viewer/frontend/") for name in names)
|
||||
assert not any(name.startswith("strix/interface/tui/cmd/") for name in names)
|
||||
PY
|
||||
- name: Install into a clean environment and run the CLI
|
||||
run: |
|
||||
uv venv --python 3.12 /tmp/strix-smoke
|
||||
uv pip install --python /tmp/strix-smoke/bin/python dist/*.whl
|
||||
/tmp/strix-smoke/bin/strix --version
|
||||
/tmp/strix-smoke/bin/strix --help >/dev/null
|
||||
- name: Vertex AI and Bedrock dependencies install with the wheel
|
||||
run: /tmp/strix-smoke/bin/python -c "import google.auth.transport.requests, boto3"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
@@ -32,6 +32,7 @@ jobs:
|
||||
wheel-platform: win_amd64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
@@ -110,6 +111,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
name: Python
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- run: uv lock --check
|
||||
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
- run: uv run --frozen ruff check .
|
||||
- run: uv run --frozen ruff format --check .
|
||||
|
||||
mypy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
- run: uv run --frozen mypy strix/
|
||||
|
||||
bandit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
- run: uv run --frozen bandit -r strix/ -c pyproject.toml
|
||||
|
||||
pytest:
|
||||
name: pytest (${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.12', '3.13', '3.14']
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- run: uv run --frozen pytest -q
|
||||
@@ -1,26 +0,0 @@
|
||||
name: TUI
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
go:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: strix/interface/tui
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version-file: strix/interface/tui/go.mod
|
||||
cache-dependency-path: strix/interface/tui/go.sum
|
||||
- run: test -z "$(gofmt -l .)"
|
||||
- run: go vet ./...
|
||||
- run: CGO_ENABLED=0 go build -trimpath -o /dev/null ./cmd/strix-tui
|
||||
- run: go test -race ./...
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Viewer
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: strix/interface/viewer/frontend
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: strix/interface/viewer/frontend/package-lock.json
|
||||
- run: npm ci --no-audit --no-fund
|
||||
- run: npm run build
|
||||
- name: Committed viewer bundle matches the build
|
||||
run: git -C "$GITHUB_WORKSPACE" diff --exit-code --stat -- strix/interface/viewer/static && test -z "$(git -C "$GITHUB_WORKSPACE" status --porcelain -- strix/interface/viewer/static)"
|
||||
+97
-28
@@ -15,38 +15,107 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
python:
|
||||
uses: ./.github/workflows/ci-python.yml
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
- run: uv lock --check
|
||||
- run: uv run --frozen ruff check .
|
||||
- run: uv run --frozen ruff format --check .
|
||||
- run: uv run --frozen mypy strix/
|
||||
- run: uv run --frozen bandit -r strix/ -c pyproject.toml
|
||||
|
||||
pytest:
|
||||
name: pytest (${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.12', '3.14']
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- run: uv run --frozen pytest -q
|
||||
|
||||
tui:
|
||||
uses: ./.github/workflows/ci-tui.yml
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: strix/interface/tui
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version-file: strix/interface/tui/go.mod
|
||||
cache-dependency-path: strix/interface/tui/go.sum
|
||||
- run: test -z "$(gofmt -l .)"
|
||||
- run: go vet ./...
|
||||
- run: CGO_ENABLED=0 go build -trimpath -o /dev/null ./cmd/strix-tui
|
||||
- run: go test -race ./...
|
||||
|
||||
viewer:
|
||||
uses: ./.github/workflows/ci-viewer.yml
|
||||
|
||||
package:
|
||||
uses: ./.github/workflows/ci-package.yml
|
||||
|
||||
workflows:
|
||||
uses: ./.github/workflows/ci-workflows.yml
|
||||
|
||||
ci-passed:
|
||||
name: ci-passed
|
||||
if: always()
|
||||
needs:
|
||||
- python
|
||||
- tui
|
||||
- viewer
|
||||
- package
|
||||
- workflows
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: strix/interface/viewer/frontend
|
||||
steps:
|
||||
- name: Every job succeeded
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: strix/interface/viewer/frontend/package-lock.json
|
||||
- run: npm ci --no-audit --no-fund
|
||||
- run: npm run build
|
||||
- name: Committed viewer bundle matches the build
|
||||
run: git -C "$GITHUB_WORKSPACE" diff --exit-code --stat -- strix/interface/viewer/static && test -z "$(git -C "$GITHUB_WORKSPACE" status --porcelain -- strix/interface/viewer/static)"
|
||||
|
||||
wheel:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version-file: strix/interface/tui/go.mod
|
||||
cache-dependency-path: strix/interface/tui/go.sum
|
||||
- uses: ./.github/actions/setup-python-env
|
||||
- run: uv build --wheel
|
||||
- name: Wheel ships the TUI sidecar and the viewer bundle
|
||||
run: |
|
||||
failing=$(jq -r 'to_entries[] | select(.value.result != "success") | "\(.key): \(.value.result)"' <<< "$NEEDS")
|
||||
if [ -n "$failing" ]; then
|
||||
echo "$failing"
|
||||
exit 1
|
||||
fi
|
||||
uv run --frozen python - <<'PY'
|
||||
import glob, zipfile
|
||||
(wheel,) = glob.glob("dist/*.whl")
|
||||
names = zipfile.ZipFile(wheel).namelist()
|
||||
assert "strix/bin/strix-tui" in names, names
|
||||
assert "strix/interface/viewer/static/index.html" in names, names
|
||||
assert not any(name.startswith("strix/interface/viewer/frontend/") for name in names)
|
||||
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
|
||||
/tmp/strix-smoke/bin/strix --version
|
||||
/tmp/strix-smoke/bin/strix --help >/dev/null
|
||||
- name: Vertex AI and Bedrock dependencies install with the wheel
|
||||
run: /tmp/strix-smoke/bin/python -c "import google.auth.transport.requests, boto3"
|
||||
@@ -1,14 +1,27 @@
|
||||
name: Workflows
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/**
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .github/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
ACTIONLINT_VERSION: 1.7.12
|
||||
ACTIONLINT_SCRIPT_REF: 914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12
|
||||
@@ -27,6 +40,7 @@ jobs:
|
||||
|
||||
zizmor:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
with:
|
||||
@@ -45,23 +45,3 @@ repos:
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [-c, pyproject.toml]
|
||||
|
||||
# Additional Python code quality checks
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
|
||||
ci:
|
||||
autofix_commit_msg: |
|
||||
[pre-commit.ci] auto fixes from pre-commit.com hooks
|
||||
|
||||
for more information, see https://pre-commit.ci
|
||||
autofix_prs: true
|
||||
autoupdate_branch: ""
|
||||
autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate"
|
||||
autoupdate_schedule: weekly
|
||||
# pre-commit.ci cannot run `language: system` hooks; mypy runs via `make check-all`.
|
||||
skip: [mypy]
|
||||
submodules: false
|
||||
@@ -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
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # any LiteLLM model id, or vercel_ai_gateway/zai/glm-5.3
|
||||
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"
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # or vercel_ai_gateway/zai/glm-5.3
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
|
||||
@@ -84,7 +84,8 @@ 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="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3" # Vercel AI Gateway
|
||||
# export STRIX_LLM="openrouter/z-ai/glm-5.3" # OpenRouter
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Run your first security assessment
|
||||
@@ -296,7 +297,8 @@ jobs:
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3" # Vercel AI Gateway
|
||||
# export STRIX_LLM="openrouter/z-ai/glm-5.3" # OpenRouter
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Optional
|
||||
@@ -352,12 +354,14 @@ Each server's tools are namespaced by `name`, for example `github_list_issues`.
|
||||
|
||||
**Recommended models for best results:**
|
||||
|
||||
- [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`
|
||||
- [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)
|
||||
- [DeepSeek V4 Pro](https://platform.deepseek.com) - `deepseek/deepseek-v4-pro`
|
||||
- [Moonshot Kimi K3](https://platform.kimi.ai) - `moonshot/kimi-k3`
|
||||
- [Moonshot Kimi K3.1 Pro](https://platform.kimi.ai) - `moonshot/kimi-k3.1-pro`
|
||||
|
||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||
|
||||
|
||||
+69
-5
@@ -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.95.9
|
||||
ARG TRUFFLEHOG_VERSION=3.99.2
|
||||
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; \
|
||||
@@ -186,20 +186,84 @@ RUN set -eux; \
|
||||
aarch64|arm64) GITLEAKS_ARCH="arm64" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
TAG="$(curl -fsSL https://api.github.com/repos/gitleaks/gitleaks/releases/latest | jq -r .tag_name)"; \
|
||||
TAG="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/gitleaks/gitleaks/releases/latest | sed 's#.*/##')"; \
|
||||
curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/${TAG}/gitleaks_${TAG#v}_linux_${GITLEAKS_ARCH}.tar.gz" -o /tmp/gitleaks.tgz; \
|
||||
tar -xzf /tmp/gitleaks.tgz -C /tmp; \
|
||||
install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks; \
|
||||
rm -f /tmp/gitleaks /tmp/gitleaks.tgz
|
||||
|
||||
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
ARG TRIVY_VERSION=0.76.1
|
||||
ARG TRIVY_SHA256_X86=c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
|
||||
ARG TRIVY_SHA256_AARCH64=e174dff2a6dba5e3ae41f8d0537a6324405bd08132497340304c6b20e0f16a1f
|
||||
RUN set -eux; \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) TRIVY_ARCH="64bit"; TRIVY_SHA256="${TRIVY_SHA256_X86}" ;; \
|
||||
aarch64|arm64) TRIVY_ARCH="ARM64"; TRIVY_SHA256="${TRIVY_SHA256_AARCH64}" ;; \
|
||||
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-${TRIVY_ARCH}.tar.gz" -o /tmp/trivy.tgz; \
|
||||
echo "${TRIVY_SHA256} /tmp/trivy.tgz" | sha256sum -c -; \
|
||||
tar -xzf /tmp/trivy.tgz -C /tmp trivy; \
|
||||
install -m 0755 /tmp/trivy /usr/local/bin/trivy; \
|
||||
rm -f /tmp/trivy /tmp/trivy.tgz; \
|
||||
trivy --version
|
||||
|
||||
RUN apt-get install -y wapiti
|
||||
|
||||
# CVE-2026-111497: out-of-bounds write in the brotli decoder, fixed in brotli
|
||||
# 1.2.1 / Debian 1.2.0-4.1. kali-rolling still ships 1.2.0-4, and node, curl and
|
||||
# python3 in this image all load the shared libbrotlidec, so every HTTP client here
|
||||
# would decode a malicious `Content-Encoding: br` response with the vulnerable
|
||||
# code. Install the fixed Debian packages until Kali picks them up.
|
||||
ARG BROTLI_DEB_VERSION=1.2.0-4.1
|
||||
RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
for pkg in libbrotli1 python3-brotli; do \
|
||||
curl -fsSL "https://deb.debian.org/debian/pool/main/b/brotli/${pkg}_${BROTLI_DEB_VERSION}_${ARCH}.deb" -o "/tmp/${pkg}.deb"; \
|
||||
done; \
|
||||
dpkg -i /tmp/libbrotli1.deb /tmp/python3-brotli.deb; \
|
||||
rm -f /tmp/libbrotli1.deb /tmp/python3-brotli.deb; \
|
||||
test "$(dpkg-query -W -f='${Version}' libbrotli1)" = "${BROTLI_DEB_VERSION}"; \
|
||||
node -e 'const z=require("zlib");z.brotliDecompressSync(z.brotliCompressSync(Buffer.from("ok")))'
|
||||
|
||||
ARG OPENGREP_VERSION=1.31.0
|
||||
ARG OPENGREP_SHA256_X86=dbbe3c2058dbe600c449a8ba22ea79a1643a6660658400090d0323a8d0d31147
|
||||
ARG OPENGREP_SHA256_AARCH64=2b713a29a00237f85408b83002fc111db5b94c7fa8d263f0a93184a960655d9d
|
||||
RUN set -eux; \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) OPENGREP_ARCH="x86"; OPENGREP_SHA256="${OPENGREP_SHA256_X86}" ;; \
|
||||
aarch64|arm64) OPENGREP_ARCH="aarch64"; OPENGREP_SHA256="${OPENGREP_SHA256_AARCH64}" ;; \
|
||||
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
curl -fsSL "https://github.com/opengrep/opengrep/releases/download/v${OPENGREP_VERSION}/opengrep_manylinux_${OPENGREP_ARCH}" -o /tmp/opengrep; \
|
||||
echo "${OPENGREP_SHA256} /tmp/opengrep" | sha256sum -c -; \
|
||||
install -m 0755 /tmp/opengrep /usr/local/bin/opengrep; \
|
||||
rm -f /tmp/opengrep; \
|
||||
opengrep --version
|
||||
|
||||
# Opengrep ships no rule registry of its own; bake the community ruleset so
|
||||
# scans run offline. Keep only rule YAML and LICENSE; drop tests, fixtures,
|
||||
# tooling, and rules marked with a proprietary license.
|
||||
ARG OPENGREP_RULES_REF=f1d2b562b414783763fd02a6ed2736eaed622efa
|
||||
RUN set -eux; \
|
||||
mkdir -p /opt/opengrep-rules; \
|
||||
curl -fsSL "https://codeload.github.com/opengrep/opengrep-rules/tar.gz/${OPENGREP_RULES_REF}" \
|
||||
| tar -xz --strip-components=1 -C /opt/opengrep-rules; \
|
||||
cd /opt/opengrep-rules; \
|
||||
rm -rf .github scripts stats; \
|
||||
find . -maxdepth 1 -type f ! -name LICENSE -delete; \
|
||||
find . -mindepth 2 -type f \( ! \( -name '*.yaml' -o -name '*.yml' \) -o -name '*.test.yaml' -o -name '*.test.yml' -o -name '.*' \) -delete; \
|
||||
grep -rlZ 'license: proprietary' . | xargs -0 -r rm -f; \
|
||||
find . -type d -empty -delete; \
|
||||
chmod -R a+rX /opt/opengrep-rules
|
||||
|
||||
# Strix-authored rules that close gaps in the community ruleset.
|
||||
COPY containers/opengrep-rules/ /opt/opengrep-rules/strix/
|
||||
RUN chmod -R a+rX /opt/opengrep-rules/strix
|
||||
|
||||
USER pentester
|
||||
|
||||
RUN pipx install semgrep && \
|
||||
pipx install bandit
|
||||
RUN pipx install bandit
|
||||
|
||||
RUN npm install -g jshint
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
rules:
|
||||
- id: github-owned-action-not-pinned-to-commit-sha
|
||||
patterns:
|
||||
- pattern-inside: "{steps: ...}"
|
||||
- pattern: |
|
||||
uses: "$USES"
|
||||
- metavariable-regex:
|
||||
metavariable: $USES
|
||||
regex: '^(actions|github)/'
|
||||
- metavariable-pattern:
|
||||
metavariable: $USES
|
||||
language: generic
|
||||
patterns:
|
||||
- pattern-not-regex: "@[0-9a-f]{40}$"
|
||||
message: >-
|
||||
GitHub-owned action `$USES` is referenced by a mutable tag or branch
|
||||
instead of a full-length commit SHA. Tags can be moved, so a compromised
|
||||
or force-pushed release changes what this workflow runs. Pin to the
|
||||
commit SHA of the release you reviewed and let a bot keep it current.
|
||||
languages:
|
||||
- yaml
|
||||
severity: WARNING
|
||||
metadata:
|
||||
cwe:
|
||||
- "CWE-1357: Reliance on Insufficiently Trustworthy Component"
|
||||
- "CWE-353: Missing Support for Integrity Check"
|
||||
owasp: A06:2021 - Vulnerable and Outdated Components
|
||||
references:
|
||||
- https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions
|
||||
category: security
|
||||
technology:
|
||||
- github-actions
|
||||
subcategory:
|
||||
- vuln
|
||||
likelihood: LOW
|
||||
impact: LOW
|
||||
confidence: HIGH
|
||||
@@ -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-5.4`).
|
||||
Model name in LiteLLM format (e.g., `openrouter/z-ai/glm-5.3`, `openai/gpt-6.1-sol`).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_API_KEY" type="string">
|
||||
@@ -19,10 +19,29 @@ Configure Strix using environment variables or a config file.
|
||||
Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_LLM_INPUT_COST_PER_MTOK" type="number">
|
||||
Price per 1M input tokens in USD for `STRIX_LLM`. Use it for gateway or self-hosted model ids that LiteLLM can't price.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_LLM_OUTPUT_COST_PER_MTOK" type="number">
|
||||
Price per 1M output tokens in USD for `STRIX_LLM`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_LLM_CACHED_INPUT_COST_PER_MTOK" type="number">
|
||||
Price per 1M cached input tokens in USD for `STRIX_LLM`. If unset, cached tokens use the input price.
|
||||
</ParamField>
|
||||
|
||||
<Note>
|
||||
Setting any of these prices replaces the provider-reported and LiteLLM cost for `STRIX_LLM` in reports and `--max-budget`. An unset input or output price counts as `0`, so set both. A separate `STRIX_DEDUPE_MODEL` keeps its LiteLLM price.
|
||||
</Note>
|
||||
|
||||
<ParamField path="STRIX_API_TYPE" type="string">
|
||||
Select the OpenAI API path for the model: `responses` or `chat_completions`.
|
||||
By default, a custom `LLM_API_BASE` uses chat completions. Set this variable
|
||||
when your gateway requires the other API. Also accepts `STRIX_FORCE_API`.
|
||||
By default the model decides: Responses when LiteLLM's model catalog lists
|
||||
`/v1/responses` for it (e.g. `gpt-5`), chat completions otherwise. A custom
|
||||
`LLM_API_BASE` does not change this, so set `STRIX_API_TYPE=chat_completions`
|
||||
when your gateway only serves chat completions. Applies to bare and `openai/`
|
||||
model names. Also accepts `STRIX_FORCE_API`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_EXTRA_HEADERS" type="string">
|
||||
@@ -167,7 +186,7 @@ When remote vars are set, Strix dual-writes telemetry to both local JSONL and th
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:1.3.0" type="string">
|
||||
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:1.4.0" type="string">
|
||||
Docker image to use for the sandbox container.
|
||||
</ParamField>
|
||||
|
||||
|
||||
+1
-1
@@ -35,8 +35,8 @@
|
||||
"llm-providers/overview",
|
||||
"llm-providers/openai",
|
||||
"llm-providers/anthropic",
|
||||
"llm-providers/openrouter",
|
||||
"llm-providers/vercel-ai-gateway",
|
||||
"llm-providers/openrouter",
|
||||
"llm-providers/vertex",
|
||||
"llm-providers/bedrock",
|
||||
"llm-providers/azure",
|
||||
|
||||
+15
-1
@@ -73,7 +73,20 @@ Strix uses a graph of specialized agents for comprehensive security testing:
|
||||
|
||||
## Quick Example
|
||||
|
||||
```bash
|
||||
<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
|
||||
# Install
|
||||
curl -sSL https://strix.ai/install | bash
|
||||
|
||||
@@ -84,6 +97,7 @@ 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-4-6"
|
||||
export STRIX_LLM="anthropic/claude-sonnet-5-5"
|
||||
export LLM_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
@@ -14,8 +14,12 @@ export LLM_API_KEY="sk-ant-..."
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed |
|
||||
| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis |
|
||||
| `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>
|
||||
|
||||
## Get API Key
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: "Configure Strix with OpenAI models via Azure"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="azure/your-gpt5-deployment"
|
||||
export STRIX_LLM="azure/your-gpt6-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-5.4-deployment"
|
||||
export STRIX_LLM="azure/gpt-6.1-sol-deployment"
|
||||
export AZURE_API_KEY="abc123..."
|
||||
export AZURE_API_BASE="https://mycompany.openai.azure.com"
|
||||
export AZURE_API_VERSION="2025-11-01-preview"
|
||||
@@ -33,5 +33,9 @@ export AZURE_API_VERSION="2025-11-01-preview"
|
||||
## Prerequisites
|
||||
|
||||
1. Create an Azure OpenAI resource
|
||||
2. Deploy a model (e.g., GPT-5.4)
|
||||
2. Deploy a model such as GPT-6.1 Sol
|
||||
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/anthropic.claude-4-5-sonnet-20251022-v1:0"
|
||||
export STRIX_LLM="bedrock/us.anthropic.claude-sonnet-5-5"
|
||||
```
|
||||
|
||||
No API key required—uses AWS credentials from environment.
|
||||
@@ -36,10 +36,13 @@ Automatically uses instance role credentials.
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `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 |
|
||||
| `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>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ description: "Configure Strix with Novita AI models"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/moonshotai/kimi-k2.5"
|
||||
export STRIX_LLM="openai/zai-org/glm-5.3"
|
||||
export LLM_API_KEY="your-novita-api-key"
|
||||
export LLM_API_BASE="https://api.novita.ai/openai"
|
||||
```
|
||||
@@ -18,11 +18,12 @@ 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` |
|
||||
| Kimi K2.5 | `openai/moonshotai/kimi-k2.5` |
|
||||
| GLM-5 | `openai/zai-org/glm-5` |
|
||||
| MiniMax M2.5 | `openai/minimax/minimax-m2.5` |
|
||||
| MiniMax M3.1 Flash | `openai/minimax/minimax-m3.1-flash` |
|
||||
| GLM-5.3 Flash | `openai/zai-org/glm-5.3-flash` |
|
||||
|
||||
## Get API Key
|
||||
|
||||
|
||||
@@ -6,13 +6,23 @@ description: "Configure Strix with OpenAI models"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export STRIX_LLM="openai/gpt-6.1-sol"
|
||||
export LLM_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
## Available Models
|
||||
|
||||
See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for the full list of 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>
|
||||
|
||||
## Get API Key
|
||||
|
||||
@@ -25,7 +35,7 @@ See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for t
|
||||
For OpenAI-compatible APIs:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export STRIX_LLM="openai/gpt-6.1-sol"
|
||||
export LLM_API_KEY="your-key"
|
||||
export LLM_API_BASE="https://your-proxy.com/v1"
|
||||
```
|
||||
@@ -18,13 +18,19 @@ Access any model on OpenRouter using the format `openrouter/<provider>/<model>`:
|
||||
|
||||
| Model | Configuration |
|
||||
|-------|---------------|
|
||||
| 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` |
|
||||
| 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` |
|
||||
| DeepSeek V4 Pro | `openrouter/deepseek/deepseek-v4-pro` |
|
||||
| Kimi K3 | `openrouter/moonshotai/kimi-k3` |
|
||||
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
|
||||
| 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.
|
||||
|
||||
## Get API Key
|
||||
|
||||
|
||||
@@ -9,19 +9,33 @@ Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibi
|
||||
|
||||
Set your model and API key:
|
||||
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
||||
```bash
|
||||
<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
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Local Models
|
||||
|
||||
@@ -47,7 +61,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 models from multiple providers through one endpoint.
|
||||
Access 100+ models through one OpenAI-compatible endpoint.
|
||||
</Card>
|
||||
<Card title="Google Vertex AI" href="/llm-providers/vertex">
|
||||
Gemini 3 models via Google Cloud.
|
||||
@@ -69,9 +83,9 @@ Use LiteLLM's `provider/model-name` format:
|
||||
|
||||
```
|
||||
openrouter/z-ai/glm-5.3
|
||||
openai/gpt-5.4
|
||||
anthropic/claude-sonnet-4-6
|
||||
vertex_ai/gemini-3-pro-preview
|
||||
bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0
|
||||
openai/gpt-6.1-sol
|
||||
anthropic/claude-sonnet-5-5
|
||||
vertex_ai/gemini-4-argon
|
||||
bedrock/us.anthropic.claude-sonnet-5-5
|
||||
ollama/llama4
|
||||
```
|
||||
@@ -1,32 +1,47 @@
|
||||
---
|
||||
title: "Vercel AI Gateway"
|
||||
description: "Configure Strix with models via Vercel AI Gateway"
|
||||
description: "Configure Strix with models through Vercel AI Gateway"
|
||||
---
|
||||
|
||||
[Vercel AI Gateway](https://vercel.com/docs/ai-gateway) provides an OpenAI-compatible API for models from multiple providers.
|
||||
[Vercel AI Gateway](https://vercel.com/docs/ai-gateway) provides access to 100+ models from multiple providers through one OpenAI-compatible API.
|
||||
|
||||
## Setup
|
||||
|
||||
Create an [AI Gateway API key](https://vercel.com/docs/ai-gateway/authentication-and-byok), then configure Strix:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/anthropic/claude-opus-5"
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3"
|
||||
export LLM_API_KEY="your-ai-gateway-api-key"
|
||||
export LLM_API_BASE="https://ai-gateway.vercel.sh/v1"
|
||||
```
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
## Available Models
|
||||
|
||||
Use any language model returned by the AI Gateway models endpoint:
|
||||
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:
|
||||
|
||||
```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)
|
||||
@@ -35,5 +50,6 @@ Prefix its model ID with `openai/` when setting `STRIX_LLM`. For example, the Ga
|
||||
|
||||
## Benefits
|
||||
|
||||
- Access models from multiple providers through one endpoint
|
||||
- Track Gateway usage and cost in Vercel
|
||||
- **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
|
||||
@@ -6,7 +6,7 @@ description: "Configure Strix with Gemini models via Google Cloud"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="vertex_ai/gemini-3-pro-preview"
|
||||
export STRIX_LLM="vertex_ai/gemini-4-argon"
|
||||
```
|
||||
|
||||
No API key required—uses Google Cloud Application Default Credentials.
|
||||
@@ -29,8 +29,12 @@ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| `vertex_ai/gemini-3-pro-preview` | Best overall performance for security testing |
|
||||
| `vertex_ai/gemini-3-flash-preview` | Faster and cheaper |
|
||||
| `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>
|
||||
|
||||
## Project Configuration
|
||||
|
||||
|
||||
+11
-4
@@ -25,15 +25,22 @@ description: "Install Strix and run your first security scan"
|
||||
|
||||
## Configuration
|
||||
|
||||
Set your LLM provider:
|
||||
Set your LLM provider. We recommend GLM-5.3 through [Vercel AI Gateway](/llm-providers/vercel-ai-gateway) or [OpenRouter](/llm-providers/openrouter):
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
<CodeGroup>
|
||||
```bash Vercel AI Gateway
|
||||
export STRIX_LLM="vercel_ai_gateway/zai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
```bash OpenRouter
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Tip>
|
||||
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`.
|
||||
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).
|
||||
</Tip>
|
||||
|
||||
## Run Your First Scan
|
||||
|
||||
@@ -49,7 +49,7 @@ Strix runs inside a Kali Linux-based Docker container with a comprehensive set o
|
||||
|
||||
| Tool | Description |
|
||||
| ------------------------------------------------------- | --------------------------------------------- |
|
||||
| [Semgrep](https://github.com/semgrep/semgrep) | Fast SAST and custom rule matching |
|
||||
| [Opengrep](https://github.com/opengrep/opengrep) | Fast SAST and custom rule matching |
|
||||
| [ast-grep](https://ast-grep.github.io) | Structural AST/CST-aware code search (`sg`) |
|
||||
| [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) | Syntax tree parsing and symbol extraction (Java/JS/TS/Python/Go/Bash/JSON/YAML grammars pre-configured) |
|
||||
| [Bandit](https://bandit.readthedocs.io) | Python security linter |
|
||||
|
||||
@@ -33,7 +33,7 @@ sqlmap -u "https://example.com/page?id=1"
|
||||
|
||||
```bash
|
||||
# Fast SAST triage
|
||||
semgrep --config auto ./src
|
||||
opengrep scan --config /opt/opengrep-rules ./src
|
||||
|
||||
# Structural AST search
|
||||
sg scan ./src
|
||||
|
||||
@@ -46,7 +46,7 @@ Thorough penetration testing. Best for:
|
||||
|
||||
**Duration**: 1-4 hours depending on target complexity
|
||||
|
||||
**White-box behavior**: Runs broad source-aware triage (`semgrep`, AST structural search, secrets, supply-chain checks) and then systematically validates top candidates dynamically.
|
||||
**White-box behavior**: Runs broad source-aware triage (`opengrep`, AST structural search, secrets, supply-chain checks) and then systematically validates top candidates dynamically.
|
||||
|
||||
<Note>
|
||||
Deep mode is the default. It explores edge cases, chained vulnerabilities, and complex attack paths.
|
||||
|
||||
@@ -73,6 +73,12 @@ 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"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
|
||||
APP=strix
|
||||
REPO="usestrix/strix"
|
||||
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.3.0"
|
||||
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.4.0"
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
fi
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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="openai/gpt-5.4" # any LiteLLM model id (openai/..., anthropic/..., openrouter/...)
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3" # 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.
|
||||
|
||||
@@ -619,6 +619,17 @@ 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,
|
||||
@@ -717,6 +728,7 @@ 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,
|
||||
@@ -725,6 +737,7 @@ def build_strix_agent(
|
||||
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.
|
||||
|
||||
@@ -740,6 +753,8 @@ def build_strix_agent(
|
||||
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
|
||||
@@ -751,9 +766,11 @@ 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 [])]
|
||||
@@ -761,6 +778,10 @@ def build_strix_agent(
|
||||
# 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]
|
||||
else:
|
||||
@@ -813,11 +834,13 @@ 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``.
|
||||
|
||||
@@ -835,11 +858,13 @@ 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,10 +83,12 @@ 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.
|
||||
|
||||
@@ -121,7 +123,9 @@ 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)
|
||||
skill_content = load_skills(
|
||||
skills_to_load, supports_images=supports_images, coverage_tools=coverage_tools
|
||||
)
|
||||
env.globals["get_skill"] = lambda name: skill_content.get(name, "")
|
||||
|
||||
# Skills every agent of this kind loads come first, so siblings share them
|
||||
@@ -131,10 +135,12 @@ 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,7 +44,11 @@ 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:
|
||||
@@ -55,6 +59,9 @@ 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>
|
||||
|
||||
@@ -108,11 +115,11 @@ BLACK-BOX TESTING (domain/subdomain only):
|
||||
|
||||
WHITE-BOX TESTING (code provided):
|
||||
- MUST perform BOTH static AND dynamic analysis
|
||||
- Static: Use source-aware triage first to map risk quickly (`semgrep`, `ast-grep`, Tree-sitter tooling, `gitleaks`, `trufflehog`, `trivy fs`). Then review code for vulnerabilities
|
||||
- Static: Use source-aware triage first to map risk quickly (`opengrep`, `ast-grep`, Tree-sitter tooling, `gitleaks`, `trufflehog`, `trivy fs`). Then review code for vulnerabilities
|
||||
- Static coverage floor: execute at least one structural AST mapping pass (`sg` and/or Tree-sitter) per repository and keep artifact output
|
||||
- Static coverage target per repository: run one `semgrep` pass, one secrets pass (`gitleaks` and/or `trufflehog`), one `trivy fs` pass, and one AST-structural pass (`sg` and/or Tree-sitter)
|
||||
- Static coverage target per repository: run one `opengrep` pass, one secrets pass (`gitleaks` and/or `trufflehog`), one `trivy fs` pass, and one AST-structural pass (`sg` and/or Tree-sitter)
|
||||
- Keep AST artifacts bounded and high-signal: scope to relevant paths/hypotheses, avoid whole-repo generic function dumps
|
||||
- AST target selection rule: build `sg-targets.txt` from `semgrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`), then run `xargs ... sg run` against that file list. Only use path-heuristic fallback if semgrep scope is unavailable.
|
||||
- AST target selection rule: build `sg-targets.txt` from `opengrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`), then run `xargs ... sg run` against that file list. Only use path-heuristic fallback if opengrep scope is unavailable.
|
||||
- Dynamic: Run the application and test live to validate exploitability
|
||||
- NEVER rely solely on static code analysis when dynamic validation is possible
|
||||
- Begin with fast source triage and dynamic run preparation in parallel; use static findings to prioritize live testing.
|
||||
@@ -181,7 +188,7 @@ EFFICIENCY TACTICS:
|
||||
- For Caido proxy automation inside Python, explicitly import from
|
||||
`caido_api`:
|
||||
`from caido_api import list_requests, view_request, repeat_request, list_sitemap, view_sitemap_entry, scope_rules`
|
||||
- Prefer established fuzzers/scanners where applicable: ffuf, sqlmap, nuclei, wapiti, arjun, httpx, katana, semgrep, bandit, trufflehog, nmap. Use scripts mainly to coordinate or validate around them, not to replace them without reason
|
||||
- Prefer established fuzzers/scanners where applicable: ffuf, sqlmap, nuclei, wapiti, arjun, httpx, katana, opengrep, bandit, trufflehog, nmap. Use scripts mainly to coordinate or validate around them, not to replace them without reason
|
||||
- For trial-heavy vectors (SQLi, XSS, XXE, SSRF, RCE, auth/JWT, deserialization), DO NOT iterate payloads manually in the browser. Always spray payloads via Python scripts through `exec_command` or terminal tools.
|
||||
- When using established fuzzers/scanners, use the proxy for inspection where helpful
|
||||
- Generate/adapt large payload corpora: combine encodings (URL, unicode, base64), comment styles, wrappers, time-based/differential probes. Expand with wordlists/templates
|
||||
@@ -202,7 +209,9 @@ 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.
|
||||
- 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`.
|
||||
{% 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 -%}
|
||||
- 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
|
||||
@@ -220,7 +229,9 @@ 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).
|
||||
@@ -447,7 +458,7 @@ JAVASCRIPT ANALYSIS:
|
||||
- js-beautify - JS beautifier/deobfuscator
|
||||
|
||||
CODE ANALYSIS:
|
||||
- semgrep - Static analysis/SAST
|
||||
- opengrep - Static analysis/SAST (bundled rules in /opt/opengrep-rules)
|
||||
- ast-grep (sg) - Structural AST/CST-aware code search
|
||||
- tree-sitter - Syntax-aware parsing and symbol extraction support
|
||||
- bandit - Python security linter
|
||||
|
||||
+21
-12
@@ -713,7 +713,7 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
if codex.subscription_model(llm.model):
|
||||
return
|
||||
_configure_litellm_compatibility()
|
||||
_configure_openrouter_attribution(llm.model)
|
||||
_configure_attribution(llm.model)
|
||||
if llm.api_key:
|
||||
set_default_openai_key(llm.api_key, use_for_tracing=False)
|
||||
_configure_litellm_default("api_key", llm.api_key)
|
||||
@@ -884,9 +884,13 @@ def _openrouter_error_type(error: object) -> object:
|
||||
return None
|
||||
|
||||
|
||||
OPENROUTER_ATTRIBUTION_HEADERS = {
|
||||
APP_ATTRIBUTION_HEADERS = {
|
||||
"HTTP-Referer": "https://strix.ai",
|
||||
"X-Title": "Strix",
|
||||
}
|
||||
|
||||
OPENROUTER_ATTRIBUTION_HEADERS = {
|
||||
**APP_ATTRIBUTION_HEADERS,
|
||||
"X-OpenRouter-Categories": "cli-agent",
|
||||
}
|
||||
|
||||
@@ -895,20 +899,25 @@ def is_openrouter_model(model_name: str | None) -> bool:
|
||||
return bool(model_name) and "openrouter/" in (model_name or "").strip().lower()
|
||||
|
||||
|
||||
def _configure_openrouter_attribution(model_name: str | None) -> None:
|
||||
def is_vercel_ai_gateway_model(model_name: str | None) -> bool:
|
||||
return (model_name or "").strip().lower().startswith("vercel_ai_gateway/")
|
||||
|
||||
|
||||
def attribution_headers(model_name: str | None) -> dict[str, str]:
|
||||
if is_openrouter_model(model_name):
|
||||
return OPENROUTER_ATTRIBUTION_HEADERS
|
||||
if is_vercel_ai_gateway_model(model_name):
|
||||
return APP_ATTRIBUTION_HEADERS
|
||||
return {}
|
||||
|
||||
|
||||
def _configure_attribution(model_name: str | None) -> None:
|
||||
import litellm
|
||||
|
||||
current: object = litellm.headers
|
||||
existing: dict[str, str] = current if isinstance(current, dict) else {}
|
||||
if not is_openrouter_model(model_name):
|
||||
if any(key in existing for key in OPENROUTER_ATTRIBUTION_HEADERS):
|
||||
remaining = {
|
||||
k: v for k, v in existing.items() if k not in OPENROUTER_ATTRIBUTION_HEADERS
|
||||
}
|
||||
litellm.headers = remaining or None # type: ignore[assignment]
|
||||
return
|
||||
|
||||
litellm.headers = {**existing, **OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
|
||||
remaining = {k: v for k, v in existing.items() if k not in OPENROUTER_ATTRIBUTION_HEADERS}
|
||||
litellm.headers = {**remaining, **attribution_headers(model_name)} or None # type: ignore[assignment]
|
||||
|
||||
|
||||
def _configure_extra_headers(llm: LlmSettings) -> None:
|
||||
|
||||
@@ -61,10 +61,23 @@ class LlmSettings(BaseSettings):
|
||||
repr=False,
|
||||
)
|
||||
reasoning_effort: ReasoningEffort = Field(default="high", alias="STRIX_REASONING_EFFORT")
|
||||
input_cost_per_mtok: float | None = Field(
|
||||
default=None, ge=0, allow_inf_nan=False, alias="STRIX_LLM_INPUT_COST_PER_MTOK"
|
||||
)
|
||||
output_cost_per_mtok: float | None = Field(
|
||||
default=None, ge=0, allow_inf_nan=False, alias="STRIX_LLM_OUTPUT_COST_PER_MTOK"
|
||||
)
|
||||
cached_input_cost_per_mtok: float | None = Field(
|
||||
default=None, ge=0, allow_inf_nan=False, alias="STRIX_LLM_CACHED_INPUT_COST_PER_MTOK"
|
||||
)
|
||||
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",
|
||||
@@ -151,7 +164,7 @@ class RuntimeSettings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
image: str = Field(
|
||||
default="ghcr.io/usestrix/strix-sandbox:1.3.0",
|
||||
default="ghcr.io/usestrix/strix-sandbox:1.4.0",
|
||||
alias="STRIX_IMAGE",
|
||||
)
|
||||
backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND")
|
||||
|
||||
+11
-2
@@ -24,6 +24,8 @@ from openai import (
|
||||
|
||||
from strix.config import codex
|
||||
from strix.core.hooks import (
|
||||
SUBAGENT_TURN_LIMITS_KEY,
|
||||
TURN_LIMIT_KEY,
|
||||
BudgetExceededError,
|
||||
BudgetPausedError,
|
||||
SubagentBudgetReservedError,
|
||||
@@ -39,6 +41,7 @@ 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:
|
||||
@@ -427,6 +430,7 @@ 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,
|
||||
@@ -510,6 +514,7 @@ 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,
|
||||
@@ -1004,7 +1009,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 exactly one tool. "
|
||||
"run continues. Continue immediately and call a 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. "
|
||||
@@ -1016,7 +1021,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 exactly one tool. "
|
||||
"ignored. Continue immediately and call a 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. "
|
||||
@@ -1161,6 +1166,7 @@ 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,
|
||||
@@ -1174,6 +1180,9 @@ 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
|
||||
|
||||
@@ -25,6 +25,11 @@ 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``.
|
||||
@@ -38,6 +43,8 @@ LLM_TURN_KEY = "llm_turn"
|
||||
# same call when the operator raises the limit or resumes.
|
||||
__all__ = [
|
||||
"LLM_TURN_KEY",
|
||||
"SUBAGENT_TURN_LIMITS_KEY",
|
||||
"TURN_LIMIT_KEY",
|
||||
"BudgetExceededError",
|
||||
"BudgetPausedError",
|
||||
"BudgetPolicy",
|
||||
@@ -213,6 +220,7 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
|
||||
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")
|
||||
@@ -276,6 +284,40 @@ 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]],
|
||||
|
||||
@@ -10,12 +10,11 @@ from openai.types.shared import Reasoning
|
||||
|
||||
from strix.config.models import (
|
||||
DEFAULT_MODEL_RETRY,
|
||||
OPENROUTER_ATTRIBUTION_HEADERS,
|
||||
attribution_headers,
|
||||
bedrock_route_supports_prompt_caching,
|
||||
is_bedrock_route,
|
||||
is_claude_model,
|
||||
is_known_openai_bare_model,
|
||||
is_openrouter_model,
|
||||
model_supports_reasoning,
|
||||
request_timeout_extra_args,
|
||||
routes_through_litellm,
|
||||
@@ -247,6 +246,7 @@ 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,
|
||||
@@ -255,7 +255,7 @@ def make_model_settings(
|
||||
) -> ModelSettings:
|
||||
headers = _request_headers(model_name, extra_headers)
|
||||
model_settings = ModelSettings(
|
||||
parallel_tool_calls=False if has_tools else None,
|
||||
parallel_tool_calls=parallel_tool_calls if has_tools else None,
|
||||
retry=DEFAULT_MODEL_RETRY,
|
||||
include_usage=True,
|
||||
extra_args=request_timeout_extra_args(request_timeout),
|
||||
@@ -286,9 +286,7 @@ def make_model_settings(
|
||||
def _request_headers(
|
||||
model_name: str, extra_headers: dict[str, str] | None
|
||||
) -> dict[str, str] | None:
|
||||
headers: dict[str, str] = {}
|
||||
if is_openrouter_model(model_name):
|
||||
headers.update(OPENROUTER_ATTRIBUTION_HEADERS)
|
||||
headers: dict[str, str] = dict(attribution_headers(model_name))
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers or None
|
||||
|
||||
+34
-2
@@ -8,7 +8,7 @@ import io
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -36,7 +36,12 @@ from strix.core.execution import (
|
||||
from strix.core.execution import (
|
||||
spawn_child_agent as start_child_agent,
|
||||
)
|
||||
from strix.core.hooks import BudgetExceededError, ReportUsageHooks, recomputed_budget_flags
|
||||
from strix.core.hooks import (
|
||||
SUBAGENT_TURN_LIMITS_KEY,
|
||||
BudgetExceededError,
|
||||
ReportUsageHooks,
|
||||
recomputed_budget_flags,
|
||||
)
|
||||
from strix.core.inputs import (
|
||||
build_root_task,
|
||||
build_scan_targets,
|
||||
@@ -171,12 +176,17 @@ 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,
|
||||
@@ -185,10 +195,12 @@ 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"
|
||||
@@ -216,17 +228,22 @@ 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`.
|
||||
@@ -247,6 +264,11 @@ async def run_strix_scan(
|
||||
``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:
|
||||
@@ -404,6 +426,7 @@ 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,
|
||||
@@ -499,9 +522,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,
|
||||
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(
|
||||
@@ -512,6 +538,7 @@ 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,
|
||||
@@ -519,6 +546,7 @@ async def run_strix_scan(
|
||||
supports_images=supports_images,
|
||||
finish_tool=root_finish_tool,
|
||||
report_tool=report_tool,
|
||||
coverage_tools=coverage_tools,
|
||||
)
|
||||
|
||||
if not is_resume:
|
||||
@@ -535,11 +563,13 @@ 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]:
|
||||
@@ -568,6 +598,8 @@ 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)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Supply-chain cooldown: only install versions published at least 5 days ago (npm >= 11.10).
|
||||
min-release-age=5
|
||||
+70
-4
@@ -29,7 +29,7 @@
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1401,6 +1401,72 @@
|
||||
"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",
|
||||
@@ -4096,9 +4162,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz",
|
||||
"integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==",
|
||||
"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.0.0"
|
||||
"vite": "^6.4.4"
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -300,7 +300,15 @@ def _parse_dedupe_response(content: str) -> dict[str, Any]:
|
||||
raise ValueError(f"No JSON object found in dedupe response: {content[:500]}")
|
||||
parsed = json.loads(text[start : end + 1])
|
||||
|
||||
duplicate_id = str(parsed.get("duplicate_id") or "")[:64]
|
||||
is_duplicate = parsed.get("is_duplicate")
|
||||
if not isinstance(is_duplicate, bool):
|
||||
raise TypeError("is_duplicate must be a boolean")
|
||||
duplicate_id = parsed.get("duplicate_id") or ""
|
||||
if is_duplicate and (not isinstance(duplicate_id, str) or not duplicate_id):
|
||||
raise ValueError("A duplicate decision must name an existing report ID")
|
||||
# Keep the ID verbatim: truncating or stripping it could turn an unknown ID
|
||||
# into a different, existing one before the membership check below.
|
||||
duplicate_id = str(duplicate_id)
|
||||
reason = str(parsed.get("reason") or "")[:500]
|
||||
try:
|
||||
confidence = float(parsed.get("confidence", 0.0))
|
||||
@@ -308,13 +316,22 @@ def _parse_dedupe_response(content: str) -> dict[str, Any]:
|
||||
confidence = 0.0
|
||||
|
||||
return {
|
||||
"is_duplicate": bool(parsed.get("is_duplicate", False)),
|
||||
"is_duplicate": is_duplicate,
|
||||
"duplicate_id": duplicate_id,
|
||||
"confidence": confidence,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
|
||||
def _validate_duplicate_reference(
|
||||
result: dict[str, Any], existing_reports: list[dict[str, Any]]
|
||||
) -> None:
|
||||
if result["is_duplicate"] and not any(
|
||||
report.get("id") == result["duplicate_id"] for report in existing_reports
|
||||
):
|
||||
raise ValueError("Duplicate decision refers to an unknown report ID")
|
||||
|
||||
|
||||
def _extract_text(response: ModelResponse) -> str:
|
||||
parts: list[str] = []
|
||||
for item in response.output:
|
||||
@@ -397,6 +414,7 @@ async def check_duplicate(
|
||||
}
|
||||
|
||||
result = _parse_dedupe_response(content)
|
||||
_validate_duplicate_reference(result, existing_reports)
|
||||
|
||||
logger.info(
|
||||
"Deduplication check: is_duplicate=%s, confidence=%.2f, reason=%s",
|
||||
|
||||
+51
-2
@@ -8,11 +8,38 @@ from typing import Any
|
||||
from agents.usage import Usage, deserialize_usage, serialize_usage
|
||||
from pydantic import BaseModel, TypeAdapter, ValidationError
|
||||
|
||||
from strix.config.loader import load_settings
|
||||
from strix.report.pricing import resolve_litellm_model
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Prices = tuple[float | None, float | None, float | None]
|
||||
|
||||
|
||||
def _configured_prices() -> tuple[str | None, Prices | None]:
|
||||
llm = load_settings().llm
|
||||
prices = (llm.input_cost_per_mtok, llm.output_cost_per_mtok, llm.cached_input_cost_per_mtok)
|
||||
if all(price is None for price in prices):
|
||||
return None, None
|
||||
return (llm.model or "").strip() or None, prices
|
||||
|
||||
|
||||
def _configured_usage_cost(usage: Usage, prices: Prices) -> float:
|
||||
input_rate, output_rate, cached_rate = prices
|
||||
input_tokens = max(0, _int_or_zero(getattr(usage, "input_tokens", 0)))
|
||||
cached_details = getattr(usage, "input_tokens_details", None)
|
||||
cached_tokens = max(0, _int_or_zero(getattr(cached_details, "cached_tokens", 0)))
|
||||
cached_tokens = min(cached_tokens, input_tokens)
|
||||
output_tokens = max(0, _int_or_zero(getattr(usage, "output_tokens", 0)))
|
||||
billable_input = input_tokens - cached_tokens
|
||||
cost = (
|
||||
billable_input * (input_rate or 0)
|
||||
+ cached_tokens * (cached_rate if cached_rate is not None else input_rate or 0)
|
||||
+ output_tokens * (output_rate or 0)
|
||||
) / 1_000_000
|
||||
return _round_cost(cost)
|
||||
|
||||
|
||||
class ProviderUsage(BaseModel):
|
||||
"""Running totals for one upstream provider OpenRouter routed calls to."""
|
||||
@@ -37,6 +64,7 @@ class LLMUsageLedger:
|
||||
self._total_usage = Usage()
|
||||
self._agent_usage: dict[str, Usage] = {}
|
||||
self._agent_metadata: dict[str, dict[str, str]] = {}
|
||||
self._agent_estimated_cost: dict[str, float] = {}
|
||||
self._observed_cost = 0.0
|
||||
self._estimated_cost = 0.0
|
||||
self._has_observed_cost = False
|
||||
@@ -73,6 +101,9 @@ class LLMUsageLedger:
|
||||
estimated = _estimate_litellm_cost(usage, model)
|
||||
if estimated:
|
||||
self._estimated_cost += estimated
|
||||
self._agent_estimated_cost[normalized_agent_id] = (
|
||||
self._agent_estimated_cost.get(normalized_agent_id, 0.0) + estimated
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -114,11 +145,23 @@ class LLMUsageLedger:
|
||||
def total_cost(self) -> float:
|
||||
if self.zero_cost:
|
||||
return 0.0
|
||||
model, prices = _configured_prices()
|
||||
if prices is not None:
|
||||
return _round_cost(
|
||||
sum(self._configured_agent_cost(aid, model, prices) for aid in self._agent_usage)
|
||||
)
|
||||
return _round_cost(self._observed_cost if self._has_observed_cost else self._estimated_cost)
|
||||
|
||||
def _configured_agent_cost(self, agent_id: str, model: str | None, prices: Prices) -> float:
|
||||
if self._agent_metadata.get(agent_id, {}).get("model") == model:
|
||||
return _configured_usage_cost(self._agent_usage[agent_id], prices)
|
||||
return self._agent_estimated_cost.get(agent_id, 0.0)
|
||||
|
||||
def to_record(self) -> dict[str, Any]:
|
||||
record = serialize_usage(self._total_usage)
|
||||
record["cost"] = self.total_cost
|
||||
model, prices = _configured_prices()
|
||||
total_cost = self.total_cost
|
||||
record["cost"] = total_cost
|
||||
record["providers"] = {name: tally.model_dump() for name, tally in self._providers.items()}
|
||||
record["agents"] = []
|
||||
|
||||
@@ -128,7 +171,11 @@ class LLMUsageLedger:
|
||||
usage = self._agent_usage[agent_id]
|
||||
metadata = self._agent_metadata.get(agent_id, {})
|
||||
agent_cost = (
|
||||
self.total_cost * (agent_tokens[agent_id] / total_tokens) if total_tokens else 0.0
|
||||
self._configured_agent_cost(agent_id, model, prices)
|
||||
if prices is not None and not self.zero_cost
|
||||
else total_cost * (agent_tokens[agent_id] / total_tokens)
|
||||
if total_tokens
|
||||
else 0.0
|
||||
)
|
||||
|
||||
agent_record = serialize_usage(usage)
|
||||
@@ -148,6 +195,7 @@ class LLMUsageLedger:
|
||||
self._total_usage = Usage()
|
||||
self._agent_usage.clear()
|
||||
self._agent_metadata.clear()
|
||||
self._agent_estimated_cost.clear()
|
||||
self._observed_cost = 0.0
|
||||
self._estimated_cost = 0.0
|
||||
self._has_observed_cost = False
|
||||
@@ -183,6 +231,7 @@ class LLMUsageLedger:
|
||||
logger.exception("Failed to hydrate llm_usage for agent %s", agent_id)
|
||||
self._agent_usage[agent_id] = Usage()
|
||||
|
||||
self._agent_estimated_cost[agent_id] = _float_or_zero(raw_agent.get("cost"))
|
||||
metadata: dict[str, str] = {}
|
||||
agent_name = raw_agent.get("agent_name")
|
||||
model = raw_agent.get("model")
|
||||
|
||||
@@ -66,6 +66,33 @@ 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."""
|
||||
@@ -233,6 +260,7 @@ 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)
|
||||
|
||||
@@ -40,7 +40,7 @@ The skills are dynamically injected into the agent's system prompt, allowing it
|
||||
|
||||
Notable source-aware skills:
|
||||
- `source_aware_whitebox` (coordination): white-box orchestration playbook
|
||||
- `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow
|
||||
- `source_aware_sast` (custom): opengrep/AST/secrets/supply-chain static triage workflow
|
||||
- `dependency_cve_scanning` (custom): trivy-based SCA workflow for reporting known dependency CVEs via `create_dependency_report`
|
||||
- `npx_confusion` (custom): npx/npm exec/bunx fallback and adjacent package-runner identity confusion, with runner-specific registry and reporting gates
|
||||
- `semantic_confusion` (vulnerabilities): cross-boundary parser, normalization, and representation mismatch analysis
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
from collections import Counter
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Iterator, Mapping
|
||||
from pathlib import Path
|
||||
from typing import TypeGuard
|
||||
|
||||
@@ -21,6 +21,7 @@ _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"})
|
||||
@@ -270,7 +271,22 @@ def _candidate_skill_files(skill_name: str) -> list[Path]:
|
||||
return _bare_skill_files(skill_name)
|
||||
|
||||
|
||||
def load_skills(skill_names: list[str], *, supports_images: bool = True) -> dict[str, str]:
|
||||
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]:
|
||||
"""Load skill markdown bodies (frontmatter stripped) by name.
|
||||
|
||||
Skill files live at ``strix/skills/<category>/<name>.md`` (or any
|
||||
@@ -303,7 +319,7 @@ def load_skills(skill_names: list[str], *, supports_images: bool = True) -> dict
|
||||
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
|
||||
supports_images=supports_images, coverage_tools=coverage_tools
|
||||
)
|
||||
skill_content[var_name] = skill_body
|
||||
logger.debug("Loaded skill: %s -> %s", skill_name, var_name)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
@@ -32,8 +33,12 @@ 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.
|
||||
@@ -97,6 +102,7 @@ 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
|
||||
@@ -125,6 +131,7 @@ 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
@@ -206,6 +207,10 @@ 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,6 +1,7 @@
|
||||
---
|
||||
name: root-agent
|
||||
description: Orchestration layer that coordinates specialized subagents for security assessments
|
||||
template: jinja
|
||||
---
|
||||
|
||||
# Root Agent
|
||||
@@ -35,10 +36,12 @@ 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:
|
||||
|
||||
@@ -14,22 +14,22 @@ Increase white-box coverage by combining source-aware triage with dynamic valida
|
||||
## Recommended Workflow
|
||||
|
||||
1. Build a quick source map before deep exploitation, including at least one AST-structural pass (`sg` or `tree-sitter`) scoped to relevant paths.
|
||||
- For `sg` baseline, derive `sg-targets.txt` from `semgrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`) and run `xargs ... sg run` on that list.
|
||||
- Only fall back to path heuristics when semgrep scope is unavailable.
|
||||
- For `sg` baseline, derive `sg-targets.txt` from `opengrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`) and run `xargs ... sg run` on that list.
|
||||
- Only fall back to path heuristics when opengrep scope is unavailable.
|
||||
2. Run first-pass static triage to rank high-risk paths.
|
||||
3. Use triage outputs to prioritize dynamic PoC validation.
|
||||
4. Keep findings evidence-driven: no report without validation.
|
||||
|
||||
## Source-Aware Triage Stack
|
||||
|
||||
- `semgrep`: fast security-first triage and custom pattern scans
|
||||
- `opengrep`: fast security-first triage and custom pattern scans
|
||||
- `ast-grep` (`sg`): structural pattern hunting and targeted repo mapping
|
||||
- `tree-sitter`: syntax-aware parsing support for symbol and route extraction
|
||||
- `gitleaks` + `trufflehog`: complementary secret detection (working tree and history coverage)
|
||||
- `trivy fs`: dependency, misconfiguration, license, and secret checks
|
||||
|
||||
Coverage target per repository:
|
||||
- one `semgrep` pass
|
||||
- one `opengrep` pass
|
||||
- one AST structural pass (`sg` and/or `tree-sitter`)
|
||||
- one secrets pass (`gitleaks` and/or `trufflehog`)
|
||||
- one `trivy fs` pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: source-aware-sast
|
||||
description: Practical source-aware SAST and AST playbook for semgrep, ast-grep, gitleaks, and trivy fs
|
||||
description: Practical source-aware SAST and AST playbook for opengrep, ast-grep, gitleaks, and trivy fs
|
||||
---
|
||||
|
||||
# Source-Aware SAST Playbook
|
||||
@@ -23,19 +23,19 @@ Run this baseline once per repository before deep narrowing:
|
||||
ART=/workspace/.source-aware
|
||||
mkdir -p "$ART"
|
||||
|
||||
semgrep scan --config p/default --config p/golang --config p/secrets \
|
||||
--metrics=off --json --output "$ART/semgrep.json" .
|
||||
# Build deterministic AST targets from semgrep scope (no hardcoded path guessing)
|
||||
opengrep scan --config /opt/opengrep-rules --taint-intrafile \
|
||||
--json --output "$ART/opengrep.json" .
|
||||
# Build deterministic AST targets from opengrep scope (no hardcoded path guessing)
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
art = Path("/workspace/.source-aware")
|
||||
semgrep_json = art / "semgrep.json"
|
||||
opengrep_json = art / "opengrep.json"
|
||||
targets_file = art / "sg-targets.txt"
|
||||
|
||||
try:
|
||||
data = json.loads(semgrep_json.read_text(encoding="utf-8"))
|
||||
data = json.loads(opengrep_json.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
targets_file.write_text("", encoding="utf-8")
|
||||
raise
|
||||
@@ -63,17 +63,18 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
|
||||
--format json --output "$ART/trivy-fs.json" . || true
|
||||
```
|
||||
|
||||
## Semgrep First Pass
|
||||
## Opengrep First Pass
|
||||
|
||||
Use Semgrep as the default static triage pass:
|
||||
Use Opengrep as the default static triage pass. Rules are bundled offline in `/opt/opengrep-rules` (one directory per language, plus `generic/secrets`):
|
||||
|
||||
```bash
|
||||
# Preferred deterministic profile set (works with --metrics=off)
|
||||
semgrep scan --config p/default --config p/golang --config p/secrets \
|
||||
--metrics=off --json --output /workspace/.source-aware/semgrep.json .
|
||||
# Full bundled ruleset
|
||||
opengrep scan --config /opt/opengrep-rules --taint-intrafile \
|
||||
--json --output /workspace/.source-aware/opengrep.json .
|
||||
|
||||
# If you choose auto config, do not combine it with --metrics=off
|
||||
semgrep scan --config auto --json --output /workspace/.source-aware/semgrep-auto.json .
|
||||
# Narrow to the target stack when the full set is slow or noisy
|
||||
opengrep scan --config /opt/opengrep-rules/python --config /opt/opengrep-rules/generic/secrets \
|
||||
--taint-intrafile --json --output /workspace/.source-aware/opengrep-python.json .
|
||||
```
|
||||
|
||||
If diff scope is active, restrict to changed files first, then expand only when needed.
|
||||
|
||||
@@ -205,10 +205,10 @@ Jinja2 backend without autoescape: `{{7*7}}`, RCE gadgets if sandbox misconfigur
|
||||
|
||||
## Tooling
|
||||
|
||||
Static analysis is the fastest way to reach the sinks above in white-box scope. The sandbox ships `python`/`pipx`, `semgrep`, `bandit`, `ast-grep`, and `ripgrep`.
|
||||
Static analysis is the fastest way to reach the sinks above in white-box scope. The sandbox ships `python`/`pipx`, `opengrep`, `bandit`, `ast-grep`, and `ripgrep`.
|
||||
|
||||
- **bandit** (preinstalled) — Python security linter; flags `mark_safe`, `extra()`, `RawSQL`, `subprocess`, weak crypto, hardcoded secrets: `bandit -r . -ll`
|
||||
- **semgrep** (preinstalled) with the Django ruleset — higher-signal than bandit for framework-specific bugs (`.extra()`, `RawSQL`, `|safe`, `csrf_exempt`, `ALLOWED_HOSTS=['*']`): `semgrep --config p/django .`
|
||||
- **opengrep** (preinstalled) with the bundled Django rules — higher-signal than bandit for framework-specific bugs (`.extra()`, `RawSQL`, `|safe`, `csrf_exempt`, `ALLOWED_HOSTS=['*']`): `opengrep scan --config /opt/opengrep-rules/python/django .`
|
||||
- **pip-audit** (PyPA) — dependency CVE scanner for known-vuln Django/DRF/simplejwt versions: `pipx install pip-audit && pip-audit -r requirements.txt`
|
||||
- **ast-grep** (preinstalled) — quick structural grep for risky calls without a full SAST run: `ast-grep run -p 'mark_safe($X)' -l python`
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Thorough understanding before exploitation. Test every parameter, every endpoint
|
||||
|
||||
**Whitebox (source available)**
|
||||
- Map every file, module, and code path in the repository
|
||||
- Start with broad source-aware triage (`semgrep`, `ast-grep`, `gitleaks`, `trufflehog`, `trivy fs`) and use outputs to drive deep review
|
||||
- Start with broad source-aware triage (`opengrep`, `ast-grep`, `gitleaks`, `trufflehog`, `trivy fs`) and use outputs to drive deep review
|
||||
- Execute at least one structural AST pass (`sg` and/or Tree-sitter) per repository and store artifacts for reuse
|
||||
- Keep AST artifacts bounded and query-driven (target relevant paths/sinks first; avoid whole-repo generic function dumps)
|
||||
- Use syntax-aware parsing (Tree-sitter tooling) to improve symbol, route, and sink extraction quality
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
@@ -69,9 +70,14 @@ 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
|
||||
|
||||
@@ -80,7 +86,9 @@ 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.
|
||||
@@ -15,7 +15,7 @@ Optimize for fast feedback on critical security issues. Skip exhaustive enumerat
|
||||
|
||||
**Whitebox (source available)**
|
||||
- Focus on recent changes: git diffs, new commits, modified files—these are most likely to contain fresh bugs
|
||||
- Run a fast static triage on changed files first (`semgrep`, then targeted `sg` queries)
|
||||
- Run a fast static triage on changed files first (`opengrep`, then targeted `sg` queries)
|
||||
- Run at least one lightweight AST pass (`sg` or Tree-sitter) so structural mapping is not skipped
|
||||
- Keep AST commands tightly scoped to changed or high-risk paths; avoid broad repository-wide pattern dumps
|
||||
- Run quick secret and dependency checks (`gitleaks`, `trufflehog`, `trivy fs`) scoped to changed areas when possible
|
||||
|
||||
@@ -15,7 +15,7 @@ Systematic testing across the full attack surface. Understand the application be
|
||||
|
||||
**Whitebox (source available)**
|
||||
- Map codebase structure: modules, entry points, routing
|
||||
- Run `semgrep` first-pass triage to prioritize risky flows before deep manual review
|
||||
- Run `opengrep` first-pass triage to prioritize risky flows before deep manual review
|
||||
- Run at least one AST-structural mapping pass (`sg` and/or Tree-sitter), then use outputs for route, sink, and trust-boundary mapping
|
||||
- Keep AST output bounded to relevant paths and hypotheses; avoid whole-repo generic function dumps
|
||||
- Identify architecture pattern (MVC, microservices, monolith)
|
||||
|
||||
@@ -207,7 +207,7 @@ agent-browser snapshot -i
|
||||
```
|
||||
|
||||
Credentials in shell history are a leak. For anything sensitive, use the
|
||||
auth vault (see [references/authentication.md](references/authentication.md)):
|
||||
auth vault (see `references/authentication.md`):
|
||||
|
||||
```bash
|
||||
agent-browser auth save my-app --url https://app.example.com/login \
|
||||
@@ -367,7 +367,7 @@ agent-browser click @e3
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
See [references/video-recording.md](references/video-recording.md) for
|
||||
See `references/video-recording.md` for
|
||||
codec options, GIF export, and more.
|
||||
|
||||
### Iframes
|
||||
@@ -491,8 +491,8 @@ snapshot — fall back to `eval` in the iframe's origin or use the
|
||||
|
||||
**Authentication expires mid-workflow**
|
||||
Use `--session-name <name>` or `state save`/`state load` so your session
|
||||
survives browser restarts. See [references/session-management.md](references/session-management.md)
|
||||
and [references/authentication.md](references/authentication.md).
|
||||
survives browser restarts. See `references/session-management.md`
|
||||
and `references/authentication.md`.
|
||||
|
||||
## Global flags worth knowing
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: opengrep
|
||||
description: Exact Opengrep CLI structure, bundled offline rules, scoped rule selection, taint mode, and automation-safe output patterns.
|
||||
---
|
||||
|
||||
# Opengrep CLI Playbook
|
||||
|
||||
Official docs:
|
||||
- https://github.com/opengrep/opengrep
|
||||
- https://github.com/opengrep/opengrep-rules
|
||||
|
||||
Canonical syntax:
|
||||
`opengrep scan [flags] [targets]`
|
||||
|
||||
Bundled rules:
|
||||
- `/opt/opengrep-rules` holds the full ruleset, one directory per language (`python`, `javascript`, `typescript`, `go`, `java`, `php`, `ruby`, …) plus `generic/secrets`, `dockerfile`, `terraform`, `yaml`, and `strix` (Strix-authored rules, e.g. GitHub-owned actions not pinned to a commit SHA)
|
||||
- Framework rules sit under the language (`python/django`, `python/flask`, `javascript/express`, …)
|
||||
|
||||
High-signal flags:
|
||||
- `--config <path>` rule file or directory; repeat for several
|
||||
- `--taint-intrafile` intra-file inter-procedural taint (follows data across functions in one file)
|
||||
- `--json` / `--sarif` output format
|
||||
- `--output <file>` write findings to file
|
||||
- `--severity <level>` filter by severity (`INFO`, `WARNING`, `ERROR`)
|
||||
- `--error` return non-zero exit when findings exist
|
||||
- `--quiet` suppress progress noise
|
||||
- `--jobs <n>` parallel workers
|
||||
- `--timeout <seconds>` per-rule, per-file timeout
|
||||
- `--exclude <pattern>` / `--include <pattern>` path filters
|
||||
- `--exclude-rule <rule_id>` suppress specific rule
|
||||
- `--baseline-commit <sha>` only report findings introduced after baseline
|
||||
|
||||
Agent-safe baseline for automation:
|
||||
`opengrep scan --config /opt/opengrep-rules --taint-intrafile --json --output opengrep.json --quiet --jobs 4 --timeout 20 /workspace`
|
||||
|
||||
Common patterns:
|
||||
- Full bundled scan:
|
||||
`opengrep scan --config /opt/opengrep-rules --json --output opengrep.json --quiet /workspace`
|
||||
- High-severity focused pass:
|
||||
`opengrep scan --config /opt/opengrep-rules --severity ERROR --json --output opengrep_high.json --quiet /workspace`
|
||||
- SARIF output:
|
||||
`opengrep scan --config /opt/opengrep-rules --sarif --output opengrep.sarif --quiet /workspace`
|
||||
- Language- or framework-specific rules:
|
||||
`opengrep scan --config /opt/opengrep-rules/python --config /opt/opengrep-rules/generic/secrets --json --output opengrep_python.json --quiet /workspace`
|
||||
- Scoped directory scan:
|
||||
`opengrep scan --config /opt/opengrep-rules --json --output opengrep_api.json --quiet /workspace/services/api`
|
||||
- Custom rule:
|
||||
`opengrep scan --config ./rules/my-rule.yaml --json --output opengrep_custom.json --quiet /workspace`
|
||||
|
||||
Critical correctness rules:
|
||||
- Always provide an explicit `--config` from `/opt/opengrep-rules` or a local rule file. Registry shorthands (`p/...`, `auto`) fetch rules from the network; do not use them.
|
||||
- Opengrep has no telemetry; there is no `--metrics` flag, and passing one fails the scan.
|
||||
- Prefer `--json --output <file>` or `--sarif --output <file>` for machine-readable downstream processing. JSON keeps `paths.scanned` and `results[].path`.
|
||||
- Keep the target path explicit; use an absolute or clearly scoped workspace path instead of `.` when possible.
|
||||
|
||||
Usage rules:
|
||||
- Start with the full `/opt/opengrep-rules` set unless the task clearly calls for a narrower language or framework directory.
|
||||
- Add `--taint-intrafile` for injection-class hunting (SQLi, command injection, SSRF, path traversal) in supported languages.
|
||||
- Use `--quiet` in automation to reduce noisy logs.
|
||||
- Use `--jobs` and `--timeout` explicitly for reproducible runtime behavior.
|
||||
- `PartialParsing` errors on templates are normal; they do not invalidate the run.
|
||||
|
||||
Failure recovery:
|
||||
- If scans are too slow, narrow the target path and the rule directories before changing engine settings.
|
||||
- If scans time out, increase `--timeout` modestly or lower `--jobs`.
|
||||
- If output is too broad, scope `--config`, add `--severity`, or exclude known irrelevant paths.
|
||||
|
||||
If uncertain, query web_search with:
|
||||
`site:github.com/opengrep/opengrep <flag>`
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
name: semgrep
|
||||
description: Exact Semgrep CLI structure, metrics-off scanning, scoped ruleset selection, and automation-safe output patterns.
|
||||
---
|
||||
|
||||
# Semgrep CLI Playbook
|
||||
|
||||
Official docs:
|
||||
- https://semgrep.dev/docs/cli-reference
|
||||
- https://semgrep.dev/docs/getting-started/cli
|
||||
- https://semgrep.dev/docs/semgrep-code/semgrep-pro-engine-intro
|
||||
|
||||
Canonical syntax:
|
||||
`semgrep scan [flags]`
|
||||
|
||||
High-signal flags:
|
||||
- `--config <rule_or_ruleset>` ruleset, registry pack, local rule file, or directory
|
||||
- `--metrics=off` disable telemetry and metrics reporting
|
||||
- `--json` JSON output
|
||||
- `--sarif` SARIF output
|
||||
- `--output <file>` write findings to file
|
||||
- `--severity <level>` filter by severity
|
||||
- `--error` return non-zero exit when findings exist
|
||||
- `--quiet` suppress progress noise
|
||||
- `--jobs <n>` parallel workers
|
||||
- `--timeout <seconds>` per-file timeout
|
||||
- `--exclude <pattern>` exclude path pattern
|
||||
- `--include <pattern>` include path pattern
|
||||
- `--exclude-rule <rule_id>` suppress specific rule
|
||||
- `--baseline-commit <sha>` only report findings introduced after baseline
|
||||
- `--pro` enable Pro engine if available
|
||||
- `--oss-only` force OSS engine only
|
||||
|
||||
Agent-safe baseline for automation:
|
||||
`semgrep scan --config p/default --metrics=off --json --output semgrep.json --quiet --jobs 4 --timeout 20 /workspace`
|
||||
|
||||
Common patterns:
|
||||
- Default security scan:
|
||||
`semgrep scan --config p/default --metrics=off --json --output semgrep.json --quiet /workspace`
|
||||
- High-severity focused pass:
|
||||
`semgrep scan --config p/default --severity ERROR --metrics=off --json --output semgrep_high.json --quiet /workspace`
|
||||
- OWASP-oriented scan:
|
||||
`semgrep scan --config p/owasp-top-ten --metrics=off --sarif --output semgrep.sarif --quiet /workspace`
|
||||
- Language- or framework-specific rules:
|
||||
`semgrep scan --config p/python --config p/secrets --metrics=off --json --output semgrep_python.json --quiet /workspace`
|
||||
- Scoped directory scan:
|
||||
`semgrep scan --config p/default --metrics=off --json --output semgrep_api.json --quiet /workspace/services/api`
|
||||
- Pro engine check or run:
|
||||
`semgrep scan --config p/default --pro --metrics=off --json --output semgrep_pro.json --quiet /workspace`
|
||||
|
||||
Critical correctness rules:
|
||||
- Always include `--metrics=off`; Semgrep sends telemetry by default.
|
||||
- Always provide an explicit `--config`; do not rely on vague or implied defaults.
|
||||
- Prefer `--json --output <file>` or `--sarif --output <file>` for machine-readable downstream processing.
|
||||
- Keep the target path explicit; use an absolute or clearly scoped workspace path instead of `.` when possible.
|
||||
- If Pro availability matters, check it explicitly with a bounded command before assuming cross-file analysis exists.
|
||||
|
||||
Usage rules:
|
||||
- Start with `p/default` unless the task clearly calls for a narrower pack.
|
||||
- Add focused packs such as `p/secrets`, `p/python`, or `p/javascript` only when they match the target stack.
|
||||
- Use `--quiet` in automation to reduce noisy logs.
|
||||
- Use `--jobs` and `--timeout` explicitly for reproducible runtime behavior.
|
||||
- Do not use `-h`/`--help` for routine operation unless absolutely necessary.
|
||||
|
||||
Failure recovery:
|
||||
- If scans are too slow, narrow the target path and reduce the active rulesets before changing engine settings.
|
||||
- If scans time out, increase `--timeout` modestly or lower `--jobs`.
|
||||
- If output is too broad, scope `--config`, add `--severity`, or exclude known irrelevant paths.
|
||||
- If Pro mode fails, rerun with `--oss-only` or without `--pro` and note the loss of cross-file coverage.
|
||||
|
||||
If uncertain, query web_search with:
|
||||
`site:semgrep.dev semgrep <flag> cli`
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: semantic-confusion
|
||||
description: Cross-component semantic confusion testing for parser differentials, normalization mismatches, overloaded fields, lifecycle state drift, internal redirects, protocol translation, and validator-to-sink inconsistencies
|
||||
description: Cross-component semantic confusion testing for parser differentials, normalization mismatches, email and IDNA identity drift, overloaded fields, lifecycle state drift, internal redirects, protocol translation, and validator-to-sink inconsistencies
|
||||
---
|
||||
|
||||
# Semantic Confusion
|
||||
@@ -59,6 +59,27 @@ The highest-signal condition is `security_check(value_A)` followed by `sink(tran
|
||||
- Test malformed Unicode and replacement behavior; a rejected code point may become an allowed delimiter or wildcard later.
|
||||
- Test path, query, and fragment separately. Browsers and routers commonly transform each source differently.
|
||||
|
||||
### Email and Account Identity Drift
|
||||
|
||||
Treat an email address as both a principal selector and a delivery destination. Trace the complete boundary before testing:
|
||||
|
||||
```text
|
||||
submitted or identity-provider address
|
||||
-> application normalization / IDNA
|
||||
-> database comparison and collation
|
||||
-> selected local principal
|
||||
-> token, session, invitation, or link binding
|
||||
-> recipient or linked identity consumed by the final service
|
||||
```
|
||||
|
||||
- Cover recovery, magic links, invitations, registration uniqueness, email changes, and OAuth/OIDC account linking. Test case/accent-insensitive collations, Unicode normalization, IDNA U-label/A-label conversion, trailing dots, and provider canonicalization only when the observed stack supports them.
|
||||
- In black-box testing, report the observed equivalence and routing behavior. Do not name a specific collation, normalizer, or provider algorithm without source, configuration, or version evidence.
|
||||
- Determine whether two non-identical addresses can select the same protected principal while SMTP or an identity provider routes them to different owners. The address that receives authority must identify the same principal selected by the security check.
|
||||
- In recovery flows, compare the submitted address with the selected account's stored canonical address. A fuzzy lookup followed by delivery to the submitted value is high risk; an exact lookup plus delivery to the stored value is the primary negative control.
|
||||
- Use only an explicitly authorized attacker-controlled mailbox or test domain. Derive a bounded variant set from the deployed collation and canonicalizers rather than spraying external domains or the Unicode range.
|
||||
|
||||
For a recovery, magic-link, or invitation finding, a generic success response is not proof. Demonstrate that the variant selects the protected principal, the issued authority reaches the controlled mailbox or identity, and redemption yields the protected principal's access. If delivery or redemption cannot be observed, keep the path `needs_follow_up`.
|
||||
|
||||
### Field and Type Overloading
|
||||
|
||||
- Identify shared fields reused for different concepts: path vs URL, content type vs handler, display name vs executable name, route vs filesystem location.
|
||||
@@ -170,6 +191,8 @@ A valid confusion finding should include:
|
||||
- Different error messages with identical final authorization and sink behavior
|
||||
- A parser accepts odd syntax but downstream consumers preserve the same safe meaning
|
||||
- A normalization difference visible only in logs, with no security decision between representations
|
||||
- A Unicode or Punycode lookalike that neither selects another principal nor receives authority intended for that principal
|
||||
- A permissive database comparison where recovery delivery still uses the selected account's stored canonical address
|
||||
- WAF bypass where the application itself rejects the request identically
|
||||
- Version-specific behavior claimed as universal without testing the relevant deployment
|
||||
- A search-path candidate that is attacker-named but cannot be created, claimed, loaded, or executed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: xss
|
||||
description: XSS testing covering reflected, stored, and DOM-based vectors with CSP bypass techniques
|
||||
description: XSS and HTML injection testing covering reflected, stored, DOM-based, and normalization-dependent vectors
|
||||
---
|
||||
|
||||
# XSS
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
@@ -20,6 +21,7 @@ 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
|
||||
|
||||
@@ -57,6 +59,24 @@ 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**
|
||||
@@ -166,7 +186,7 @@ Keep a compact set tuned per context:
|
||||
## Testing Methodology
|
||||
|
||||
1. **Identify sources** - URL/query/hash/referrer, postMessage, storage, WebSocket, server JSON
|
||||
2. **Trace to sinks** - Map data flow from source to sink
|
||||
2. **Trace transformations and sinks** - Record decoding, normalization, sanitization, reparsing, and the final 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
|
||||
@@ -175,9 +195,11 @@ Keep a compact set tuned per context:
|
||||
## Validation
|
||||
|
||||
1. Provide minimal payload and context (sink type) with before/after DOM or network evidence
|
||||
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
|
||||
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
|
||||
|
||||
## False Positives
|
||||
|
||||
@@ -185,6 +207,8 @@ 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
|
||||
|
||||
@@ -207,4 +231,4 @@ Keep a compact set tuned per context:
|
||||
|
||||
## Summary
|
||||
|
||||
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.
|
||||
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.
|
||||
@@ -535,9 +535,8 @@ 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,
|
||||
which coverage entry it is picking up. A child that is not
|
||||
told what is already known repeats it.
|
||||
recon already mapped, which surfaces are already covered.
|
||||
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.
|
||||
@@ -637,9 +636,8 @@ 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, every surface you
|
||||
assessed should have a ``record_coverage`` entry, and anything you
|
||||
could neither confirm nor rule out belongs in ``open_items`` — an
|
||||
**Close out honestly.** Before calling this, 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,7 +29,11 @@ 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))
|
||||
contents = load_skills(
|
||||
requested,
|
||||
supports_images=context.get("supports_images", True),
|
||||
coverage_tools=context.get("coverage_tools", 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()]
|
||||
|
||||
@@ -69,6 +69,15 @@ 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],
|
||||
@@ -81,16 +90,24 @@ async def existing_request_ids(
|
||||
if client is None:
|
||||
raise RuntimeError("Caido client is not available")
|
||||
|
||||
# Request IDs are not an HTTPQL field. Resolve each ID through the same
|
||||
# project-bound lookup as view_request rather than constructing a filter.
|
||||
# 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.
|
||||
existing: set[str] = set()
|
||||
for request_id in request_ids:
|
||||
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))
|
||||
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
|
||||
return existing
|
||||
|
||||
|
||||
|
||||
@@ -222,9 +222,9 @@ def _normalize_http_exchange_ids(raw: Any) -> tuple[list[str] | None, list[str]]
|
||||
return normalized, errors
|
||||
|
||||
|
||||
_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."
|
||||
_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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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 dropped and a warning is returned instead, so a proxy
|
||||
outage never blocks a finding and unverified IDs are never recorded as
|
||||
evidence.
|
||||
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.
|
||||
"""
|
||||
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 None, [], _HTTP_EXCHANGE_DROPPED_WARNING
|
||||
return request_ids, [], _HTTP_EXCHANGE_UNVERIFIED_WARNING
|
||||
|
||||
missing_ids = [request_id for request_id in request_ids if request_id not in existing_ids]
|
||||
if missing_ids:
|
||||
|
||||
@@ -104,6 +104,40 @@ def test_report_tool_replaces_create_vulnerability_report() -> None:
|
||||
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."
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""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
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Only validated model decisions may suppress a finding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from agents.items import ModelResponse
|
||||
from agents.usage import Usage
|
||||
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
|
||||
|
||||
from strix.config import loader
|
||||
from strix.report import dedupe
|
||||
from strix.report.state import ReportState, get_global_report_state, set_global_report_state
|
||||
from strix.tools.reporting.tool import _do_create
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def report_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[ReportState]:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
previous = get_global_report_state()
|
||||
state = ReportState(run_name="dedupe-decisions")
|
||||
set_global_report_state(state)
|
||||
state.add_vulnerability_report(title="Existing finding", severity="low")
|
||||
try:
|
||||
yield state
|
||||
finally:
|
||||
set_global_report_state(previous)
|
||||
|
||||
|
||||
def _mock_judge(monkeypatch: pytest.MonkeyPatch, decision: dict[str, Any]) -> AsyncMock:
|
||||
settings = loader.load_settings().model_copy(deep=True)
|
||||
settings.llm.model = "openai/synthetic-model"
|
||||
model = AsyncMock()
|
||||
model.get_response.return_value = ModelResponse(
|
||||
output=[
|
||||
ResponseOutputMessage(
|
||||
id="msg_synthetic",
|
||||
type="message",
|
||||
role="assistant",
|
||||
status="completed",
|
||||
content=[
|
||||
ResponseOutputText(
|
||||
type="output_text",
|
||||
annotations=[],
|
||||
text=json.dumps({"confidence": 0.95, "reason": "Synthetic", **decision}),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
usage=Usage(),
|
||||
response_id="synthetic",
|
||||
)
|
||||
monkeypatch.setattr(dedupe, "load_settings", lambda: settings)
|
||||
monkeypatch.setattr(dedupe, "configure_sdk_model_defaults", lambda _: None)
|
||||
monkeypatch.setattr(dedupe, "resolve_dedupe_model", lambda *_: model)
|
||||
return model
|
||||
|
||||
|
||||
async def _file_finding() -> dict[str, Any]:
|
||||
return await _do_create(
|
||||
title="New finding",
|
||||
description="The endpoint reflects unencoded input.",
|
||||
impact="The reflected script executes in a victim's browser.",
|
||||
target="https://example.invalid",
|
||||
technical_analysis="Input is interpolated into HTML without encoding.",
|
||||
poc_description="Open the endpoint with the reflected parameter.",
|
||||
poc_script_code="print('synthetic reproduction')",
|
||||
remediation_steps="Encode output for its HTML context.",
|
||||
evidence="The synthetic response contains the unencoded parameter.",
|
||||
assumptions="A victim follows the crafted link.",
|
||||
counterevidence="No encoding or content security policy is present.",
|
||||
confidence="high",
|
||||
severity_change_conditions="A strict content security policy would lower the impact.",
|
||||
fix_effort="low",
|
||||
cvss_breakdown={
|
||||
"attack_vector": "N",
|
||||
"attack_complexity": "L",
|
||||
"privileges_required": "N",
|
||||
"user_interaction": "R",
|
||||
"scope": "U",
|
||||
"confidentiality": "L",
|
||||
"integrity": "L",
|
||||
"availability": "N",
|
||||
},
|
||||
endpoint="/search",
|
||||
method="GET",
|
||||
cve=None,
|
||||
cwe="CWE-79",
|
||||
code_locations=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["false", "true", 0, 1, None, [], {}])
|
||||
def test_dedupe_parser_requires_a_boolean(value: Any) -> None:
|
||||
with pytest.raises(TypeError, match="is_duplicate"):
|
||||
dedupe._parse_dedupe_response(json.dumps({"is_duplicate": value}))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"decision",
|
||||
[
|
||||
{"is_duplicate": "false", "duplicate_id": "vuln-0001"},
|
||||
{"is_duplicate": "true", "duplicate_id": "vuln-0001"},
|
||||
{"is_duplicate": 1, "duplicate_id": "vuln-0001"},
|
||||
{"is_duplicate": True, "duplicate_id": "vuln-9999"},
|
||||
{"is_duplicate": True, "duplicate_id": ""},
|
||||
{"is_duplicate": True, "duplicate_id": None},
|
||||
{"is_duplicate": True, "duplicate_id": ["vuln-0001"]},
|
||||
{"is_duplicate": True},
|
||||
],
|
||||
)
|
||||
async def test_invalid_dedupe_decision_does_not_suppress_finding(
|
||||
report_state: ReportState, monkeypatch: pytest.MonkeyPatch, decision: dict[str, Any]
|
||||
) -> None:
|
||||
model = _mock_judge(monkeypatch, decision)
|
||||
|
||||
result = await _file_finding()
|
||||
|
||||
assert result["success"] is True
|
||||
model.get_response.assert_awaited_once()
|
||||
saved = json.loads((report_state.get_run_dir() / "vulnerabilities.json").read_text())
|
||||
assert [report["title"] for report in saved] == ["Existing finding", "New finding"]
|
||||
assert (report_state.get_run_dir() / "vulnerabilities" / "vuln-0002.md").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_duplicate", [False, True])
|
||||
async def test_valid_dedupe_decisions_keep_their_meaning(
|
||||
report_state: ReportState, monkeypatch: pytest.MonkeyPatch, is_duplicate: bool
|
||||
) -> None:
|
||||
# An extraneous ID must not turn a valid false decision into a duplicate.
|
||||
_mock_judge(monkeypatch, {"is_duplicate": is_duplicate, "duplicate_id": "vuln-0001"})
|
||||
|
||||
result = await _file_finding()
|
||||
|
||||
assert result["success"] is not is_duplicate
|
||||
saved = json.loads((report_state.get_run_dir() / "vulnerabilities.json").read_text())
|
||||
assert len(saved) == (1 if is_duplicate else 2)
|
||||
if is_duplicate:
|
||||
assert result["duplicate_of"] == "vuln-0001"
|
||||
|
||||
|
||||
async def test_dedupe_id_is_matched_without_truncation(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
existing_id = "x" * 64
|
||||
_mock_judge(monkeypatch, {"is_duplicate": True, "duplicate_id": existing_id + "-unknown"})
|
||||
|
||||
result = await dedupe.check_duplicate(
|
||||
{"title": "New finding"}, [{"id": existing_id, "title": "Existing finding"}]
|
||||
)
|
||||
|
||||
assert result["is_duplicate"] is False
|
||||
assert "error" in result
|
||||
@@ -0,0 +1,32 @@
|
||||
"""``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"]
|
||||
+68
-1
@@ -2,11 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agents.usage import Usage
|
||||
|
||||
from strix.config import loader
|
||||
from strix.core.agents import AgentCoordinator
|
||||
from strix.core.hooks import (
|
||||
BudgetExceededError,
|
||||
@@ -15,6 +18,11 @@ from strix.core.hooks import (
|
||||
SubagentBudgetReservedError,
|
||||
recomputed_budget_flags,
|
||||
)
|
||||
from strix.report.usage import LLMUsageLedger
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _make_hooks(max_budget: float | None) -> ReportUsageHooks:
|
||||
@@ -85,6 +93,38 @@ async def test_over_budget_raises() -> None:
|
||||
await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manual_rates_make_unpriced_model_trip_budget(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "test-model")
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", "10")
|
||||
monkeypatch.delenv("STRIX_LLM_OUTPUT_COST_PER_MTOK", raising=False)
|
||||
monkeypatch.delenv("STRIX_LLM_CACHED_INPUT_COST_PER_MTOK", raising=False)
|
||||
monkeypatch.setattr(loader, "_override", tmp_path / "missing.json")
|
||||
monkeypatch.setattr(loader, "_cached", None)
|
||||
usage = Usage()
|
||||
usage.requests = 1
|
||||
usage.input_tokens = 1000
|
||||
usage.total_tokens = 1000
|
||||
ledger = LLMUsageLedger()
|
||||
|
||||
class ReportState:
|
||||
def record_sdk_usage(self, **kwargs: Any) -> None:
|
||||
ledger.record(**kwargs)
|
||||
|
||||
def get_total_llm_cost(self) -> float:
|
||||
return ledger.total_cost
|
||||
|
||||
hooks = _make_hooks(0.005)
|
||||
response: Any = SimpleNamespace(usage=usage)
|
||||
with (
|
||||
patch("strix.core.hooks.get_global_report_state", return_value=ReportState()),
|
||||
pytest.raises(BudgetExceededError),
|
||||
):
|
||||
await hooks.on_llm_end(_make_context(), MagicMock(), response)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_budget_check_uses_live_cost_accessor() -> None:
|
||||
# The check must read the live ledger, not the persisted run-record snapshot,
|
||||
@@ -507,3 +547,30 @@ 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,6 +314,12 @@ 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)
|
||||
|
||||
@@ -468,6 +474,11 @@ def test_openrouter_attribution_rides_on_the_request_headers() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_vercel_ai_gateway_attribution_rides_on_the_request_headers() -> None:
|
||||
headers = make_model_settings(None, model_name="vercel_ai_gateway/zai/glm-5.3").extra_headers
|
||||
assert headers == {"HTTP-Referer": "https://strix.ai", "X-Title": "Strix"}
|
||||
|
||||
|
||||
def test_openrouter_attribution_absent_for_other_providers() -> None:
|
||||
assert make_model_settings(None, model_name="anthropic/claude-sonnet-4-5").extra_headers is None
|
||||
|
||||
|
||||
@@ -1,14 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
|
||||
import litellm
|
||||
import pytest
|
||||
from agents.usage import Usage
|
||||
from pydantic import ValidationError
|
||||
|
||||
from strix.config import loader
|
||||
from strix.config.settings import LlmSettings
|
||||
from strix.report.pricing import resolve_litellm_model
|
||||
from strix.report.usage import LLMUsageLedger
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PRICE_ENV = (
|
||||
"STRIX_LLM_INPUT_COST_PER_MTOK",
|
||||
"STRIX_LLM_OUTPUT_COST_PER_MTOK",
|
||||
"STRIX_LLM_CACHED_INPUT_COST_PER_MTOK",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_settings(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
for name in PRICE_ENV:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setenv("STRIX_LLM", "gateway/unpriced-model")
|
||||
monkeypatch.setattr(loader, "_override", tmp_path / "missing.json")
|
||||
monkeypatch.setattr(loader, "_cached", None)
|
||||
|
||||
|
||||
def _usage(input_tokens: int, output_tokens: int = 0, cached_tokens: int = 0) -> Usage:
|
||||
usage = Usage()
|
||||
usage.requests = 1
|
||||
usage.input_tokens = input_tokens
|
||||
usage.output_tokens = output_tokens
|
||||
usage.total_tokens = input_tokens + output_tokens
|
||||
usage.input_tokens_details.cached_tokens = cached_tokens
|
||||
return usage
|
||||
|
||||
|
||||
def test_resolves_common_bare_model_names() -> None:
|
||||
resolve_litellm_model.cache_clear()
|
||||
assert resolve_litellm_model("deepseek-v4-flash") == "deepseek/deepseek-v4-flash"
|
||||
@@ -60,6 +96,112 @@ def test_ledger_prefers_observed_cost_over_estimate() -> None:
|
||||
assert ledger.total_cost == 0.17
|
||||
|
||||
|
||||
def test_unpriced_model_without_manual_rates_costs_zero() -> None:
|
||||
ledger = LLMUsageLedger()
|
||||
|
||||
ledger.record(agent_id="a", usage=_usage(1000, 200), model="gateway/unpriced-model")
|
||||
|
||||
assert ledger.total_cost == 0.0
|
||||
|
||||
|
||||
def test_manual_rates_price_usage_and_override_observed_cost(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", "2")
|
||||
monkeypatch.setenv("STRIX_LLM_OUTPUT_COST_PER_MTOK", "8")
|
||||
ledger = LLMUsageLedger()
|
||||
|
||||
ledger.record(agent_id="a", usage=_usage(1000, 200), model="gateway/unpriced-model")
|
||||
ledger.record_observed_cost(0.17)
|
||||
|
||||
assert ledger.total_cost == 0.0036
|
||||
assert ledger.to_record()["agents"][0]["cost"] == 0.0036
|
||||
|
||||
|
||||
def test_manual_rates_from_config_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"env": {
|
||||
"STRIX_LLM_INPUT_COST_PER_MTOK": "2",
|
||||
"STRIX_LLM_OUTPUT_COST_PER_MTOK": "8",
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(loader, "_override", path)
|
||||
ledger = LLMUsageLedger()
|
||||
|
||||
ledger.record(agent_id="a", usage=_usage(1000, 200), model="gateway/unpriced-model")
|
||||
|
||||
assert ledger.total_cost == 0.0036
|
||||
|
||||
|
||||
def test_manual_rates_only_price_the_configured_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", "2")
|
||||
monkeypatch.setenv("STRIX_LLM_OUTPUT_COST_PER_MTOK", "8")
|
||||
ledger = LLMUsageLedger()
|
||||
|
||||
ledger.record(agent_id="a", usage=_usage(1000, 200), model="gateway/unpriced-model")
|
||||
with patch("litellm.completion_cost", return_value=0.42):
|
||||
ledger.record(agent_id="dedupe", usage=_usage(500, 50), model="deepseek/deepseek-v4-flash")
|
||||
record = ledger.to_record()
|
||||
|
||||
assert ledger.total_cost == 0.4236
|
||||
assert {agent["agent_id"]: agent["cost"] for agent in record["agents"]} == {
|
||||
"a": 0.0036,
|
||||
"dedupe": 0.42,
|
||||
}
|
||||
|
||||
|
||||
def test_cached_tokens_fall_back_to_input_rate(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", "5")
|
||||
monkeypatch.setenv("STRIX_LLM_OUTPUT_COST_PER_MTOK", "7")
|
||||
ledger = LLMUsageLedger()
|
||||
|
||||
ledger.record(
|
||||
agent_id="a", usage=_usage(10, 1, cached_tokens=2), model="gateway/unpriced-model"
|
||||
)
|
||||
|
||||
assert ledger.total_cost == 0.000057
|
||||
|
||||
|
||||
def test_hydrated_usage_uses_configured_rates_not_persisted_cost(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", "2")
|
||||
monkeypatch.setenv("STRIX_LLM_OUTPUT_COST_PER_MTOK", "8")
|
||||
original = LLMUsageLedger()
|
||||
original.record(agent_id="a", usage=_usage(1000, 200), model="gateway/unpriced-model")
|
||||
persisted = original.to_record()
|
||||
persisted["cost"] = 42.0
|
||||
persisted["agents"][0]["cost"] = 42.0
|
||||
hydrated = LLMUsageLedger()
|
||||
|
||||
hydrated.hydrate(persisted)
|
||||
|
||||
assert hydrated.total_cost == 0.0036
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["invalid", "-1", "nan", "inf"])
|
||||
def test_invalid_manual_rate_is_rejected(value: str, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", value)
|
||||
with pytest.raises(ValidationError):
|
||||
LlmSettings()
|
||||
|
||||
|
||||
def test_zero_cost_overrides_manual_rates(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM_INPUT_COST_PER_MTOK", "10")
|
||||
ledger = LLMUsageLedger()
|
||||
ledger.zero_cost = True
|
||||
|
||||
ledger.record(agent_id="a", usage=_usage(1000), model="gateway/unpriced-model")
|
||||
|
||||
assert ledger.total_cost == 0.0
|
||||
|
||||
|
||||
def test_hydrated_estimate_continues_accumulating_new_estimates() -> None:
|
||||
usage = Usage()
|
||||
usage.requests = 1
|
||||
|
||||
+45
-15
@@ -9,7 +9,6 @@ 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
|
||||
@@ -27,6 +26,7 @@ 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,20 +231,23 @@ async def test_ctx_client_degrades_when_bootstrap_failed() -> None:
|
||||
assert await tools._ctx_client(cast("Any", _Ctx({"caido_client": handle}))) is None
|
||||
|
||||
|
||||
async def test_existing_request_ids_queries_current_project(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> 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:
|
||||
client = _FakeClient("host")
|
||||
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)
|
||||
graphql = _FakeGraphQL({"1042": [{"id": "1042"}], "1088": [None]})
|
||||
client.graphql = graphql
|
||||
|
||||
existing = await tools.existing_request_ids(
|
||||
cast("Any", _Ctx({"caido_client": client})),
|
||||
@@ -252,7 +255,34 @@ async def test_existing_request_ids_queries_current_project(
|
||||
)
|
||||
|
||||
assert existing == {"1042"}
|
||||
assert looked_up == ["1042", "1088"]
|
||||
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:
|
||||
|
||||
@@ -1640,7 +1640,7 @@ async def test_http_exchange_ids_must_exist_in_current_proxy_project(
|
||||
assert warning is None
|
||||
|
||||
|
||||
async def test_http_exchange_ids_are_dropped_when_proxy_cannot_be_queried(
|
||||
async def test_http_exchange_ids_are_kept_when_proxy_cannot_be_queried(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def existing_request_ids(
|
||||
@@ -1656,11 +1656,10 @@ async def test_http_exchange_ids_are_dropped_when_proxy_cannot_be_queried(
|
||||
["1042", "1042", "1088"],
|
||||
)
|
||||
|
||||
assert request_ids is None
|
||||
assert request_ids == ["1042", "1088"]
|
||||
assert errors == []
|
||||
assert warning is not None
|
||||
assert "not stored" in warning
|
||||
assert "update_vulnerability_report" in warning
|
||||
assert "without verification" in warning
|
||||
|
||||
|
||||
async def test_create_reports_persistence_failure_as_tool_error(
|
||||
@@ -1679,7 +1678,7 @@ async def test_create_reports_persistence_failure_as_tool_error(
|
||||
assert report_state.vulnerability_reports == []
|
||||
|
||||
|
||||
async def test_evidence_only_update_reports_proxy_outage_as_retryable(
|
||||
async def test_evidence_only_update_keeps_ids_when_proxy_cannot_be_queried(
|
||||
report_state: ReportState,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -1712,11 +1711,11 @@ async def test_evidence_only_update_reports_proxy_outage_as_retryable(
|
||||
)
|
||||
result = json.loads(raw)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "No fields to update" not in result["error"]
|
||||
assert "update_vulnerability_report" in result["error"]
|
||||
assert result["success"] is True
|
||||
assert "without verification" in result["warning"]
|
||||
assert result["report_id"] == "vuln-0009"
|
||||
assert report_state.vulnerability_reports[0] == original
|
||||
assert report_state.vulnerability_reports[0]["http_exchange_ids"] == ["204"]
|
||||
assert report_state.vulnerability_reports[0] != original
|
||||
|
||||
|
||||
def test_update_reports_persistence_failure_as_tool_error(report_state: ReportState) -> None:
|
||||
|
||||
@@ -25,6 +25,7 @@ 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,
|
||||
|
||||
@@ -41,6 +41,7 @@ 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,
|
||||
|
||||
@@ -39,6 +39,7 @@ 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,
|
||||
|
||||
@@ -61,12 +61,14 @@ 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)
|
||||
@@ -160,6 +162,36 @@ 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,
|
||||
@@ -285,6 +317,19 @@ 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,6 +49,7 @@ 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,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -282,3 +283,16 @@ 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
|
||||
@@ -9,6 +9,10 @@ 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"
|
||||
@@ -20,7 +24,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.14.3"
|
||||
version = "3.14.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -32,90 +36,132 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -747,11 +793,11 @@ websockets = [
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.11"
|
||||
version = "3.2.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2659,84 +2705,118 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.24.2"
|
||||
version = "1.25.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user