Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8195e9753d | |||
| b9994e2e0e | |||
| 0fb005c73f | |||
| e1940769de | |||
| 9f278b9a5c |
@@ -169,6 +169,9 @@ strix --target https://your-app.com --instruction "Perform authenticated testing
|
|||||||
# Multi-target testing (source code + deployed app)
|
# Multi-target testing (source code + deployed app)
|
||||||
strix -t https://github.com/org/app -t https://your-app.com
|
strix -t https://github.com/org/app -t https://your-app.com
|
||||||
|
|
||||||
|
# Targets from a file, one target per non-empty, non-comment line
|
||||||
|
strix --target-list ./targets.txt
|
||||||
|
|
||||||
# White-box source-aware scan (local repository)
|
# White-box source-aware scan (local repository)
|
||||||
strix --target ./app-directory --scan-mode standard
|
strix --target ./app-directory --scan-mode standard
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,24 @@ if [ ! -f /app/certs/ca.p12 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Caido enforces a Host allowlist (DNS-rebinding protection) and rejects requests
|
||||||
|
# whose Host header is a hostname it doesn't recognize. To reach Caido over a
|
||||||
|
# hostname (rather than an IP literal), set STRIX_CAIDO_ALLOWED_DOMAINS to a
|
||||||
|
# comma-separated list of hostnames to allow. Unset by default.
|
||||||
|
# See https://docs.caido.io/app/guides/domain_allowlist
|
||||||
|
CAIDO_UI_DOMAIN_ARGS=()
|
||||||
|
if [ -n "${STRIX_CAIDO_ALLOWED_DOMAINS:-}" ]; then
|
||||||
|
IFS=',' read -ra _caido_domains <<< "${STRIX_CAIDO_ALLOWED_DOMAINS}"
|
||||||
|
for _d in "${_caido_domains[@]}"; do
|
||||||
|
[ -n "$_d" ] && CAIDO_UI_DOMAIN_ARGS+=(--ui-domain "$_d")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
caido-cli --listen 0.0.0.0:${CAIDO_PORT} \
|
caido-cli --listen 0.0.0.0:${CAIDO_PORT} \
|
||||||
--allow-guests \
|
--allow-guests \
|
||||||
--no-logging \
|
--no-logging \
|
||||||
--no-open \
|
--no-open \
|
||||||
|
"${CAIDO_UI_DOMAIN_ARGS[@]}" \
|
||||||
--import-ca-cert /app/certs/ca.p12 \
|
--import-ca-cert /app/certs/ca.p12 \
|
||||||
--import-ca-cert-pass "" > "$CAIDO_LOG" 2>&1 &
|
--import-ca-cert-pass "" > "$CAIDO_LOG" 2>&1 &
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ title: "AWS Bedrock"
|
|||||||
description: "Configure Strix with models via AWS Bedrock"
|
description: "Configure Strix with models via AWS Bedrock"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Bedrock requires the AWS SDK dependency. Install Strix with the bedrock extra:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install "strix-agent[bedrock]"
|
||||||
|
```
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ strix --target https://your-app.com
|
|||||||
|
|
||||||
# Multiple targets (white-box testing)
|
# Multiple targets (white-box testing)
|
||||||
strix -t https://github.com/org/repo -t https://your-app.com
|
strix -t https://github.com/org/repo -t https://your-app.com
|
||||||
|
|
||||||
|
# Targets from a file, one target per non-empty, non-comment line
|
||||||
|
strix --target-list ./targets.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|||||||
+10
-3
@@ -6,13 +6,17 @@ description: "Command-line options for Strix"
|
|||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
strix --target <target> [options]
|
strix (--target <target> | --target-list <path> | --mount <path>) [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
<ParamField path="--target, -t" type="string" required>
|
<ParamField path="--target, -t" type="string">
|
||||||
Target to test. Accepts URLs, repositories, local directories, domains, or IP addresses. Can be specified multiple times.
|
Target to test. Accepts URLs, repositories, local directories, domains, or IP addresses. Can be specified multiple times. Fresh runs require at least one target source: `--target`, `--target-list`, or `--mount`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="--target-list" type="string">
|
||||||
|
Path to a file containing targets, one per non-empty, non-comment line. Lines starting with `#` are ignored. Can be specified multiple times and combined with `--target`.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="--mount" type="string">
|
<ParamField path="--mount" type="string">
|
||||||
@@ -101,6 +105,9 @@ strix -n --target ./ --scan-mode quick --scope-mode diff --diff-base origin/main
|
|||||||
# Multi-target white-box testing
|
# Multi-target white-box testing
|
||||||
strix -t https://github.com/org/app -t https://staging.example.com
|
strix -t https://github.com/org/app -t https://staging.example.com
|
||||||
|
|
||||||
|
# Targets from a file
|
||||||
|
strix --target-list ./targets.txt
|
||||||
|
|
||||||
# Large local repository — bind-mount instead of copying it in
|
# Large local repository — bind-mount instead of copying it in
|
||||||
strix --mount ./huge-monorepo
|
strix --mount ./huge-monorepo
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ dependencies = [
|
|||||||
"caido-sdk-client>=0.2.0",
|
"caido-sdk-client>=0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
vertex = ["google-auth>=2.0.0"]
|
||||||
|
bedrock = ["boto3>=1.28.0"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
strix = "strix.interface.main:main"
|
strix = "strix.interface.main:main"
|
||||||
|
|
||||||
|
|||||||
+52
-7
@@ -44,6 +44,7 @@ from strix.interface.utils import (
|
|||||||
infer_target_type,
|
infer_target_type,
|
||||||
is_whitebox_scan,
|
is_whitebox_scan,
|
||||||
process_pull_line,
|
process_pull_line,
|
||||||
|
read_target_list_file,
|
||||||
resolve_diff_scope_context,
|
resolve_diff_scope_context,
|
||||||
rewrite_localhost_targets,
|
rewrite_localhost_targets,
|
||||||
validate_config_file,
|
validate_config_file,
|
||||||
@@ -213,10 +214,31 @@ def check_docker_installed() -> None:
|
|||||||
logger.debug("Docker CLI present")
|
logger.debug("Docker CLI present")
|
||||||
|
|
||||||
|
|
||||||
|
def _provider_import_hint(exc: BaseException, model: str) -> str | None:
|
||||||
|
"""Return an install hint when *exc* is a missing provider dependency.
|
||||||
|
|
||||||
|
Bedrock and Vertex AI ship as optional extras: Bedrock needs ``boto3`` and
|
||||||
|
Vertex AI needs ``google-auth``. When either is absent, litellm raises an
|
||||||
|
``ImportError``/``ModuleNotFoundError`` naming the missing package. Map that
|
||||||
|
back to the matching extra so the user knows what to install. Returns
|
||||||
|
``None`` for any unrelated error.
|
||||||
|
"""
|
||||||
|
if not isinstance(exc, ImportError):
|
||||||
|
return None
|
||||||
|
message = str(exc)
|
||||||
|
model_name = model.lower()
|
||||||
|
if "boto3" in message and model_name.startswith("bedrock/"):
|
||||||
|
return 'Bedrock support is optional. Install it with: pipx install "strix-agent[bedrock]"'
|
||||||
|
if "google" in message and "vertex" in model_name:
|
||||||
|
return 'Vertex AI support is optional. Install it with: pipx install "strix-agent[vertex]"'
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def warm_up_llm() -> None:
|
async def warm_up_llm() -> None:
|
||||||
console = Console()
|
console = Console()
|
||||||
logger.info("Warming up LLM connection")
|
logger.info("Warming up LLM connection")
|
||||||
|
|
||||||
|
raw_model = ""
|
||||||
try:
|
try:
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
configure_sdk_model_defaults(settings)
|
configure_sdk_model_defaults(settings)
|
||||||
@@ -279,6 +301,9 @@ async def warm_up_llm() -> None:
|
|||||||
error_text.append("\n\n", style="white")
|
error_text.append("\n\n", style="white")
|
||||||
error_text.append("Could not establish connection to the language model.\n", style="white")
|
error_text.append("Could not establish connection to the language model.\n", style="white")
|
||||||
error_text.append("Please check your configuration and try again.\n", style="white")
|
error_text.append("Please check your configuration and try again.\n", style="white")
|
||||||
|
hint = _provider_import_hint(e, raw_model)
|
||||||
|
if hint is not None:
|
||||||
|
error_text.append(f"\n{hint}\n", style="bold yellow")
|
||||||
error_text.append(f"\nError: {e}", style="dim white")
|
error_text.append(f"\nError: {e}", style="dim white")
|
||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
@@ -310,6 +335,7 @@ def _positive_budget(value: str) -> float:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise argparse.ArgumentTypeError(f"invalid float value: {value!r}") from exc
|
raise argparse.ArgumentTypeError(f"invalid float value: {value!r}") from exc
|
||||||
import math
|
import math
|
||||||
|
|
||||||
if not math.isfinite(budget) or budget <= 0:
|
if not math.isfinite(budget) or budget <= 0:
|
||||||
raise argparse.ArgumentTypeError("must be a finite number greater than 0")
|
raise argparse.ArgumentTypeError("must be a finite number greater than 0")
|
||||||
return budget
|
return budget
|
||||||
@@ -344,6 +370,9 @@ Examples:
|
|||||||
strix --target https://github.com/user/repo --target https://example.com
|
strix --target https://github.com/user/repo --target https://example.com
|
||||||
strix --target ./my-project --target https://staging.example.com --target https://prod.example.com
|
strix --target ./my-project --target https://staging.example.com --target https://prod.example.com
|
||||||
|
|
||||||
|
# Targets from a file, one target per non-empty, non-comment line
|
||||||
|
strix --target-list ./targets.txt
|
||||||
|
|
||||||
# Custom instructions (inline)
|
# Custom instructions (inline)
|
||||||
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
||||||
|
|
||||||
@@ -367,7 +396,15 @@ Examples:
|
|||||||
action="append",
|
action="append",
|
||||||
help="Target to test (URL, repository, local directory path, domain name, or IP address). "
|
help="Target to test (URL, repository, local directory path, domain name, or IP address). "
|
||||||
"Can be specified multiple times for multi-target scans. "
|
"Can be specified multiple times for multi-target scans. "
|
||||||
"Required for fresh runs; loaded from disk when ``--resume`` is set.",
|
"Fresh runs require at least one of --target, --target-list, or --mount.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--target-list",
|
||||||
|
type=str,
|
||||||
|
action="append",
|
||||||
|
metavar="PATH",
|
||||||
|
help="Path to a file containing targets, one per non-empty, non-comment line. "
|
||||||
|
"Can be specified multiple times and combined with --target.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mount",
|
"--mount",
|
||||||
@@ -488,10 +525,11 @@ Examples:
|
|||||||
args.user_explicit_instruction = args.instruction if args.resume else None
|
args.user_explicit_instruction = args.instruction if args.resume else None
|
||||||
|
|
||||||
if args.resume:
|
if args.resume:
|
||||||
if args.target or args.mount:
|
if args.target or args.target_list or args.mount:
|
||||||
parser.error(
|
parser.error(
|
||||||
"Cannot combine --resume with --target/--mount. --resume picks up where "
|
"Cannot combine --resume with --target/--target-list/--mount. "
|
||||||
"the prior run left off, including the original target list."
|
"--resume picks up where the prior run left off, including the "
|
||||||
|
"original target list."
|
||||||
)
|
)
|
||||||
_load_resume_state(args, parser)
|
_load_resume_state(args, parser)
|
||||||
agents_path = runtime_state_dir(run_dir_for(args.resume)) / "agents.json"
|
agents_path = runtime_state_dir(run_dir_for(args.resume)) / "agents.json"
|
||||||
@@ -503,13 +541,20 @@ Examples:
|
|||||||
f"or remove --resume to start over with the same targets."
|
f"or remove --resume to start over with the same targets."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not args.target and not args.mount:
|
if not args.target and not args.target_list and not args.mount:
|
||||||
parser.error(
|
parser.error(
|
||||||
"the following arguments are required: -t/--target or --mount "
|
"the following arguments are required: -t/--target, --target-list, or --mount "
|
||||||
"(or use --resume <run_name> to continue a prior scan)"
|
"(or use --resume <run_name> to continue a prior scan)"
|
||||||
)
|
)
|
||||||
args.targets_info = []
|
args.targets_info = []
|
||||||
for target in args.target or []:
|
targets = list(args.target or [])
|
||||||
|
for target_list_path in args.target_list or []:
|
||||||
|
try:
|
||||||
|
targets.extend(read_target_list_file(target_list_path))
|
||||||
|
except ValueError as e:
|
||||||
|
parser.error(str(e))
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
try:
|
try:
|
||||||
target_type, target_dict = infer_target_type(target)
|
target_type, target_dict = infer_target_type(target)
|
||||||
|
|
||||||
|
|||||||
@@ -1131,6 +1131,34 @@ def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR09
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_target_list_file(path_str: str) -> list[str]:
|
||||||
|
"""Read scan targets from a file, one target per non-empty, non-comment line."""
|
||||||
|
if not path_str or not path_str.strip():
|
||||||
|
raise ValueError("--target-list path must not be empty.")
|
||||||
|
|
||||||
|
path = Path(path_str).expanduser()
|
||||||
|
if not path.is_file():
|
||||||
|
raise ValueError(f"Target list file '{path_str}' is not an existing file.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
targets = [
|
||||||
|
target
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines()
|
||||||
|
if (target := line.strip()) and not target.startswith("#")
|
||||||
|
]
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Target list file '{path_str}' must be valid UTF-8 text: {e!s}"
|
||||||
|
) from e
|
||||||
|
except OSError as e:
|
||||||
|
raise ValueError(f"Failed to read target list file '{path_str}': {e!s}") from e
|
||||||
|
|
||||||
|
targets = [target for target in targets if target]
|
||||||
|
if not targets:
|
||||||
|
raise ValueError(f"Target list file '{path_str}' is empty.")
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
def sanitize_name(name: str) -> str:
|
def sanitize_name(name: str) -> str:
|
||||||
sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip())
|
sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip())
|
||||||
return sanitized or "target"
|
return sanitized or "target"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from docker import errors as docker_errors # type: ignore[import-untyped, unuse
|
|||||||
from docker.models.containers import Container # type: ignore[import-untyped, unused-ignore]
|
from docker.models.containers import Container # type: ignore[import-untyped, unused-ignore]
|
||||||
from docker.types import Mount as DockerSDKMount # type: ignore[import-untyped, unused-ignore]
|
from docker.types import Mount as DockerSDKMount # type: ignore[import-untyped, unused-ignore]
|
||||||
from docker.utils import parse_repository_tag # type: ignore[import-untyped, unused-ignore]
|
from docker.utils import parse_repository_tag # type: ignore[import-untyped, unused-ignore]
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -148,6 +149,15 @@ class StrixDockerSandboxClient(DockerSandboxClient):
|
|||||||
async def delete(self, session: SandboxSession) -> SandboxSession:
|
async def delete(self, session: SandboxSession) -> SandboxSession:
|
||||||
container_id = getattr(getattr(session._inner, "state", None), "container_id", None)
|
container_id = getattr(getattr(session._inner, "state", None), "container_id", None)
|
||||||
if container_id:
|
if container_id:
|
||||||
with contextlib.suppress(docker_errors.NotFound, docker_errors.APIError):
|
# Best-effort kill: NotFound/APIError cover a gone or unhappy
|
||||||
|
# container. RequestException covers a torn-down daemon socket —
|
||||||
|
# containers.get() -> inspect_container raises requests'
|
||||||
|
# ConnectionError, which is a sibling of docker.errors.APIError
|
||||||
|
# under requests.RequestException (not a subclass), so it escapes
|
||||||
|
# an APIError-only suppress and surfaces a full traceback even
|
||||||
|
# though this teardown is meant to be best-effort.
|
||||||
|
with contextlib.suppress(
|
||||||
|
docker_errors.NotFound, docker_errors.APIError, RequestException
|
||||||
|
):
|
||||||
self.docker_client.containers.get(container_id).kill()
|
self.docker_client.containers.get(container_id).kill()
|
||||||
return await super().delete(session)
|
return await super().delete(session)
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ async def create_or_reuse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
|
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
|
||||||
host_caido_url = f"http://{caido_endpoint.host}:{caido_endpoint.port}"
|
scheme = "https" if caido_endpoint.tls else "http"
|
||||||
|
host_caido_url = f"{scheme}://{caido_endpoint.host}:{caido_endpoint.port}"
|
||||||
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
|
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
|
||||||
|
|
||||||
caido_client = await bootstrap_caido(
|
caido_client = await bootstrap_caido(
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""Tests for CLI target-list argument parsing."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
cli_main: Any = importlib.import_module("strix.interface.main")
|
||||||
|
|
||||||
|
|
||||||
|
def _stub_settings(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cli_main,
|
||||||
|
"load_settings",
|
||||||
|
lambda: SimpleNamespace(runtime=SimpleNamespace(max_local_copy_mb=1024)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_accepts_target_list_file(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_text(
|
||||||
|
"https://test1.com/\n"
|
||||||
|
"\n"
|
||||||
|
"http://test2.com:5789/\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
_stub_settings(monkeypatch)
|
||||||
|
monkeypatch.setattr(sys, "argv", ["strix", "--target-list", str(target_list), "-n"])
|
||||||
|
|
||||||
|
args = cli_main.parse_arguments()
|
||||||
|
|
||||||
|
assert [target["original"] for target in args.targets_info] == [
|
||||||
|
"https://test1.com/",
|
||||||
|
"http://test2.com:5789/",
|
||||||
|
]
|
||||||
|
assert [target["type"] for target in args.targets_info] == [
|
||||||
|
"web_application",
|
||||||
|
"web_application",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_combines_target_and_target_list(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_text("http://test2.com:5789/\n", encoding="utf-8")
|
||||||
|
_stub_settings(monkeypatch)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
["strix", "-t", "https://test1.com/", "--target-list", str(target_list)],
|
||||||
|
)
|
||||||
|
|
||||||
|
args = cli_main.parse_arguments()
|
||||||
|
|
||||||
|
assert [target["original"] for target in args.targets_info] == [
|
||||||
|
"https://test1.com/",
|
||||||
|
"http://test2.com:5789/",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_rejects_resume_with_target_list(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_text("https://test1.com/\n", encoding="utf-8")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
["strix", "--resume", "old-run", "--target-list", str(target_list)],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
cli_main.parse_arguments()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"Cannot combine --resume with --target/--target-list/--mount"
|
||||||
|
in capsys.readouterr().err
|
||||||
|
)
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""StrixDockerSandboxClient.delete() best-effort teardown.
|
||||||
|
|
||||||
|
delete() kills the sandbox container before delegating to the SDK's delete().
|
||||||
|
The kill is meant to be best-effort, but the ``contextlib.suppress`` around it
|
||||||
|
must cover the case where the docker daemon socket is already gone: then
|
||||||
|
``containers.get()`` -> ``inspect_container`` raises requests'
|
||||||
|
``ConnectionError``, which is a *sibling* of ``docker.errors.APIError`` under
|
||||||
|
``requests.RequestException`` (not a subclass), so an APIError-only suppress
|
||||||
|
would let it escape and surface a traceback on every teardown.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from agents.sandbox.sandboxes.docker import DockerSandboxClient
|
||||||
|
from docker import errors as docker_errors
|
||||||
|
from requests.exceptions import ConnectionError as RequestsConnectionError
|
||||||
|
|
||||||
|
from strix.runtime.docker_client import StrixDockerSandboxClient
|
||||||
|
|
||||||
|
|
||||||
|
def _client_with_kill_error(exc: Exception) -> StrixDockerSandboxClient:
|
||||||
|
"""A StrixDockerSandboxClient whose containers.get(...).kill() raises ``exc``."""
|
||||||
|
client = StrixDockerSandboxClient.__new__(StrixDockerSandboxClient)
|
||||||
|
docker_client = MagicMock()
|
||||||
|
docker_client.containers.get.side_effect = exc
|
||||||
|
client.docker_client = docker_client
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _session() -> object:
|
||||||
|
# delete() reads session._inner.state.container_id
|
||||||
|
return SimpleNamespace(_inner=SimpleNamespace(state=SimpleNamespace(container_id="abc123")))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"exc",
|
||||||
|
[
|
||||||
|
RequestsConnectionError("Connection aborted", FileNotFoundError(2, "No such file")),
|
||||||
|
docker_errors.NotFound("gone"),
|
||||||
|
docker_errors.APIError("unhappy"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_swallows_best_effort_kill_errors(exc):
|
||||||
|
"""A torn-down socket (ConnectionError) or a gone/unhappy container
|
||||||
|
(NotFound/APIError) during the kill must not propagate; delete() still
|
||||||
|
delegates to the SDK's delete()."""
|
||||||
|
client = _client_with_kill_error(exc)
|
||||||
|
session = _session()
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
DockerSandboxClient, "delete", new=AsyncMock(return_value=session)
|
||||||
|
) as super_delete:
|
||||||
|
result = await client.delete(session)
|
||||||
|
|
||||||
|
assert result is session
|
||||||
|
super_delete.assert_awaited_once() # teardown proceeded despite the kill error
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_does_not_swallow_unrelated_errors():
|
||||||
|
"""A programming error (e.g. ValueError) is not part of best-effort kill and
|
||||||
|
must still propagate."""
|
||||||
|
client = _client_with_kill_error(ValueError("boom"))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await client.delete(_session())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_noop_without_container_id():
|
||||||
|
"""No container_id -> no kill attempt, just delegate."""
|
||||||
|
client = StrixDockerSandboxClient.__new__(StrixDockerSandboxClient)
|
||||||
|
client.docker_client = MagicMock()
|
||||||
|
session = SimpleNamespace(_inner=SimpleNamespace(state=SimpleNamespace(container_id=None)))
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
DockerSandboxClient, "delete", new=AsyncMock(return_value=session)
|
||||||
|
) as super_delete:
|
||||||
|
await client.delete(session)
|
||||||
|
|
||||||
|
client.docker_client.containers.get.assert_not_called()
|
||||||
|
super_delete.assert_awaited_once()
|
||||||
@@ -19,6 +19,7 @@ from strix.interface.utils import (
|
|||||||
dedupe_local_targets,
|
dedupe_local_targets,
|
||||||
directory_size_bytes,
|
directory_size_bytes,
|
||||||
find_oversized_local_targets,
|
find_oversized_local_targets,
|
||||||
|
read_target_list_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -157,6 +158,66 @@ def test_build_mount_targets_info_rejects_empty_path(empty: str) -> None:
|
|||||||
build_mount_targets_info([empty])
|
build_mount_targets_info([empty])
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_target_list_file_strips_blank_lines(tmp_path: Path) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_text(
|
||||||
|
"\n"
|
||||||
|
" https://test1.com/ \n"
|
||||||
|
"\n"
|
||||||
|
"http://test2.com:5789/\n"
|
||||||
|
" \n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert read_target_list_file(str(target_list)) == [
|
||||||
|
"https://test1.com/",
|
||||||
|
"http://test2.com:5789/",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_target_list_file_ignores_comment_lines(tmp_path: Path) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_text(
|
||||||
|
"# production targets\n"
|
||||||
|
"https://test1.com/\n"
|
||||||
|
" # staging targets\n"
|
||||||
|
"http://test2.com:5789/\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert read_target_list_file(str(target_list)) == [
|
||||||
|
"https://test1.com/",
|
||||||
|
"http://test2.com:5789/",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_target_list_file_rejects_empty_file(tmp_path: Path) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_text(" \n# no targets yet\n\n", encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="is empty"):
|
||||||
|
read_target_list_file(str(target_list))
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_target_list_file_rejects_missing_path(tmp_path: Path) -> None:
|
||||||
|
with pytest.raises(ValueError, match="not an existing file"):
|
||||||
|
read_target_list_file(str(tmp_path / "missing.txt"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_target_list_file_rejects_non_utf8_file(tmp_path: Path) -> None:
|
||||||
|
target_list = tmp_path / "targets.txt"
|
||||||
|
target_list.write_bytes(b"https://test1.com/\xff\n")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="must be valid UTF-8 text"):
|
||||||
|
read_target_list_file(str(target_list))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("empty", ["", " "])
|
||||||
|
def test_read_target_list_file_rejects_empty_path(empty: str) -> None:
|
||||||
|
with pytest.raises(ValueError, match="must not be empty"):
|
||||||
|
read_target_list_file(empty)
|
||||||
|
|
||||||
|
|
||||||
def test_dedupe_keeps_distinct_targets_in_order() -> None:
|
def test_dedupe_keeps_distinct_targets_in_order() -> None:
|
||||||
targets = [
|
targets = [
|
||||||
_local_target("/a"),
|
_local_target("/a"),
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""Tests for the optional-dependency extras declared in pyproject.toml."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_dependencies() -> dict[str, list[str]]:
|
||||||
|
data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))
|
||||||
|
return data["project"]["optional-dependencies"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_vertex_extra_pins_google_auth() -> None:
|
||||||
|
extras = _optional_dependencies()
|
||||||
|
assert "vertex" in extras
|
||||||
|
assert any(req.startswith("google-auth") for req in extras["vertex"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_bedrock_extra_pins_boto3() -> None:
|
||||||
|
extras = _optional_dependencies()
|
||||||
|
assert "bedrock" in extras
|
||||||
|
assert any(req.startswith("boto3") for req in extras["bedrock"])
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Tests for the provider import-error hint helper in interface/main.py."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from strix.interface.main import _provider_import_hint
|
||||||
|
|
||||||
|
|
||||||
|
def test_bedrock_boto3_hint() -> None:
|
||||||
|
exc = ModuleNotFoundError("No module named 'boto3'")
|
||||||
|
hint = _provider_import_hint(exc, "bedrock/anthropic.claude-4-5-sonnet")
|
||||||
|
assert hint is not None
|
||||||
|
assert 'pipx install "strix-agent[' in hint
|
||||||
|
assert "bedrock" in hint
|
||||||
|
|
||||||
|
|
||||||
|
def test_vertex_google_hint() -> None:
|
||||||
|
exc = ImportError("No module named 'google'")
|
||||||
|
hint = _provider_import_hint(exc, "vertex_ai/gemini-3-pro-preview")
|
||||||
|
assert hint is not None
|
||||||
|
assert 'pipx install "strix-agent[' in hint
|
||||||
|
assert "vertex" in hint
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_import_error_returns_none() -> None:
|
||||||
|
assert _provider_import_hint(ConnectionError("boom"), "bedrock/whatever") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_unrelated_provider_returns_none() -> None:
|
||||||
|
exc = ImportError("No module named 'something'")
|
||||||
|
assert _provider_import_hint(exc, "openai/gpt-4") is None
|
||||||
@@ -244,6 +244,34 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boto3"
|
||||||
|
version = "1.43.36"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore" },
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "s3transfer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/897287e955db0f50b12fd69ef45956e4fd2c7ddb48c736872f7ea2314443/boto3-1.43.36.tar.gz", hash = "sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28", size = 112690, upload-time = "2026-06-23T02:47:14.561Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/f1/274303f52483ecf199eae6f8d9b6f5951670397ee4d72c06cfd4eb644612/boto3-1.43.36-py3-none-any.whl", hash = "sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f", size = 140031, upload-time = "2026-06-23T02:47:13.178Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "botocore"
|
||||||
|
version = "1.43.36"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jmespath" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7c/37/da9e7f6ca73ac73afd7f0bb7f238aa5daba35c081e98d7f48a7c399599c0/botocore-1.43.36.tar.gz", hash = "sha256:4cae47d1b2d426316b85a0087d9e69e048f13bc003b5177d74639fe9dfd28205", size = 15625488, upload-time = "2026-06-23T02:47:03.192Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/19/934f81592527a3f7f9b943c893e334c721a4644948642bc33885d584e9ec/botocore-1.43.36-py3-none-any.whl", hash = "sha256:3c65fdc39ed01d8dfde1e961b34038aed03c459f8ddf80717a12ac006475e49d", size = 15313630, upload-time = "2026-06-23T02:46:59.327Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "caido-sdk-client"
|
name = "caido-sdk-client"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -678,6 +706,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google-auth"
|
||||||
|
version = "2.55.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "pyasn1-modules" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/f3f4ac177c67bbee8fe8e88f2ab4f36af88c44a096e165c5217accf6e5d3/google_auth-2.55.1.tar.gz", hash = "sha256:fb2d9b730f2c9b8d326ec8d7222f21aef2ead15bf0513793d6442485d87af0a1", size = 349527, upload-time = "2026-06-25T23:39:27.182Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl", hash = "sha256:eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995", size = 252349, upload-time = "2026-06-25T23:38:52.946Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gql"
|
name = "gql"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
@@ -937,6 +978,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jmespath"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.26.0"
|
version = "4.26.0"
|
||||||
@@ -1556,6 +1606,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyasn1-modules"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pyasn1" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
@@ -1774,6 +1845,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-discovery"
|
name = "python-discovery"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
@@ -2144,6 +2227,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "s3transfer"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "82.0.1"
|
version = "82.0.1"
|
||||||
@@ -2162,6 +2257,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -2222,6 +2326,14 @@ dependencies = [
|
|||||||
{ name = "textual" },
|
{ name = "textual" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
bedrock = [
|
||||||
|
{ name = "boto3" },
|
||||||
|
]
|
||||||
|
vertex = [
|
||||||
|
{ name = "google-auth" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "bandit" },
|
{ name = "bandit" },
|
||||||
@@ -2236,9 +2348,11 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.28.0" },
|
||||||
{ name = "caido-sdk-client", specifier = ">=0.2.0" },
|
{ name = "caido-sdk-client", specifier = ">=0.2.0" },
|
||||||
{ name = "cvss", specifier = ">=3.2" },
|
{ name = "cvss", specifier = ">=3.2" },
|
||||||
{ name = "docker", specifier = ">=7.1.0" },
|
{ name = "docker", specifier = ">=7.1.0" },
|
||||||
|
{ name = "google-auth", marker = "extra == 'vertex'", specifier = ">=2.0.0" },
|
||||||
{ name = "openai-agents", extras = ["litellm"], specifier = "==0.14.6" },
|
{ name = "openai-agents", extras = ["litellm"], specifier = "==0.14.6" },
|
||||||
{ name = "pydantic", specifier = ">=2.11.3" },
|
{ name = "pydantic", specifier = ">=2.11.3" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.13.0" },
|
{ name = "pydantic-settings", specifier = ">=2.13.0" },
|
||||||
@@ -2246,6 +2360,7 @@ requires-dist = [
|
|||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "textual", specifier = ">=6.0.0" },
|
{ name = "textual", specifier = ">=6.0.0" },
|
||||||
]
|
]
|
||||||
|
provides-extras = ["vertex", "bedrock"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
Reference in New Issue
Block a user