mirror of
https://github.com/usestrix/strix.git
synced 2026-11-09 05:19:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d1a4105d2 |
No files matched your search
+14
-15
@@ -1,6 +1,3 @@
|
||||
# Built viewer bundles are generated output, not hand-edited source.
|
||||
exclude: ^strix/interface/viewer/static/assets/
|
||||
|
||||
repos:
|
||||
# Ruff for fast linting and formatting
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
@@ -12,18 +9,21 @@ repos:
|
||||
- id: ruff-format
|
||||
name: ruff-format
|
||||
|
||||
# MyPy for static type checking. Runs the project's own mypy from the uv
|
||||
# environment (`make dev-install`) so it sees the same dependencies and
|
||||
# stubs as `make check-all`.
|
||||
- repo: local
|
||||
# MyPy for static type checking
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.17.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
entry: uv run mypy
|
||||
language: system
|
||||
types_or: [python, pyi]
|
||||
files: ^(strix|tests)/
|
||||
require_serial: true
|
||||
additional_dependencies: [
|
||||
types-requests,
|
||||
types-python-dateutil,
|
||||
pydantic,
|
||||
fastapi,
|
||||
pytest,
|
||||
hatchling,
|
||||
"openai-agents[litellm]>=0.19.0,<0.20",
|
||||
]
|
||||
args: [--install-types, --non-interactive]
|
||||
|
||||
# Built-in hooks for basic file checks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
@@ -62,6 +62,5 @@ ci:
|
||||
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]
|
||||
skip: []
|
||||
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="openai/gpt-5.4" # any LiteLLM model id
|
||||
export LLM_API_KEY="<key>"
|
||||
strix -n -t ./ --scan-mode quick --max-budget 10 # headless scan; always use -n
|
||||
```
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ Thank you for your interest in contributing to Strix! This guide will help you g
|
||||
|
||||
3. **Configure your LLM provider**
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
<a href="https://strix.ai"><img src="https://img.shields.io/badge/Website-strix.ai-f0f0f0?style=for-the-badge&logoColor=000000" alt="Website"></a>
|
||||
[](https://discord.gg/strix-ai)
|
||||
|
||||
<a href="https://app.strix.ai?utm_source=github&utm_medium=readme&utm_content=badge_cloud"><img src="https://img.shields.io/badge/Strix%20Cloud-app.strix.ai-2b9246?style=for-the-badge&logoColor=white" alt="Strix Cloud"></a>
|
||||
<a href="https://strix.ai/demo?utm_source=github&utm_medium=readme&utm_content=badge_demo"><img src="https://img.shields.io/badge/Try%20Strix%20Enterprise-555555?style=for-the-badge&logoColor=white" alt="Try Strix Enterprise"></a>
|
||||
|
||||
<a href="https://deepwiki.com/usestrix/strix"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||
<a href="https://github.com/usestrix/strix"><img src="https://img.shields.io/github/stars/usestrix/strix?style=flat-square" alt="GitHub Stars"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-3b82f6?style=flat-square" alt="License"></a>
|
||||
@@ -37,7 +34,7 @@
|
||||
|
||||
|
||||
> [!TIP]
|
||||
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production - [Get started with no setup required](https://app.strix.ai?utm_source=github&utm_medium=readme&utm_content=tip_ci).
|
||||
> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production - [Get started with no setup required](https://app.strix.ai).
|
||||
|
||||
---
|
||||
|
||||
@@ -85,7 +82,7 @@ Strix are autonomous AI penetration testing agents that act just like real hacke
|
||||
curl -sSL https://strix.ai/install | bash
|
||||
|
||||
# Configure your AI provider
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Run your first security assessment
|
||||
@@ -97,17 +94,9 @@ strix --target ./app-directory
|
||||
|
||||
---
|
||||
|
||||
## Ways to Run Strix
|
||||
## ☁️ Strix Platform
|
||||
|
||||
- **Open Source** - free, runs locally with Docker and your own LLM key. [Quick Start](https://docs.strix.ai/quickstart)
|
||||
- **Strix Cloud** - no setup, validated findings, one-click autofix, and PR reviews. [Run a pentest →](https://app.strix.ai?intent=pentest&utm_source=github&utm_medium=readme&utm_content=table_cloud)
|
||||
- **Enterprise** - SSO, compliance-ready reports, VPC or self-hosted deployment. [Try Strix Enterprise →](https://strix.ai/demo?utm_source=github&utm_medium=readme&utm_content=table_demo)
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Strix Cloud
|
||||
|
||||
Try the Strix full-stack penetration testing platform at **[app.strix.ai](https://app.strix.ai?utm_source=github&utm_medium=readme&utm_content=cloud_heading)** - sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
Try the Strix full-stack penetration testing platform at **[app.strix.ai](https://app.strix.ai)** - sign up for free, connect your repos and domains, and launch a pentest in minutes.
|
||||
|
||||
- **Validated findings with PoCs** - every vulnerability includes a working proof-of-concept exploit and reproduction steps
|
||||
- **One-click autofix** - AI-generated security patches as ready-to-merge pull requests
|
||||
@@ -115,13 +104,7 @@ Try the Strix full-stack penetration testing platform at **[app.strix.ai](https:
|
||||
- **DevSecOps integrations** - GitHub, GitLab, Bitbucket, Slack, Jira, Linear, and CI/CD pipelines
|
||||
- **Continuous learning** - AI that builds on past findings, adapts to your codebase, and reduces false positives over time
|
||||
|
||||
[**Run a pentest →**](https://app.strix.ai?intent=pentest&utm_source=github&utm_medium=readme&utm_content=cloud_cta)
|
||||
|
||||
## 🏢 Enterprise
|
||||
|
||||
Get the same Strix experience with enterprise-grade controls: SSO (SAML/OIDC), custom compliance-ready penetration testing reports (SOC 2, ISO 27001, PCI DSS), dedicated support and SLA, custom deployment options (VPC or self-hosted), BYOK model support, and tailored AI pentesting agents optimized for your environment.
|
||||
|
||||
[**Try Strix Enterprise →**](https://strix.ai/demo?utm_source=github&utm_medium=readme&utm_content=enterprise_cta)
|
||||
[**Start your first pentest →**](https://app.strix.ai)
|
||||
|
||||
---
|
||||
|
||||
@@ -133,9 +116,7 @@ Strix is agent-ready. Give Claude Code, Cursor, Codex, or any [SKILL.md-compatib
|
||||
npx skills add usestrix/strix
|
||||
```
|
||||
|
||||
This installs nine skills for running pentests, fixing findings, and CI scanning, against code, web apps, APIs, and the OWASP Top 10. Agents can use the local CLI or the managed cloud with the same engine.
|
||||
|
||||
See [`AGENTS.md`](AGENTS.md) for the quick reference, [docs.strix.ai/llms.txt](https://docs.strix.ai/llms.txt) for the CLI, and [docs.app.strix.ai](https://docs.app.strix.ai) for the API.
|
||||
This installs nine skills: **penetration-testing-with-strix** (run headless scans and read results), **managed-pentesting-with-strix** (drive the managed [app.strix.ai](https://app.strix.ai) platform via REST — no local Docker or LLM key), **fix-security-vulnerabilities-with-strix** (remediate + re-scan to verify), **ci-security-scanning-with-strix** (PR scanning in CI), plus target-specific workflows: **application-security-testing**, **web-app-penetration-testing**, **api-security-testing**, **owasp-top-10-testing**, and **find-security-vulnerabilities-in-code**. Agents can run Strix two ways with the same engine — the open-source CLI locally, or the managed cloud when there's no local infra — and read [`AGENTS.md`](AGENTS.md) for a quick reference, [docs.strix.ai/llms.txt](https://docs.strix.ai/llms.txt) for the CLI docs, and [docs.app.strix.ai](https://docs.app.strix.ai) for the API.
|
||||
|
||||
---
|
||||
|
||||
@@ -191,9 +172,18 @@ strix view my-run-name
|
||||
strix view --host 0.0.0.0 --port 8080 --no-open
|
||||
```
|
||||
|
||||
The dashboard shows the findings, a live map of the agent team, and past runs. Nothing leaves your machine, and the UI ships prebuilt. `strix view` binds to `127.0.0.1` and prints a tokened link that grants access to the run, so share it carefully.
|
||||
`strix view` starts a lightweight local server (bound to `127.0.0.1` on a random port) and opens your browser to a private, tokened link. Nothing leaves your machine: the dashboard reads the run's files straight off disk, with no cloud account or upload required. The UI ships prebuilt with Strix, so there is no extra install and no JS build step.
|
||||
|
||||
See the [viewer documentation](https://docs.strix.ai/usage/viewer) for the options and for reaching the viewer from another machine.
|
||||
Use `--host 0.0.0.0` to make the viewer reachable from other machines. Replace `0.0.0.0` in the printed URL with the server's reachable IP or hostname. The token in that URL grants access to the selected run's scan data, history, and steering, so only share it with trusted users and restrict the port with your firewall. Requests without the token-derived session cannot read run data.
|
||||
|
||||
### What's in the dashboard
|
||||
|
||||
- **Overview**: run status, target, and a severity breakdown of everything found so far.
|
||||
- **Vulnerabilities**: each validated finding with its severity, details, and reproduction steps.
|
||||
- **Agent graph**: a live map of the multi-agent team, showing which agent is doing what.
|
||||
- **Steering**: send instructions to a live scan from the browser to redirect the agents mid-run.
|
||||
- **History**: browse past runs on this machine and jump between them.
|
||||
- **Reports**: generate a shareable report and email it to yourself or your team.
|
||||
|
||||
---
|
||||
|
||||
@@ -219,9 +209,18 @@ having to discover them by crawling. Pair the spec with the live base URL so the
|
||||
agent knows where to send traffic:
|
||||
|
||||
```bash
|
||||
# OpenAPI / Swagger file, Postman export, or a live collection by id
|
||||
# OpenAPI / Swagger file (.json / .yaml)
|
||||
strix --target ./openapi.yaml --target https://api.your-app.com
|
||||
strix --target postman://<collection-uuid> --target https://api.your-app.com
|
||||
|
||||
# Postman collection export
|
||||
strix --target ./collection.postman_collection.json --target https://api.your-app.com
|
||||
|
||||
# Postman collection pulled live by id (no manual export)
|
||||
export POSTMAN_API_KEY="PMAK-..."
|
||||
strix --target postman://<collection-uuid>
|
||||
|
||||
# ...with a Postman environment to resolve {{baseUrl}} / token variables
|
||||
strix --target "postman://<collection-uuid>?env=<environment-uuid>"
|
||||
```
|
||||
|
||||
|
||||
@@ -236,9 +235,19 @@ 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
|
||||
```
|
||||
|
||||
See the [CLI reference](https://docs.strix.ai/usage/cli) for every option, including scan modes, diff scope, instruction files, and budgets.
|
||||
# White-box source-aware scan (local repository)
|
||||
strix --target ./app-directory --scan-mode standard
|
||||
|
||||
# Focused testing with custom instructions
|
||||
strix --target api.your-app.com --instruction "Focus on business logic flaws and IDOR vulnerabilities"
|
||||
|
||||
# Provide detailed instructions through file (e.g., rules of engagement, scope, exclusions)
|
||||
strix --target api.your-app.com --instruction-file ./instruction.md
|
||||
|
||||
# Force PR diff-scope against a specific base branch
|
||||
strix -n --target ./ --scan-mode quick --scope-mode diff --diff-base origin/main
|
||||
```
|
||||
|
||||
### Headless Mode
|
||||
|
||||
@@ -278,55 +287,153 @@ jobs:
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> In CI pull request runs, Strix automatically scopes quick reviews to changed files, which is why the
|
||||
> checkout above fetches full history. See the
|
||||
> [CI/CD documentation](https://docs.strix.ai/integrations/github-actions) for the details.
|
||||
> In CI pull request runs, Strix automatically scopes quick reviews to changed files.
|
||||
> If diff-scope cannot resolve, ensure checkout uses full history (`fetch-depth: 0`) or pass
|
||||
> `--diff-base` explicitly.
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Optional
|
||||
export LLM_API_BASE="your-api-base-url" # if using a local model, e.g. Ollama, LMStudio
|
||||
export PERPLEXITY_API_KEY="your-api-key" # for search capabilities
|
||||
export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high, quick scan: medium)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Strix automatically saves your configuration to `~/.strix/cli-config.json`, so you don't have to re-enter it on every run.
|
||||
> See the [configuration reference](https://docs.strix.ai/advanced/configuration) for every environment variable.
|
||||
|
||||
#### Sign in with a ChatGPT subscription
|
||||
|
||||
Instead of a metered API key, you can run Strix on your ChatGPT Plus/Pro subscription:
|
||||
|
||||
```bash
|
||||
strix auth login chatgpt # sign in with your ChatGPT account
|
||||
strix auth login chatgpt # sign in with your ChatGPT account
|
||||
|
||||
export STRIX_LLM="chatgpt/gpt-5.4" # chatgpt/<model> runs on the subscription
|
||||
strix auth status # show the active sign-in, or logout to forget it
|
||||
strix --target ./app-directory
|
||||
|
||||
strix auth status # show the active sign-in
|
||||
strix auth logout # forget the sign-in
|
||||
```
|
||||
|
||||
#### Use the managed platform: `strix cloud`
|
||||
|
||||
Run scans on [app.strix.ai](https://app.strix.ai) from the terminal, without Docker or an LLM key:
|
||||
The `strix cloud` commands drive the managed platform ([app.strix.ai](https://app.strix.ai)) from the terminal. Sign in once with the device flow. The sign-in creates your account and workspace on first use and stores a personal API token in `~/.strix/platform-auth.json`:
|
||||
|
||||
```bash
|
||||
strix cloud login # browser sign-in, one credential per install
|
||||
strix cloud scans start --source . --yes --wait # scan local code, approving the upload
|
||||
strix cloud scans start --engagement-type live_test --domain-ids <uuid> --wait
|
||||
strix cloud vulns list --severity critical
|
||||
strix cloud login # browser approval, then workspace + scope profile
|
||||
strix cloud login --workspace "My Team" # select a workspace by name or ID
|
||||
strix cloud whoami # fast local account/workspace status
|
||||
strix cloud session # verify remote session + consent ceiling
|
||||
strix cloud logout # revoke remotely, then remove locally
|
||||
```
|
||||
|
||||
Every [REST API](https://docs.app.strix.ai) operation has a matching `strix cloud <resource> <verb>` command. Run `strix cloud` to list the resources, and add `help` to a resource to list its verbs. Output is JSON when stdout is not a terminal or when you pass `--json`. Binary downloads are the exception: redirect the raw bytes, or combine `--output FILE --json` for download metadata.
|
||||
The default **Recommended** scope preset supports normal scan work, local source uploads,
|
||||
workspace switching, and user-approved credit top-ups. It excludes credential creation;
|
||||
request `tokens:write` explicitly (or choose Full) when needed. For strict least privilege, pass an explicit list such as
|
||||
`--scopes scans:read scans:write uploads:write billing:read`. Named automation
|
||||
profiles are also available with `--scope-profile minimal|recommended|full`.
|
||||
|
||||
See the [cloud CLI documentation](https://docs.strix.ai/cloud/cli) for scopes, workspaces, billing, and source-upload options.
|
||||
Every operation of the [REST API](https://docs.app.strix.ai) has a matching command in the form `strix cloud <resource> <verb>`:
|
||||
|
||||
```bash
|
||||
strix cloud # list all resources
|
||||
strix cloud scans # run the safe default (`scans list`)
|
||||
strix cloud scans help # list the verbs of a resource
|
||||
strix cloud domains add --domain example.com --asset-type web_app
|
||||
strix cloud scans start --engagement-type live_test --domain-ids <uuid> --wait
|
||||
strix cloud scans start --source . --dry-run --show-files --json # review + capture source.archive_sha256
|
||||
SOURCE_SHA256="<reviewed source.archive_sha256>"
|
||||
strix cloud scans start --source . --approve-sha256 "$SOURCE_SHA256" --wait
|
||||
strix cloud vulns list --severity critical
|
||||
strix cloud credits # credit balance
|
||||
strix cloud billing topup --credits 20 --yes # explicitly approve agent payment after HTTP 402
|
||||
```
|
||||
|
||||
Workspaces and account setup also work from the terminal:
|
||||
|
||||
```bash
|
||||
strix cloud workspaces list # numbered list; `workspace` is also accepted
|
||||
strix cloud workspaces create --name "My Team" # admin + organizations:write
|
||||
strix cloud workspaces use 2 # switch by list number, exact name, or ID
|
||||
strix cloud session scopes # granted scopes + login ceiling
|
||||
strix cloud session scopes set minimal # narrow without another browser sign-in
|
||||
strix cloud billing subscribe --plan strix_cloud # opens the hosted checkout page
|
||||
strix cloud billing portal # opens the billing portal
|
||||
strix cloud integrations install github # opens the app installation page
|
||||
strix cloud domains verify <domain-id> # prints the DNS record to add
|
||||
```
|
||||
|
||||
The last four commands end at a person. Strix creates the link, opens the browser for an interactive terminal, and always prints the URL. The user enters the card, approves the installation, or adds the DNS record. Pass `--no-browser` to print the URL only.
|
||||
|
||||
The commands work for humans and agents: terminal output favors names, branches, lifecycle states, and numbered selectors, while redirected output (or `--json`) preserves complete machine-readable records and IDs. Human lists retain the selectors needed by follow-up commands but omit internal organization/user IDs; a selector too long for the compact table is repeated losslessly in a copyable block. Paginated lists print the next `--page` or `--offset`, and detail views preserve useful prose within a safe terminal bound; use `--json` for the complete record. Token lists distinguish API keys from named CLI device sessions. Binary downloads are the exception: intentionally redirect their raw bytes, or use `--output FILE --json` to write the file and receive structured download metadata. There are no prompts when stdin is not a terminal. Exit codes: `0` success, `1` error, `2` invalid usage, `4` authentication or plan limit, `5` payment required. `--token` and `STRIX_API_TOKEN` are stateless per-command overrides and never replace the stored sign-in; pair a CLI-session override with `--workspace-id` or `STRIX_WORKSPACE_ID`.
|
||||
|
||||
A browser sign-in creates one reusable credential per CLI installation. Logging in again on the
|
||||
same installation replaces its secret instead of accumulating keys. Workspace switches keep that
|
||||
credential and expiry, preserve the server-side scope preference, cap access by the target role,
|
||||
and can never exceed the login consent ceiling. Each process pins its starting workspace, so a
|
||||
concurrent switch fails safely instead of sending a stale command to another organization.
|
||||
`strix cloud logout` revokes the server session before deleting the local token; use
|
||||
`--local-only` only when you deliberately cannot reach the server.
|
||||
|
||||
Write commands take request fields as flags, and every write command also accepts one JSON object with `--data`:
|
||||
|
||||
```bash
|
||||
strix cloud scans start --data '{"engagement_type":"code_review"}' # literal JSON
|
||||
strix cloud scans start --data @request.json # read a file
|
||||
cat request.json | strix cloud scans start --data - # read standard input
|
||||
```
|
||||
|
||||
For an agent or CI local-source scan, run `--dry-run --show-files --json`, review the manifest,
|
||||
and capture `source.archive_sha256`. Rerun with the same `--source`, every `--exclude`, and any
|
||||
`--include-*` selection flags, replacing `--dry-run` with `--approve-sha256 HASH`; Strix
|
||||
rebuilds the archive and refuses to upload it if the digest changed. `--yes` instead approves
|
||||
only the snapshot built in that one invocation. It is suitable for a deliberate human or
|
||||
one-shot approval, not as a digest-bound two-step agent/CI handoff.
|
||||
|
||||
The safe default honors `.gitignore` and `.strixignore` and excludes hidden paths, secret-like
|
||||
files, VCS metadata, dependencies/build output, symlinks, and nested archives. Opt in
|
||||
separately with `--include-hidden`, `--include-sensitive`, or `--include-archives`. The client
|
||||
caps a bundle at 20,000 files, 25 MiB per file, 250 MiB expanded, and 50 MiB compressed, and
|
||||
the service independently validates the archive. Source alone infers a code review; adding a
|
||||
domain infers a live test. You can always pass `--engagement-type` explicitly.
|
||||
|
||||
Strix removes the temporary local archive after every invocation. It deletes a staged remote
|
||||
upload after a definitive scan rejection. If a network error, `5xx` response, malformed
|
||||
success response, or interruption makes the launch outcome ambiguous, it retains the upload and reports its `upload_id` with
|
||||
`launch_outcome_unknown: true`; if automatic deletion cannot be confirmed, it reports the ID
|
||||
with `cleanup_unknown: true`. Check `strix cloud scans list` before retrying. If no scan is
|
||||
linked to the retained upload, delete it with `strix cloud uploads delete UPLOAD_ID`.
|
||||
|
||||
Non-Enterprise scans consume the deterministic estimate shown for their scope (a source-only
|
||||
code review at the default `ultra` tier currently starts at 60 credits). Enterprise scans are
|
||||
plan-included and do not consume the credit wallet. Report downloads need Enterprise,
|
||||
schedules need Pro, and billing writes need an admin token. Plan blocks exit `4`; an
|
||||
insufficient credit wallet exits `5` without creating or charging a scan.
|
||||
|
||||
Enable native tab completion once per shell session:
|
||||
|
||||
```bash
|
||||
source <(strix completions zsh) # use bash instead of zsh when appropriate
|
||||
strix completions fish | source
|
||||
```
|
||||
|
||||
#### Connect your own MCP servers
|
||||
|
||||
Strix can connect to Model Context Protocol (MCP) servers you list and expose their tools to the agent during a run. Create `~/.strix/mcp-servers.json` with a JSON list of local `stdio` servers or remote `http` servers:
|
||||
Strix can connect to Model Context Protocol (MCP) servers you list and expose their tools to the agent during a run. Create `~/.strix/mcp-servers.json` with a JSON list of servers. Each entry is either a local `stdio` server that Strix launches as a subprocess, or a remote `http` server:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "local_fs",
|
||||
"transport": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
|
||||
},
|
||||
{
|
||||
"name": "github",
|
||||
"transport": "http",
|
||||
@@ -337,19 +444,20 @@ Strix can connect to Model Context Protocol (MCP) servers you list and expose th
|
||||
]
|
||||
```
|
||||
|
||||
Each server's tools are namespaced by `name`, for example `github_list_issues`. See the [MCP documentation](https://docs.strix.ai/integrations/mcp) for the full schema, tool filtering, and `stdio` servers.
|
||||
Each server's tools are namespaced by `name` (for example `local_fs_read_file`). Omit `allowed_tools` to expose every tool the server offers, or set it to a list to restrict which tools the agent can call. The file is optional, and a server that fails to connect is skipped without failing the run. You can point Strix at a different file with `STRIX_MCP_CONFIG`.
|
||||
|
||||
**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`
|
||||
- [DeepSeek V4 Pro](https://platform.deepseek.com) - `deepseek/deepseek-v4-pro`
|
||||
- [Moonshot Kimi K3](https://platform.kimi.ai) - `moonshot/kimi-k3`
|
||||
|
||||
See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models.
|
||||
|
||||
## Enterprise Pentesting
|
||||
|
||||
Get the same Strix experience with [enterprise-grade](https://strix.ai/demo) controls: SSO (SAML/OIDC), custom compliance-ready penetration testing reports (SOC 2, ISO 27001, PCI DSS), dedicated support & SLA, custom deployment options (VPC/self-hosted), BYOK model support, and tailored AI pentesting agents optimized for your environment. [Learn more](https://strix.ai/demo).
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at **[docs.strix.ai](https://docs.strix.ai)** - including detailed guides for usage, CI/CD integrations, skills, and advanced configuration.
|
||||
|
||||
@@ -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., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_API_KEY" type="string">
|
||||
@@ -19,12 +19,6 @@ 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_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`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_EXTRA_HEADERS" type="string">
|
||||
Extra HTTP headers sent on every LLM request, as a JSON object (e.g.
|
||||
`{"X-Feature-Key":"value","X-Tenant":"acme"}`). Useful for OpenAI-compatible
|
||||
@@ -86,22 +80,6 @@ affecting the agents that do the actual testing.
|
||||
API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="EXA_API_KEY" type="string">
|
||||
API key for Exa. Enables real-time web search through the Exa `/search` endpoint. Exa also powers the `web_get_contents` tool, which fetches the full text of a page through the Exa `/contents` endpoint. This is the preferred web search provider.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_WEB_SEARCH_PROVIDER" default="auto" type="string">
|
||||
Web search provider: `auto`, `perplexity`, or `exa`. With `auto`, Strix uses Exa when `EXA_API_KEY` is set, and Perplexity otherwise. Set an explicit provider to pin one when you configure both keys.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_EXA_SEARCH_TYPE" default="auto" type="string">
|
||||
Exa search mode: `auto`, `fast`, `instant`, `deep-lite`, `deep`, or `deep-reasoning`. Lower modes return results faster. Higher modes plan across more steps and take more time. This setting applies only to the Exa provider.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_EXA_NUM_RESULTS" default="5" type="integer">
|
||||
Number of Exa results to return, from `1` to `100`. Each result includes a title, a URL, and a short security-focused summary. To read a full page, the agent calls `web_get_contents` with the result URL. This setting applies only to the Exa provider.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="POSTMAN_API_KEY" type="string">
|
||||
Postman API key (`PMAK-…`). Enables fetching Postman collections by id as a target (`postman://<collection-uid>`), and Postman environments (`postman://<collection-uid>?env=<environment-uid>`) to resolve collection variables. Not needed when passing a local collection export file.
|
||||
</ParamField>
|
||||
@@ -167,7 +145,7 @@ strix --target ./app --config /path/to/config.json
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"STRIX_LLM": "openrouter/z-ai/glm-5.3",
|
||||
"STRIX_LLM": "openai/gpt-5.4",
|
||||
"LLM_API_KEY": "sk-...",
|
||||
"STRIX_REASONING_EFFORT": "high"
|
||||
}
|
||||
@@ -178,11 +156,10 @@ strix --target ./app --config /path/to/config.json
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="sk-..."
|
||||
|
||||
# Optional: Enable web search (Exa preferred, Perplexity supported)
|
||||
export EXA_API_KEY="..."
|
||||
# Optional: Enable web search
|
||||
export PERPLEXITY_API_KEY="pplx-..."
|
||||
|
||||
# Optional: Custom timeouts
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
---
|
||||
title: "Cloud CLI"
|
||||
description: "Drive app.strix.ai from the terminal with strix cloud"
|
||||
---
|
||||
|
||||
The `strix cloud` commands drive the managed platform ([app.strix.ai](https://app.strix.ai)) from the terminal. You do not need Docker or an LLM key.
|
||||
|
||||
## Sign In
|
||||
|
||||
Sign in once with the browser device flow. The sign-in creates your account and workspace on first use, and it stores a personal API token in `~/.strix/platform-auth.json`.
|
||||
|
||||
```bash
|
||||
strix cloud login # browser approval, then workspace and scope profile
|
||||
strix cloud login --workspace "My Team" # select a workspace by name or ID
|
||||
strix cloud whoami # local account and workspace status
|
||||
strix cloud session # verify the remote session and consent ceiling
|
||||
strix cloud logout # revoke remotely, then remove the local token
|
||||
```
|
||||
|
||||
A browser sign-in creates one reusable credential for each CLI installation. A second sign-in on the same installation replaces the secret instead of adding another key. `strix cloud logout` revokes the server session before it deletes the local token. Use `--local-only` when you cannot reach the server.
|
||||
|
||||
## Scopes
|
||||
|
||||
The default **Recommended** preset covers normal scan work, local source uploads, workspace switching, and user-approved credit top-ups. It excludes credential creation, so request `tokens:write` when you need it.
|
||||
|
||||
```bash
|
||||
strix cloud login --scopes scans:read scans:write uploads:write billing:read
|
||||
strix cloud login --scope-profile minimal # also accepts recommended or full
|
||||
strix cloud session scopes # granted scopes and the login ceiling
|
||||
strix cloud session scopes set minimal # narrow without another browser sign-in
|
||||
```
|
||||
|
||||
A workspace switch keeps the credential and its expiry, preserves the server-side scope preference, and caps access by the target role. A switch can never exceed the login consent ceiling. Each process pins the workspace it started with, so a concurrent switch fails safely instead of sending a stale command to another organization.
|
||||
|
||||
## Commands
|
||||
|
||||
Every operation of the [REST API](https://docs.app.strix.ai) has a matching command in the form `strix cloud <resource> <verb>`.
|
||||
|
||||
```bash
|
||||
strix cloud # list all resources
|
||||
strix cloud scans # run the safe default (scans list)
|
||||
strix cloud scans help # list the verbs of a resource
|
||||
strix cloud domains add --domain example.com --asset-type web_app
|
||||
strix cloud scans start --engagement-type live_test --domain-ids <uuid> --wait
|
||||
strix cloud vulns list --severity critical
|
||||
strix cloud credits # credit balance
|
||||
```
|
||||
|
||||
Write commands take request fields as flags. Every write command also accepts one JSON object with `--data`:
|
||||
|
||||
```bash
|
||||
strix cloud scans start --data '{"engagement_type":"code_review"}' # literal JSON
|
||||
strix cloud scans start --data @request.json # read a file
|
||||
cat request.json | strix cloud scans start --data - # read standard input
|
||||
```
|
||||
|
||||
`--token` and `STRIX_API_TOKEN` are stateless overrides for a single command, and they never replace the stored sign-in. Pair a CLI-session override with `--workspace-id` or `STRIX_WORKSPACE_ID`.
|
||||
|
||||
## Workspaces And Account Setup
|
||||
|
||||
```bash
|
||||
strix cloud workspaces list # numbered list; workspace is also accepted
|
||||
strix cloud workspaces create --name "My Team" # needs admin and organizations:write
|
||||
strix cloud workspaces use 2 # switch by list number, exact name, or ID
|
||||
strix cloud billing topup --credits 20 --yes # approve an agent payment after HTTP 402
|
||||
strix cloud billing subscribe --plan strix_cloud # opens the hosted checkout page
|
||||
strix cloud billing portal # opens the billing portal
|
||||
strix cloud integrations install github # opens the app installation page
|
||||
strix cloud domains verify <domain-id> # prints the DNS record to add
|
||||
```
|
||||
|
||||
The last four commands end at a person. Strix creates the link, opens the browser for an interactive terminal, and always prints the URL. The user enters the card, approves the installation, or adds the DNS record. Pass `--no-browser` to print the URL only.
|
||||
|
||||
## Output And Exit Codes
|
||||
|
||||
The commands work for people and for agents. Terminal output favors names, branches, lifecycle states, and numbered selectors. Redirected output, and `--json`, preserve the complete machine-readable record.
|
||||
|
||||
- Human lists keep the selectors that follow-up commands need, and they omit internal organization and user IDs. A selector that is too long for the compact table is repeated losslessly in a copyable block.
|
||||
- Paginated lists print the next `--page` or `--offset`. Detail views keep useful prose within a safe terminal bound, so use `--json` for the complete record.
|
||||
- Token lists separate API keys from named CLI device sessions.
|
||||
- Binary downloads are the exception to JSON output. Redirect the raw bytes on purpose, or use `--output FILE --json` to write the file and receive structured download metadata.
|
||||
- There are no prompts when stdin is not a terminal.
|
||||
|
||||
Exit codes: `0` success, `1` error, `2` invalid usage, `4` authentication or plan limit, `5` payment required.
|
||||
|
||||
## Credits And Plan Limits
|
||||
|
||||
Non-Enterprise scans consume the deterministic estimate shown for their scope. A source-only code review at the default `ultra` tier currently starts at 60 credits. Enterprise scans are plan-included and do not consume the credit wallet.
|
||||
|
||||
Report downloads need Enterprise, schedules need Pro, and billing writes need an admin token. A plan block exits `4`. An insufficient credit wallet exits `5` without the creation of a scan and without a charge.
|
||||
|
||||
## Local Source Scans
|
||||
|
||||
See [Scan Local Source](/cloud/overview#scan-local-source) for the upload approval flow, the exclusion rules, and the size limits.
|
||||
|
||||
## Tab Completion
|
||||
|
||||
Enable native tab completion once for each shell session:
|
||||
|
||||
```bash
|
||||
source <(strix completions zsh) # use bash instead of zsh when appropriate
|
||||
strix completions fish | source
|
||||
```
|
||||
@@ -33,7 +33,7 @@ description: "Contribute to Strix development"
|
||||
</Step>
|
||||
<Step title="Configure LLM">
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
</Step>
|
||||
|
||||
+2
-5
@@ -25,8 +25,7 @@
|
||||
"pages": [
|
||||
"usage/cli",
|
||||
"usage/scan-modes",
|
||||
"usage/instructions",
|
||||
"usage/viewer"
|
||||
"usage/instructions"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -36,7 +35,6 @@
|
||||
"llm-providers/openai",
|
||||
"llm-providers/anthropic",
|
||||
"llm-providers/openrouter",
|
||||
"llm-providers/vercel-ai-gateway",
|
||||
"llm-providers/vertex",
|
||||
"llm-providers/bedrock",
|
||||
"llm-providers/azure",
|
||||
@@ -79,8 +77,7 @@
|
||||
{
|
||||
"group": "Strix Cloud",
|
||||
"pages": [
|
||||
"cloud/overview",
|
||||
"cloud/cli"
|
||||
"cloud/overview"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ Strix uses a graph of specialized agents for comprehensive security testing:
|
||||
curl -sSL https://strix.ai/install | bash
|
||||
|
||||
# Configure
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
|
||||
# Scan
|
||||
|
||||
@@ -37,7 +37,7 @@ Add these secrets to your repository:
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `STRIX_LLM` | Model name (e.g., `openrouter/z-ai/glm-5.3`) |
|
||||
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5.4`) |
|
||||
| `LLM_API_KEY` | API key for your LLM provider |
|
||||
|
||||
## Exit Codes
|
||||
|
||||
@@ -17,9 +17,6 @@ export LLM_API_BASE="https://api.novita.ai/openai"
|
||||
|
||||
| Model | Configuration |
|
||||
|-------|---------------|
|
||||
| GLM-5.3 | `openai/zai-org/glm-5.3` |
|
||||
| Kimi K3 | `openai/moonshotai/kimi-k3` |
|
||||
| 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` |
|
||||
|
||||
@@ -8,7 +8,7 @@ description: "Configure Strix with models via OpenRouter"
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openrouter/openai/gpt-5.4"
|
||||
export LLM_API_KEY="sk-or-..."
|
||||
```
|
||||
|
||||
@@ -18,12 +18,9 @@ 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` |
|
||||
| DeepSeek V4 Pro | `openrouter/deepseek/deepseek-v4-pro` |
|
||||
| Kimi K3 | `openrouter/moonshotai/kimi-k3` |
|
||||
| GLM-4.7 | `openrouter/z-ai/glm-4.7` |
|
||||
|
||||
## Get API Key
|
||||
|
||||
@@ -9,17 +9,14 @@ 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 |
|
||||
| ----------------- | ------------- | -------------------------------- |
|
||||
| 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` |
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
@@ -46,9 +43,6 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re
|
||||
<Card title="OpenRouter" href="/llm-providers/openrouter">
|
||||
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.
|
||||
</Card>
|
||||
<Card title="Google Vertex AI" href="/llm-providers/vertex">
|
||||
Gemini 3 models via Google Cloud.
|
||||
</Card>
|
||||
@@ -68,7 +62,6 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re
|
||||
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
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
title: "Vercel AI Gateway"
|
||||
description: "Configure Strix with models via Vercel AI Gateway"
|
||||
---
|
||||
|
||||
[Vercel AI Gateway](https://vercel.com/docs/ai-gateway) provides an OpenAI-compatible API for models from multiple providers.
|
||||
|
||||
## Setup
|
||||
|
||||
Create an [AI Gateway API key](https://vercel.com/docs/ai-gateway/authentication-and-byok), then configure Strix:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/anthropic/claude-opus-5"
|
||||
export LLM_API_KEY="your-ai-gateway-api-key"
|
||||
export LLM_API_BASE="https://ai-gateway.vercel.sh/v1"
|
||||
```
|
||||
|
||||
The first `openai/` segment tells Strix to use its OpenAI-compatible client. The remaining value is the [AI Gateway model ID](https://vercel.com/docs/ai-gateway/models-and-providers).
|
||||
|
||||
## Available Models
|
||||
|
||||
Use any language model returned by the AI Gateway models endpoint:
|
||||
|
||||
```text
|
||||
https://ai-gateway.vercel.sh/v1/models
|
||||
```
|
||||
|
||||
Prefix its model ID with `openai/` when setting `STRIX_LLM`. For example, the Gateway model ID `anthropic/claude-opus-5` becomes `openai/anthropic/claude-opus-5` in Strix.
|
||||
|
||||
## Get API Key
|
||||
|
||||
1. Open the [AI Gateway API key settings](https://vercel.com/docs/ai-gateway/authentication-and-byok)
|
||||
2. Create an API key
|
||||
3. Set the key as `LLM_API_KEY`
|
||||
|
||||
## Benefits
|
||||
|
||||
- Access models from multiple providers through one endpoint
|
||||
- Track Gateway usage and cost in Vercel
|
||||
+2
-2
@@ -28,12 +28,12 @@ description: "Install Strix and run your first security scan"
|
||||
Set your LLM provider:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openrouter/z-ai/glm-5.3"
|
||||
export STRIX_LLM="openai/gpt-5.4"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
<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 `openai/gpt-5.4`, `anthropic/claude-opus-4-6`, or `openai/gpt-5.2`.
|
||||
</Tip>
|
||||
|
||||
## Run Your First Scan
|
||||
|
||||
@@ -28,6 +28,6 @@ Strix agents use specialized tools to test your applications like a real penetra
|
||||
| -------------- | ---------------------------------------- |
|
||||
| Python Runtime | Write and execute custom exploit scripts |
|
||||
| File Editor | Read and modify source code |
|
||||
| Web Search | Real-time OSINT with Exa or Perplexity |
|
||||
| Web Search | Real-time OSINT via Perplexity |
|
||||
| Notes | Document findings during the scan |
|
||||
| Reporting | Generate vulnerability reports with PoCs |
|
||||
@@ -96,8 +96,7 @@ agent task, so the agent knows where to read them.
|
||||
|
||||
Rules that apply to every workspace file:
|
||||
|
||||
- Strix copies the file into the sandbox. The agent can edit the copy, but the
|
||||
file on your machine does not change.
|
||||
- The file is read-only inside the sandbox.
|
||||
- The destination must stay inside `/workspace`.
|
||||
- The destination must not fall inside a target directory, because target files
|
||||
come from the target itself. Strix skips such a file and logs a warning.
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: "Local Web Viewer"
|
||||
description: "Browse a run in a local dashboard with strix view"
|
||||
---
|
||||
|
||||
Every scan writes its results to disk as it runs. `strix view` serves those files in a local dashboard, for a live run or a finished one.
|
||||
|
||||
```bash
|
||||
strix view # the most recent run
|
||||
strix view my-run-name # a specific run under ./strix_runs
|
||||
strix view --host 0.0.0.0 --port 8080 --no-open
|
||||
```
|
||||
|
||||
The UI ships prebuilt with Strix, so there is no extra install and no JavaScript build step. The dashboard reads the run files straight off disk. Nothing leaves your machine, and you do not need a cloud account.
|
||||
|
||||
## Options
|
||||
|
||||
<ParamField path="run" type="string">
|
||||
Run name under `./strix_runs`. Defaults to the most recent run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="--host" type="string" default="127.0.0.1">
|
||||
Host to bind to. Use `0.0.0.0` to reach the viewer from other machines.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="--port" type="number" default="0">
|
||||
Port to serve on. The default selects an available ephemeral port.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="--no-open" type="boolean">
|
||||
Do not open the browser automatically.
|
||||
</ParamField>
|
||||
|
||||
## What Is In The Dashboard
|
||||
|
||||
- **Overview** — run status, target, and a severity breakdown of everything found so far.
|
||||
- **Vulnerabilities** — each validated finding with its severity, details, and reproduction steps.
|
||||
- **Agent graph** — a live map of the multi-agent team, and what each agent is doing.
|
||||
- **Steering** — send instructions to a live scan to redirect the agents during the run. Steering works only in the dashboard the running scan opens. A standalone `strix view` has no live scan to steer.
|
||||
- **History** — browse past runs on this machine and move between them. Verify your email address in the dashboard to unlock the other runs.
|
||||
- **Reports** — generate a shareable report and send it by email. Verify your email address first.
|
||||
|
||||
## Sharing The Link
|
||||
|
||||
<Warning>
|
||||
The token in the printed URL grants access to the run data, and to the steering of a live scan. Share it only with trusted users.
|
||||
</Warning>
|
||||
|
||||
To reach the viewer from another machine, start it with `--host 0.0.0.0` and replace `0.0.0.0` in the printed URL with a reachable IP address or hostname. Restrict the port with your firewall. A request without the token-derived session cannot read run data.
|
||||
+3
-9
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "strix-agent"
|
||||
version = "1.6.2"
|
||||
version = "1.6.0"
|
||||
description = "Open-source AI Hackers for your apps"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
@@ -35,7 +35,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"openai-agents[litellm]>=0.19.0,<0.20",
|
||||
"openai>=2.45.0,<3",
|
||||
"litellm>=1.101.0",
|
||||
"litellm",
|
||||
"pydantic>=2.11.3",
|
||||
"pydantic-settings>=2.13.0",
|
||||
"rich",
|
||||
@@ -250,7 +250,6 @@ ignore = [
|
||||
# Stdlib HTTP handler overrides (do_GET/do_POST) and lazy imports that avoid a
|
||||
# circular dependency with strix.telemetry / strix.interface.viewer.report_pdf.
|
||||
"strix/interface/viewer/server.py" = ["N802", "PLC0415"]
|
||||
"strix/interface/cloud/payment_proxy.py" = ["N802"]
|
||||
# Lazy telemetry import to avoid importing PostHog before the viewer starts.
|
||||
"strix/interface/viewer/cli.py" = ["PLC0415"]
|
||||
# Lazy imports inside functions to avoid circular dependency with
|
||||
@@ -308,9 +307,6 @@ ignore = [
|
||||
"strix/report/dedupe.py" = ["PLC0415"]
|
||||
"strix/telemetry/logging.py" = ["PLC0415"]
|
||||
"strix/config/models.py" = ["PLC0415"]
|
||||
# LiteLLM is imported lazily: the request log is wired at startup on every
|
||||
# route, including the native OpenAI ones that never load LiteLLM.
|
||||
"strix/llm/request_log.py" = ["PLC0415"]
|
||||
# Heavy inference deps (httpx, openai) imported lazily so auth-status checks
|
||||
# don't pull them in.
|
||||
"strix/config/codex.py" = ["PLC0415"]
|
||||
@@ -417,8 +413,6 @@ known_third_party = ["pydantic", "litellm"]
|
||||
# ============================================================================
|
||||
|
||||
[tool.bandit]
|
||||
# Tests are covered by ruff's flake8-bandit rules (see per-file-ignores above),
|
||||
# which is where fixture tokens and loopback URL opens are already waived.
|
||||
exclude_dirs = ["docs", "build", "dist", "tests"]
|
||||
exclude_dirs = ["docs", "build", "dist"]
|
||||
skips = ["B101", "B601", "B404", "B603", "B607"] # Skip assert, shell injection, subprocess import and partial path checks
|
||||
severity = "medium"
|
||||
@@ -346,9 +346,6 @@ echo -e "${MUTED}For more information visit ${NC}https://strix.ai"
|
||||
echo -e "${MUTED}Supported models ${NC}https://docs.strix.ai/llm-providers/overview"
|
||||
echo -e "${MUTED}Join our community ${NC}https://discord.gg/strix-ai"
|
||||
echo ""
|
||||
echo -e "${MUTED}Run a pentest in Strix Cloud ${NC}https://app.strix.ai"
|
||||
echo -e "${MUTED}Enterprise ${NC}https://strix.ai/demo"
|
||||
echo ""
|
||||
|
||||
echo -e "${YELLOW}→${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal"
|
||||
echo ""
|
||||
@@ -12,7 +12,7 @@ from typing import Any
|
||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||
|
||||
|
||||
class CustomBuildHook(BuildHookInterface): # type: ignore[type-arg]
|
||||
class CustomBuildHook(BuildHookInterface[Any]):
|
||||
"""Compile the Bubble Tea sidecar and ship it inside the wheel.
|
||||
|
||||
The sidecar is the only interactive interface, so every wheel is a
|
||||
|
||||
@@ -48,7 +48,7 @@ Out of scope: POST /billing/*, POST /notifications/broadcast."
|
||||
- **GraphQL:** point at the GraphQL endpoint and say whether introspection is enabled; call out that you want batching/aliasing abuse, depth/complexity limits, and per-field authorization tested.
|
||||
- **Internal/private APIs** unreachable from your machine: use the managed platform's network connector — see **managed-pentesting-with-strix**.
|
||||
- Use `--instruction-file` when the credential/context block gets long, and keep tokens out of shell history and out of committed files.
|
||||
- **Supporting files** the agents should read but not test, such as an endpoint wordlist or handwritten notes about the tenancy model: pass `--workspace-file ./notes.md`. Strix copies the file into `/workspace`. The file on your machine does not change. Add `:DEST` to choose the path, for example `--workspace-file ./wordlist.txt:lists/wordlist.txt`.
|
||||
- **Supporting files** the agents should read but not test, such as an endpoint wordlist or handwritten notes about the tenancy model: pass `--workspace-file ./notes.md`. The file lands read-only in `/workspace`. Add `:DEST` to choose the path, for example `--workspace-file ./wordlist.txt:lists/wordlist.txt`.
|
||||
|
||||
## 3. Verify findings
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ Key flags:
|
||||
| `-n, --non-interactive` | Headless, exits on completion. Required for agents. |
|
||||
| `-m, --scan-mode` | `quick` (minutes) / `standard` (~30 min) / `deep` (hours, default). |
|
||||
| `--instruction` / `--instruction-file` | Credentials, focus areas, scope rules. |
|
||||
| `--workspace-file PATH[:DEST]` | Copy a file from this machine into `/workspace` before the scan, for a wordlist, a spec, or notes. Repeatable. |
|
||||
| `--workspace-file PATH[:DEST]` | Place a file from this machine into `/workspace` read-only before the scan, for a wordlist, a spec, or notes. Repeatable. |
|
||||
| `--max-budget USD` | Hard LLM spend cap; scan wraps up cleanly at the limit. |
|
||||
| `--max-turns N` | Per-agent turn cap (default 500). |
|
||||
| `--resume RUN_NAME` | Resume a prior run from `strix_runs/`, with its agent history and targets. Cannot be combined with `-t`. |
|
||||
|
||||
+2
-13
@@ -29,13 +29,7 @@ from strix.tools.agents_graph.tools import (
|
||||
from strix.tools.coverage.tools import list_coverage, record_coverage, update_coverage
|
||||
from strix.tools.finish.tool import finish_scan
|
||||
from strix.tools.load_skill.tool import load_skill
|
||||
from strix.tools.mcp import (
|
||||
call_mcp,
|
||||
describe_mcp,
|
||||
get_mcp_tool_schema,
|
||||
list_mcps,
|
||||
search_mcp_tools,
|
||||
)
|
||||
from strix.tools.mcp import call_mcp, describe_mcp, list_mcps
|
||||
from strix.tools.notes.tools import (
|
||||
create_note,
|
||||
delete_note,
|
||||
@@ -56,7 +50,6 @@ from strix.tools.proxy.tools import (
|
||||
from strix.tools.reporting.tool import (
|
||||
create_dependency_report,
|
||||
create_vulnerability_report,
|
||||
delete_vulnerability_report,
|
||||
get_report,
|
||||
list_reports,
|
||||
update_vulnerability_report,
|
||||
@@ -76,7 +69,7 @@ from strix.tools.todo.tools import (
|
||||
mark_todo_pending,
|
||||
update_todo,
|
||||
)
|
||||
from strix.tools.web_search.tool import web_get_contents, web_search
|
||||
from strix.tools.web_search.tool import web_search
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -586,11 +579,9 @@ _BASE_TOOLS: tuple[Tool, ...] = (
|
||||
save_threat_model,
|
||||
amend_threat_model,
|
||||
web_search,
|
||||
web_get_contents,
|
||||
create_vulnerability_report,
|
||||
create_dependency_report,
|
||||
update_vulnerability_report,
|
||||
delete_vulnerability_report,
|
||||
list_reports,
|
||||
get_report,
|
||||
list_requests,
|
||||
@@ -600,8 +591,6 @@ _BASE_TOOLS: tuple[Tool, ...] = (
|
||||
view_sitemap_entry,
|
||||
scope_rules,
|
||||
list_mcps,
|
||||
search_mcp_tools,
|
||||
get_mcp_tool_schema,
|
||||
describe_mcp,
|
||||
call_mcp,
|
||||
view_agent_graph,
|
||||
|
||||
@@ -77,20 +77,18 @@ AUTHORIZED TARGETS:
|
||||
|
||||
{% if system_prompt_context and system_prompt_context.mcp_available %}
|
||||
MCP CONNECTIONS (available this run):
|
||||
- The user connected one or more MCP (Model Context Protocol) servers. Their individual tools do NOT appear in your tool list. Use the four discovery and dispatch tools to reach them.
|
||||
- The user connected one or more MCP (Model Context Protocol) servers — external tool providers you can reach on demand. Their individual tools do NOT appear in your tool list; three dispatch tools are the only way in.
|
||||
{% if system_prompt_context.mcp_connections %}
|
||||
- Connected this run (search one to find relevant tools):
|
||||
- Connected this run (call describe_mcp on one to see its tools):
|
||||
{% for connection in system_prompt_context.mcp_connections %}
|
||||
- {{ connection.name }} ({{ connection.tool_count }} tools){% if connection.purpose %}: {{ connection.purpose }}{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
- Reach for a connection whenever the target itself cannot give you information a connection could: its database schema and access policies, real deployment or infrastructure configuration, known issues or prior findings, or server logs. In those cases call list_mcps early to see what is available, and prefer a connection's authoritative data over inferring from the target's responses. Do not wait to be told a connection exists.
|
||||
1. Call list_mcps() to discover the available connections.
|
||||
2. Call search_mcp_tools(connection="<name>", query="<capability>") for a short candidate list.
|
||||
3. Call get_mcp_tool_schema(connection="<name>", tool="<tool>") for the one schema you need.
|
||||
4. Call call_mcp(connection="<name>", tool="<tool>", arguments={...}) to run it, passing an arguments object that matches the schema (omit arguments for a tool that takes none).
|
||||
- Do not assume a connection or tool exists; discover it with list_mcps and search_mcp_tools before calling.
|
||||
- Use describe_mcp only as a compatibility fallback when targeted search cannot identify an expected tool. Its full catalog can be large.
|
||||
2. Call describe_mcp(connection="<name>") to inspect one connection's tools, each with its name, description, and JSON input schema.
|
||||
3. Call call_mcp(connection="<name>", tool="<tool>", arguments={...}) to run one, passing an arguments object that matches the schema (omit arguments for a tool that takes none).
|
||||
- Do not assume a connection or tool exists; discover it with list_mcps and describe it with describe_mcp before calling.
|
||||
{% endif %}
|
||||
|
||||
AUTHORIZATION STATUS:
|
||||
@@ -153,9 +151,8 @@ WHITE-BOX TESTING (code provided):
|
||||
- Local execution, unit/integration testing, patch verification, and HTTP requests against locally started in-scope services are normal authorized white-box validation
|
||||
- If dynamically running the code proves impossible after exhaustive attempts, pivot to comprehensive static analysis.
|
||||
- Try to infer how to run the code based on its structure and content.
|
||||
- Draft the initial fix candidate when you file the report. Use `code_locations` with verbatim `fix_before` and `fix_after`, plus `fix_pr_body`.
|
||||
- Treat the inline changes as a candidate, not as a completed fix. A later preparation stage can inspect and modify any required repository file.
|
||||
- Record checks you ran in `fix_verification`. Do not describe reasoned checks as executed checks.
|
||||
- Derive the code fix as PART OF reporting, not as a separate later pass: create_vulnerability_report already requires the concrete patch inline (`code_locations` with verbatim `fix_before`/`fix_after` and `fix_pr_body`), so the reporting agent that analyzes the root cause is the one that produces the fix. Do NOT spawn a downstream agent afterwards to re-derive/re-apply the same patch.
|
||||
- If you also apply and verify the patch in the repo (edit the file, re-test that the vulnerability is gone), do it in the same agent/turn while the analysis is fresh — right before or as part of filing the report — never as a second re-analysis pass.
|
||||
|
||||
COMBINED MODE (code + deployed target present):
|
||||
- Treat this as static analysis plus dynamic testing simultaneously
|
||||
@@ -241,13 +238,9 @@ VALIDATION REQUIREMENTS:
|
||||
- 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 create_vulnerability_report (or create_dependency_report for known-CVE dependency/supply-chain findings) with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
|
||||
- When source is available, the reporting agent files an initial fix candidate with the report. The candidate uses `code_locations` with `fix_before` and `fix_after`, plus `fix_pr_body`.
|
||||
- Do not treat the candidate as a prepared fix. The fix preparation stage applies, repairs, tests, and independently verifies it after finding discovery closes.
|
||||
- Do not silently patch a finding without filing a report.
|
||||
- Reporting and fixing are ONE step, not two: when source is available, the reporting agent derives the concrete fix and files it INLINE via create_vulnerability_report (`code_locations` with `fix_before`/`fix_after` + `fix_pr_body`) — the report is not complete without it. Do NOT report first and then spawn a separate downstream agent to re-derive and re-apply the same patch; that just re-does the analysis and wastes tokens. (Do not silently patch a finding WITHOUT filing a report — the report, with its embedded fix, is the deliverable.)
|
||||
- DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent. If your evidence proves more than the finding it matched (a working exploit where that one had only a static trace, a chain that raises the impact), revise that finding with update_vulnerability_report using the duplicate_of id — never re-file it.
|
||||
- HTTP EVIDENCE: a finding you validated through the proxy is not fully filed until `http_exchange_ids` carries the proxy request ids of the exchanges that prove it — the request that triggers the vulnerability plus the baseline/control request it differs from (an unauthenticated success next to the authenticated one, the payload response next to the benign one). Copy the ids exactly as `list_requests`/`view_request` show them, never invent or guess one, and never omit the field to bypass validation. Leave it out only when there is no captured HTTP exchange at all (static-only code findings, dependency CVEs). If you filed before the proving exchanges existed, attach them afterwards with update_vulnerability_report. Without the ids, the finding ships as prose nobody can replay.
|
||||
- REVISING A FINDING: use update_vulnerability_report (report id + the fields you want to replace + update_reason) when you learn something a finding already on file does not carry — you built the PoC after filing it, a chain raised its impact, further testing weakened it, or its counterevidence/remediation/code locations were wrong. Editing a finding needs no duplicate verdict, and it is always better than filing a second report for the same issue. Read the finding first with get_report, and pass only the fields that change.
|
||||
- WITHDRAWING A FINDING: when re-testing proves a finding you filed is not a vulnerability at all — the exploit only worked because of your own test setup (a mixed-up session, a self-inflicted state change, a misread response), the behaviour is intended, or the control is in fact enforced — delete it with delete_vulnerability_report (report id + delete_reason). Never leave a disproved finding on file as a zero-CVSS, "retracted", or "false positive" report, and never rewrite its text into a note saying it was wrong: a report that exists is read as a finding. Delete is for a finding that does not exist; a finding that is real but weaker than filed is revised, not deleted.
|
||||
- REVIEWING FILED FINDINGS (orchestrator/root agent): use list_reports to see every vulnerability filed so far in this scan (by any agent, root or child) — metadata-first with per-severity counts — and get_report to read one finding in full by its id. These are read-only orchestration tools: the root agent uses them to track coverage, avoid dispatching work on already-covered ground, assemble the finish_scan executive summary, and reason about attack-chaining across confirmed findings. Leaf/specialist agents should NOT call them — just do your assigned testing and file findings. Each entry shows which agent filed it (agent_name), and your own entries are flagged by_you. list_notes/get_note do the same for notes.
|
||||
|
||||
STATE & COORDINATION TOOLS (when and how):
|
||||
@@ -363,7 +356,7 @@ ROOT AGENT ROLE:
|
||||
|
||||
1. **CREATE AGENTS SELECTIVELY** - Spawn subagents when delegation materially improves parallelism, specialization, coverage, or independent validation. Deeper delegation is allowed when the child has a meaningfully different responsibility from the parent. Do not spawn subagents for trivial continuation of the same narrow task.
|
||||
2. **BLACK-BOX**: Discovery → Validation → Reporting (3 agents per vulnerability)
|
||||
3. **WHITE-BOX**: Discovery → Validation → Reporting with an initial fix candidate. The later preparation phase repairs and verifies the candidate.
|
||||
3. **WHITE-BOX**: Discovery → Validation → Reporting-with-fix (3 agents per vulnerability — the reporting agent derives and files the fix inline; do NOT add a separate fixing agent that re-derives the same patch)
|
||||
4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain
|
||||
5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces
|
||||
6. **ONE JOB PER AGENT** - Each agent has ONE specific task only
|
||||
@@ -382,7 +375,7 @@ BLACK-BOX (domain/URL only):
|
||||
WHITE-BOX (source code provided):
|
||||
- Found authentication code issues? → Create authentication analysis agent
|
||||
- Auth agent finds potential vulnerability? → Create "Auth Validation Agent"
|
||||
- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" that files the report and an initial fix candidate (`code_locations` + `fix_pr_body`)
|
||||
- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" that files the report AND its inline fix (`code_locations` + `fix_pr_body`) in one shot — no separate fixing agent
|
||||
|
||||
VULNERABILITY WORKFLOW (MANDATORY FOR EVERY FINDING):
|
||||
|
||||
@@ -404,10 +397,10 @@ Authentication Code Agent finds weak password validation
|
||||
Spawns "Auth Validation Agent" (proves it's exploitable)
|
||||
↓
|
||||
If valid → Spawns "Auth Reporting Agent" (creates the vulnerability report
|
||||
with the initial fix candidate: code_locations fix_before/fix_after
|
||||
+ fix_pr_body)
|
||||
WITH the fix inline: code_locations fix_before/fix_after + fix_pr_body,
|
||||
applying/verifying the patch in the same turn if desired)
|
||||
↓
|
||||
STOP - the scan preparation phase handles repository-wide repair and verification
|
||||
STOP - no separate fixing agent; the fix was derived once, at report time
|
||||
```
|
||||
|
||||
CRITICAL RULES:
|
||||
@@ -443,7 +436,7 @@ FOCUS PRINCIPLES:
|
||||
REALISTIC TESTING OUTCOMES:
|
||||
- **No Findings**: Agent completes testing but finds no vulnerabilities
|
||||
- **Validation Failed**: Initial finding was false positive, validation agent confirms it's not exploitable
|
||||
- **Valid Vulnerability**: Validation succeeds. The reporting agent files the report and an initial fix candidate for white-box findings.
|
||||
- **Valid Vulnerability**: Validation succeeds, spawns a reporting agent that files the report with the fix inline (white-box) — no separate fixing agent
|
||||
|
||||
PERSISTENCE IS MANDATORY:
|
||||
- Real vulnerabilities take TIME - expect to need 2000+ steps minimum
|
||||
@@ -500,8 +493,6 @@ SPECIALIZED TOOLS:
|
||||
PROXY & INTERCEPTION:
|
||||
- Caido CLI - Modern web proxy (already running). Use the proxy tools
|
||||
directly, or import `caido_api` from sandbox Python scripts.
|
||||
- Every proxied exchange has a request id (`list_requests`/`view_request`). Note the ids of the
|
||||
requests that prove a finding as you test — they go into `http_exchange_ids` when you report it.
|
||||
- HTTPQL filters (for `list_requests`): quote string values, leave integers unquoted (`resp.code.eq:200`, not `"200"`); combine terms with `AND`/`OR` (there is no `NOT` — use the negated operator `ne`/`ncont`/`nregex`). Numeric fields (`resp.code`, `req.port`) use `eq`/`ne`/`gt`/`gte`/`lt`/`lte`; text fields (`req.host`, `req.path`, `req.method`, `req.raw`) use `cont`/`ncont`/`eq`/`regex`. Example: `resp.code.gte:200 AND resp.code.lt:300 AND req.host.cont:"api"`.
|
||||
|
||||
CAIDO PROXY ERROR PAGES — NOT RESPONSES FROM THE TARGET:
|
||||
|
||||
@@ -183,7 +183,7 @@ def build_authorize_url(challenge: str, state: str) -> str:
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "S256",
|
||||
"state": state,
|
||||
"id_token_add_organizations": "true", # nosec B105 - boolean flag, not a secret
|
||||
"id_token_add_organizations": "true",
|
||||
"codex_cli_simplified_flow": "true",
|
||||
"originator": ORIGINATOR,
|
||||
}
|
||||
@@ -356,8 +356,6 @@ def build_openai_client() -> AsyncOpenAI:
|
||||
import httpx
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from strix.llm import request_log
|
||||
|
||||
get_valid_token() # fail fast at configure time if the sign-in is dead
|
||||
|
||||
async def _auth_hook(request: httpx.Request) -> None:
|
||||
@@ -369,7 +367,6 @@ def build_openai_client() -> AsyncOpenAI:
|
||||
timeout=httpx.Timeout(600.0, connect=30.0),
|
||||
event_hooks={"request": [_auth_hook]},
|
||||
)
|
||||
request_log.observe_http_client(http_client)
|
||||
return AsyncOpenAI(
|
||||
api_key="strix-codex-oauth", # placeholder; the hook overwrites Authorization
|
||||
base_url=CODEX_BASE_URL,
|
||||
|
||||
+19
-62
@@ -10,13 +10,11 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pydantic import AliasChoices, BaseModel
|
||||
|
||||
from strix.config.settings import LlmSettings, Settings
|
||||
from strix.config.settings import Settings
|
||||
from strix.utils.secret_files import write_secret_text
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
|
||||
@@ -27,11 +25,6 @@ _DEFAULT_PATH: Path = Path.home() / ".strix" / "cli-config.json"
|
||||
_override: Path | None = None
|
||||
_cached: Settings | None = None
|
||||
|
||||
# Model, API key, and API base describe one provider connection. When the shell
|
||||
# changes any of them, the stored values of the others no longer belong together
|
||||
# and are dropped rather than mixed with the new value.
|
||||
_LINKED_LLM_FIELDS = ("model", "api_key", "api_base")
|
||||
|
||||
|
||||
def load_settings() -> Settings:
|
||||
"""Resolve settings from env + JSON file + defaults. Memoized.
|
||||
@@ -61,31 +54,22 @@ def apply_config_override(path: Path) -> None:
|
||||
|
||||
|
||||
def persist_current() -> None:
|
||||
"""Merge currently-set env vars into the active config file (0o600).
|
||||
|
||||
Values already in the file survive when their env var is unset, so a
|
||||
run that gets its settings from the file does not erase them. An env
|
||||
var set to the empty string clears the field from the file. A change to
|
||||
any linked LLM connection var drops the whole stored connection first.
|
||||
"""
|
||||
"""Write currently-set env vars to the active config file (0o600)."""
|
||||
s = load_settings()
|
||||
target = _override or _DEFAULT_PATH
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
env_block = _drop_stale_llm_connection(_read_env_block(target))
|
||||
for sub_name in type(s).model_fields:
|
||||
env_block: dict[str, str] = {}
|
||||
for sub_name in s.model_fields:
|
||||
sub_model = getattr(s, sub_name)
|
||||
if not isinstance(sub_model, BaseModel):
|
||||
continue
|
||||
for finfo in type(sub_model).model_fields.values():
|
||||
aliases = [alias.upper() for alias in _aliases_for(finfo)]
|
||||
active = next((alias for alias in aliases if alias in os.environ), None)
|
||||
if active is None:
|
||||
continue
|
||||
for alias in aliases:
|
||||
env_block.pop(alias, None)
|
||||
if os.environ[active]:
|
||||
env_block[active] = os.environ[active]
|
||||
for alias in _aliases_for(finfo):
|
||||
value = os.environ.get(alias.upper())
|
||||
if value:
|
||||
env_block[alias.upper()] = value
|
||||
break
|
||||
|
||||
write_secret_text(target, json.dumps({"env": env_block}, indent=2))
|
||||
|
||||
@@ -109,9 +93,17 @@ def _read_json_overrides(path: Path) -> dict[str, dict[str, Any]]:
|
||||
Only includes keys whose env var is NOT already set, so env always
|
||||
wins over the persisted file.
|
||||
"""
|
||||
env_block_upper = _drop_stale_llm_connection(_read_env_block(path))
|
||||
if not env_block_upper:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
env_block = data.get("env", {}) if isinstance(data, dict) else {}
|
||||
if not isinstance(env_block, dict):
|
||||
return {}
|
||||
|
||||
env_block_upper = {str(k).upper(): v for k, v in env_block.items()}
|
||||
env_present = {k.upper() for k in os.environ}
|
||||
|
||||
nested: dict[str, dict[str, Any]] = {}
|
||||
@@ -131,38 +123,3 @@ def _read_json_overrides(path: Path) -> dict[str, dict[str, Any]]:
|
||||
if sub_data:
|
||||
nested[sub_name] = sub_data
|
||||
return nested
|
||||
|
||||
|
||||
def _first_alias_value(aliases: list[str], source: Mapping[str, Any]) -> Any | None:
|
||||
return next((source[alias] for alias in aliases if alias in source), None)
|
||||
|
||||
|
||||
def _drop_stale_llm_connection(env_block: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Remove every linked LLM var from ``env_block`` if the shell changed any of them."""
|
||||
linked_aliases = [
|
||||
[alias.upper() for alias in _aliases_for(LlmSettings.model_fields[name])]
|
||||
for name in _LINKED_LLM_FIELDS
|
||||
]
|
||||
changed = any(
|
||||
(env_value := _first_alias_value(aliases, os.environ)) is not None
|
||||
and env_value != _first_alias_value(aliases, env_block)
|
||||
for aliases in linked_aliases
|
||||
)
|
||||
if not changed:
|
||||
return env_block
|
||||
stale = {alias for aliases in linked_aliases for alias in aliases}
|
||||
return {k: v for k, v in env_block.items() if k not in stale}
|
||||
|
||||
|
||||
def _read_env_block(path: Path) -> dict[str, Any]:
|
||||
"""Return the ``env`` block stored in ``path`` with upper-cased keys, or ``{}``."""
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
env_block = data.get("env", {}) if isinstance(data, dict) else {}
|
||||
if not isinstance(env_block, dict):
|
||||
return {}
|
||||
return {str(k).upper(): v for k, v in env_block.items()}
|
||||
+55
-87
@@ -40,7 +40,6 @@ from strix.config import codex
|
||||
from strix.config.loader import load_settings
|
||||
from strix.config.tool_call_ids import TurnCallIdRewriter, dedupe_input
|
||||
from strix.config.tool_call_limits import TurnToolCallLimiter
|
||||
from strix.llm import request_log
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -391,7 +390,7 @@ def _guard_event(
|
||||
event: TResponseStreamEvent, rewriter: TurnCallIdRewriter, limiter: TurnToolCallLimiter
|
||||
) -> TResponseStreamEvent | None:
|
||||
if isinstance(event, ResponseOutputItemAddedEvent | ResponseOutputItemDoneEvent):
|
||||
rewritten = rewriter.rewrite_item(event.item, event.output_index)
|
||||
rewritten = rewriter.rewrite_item(event.item)
|
||||
if not limiter.allow(rewritten):
|
||||
return None
|
||||
if rewritten is not event.item:
|
||||
@@ -526,37 +525,13 @@ class StrixProvider(MultiProvider):
|
||||
# The ChatGPT subscription backend is always streamed; it has no
|
||||
# non-streaming mode to fall back to, so LLM_DISABLE_STREAMING
|
||||
# does not apply here.
|
||||
model: Model = request_log.RequestLoggingModel(
|
||||
_CodexResponsesModel(
|
||||
slug,
|
||||
codex.get_subscription_client(),
|
||||
reasoning_effort=llm.reasoning_effort,
|
||||
),
|
||||
model_name=slug,
|
||||
provider="openai-codex",
|
||||
base_url=None,
|
||||
model: Model = _CodexResponsesModel(
|
||||
slug,
|
||||
codex.get_subscription_client(),
|
||||
reasoning_effort=llm.reasoning_effort,
|
||||
)
|
||||
else:
|
||||
model = super().get_model(model_name)
|
||||
resolved_name = model_name or llm.model or "unknown"
|
||||
if _routes_via_litellm(model):
|
||||
# LiteLLM's callbacks log every reply; only a cancelled attempt
|
||||
# (stream idle timeout, abandoned turn) escapes them.
|
||||
model = request_log.RequestLoggingModel(
|
||||
model,
|
||||
model_name=resolved_name,
|
||||
provider=_litellm_provider(resolved_name),
|
||||
base_url=self._override_base_url or llm.api_base,
|
||||
route="litellm",
|
||||
abandoned_only=True,
|
||||
)
|
||||
else:
|
||||
model = request_log.RequestLoggingModel(
|
||||
model,
|
||||
model_name=resolved_name,
|
||||
provider="openai",
|
||||
base_url=self._override_base_url or llm.api_base,
|
||||
)
|
||||
if llm.disable_streaming:
|
||||
model = _NonStreamingModel(model)
|
||||
# The wrapper emits its single event only once the whole request
|
||||
@@ -570,23 +545,6 @@ class StrixProvider(MultiProvider):
|
||||
)
|
||||
|
||||
|
||||
def _routes_via_litellm(model: Model) -> bool:
|
||||
"""LiteLLM-backed models are captured by the LiteLLM callback, not the wrapper."""
|
||||
from agents.extensions.models.litellm_model import LitellmModel
|
||||
|
||||
return isinstance(model, LitellmModel)
|
||||
|
||||
|
||||
def _litellm_provider(model_name: str) -> str | None:
|
||||
"""The provider LiteLLM will route ``model_name`` to, if it can tell."""
|
||||
try:
|
||||
import litellm
|
||||
|
||||
return str(litellm.get_llm_provider(model_name)[1])
|
||||
except Exception: # noqa: BLE001 - unknown model ids are the provider's problem, not the log's
|
||||
return None
|
||||
|
||||
|
||||
DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
||||
max_retries=5,
|
||||
backoff=ModelRetryBackoffSettings(
|
||||
@@ -604,8 +562,6 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
||||
)
|
||||
|
||||
RECOMMENDED_MODEL_NAMES = (
|
||||
"zai/glm-5.3",
|
||||
"zai/glm-5.3-flash",
|
||||
"openai/gpt-5.6-sol",
|
||||
"openai/gpt-5.6-terra",
|
||||
"openai/gpt-5.6-luna",
|
||||
@@ -614,7 +570,6 @@ RECOMMENDED_MODEL_NAMES = (
|
||||
"openai/gpt-5.5",
|
||||
"openai/gpt-5.4",
|
||||
"openai/gpt-5.3-codex",
|
||||
"anthropic/claude-fable-5-1",
|
||||
"anthropic/claude-fable-5",
|
||||
"anthropic/claude-opus-5",
|
||||
"anthropic/claude-opus-4-8",
|
||||
@@ -622,8 +577,6 @@ RECOMMENDED_MODEL_NAMES = (
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
"vertex_ai/gemini-3.1-pro-preview",
|
||||
"gemini/gemini-3.1-pro-preview",
|
||||
"vertex_ai/gemini-3.7-flash",
|
||||
"gemini/gemini-3.7-flash",
|
||||
"gemini/gemini-3.6-flash",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
@@ -635,27 +588,16 @@ RECOMMENDED_MODEL_NAMES = (
|
||||
|
||||
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
|
||||
|
||||
# Matched against the bare model name only: the route (``openai/``, ``openrouter/``,
|
||||
# a local gateway, ...) says nothing about the model's quality.
|
||||
FRONTIER_MODEL_PREFIXES = (
|
||||
"gpt-5",
|
||||
"claude-fable-5",
|
||||
"claude-opus-5",
|
||||
"claude-opus-4",
|
||||
"claude-sonnet-5",
|
||||
"claude-sonnet-4",
|
||||
"gemini-3",
|
||||
"deepseek-v4",
|
||||
"deepseek-r1",
|
||||
"deepseek-reasoner",
|
||||
"qwen3.8",
|
||||
"qwen3.7",
|
||||
"qwen3-max",
|
||||
"kimi-k3",
|
||||
"kimi-k2.7",
|
||||
"kimi-k2.6",
|
||||
"glm-5.3",
|
||||
"glm-5.2",
|
||||
FRONTIER_MODEL_FAMILIES = (
|
||||
(("azure", "azure_ai", "bedrock_mantle", "chatgpt", "openai"), ("gpt-5",)),
|
||||
(
|
||||
("anthropic", "azure_ai", "bedrock", "claude", "databricks", "snowflake", "vertex_ai"),
|
||||
("claude-fable-5", "claude-opus-5", "claude-opus-4", "claude-sonnet-5", "claude-sonnet-4"),
|
||||
),
|
||||
(("google", "gemini", "vertex_ai"), ("gemini-3",)),
|
||||
(("deepseek",), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
|
||||
(("alibaba", "dashscope", "qwen"), ("qwen3.8", "qwen3.7", "qwen3-max")),
|
||||
(("moonshot", "moonshotai", "kimi"), ("kimi-k3", "kimi-k2.7", "kimi-k2.6")),
|
||||
)
|
||||
|
||||
|
||||
@@ -663,7 +605,6 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
"""Apply Strix config to SDK-native defaults."""
|
||||
llm = settings.llm
|
||||
set_tracing_disabled(True)
|
||||
request_log.install()
|
||||
if codex.subscription_model(llm.model):
|
||||
return
|
||||
_configure_litellm_compatibility()
|
||||
@@ -675,11 +616,9 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
if llm.api_base:
|
||||
os.environ["OPENAI_BASE_URL"] = llm.api_base
|
||||
_configure_litellm_default("api_base", llm.api_base)
|
||||
api_type = llm.api_type
|
||||
if api_type is None:
|
||||
api_type = "chat_completions" if llm.api_base else "responses"
|
||||
|
||||
set_default_openai_api(api_type)
|
||||
set_default_openai_api("chat_completions")
|
||||
else:
|
||||
set_default_openai_api("responses")
|
||||
_configure_extra_headers(llm)
|
||||
|
||||
|
||||
@@ -816,16 +755,12 @@ def _merge_litellm_headers(headers: dict[str, str]) -> None:
|
||||
|
||||
def _register_openai_client_with_headers(llm: LlmSettings, headers: dict[str, str]) -> None:
|
||||
from agents import set_default_openai_client
|
||||
from agents.models.openai_provider import shared_http_client
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=llm.api_key or "not-needed",
|
||||
base_url=llm.api_base,
|
||||
default_headers=dict(headers),
|
||||
# The SDK's shared client is the one the request log observes for
|
||||
# reply status, headers and provider request ids.
|
||||
http_client=shared_http_client(),
|
||||
)
|
||||
set_default_openai_client(client, use_for_tracing=False)
|
||||
|
||||
@@ -858,8 +793,6 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo
|
||||
model = model_name.strip().lower()
|
||||
if "/" in model and not model.startswith("openai/"):
|
||||
return True
|
||||
if settings.llm.api_type is not None:
|
||||
return settings.llm.api_type == "chat_completions"
|
||||
if settings.llm.api_base:
|
||||
return True
|
||||
return not model_supports_reasoning(model_name)
|
||||
@@ -898,8 +831,11 @@ def is_recommended_or_frontier_model(model_name: str) -> bool:
|
||||
return False
|
||||
if name in _RECOMMENDED_MODEL_NAME_SET:
|
||||
return True
|
||||
bare_model_name = name.rsplit("/", 1)[-1]
|
||||
return _matches_model_prefix(bare_model_name, FRONTIER_MODEL_PREFIXES)
|
||||
provider_name, bare_model_name = _split_model_provider(name)
|
||||
return any(
|
||||
_matches_frontier_family(provider_name, bare_model_name, provider_markers, prefixes)
|
||||
for provider_markers, prefixes in FRONTIER_MODEL_FAMILIES
|
||||
)
|
||||
|
||||
|
||||
def _normalized_model_name(model_name: str) -> str:
|
||||
@@ -911,6 +847,28 @@ def _normalized_model_name(model_name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def _split_model_provider(model_name: str) -> tuple[str | None, str]:
|
||||
if "/" not in model_name:
|
||||
return None, model_name
|
||||
provider_name, bare_model_name = model_name.rsplit("/", 1)
|
||||
return provider_name, bare_model_name
|
||||
|
||||
|
||||
def _matches_frontier_family(
|
||||
provider_name: str | None,
|
||||
model_name: str,
|
||||
provider_markers: tuple[str, ...],
|
||||
model_prefixes: tuple[str, ...],
|
||||
) -> bool:
|
||||
if not _matches_model_prefix(model_name, model_prefixes):
|
||||
return False
|
||||
if provider_name is None:
|
||||
return True
|
||||
return _contains_provider_marker(
|
||||
provider_name, provider_markers, split_compound_names=True
|
||||
) or _contains_provider_marker(model_name, provider_markers)
|
||||
|
||||
|
||||
def _matches_model_prefix(model_name: str, model_prefixes: tuple[str, ...]) -> bool:
|
||||
return any(
|
||||
candidate.startswith(prefix)
|
||||
@@ -928,6 +886,16 @@ def _model_name_candidates(model_name: str) -> tuple[str, ...]:
|
||||
return (model_name, *suffixes)
|
||||
|
||||
|
||||
def _contains_provider_marker(
|
||||
value: str, provider_markers: tuple[str, ...], *, split_compound_names: bool = False
|
||||
) -> bool:
|
||||
parts = set(value.replace(".", "/").split("/"))
|
||||
if split_compound_names:
|
||||
for separator in ("_", "-"):
|
||||
parts.update(piece for part in tuple(parts) for piece in part.split(separator))
|
||||
return any(marker in parts for marker in provider_markers)
|
||||
|
||||
|
||||
def is_known_openai_bare_model(model_name: str) -> bool:
|
||||
import litellm
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
|
||||
ApiType = Literal["responses", "chat_completions"]
|
||||
|
||||
DEFAULT_MAX_TURNS = 500
|
||||
|
||||
@@ -24,11 +23,6 @@ class LlmSettings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
model: str | None = Field(default=None, alias="STRIX_LLM")
|
||||
api_type: ApiType | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("STRIX_API_TYPE", "STRIX_FORCE_API"),
|
||||
description="Force 'responses' or 'chat_completions' API path",
|
||||
)
|
||||
api_key: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"),
|
||||
@@ -126,10 +120,6 @@ class TelemetrySettings(BaseSettings):
|
||||
enabled: bool = Field(default=True, alias="STRIX_TELEMETRY")
|
||||
|
||||
|
||||
WebSearchProvider = Literal["auto", "perplexity", "exa"]
|
||||
ExaSearchType = Literal["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"]
|
||||
|
||||
|
||||
class IntegrationSettings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
@@ -138,25 +128,6 @@ class IntegrationSettings(BaseSettings):
|
||||
alias="PERPLEXITY_API_KEY",
|
||||
repr=False,
|
||||
)
|
||||
exa_api_key: str | None = Field(
|
||||
default=None,
|
||||
alias="EXA_API_KEY",
|
||||
repr=False,
|
||||
)
|
||||
web_search_provider: WebSearchProvider = Field(
|
||||
default="auto",
|
||||
alias="STRIX_WEB_SEARCH_PROVIDER",
|
||||
)
|
||||
exa_search_type: ExaSearchType = Field(
|
||||
default="auto",
|
||||
alias="STRIX_EXA_SEARCH_TYPE",
|
||||
)
|
||||
exa_num_results: int = Field(
|
||||
default=5,
|
||||
ge=1,
|
||||
le=100,
|
||||
alias="STRIX_EXA_NUM_RESULTS",
|
||||
)
|
||||
postman_api_key: str | None = Field(
|
||||
default=None,
|
||||
alias="POSTMAN_API_KEY",
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
"""Keep tool-call ids present and unique within a conversation.
|
||||
"""Keep tool-call ids unique within a conversation.
|
||||
|
||||
Some providers return per-turn tool-call ids (``exec_command:0``,
|
||||
``exec_command:1``, ...) whose counter restarts on every turn. Once the same
|
||||
id appears twice in one conversation, the request payload has two assistant
|
||||
tool calls sharing an id and strict providers reject the whole turn, which
|
||||
permanently kills the agent because the malformed history is replayed on
|
||||
every retry.
|
||||
|
||||
Others omit the id altogether, or return it as an empty string. That turns
|
||||
the paired tool result into a ``tool`` message with an empty
|
||||
``tool_call_id``, which strict providers reject the same way and with the
|
||||
same permanent outcome. Rewriting both blank and duplicate ids to fresh
|
||||
unique ones keeps the history valid for any provider.
|
||||
every retry. Rewriting duplicates to fresh unique ids keeps the history
|
||||
valid for any provider.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,7 +15,6 @@ from collections import defaultdict, deque
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from agents.models.fake_id import FAKE_RESPONSES_ID
|
||||
from openai.types.responses import ResponseFunctionToolCall
|
||||
|
||||
|
||||
@@ -28,29 +22,23 @@ def new_call_id() -> str:
|
||||
return f"call_{uuid4().hex}"
|
||||
|
||||
|
||||
def _pairing_key(call_id: Any) -> str:
|
||||
"""Bucket an id for call/output pairing; all blank ids share one bucket."""
|
||||
return call_id if isinstance(call_id, str) else ""
|
||||
|
||||
|
||||
def collect_call_ids(items: list[Any]) -> set[str]:
|
||||
used: set[str] = set()
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
call_id = item.get("call_id")
|
||||
if isinstance(call_id, str) and call_id:
|
||||
if isinstance(call_id, str):
|
||||
used.add(call_id)
|
||||
elif isinstance(item, ResponseFunctionToolCall) and item.call_id:
|
||||
elif isinstance(item, ResponseFunctionToolCall):
|
||||
used.add(item.call_id)
|
||||
return used
|
||||
|
||||
|
||||
def dedupe_history_call_ids(items: list[Any]) -> tuple[list[Any], bool]:
|
||||
"""Rewrite blank and duplicate call ids in a conversation history.
|
||||
"""Rewrite duplicate call ids in a conversation history.
|
||||
|
||||
Outputs are paired with their call by order, so parallel calls that share
|
||||
an id — or are all missing one — keep answering the right call after the
|
||||
rewrite.
|
||||
an id keep answering the right call after the rewrite.
|
||||
"""
|
||||
used: set[str] = set()
|
||||
pending: dict[str, deque[str]] = defaultdict(deque)
|
||||
@@ -61,24 +49,22 @@ def dedupe_history_call_ids(items: list[Any]) -> tuple[list[Any], bool]:
|
||||
if not isinstance(item, dict):
|
||||
rebuilt.append(item)
|
||||
continue
|
||||
|
||||
kind = item.get("type")
|
||||
if kind not in ("function_call", "function_call_output"):
|
||||
call_id = item.get("call_id")
|
||||
if not isinstance(call_id, str):
|
||||
rebuilt.append(item)
|
||||
continue
|
||||
|
||||
call_id = item.get("call_id")
|
||||
key = _pairing_key(call_id)
|
||||
kind = item.get("type")
|
||||
if kind == "function_call":
|
||||
effective = key
|
||||
if not effective or effective in used:
|
||||
effective = call_id
|
||||
if call_id in used:
|
||||
effective = new_call_id()
|
||||
item = {**item, "call_id": effective} # noqa: PLW2901
|
||||
changed = True
|
||||
used.add(effective)
|
||||
pending[key].append(effective)
|
||||
else:
|
||||
queue = pending.get(key)
|
||||
pending[call_id].append(effective)
|
||||
elif kind == "function_call_output":
|
||||
queue = pending.get(call_id)
|
||||
if queue:
|
||||
effective = queue.popleft()
|
||||
if effective != call_id:
|
||||
@@ -97,7 +83,7 @@ def dedupe_input(model_input: str | list[Any]) -> str | list[Any]:
|
||||
|
||||
|
||||
class TurnCallIdRewriter:
|
||||
"""Rewrite a single turn's tool-call ids that are blank or collide with the history.
|
||||
"""Rewrite a single turn's tool-call ids that collide with the history.
|
||||
|
||||
A turn's items surface several times (streamed item events, then the
|
||||
completed response), so the same original id must always map to the same
|
||||
@@ -107,37 +93,12 @@ class TurnCallIdRewriter:
|
||||
def __init__(self, model_input: str | list[Any]) -> None:
|
||||
self._used = set() if isinstance(model_input, str) else collect_call_ids(model_input)
|
||||
self._remap: dict[str, str] = {}
|
||||
self._blank_remap: dict[str, str] = {}
|
||||
self._settled: set[str] = set()
|
||||
|
||||
def _rewrite_blank(
|
||||
self, item: ResponseFunctionToolCall, position: int | None
|
||||
) -> ResponseFunctionToolCall:
|
||||
"""Give a call with no id one that stays the same on every sighting.
|
||||
|
||||
A real item id tells parallel calls apart. Chat Completions routes give
|
||||
every item the same placeholder id, so there the call's position in
|
||||
the turn's output tells them apart instead.
|
||||
"""
|
||||
if item.id and item.id != FAKE_RESPONSES_ID:
|
||||
key = item.id
|
||||
else:
|
||||
key = f"{FAKE_RESPONSES_ID}#{position}"
|
||||
replacement = self._blank_remap.get(key)
|
||||
if replacement is None:
|
||||
replacement = new_call_id()
|
||||
self._blank_remap[key] = replacement
|
||||
self._used.add(replacement)
|
||||
self._settled.add(replacement)
|
||||
return item.model_copy(update={"call_id": replacement})
|
||||
|
||||
def rewrite_item(self, item: Any, position: int | None = None) -> Any:
|
||||
"""Rewrite one item; ``position`` is its index in the turn's output."""
|
||||
def rewrite_item(self, item: Any) -> Any:
|
||||
if not isinstance(item, ResponseFunctionToolCall):
|
||||
return item
|
||||
original = item.call_id
|
||||
if not original:
|
||||
return self._rewrite_blank(item, position)
|
||||
if original in self._settled:
|
||||
return item
|
||||
replacement = self._remap.get(original)
|
||||
@@ -153,4 +114,4 @@ class TurnCallIdRewriter:
|
||||
return item.model_copy(update={"call_id": replacement})
|
||||
|
||||
def rewrite_items(self, items: list[Any]) -> list[Any]:
|
||||
return [self.rewrite_item(item, position) for position, item in enumerate(items)]
|
||||
return [self.rewrite_item(item) for item in items]
|
||||
+1
-36
@@ -24,8 +24,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
Status = Literal["running", "waiting", "completed", "stopped", "crashed", "failed", "budget_paused"]
|
||||
|
||||
TERMINAL_STATUSES: frozenset[str] = frozenset({"completed", "stopped", "crashed", "failed"})
|
||||
|
||||
# Why an agent parked. The user can message any agent, so this - not the agent's
|
||||
# position in the tree - decides whether waiting is bounded: only an agent waiting
|
||||
# on other agents is re-checked on a timer.
|
||||
@@ -38,10 +36,6 @@ class AgentRuntime:
|
||||
task: asyncio.Task[Any] | None = None
|
||||
stream: Any | None = None
|
||||
interrupt_on_message: bool = False
|
||||
# Whether the agent's loop parks after a terminal state and can be woken by a
|
||||
# later message. A non-interactive loop returns instead, so once such an
|
||||
# agent is terminal nothing will ever read its mailbox again.
|
||||
resumable: bool = True
|
||||
wake: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
mailbox: list[dict[str, Any]] = field(default_factory=list)
|
||||
user_wake_required: bool = False
|
||||
@@ -181,7 +175,6 @@ class AgentCoordinator:
|
||||
session: Session | None = None,
|
||||
task: asyncio.Task[Any] | None = None,
|
||||
interrupt_on_message: bool | None = None,
|
||||
resumable: bool | None = None,
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
runtime = self.runtimes.setdefault(agent_id, AgentRuntime())
|
||||
@@ -191,8 +184,6 @@ class AgentCoordinator:
|
||||
runtime.task = task
|
||||
if interrupt_on_message is not None:
|
||||
runtime.interrupt_on_message = interrupt_on_message
|
||||
if resumable is not None:
|
||||
runtime.resumable = resumable
|
||||
|
||||
async def mark_running(self, agent_id: str) -> None:
|
||||
async with self._lock:
|
||||
@@ -284,29 +275,10 @@ class AgentCoordinator:
|
||||
self._parent_notified.add(agent_id)
|
||||
return True
|
||||
|
||||
def _unreachable_locked(self, agent_id: str) -> bool:
|
||||
"""True when the agent is terminal and no loop will ever read its mailbox."""
|
||||
if self.statuses.get(agent_id) not in TERMINAL_STATUSES:
|
||||
return False
|
||||
runtime = self.runtimes.get(agent_id)
|
||||
return runtime is not None and not runtime.resumable
|
||||
|
||||
async def reachability(self, agent_id: str) -> tuple[bool, Status | None]:
|
||||
"""Whether a message to ``agent_id`` can still be acted on, plus its status."""
|
||||
async with self._lock:
|
||||
status = self.statuses.get(agent_id)
|
||||
if status is None:
|
||||
return False, None
|
||||
return not self._unreachable_locked(agent_id), status
|
||||
|
||||
async def send(
|
||||
self, target_agent_id: str, message: dict[str, Any], *, interrupt: bool = True
|
||||
) -> bool:
|
||||
"""Queue a user/peer message in the target's mailbox and wake it.
|
||||
|
||||
Returns False when nothing will ever read the message: the target is
|
||||
unknown, or it is terminal and its loop does not park for wake-ups.
|
||||
"""
|
||||
"""Queue a user/peer message in the target's mailbox and wake it."""
|
||||
from_user = message.get("from") == "user"
|
||||
if from_user and self._budget_paused:
|
||||
await self.resume_from_budget_pause(exclude=target_agent_id)
|
||||
@@ -314,13 +286,6 @@ class AgentCoordinator:
|
||||
if target_agent_id not in self.statuses:
|
||||
logger.debug("agent.send dropped unknown target=%s", target_agent_id)
|
||||
return False
|
||||
if self._unreachable_locked(target_agent_id):
|
||||
logger.info(
|
||||
"agent.send dropped: target=%s is %s and cannot be woken",
|
||||
target_agent_id,
|
||||
self.statuses[target_agent_id],
|
||||
)
|
||||
return False
|
||||
runtime = self.runtimes.setdefault(target_agent_id, AgentRuntime())
|
||||
runtime.mailbox.append(dict(message))
|
||||
self.pending_counts[target_agent_id] = self.pending_counts.get(target_agent_id, 0) + 1
|
||||
|
||||
+4
-48
@@ -33,7 +33,6 @@ from strix.core.sessions import (
|
||||
seed_initial_input,
|
||||
strip_all_images_from_session,
|
||||
)
|
||||
from strix.llm import request_log
|
||||
from strix.llm.compaction import is_context_overflow, maybe_compact
|
||||
|
||||
|
||||
@@ -198,50 +197,11 @@ async def run_agent_loop(
|
||||
start_parked: bool = False,
|
||||
event_sink: StreamEventSink | None = None,
|
||||
hooks: RunHooks[dict[str, Any]] | None = None,
|
||||
) -> RunResultBase | None:
|
||||
agent_name = getattr(agent, "name", None)
|
||||
token = request_log.bind_call_context(
|
||||
agent_id, agent_name if isinstance(agent_name, str) else None
|
||||
)
|
||||
try:
|
||||
return await _run_agent_loop(
|
||||
agent=agent,
|
||||
initial_input=initial_input,
|
||||
run_config=run_config,
|
||||
context=context,
|
||||
max_turns=max_turns,
|
||||
coordinator=coordinator,
|
||||
agent_id=agent_id,
|
||||
interactive=interactive,
|
||||
session=session,
|
||||
start_parked=start_parked,
|
||||
event_sink=event_sink,
|
||||
hooks=hooks,
|
||||
)
|
||||
finally:
|
||||
request_log.reset_call_context(token)
|
||||
|
||||
|
||||
async def _run_agent_loop(
|
||||
*,
|
||||
agent: Any,
|
||||
initial_input: Any,
|
||||
run_config: RunConfig,
|
||||
context: dict[str, Any],
|
||||
max_turns: int,
|
||||
coordinator: AgentCoordinator,
|
||||
agent_id: str,
|
||||
interactive: bool,
|
||||
session: Session | None,
|
||||
start_parked: bool,
|
||||
event_sink: StreamEventSink | None,
|
||||
hooks: RunHooks[dict[str, Any]] | None,
|
||||
) -> RunResultBase | None:
|
||||
await coordinator.attach_runtime(
|
||||
agent_id,
|
||||
session=session,
|
||||
interrupt_on_message=interactive,
|
||||
resumable=interactive,
|
||||
)
|
||||
result: RunResultBase | None = None
|
||||
|
||||
@@ -675,7 +635,7 @@ async def _run_cycle_parked(
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("error escaped the run cycle for %s; parking as failed", agent_id)
|
||||
await coordinator.set_status(agent_id, "failed", error=request_log.failure_text(exc))
|
||||
await coordinator.set_status(agent_id, "failed", error=str(exc) or type(exc).__name__)
|
||||
await notify_parent_on_terminal(coordinator, agent_id, "failed")
|
||||
return None
|
||||
|
||||
@@ -697,7 +657,6 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
image_strips = 0
|
||||
compactions = 0
|
||||
model_retries = 0
|
||||
request_log.set_retry_attempt(0)
|
||||
while True:
|
||||
stream: Any = None
|
||||
pre_run_items: list[Any] = []
|
||||
@@ -825,7 +784,6 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
exc,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
request_log.set_retry_attempt(model_retries)
|
||||
if session is not None:
|
||||
input_data = []
|
||||
continue
|
||||
@@ -833,9 +791,7 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
await _salvage_stream_to_session(session, pre_run_items, stream, agent_id)
|
||||
if isinstance(exc, ProviderRefusalError):
|
||||
logger.warning("agent %s refused by the model provider: %s", agent_id, exc)
|
||||
await coordinator.set_status(
|
||||
agent_id, "failed", error=request_log.failure_text(exc)
|
||||
)
|
||||
await coordinator.set_status(agent_id, "failed", error=str(exc))
|
||||
await notify_parent_on_terminal(coordinator, agent_id, "failed")
|
||||
return None
|
||||
if isinstance(exc, MaxTurnsExceeded):
|
||||
@@ -849,7 +805,7 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
# non-interactive agent's task: a child that dies still owes its parent a
|
||||
# report, and the parent would otherwise wait out its timeout on a message
|
||||
# the dead child can no longer send.
|
||||
await coordinator.set_status(agent_id, status, error=request_log.failure_text(exc))
|
||||
await coordinator.set_status(agent_id, status, error=str(exc) or type(exc).__name__)
|
||||
await notify_parent_on_terminal(coordinator, agent_id, status)
|
||||
if not interactive:
|
||||
raise
|
||||
@@ -1050,7 +1006,7 @@ async def _start_child_runner(
|
||||
) -> None:
|
||||
session = open_agent_session(child_id, agents_db_path)
|
||||
sessions_to_close.append(session)
|
||||
await coordinator.attach_runtime(child_id, session=session, resumable=interactive)
|
||||
await coordinator.attach_runtime(child_id, session=session)
|
||||
|
||||
child_ctx: dict[str, Any] = dict(parent_ctx)
|
||||
child_ctx["agent_id"] = child_id
|
||||
|
||||
+69
-52
@@ -45,7 +45,6 @@ from strix.core.paths import run_dir_for, runtime_state_dir
|
||||
from strix.core.sessions import open_agent_session
|
||||
from strix.report.state import get_global_report_state
|
||||
from strix.runtime import session_manager
|
||||
from strix.telemetry import set_scan_phase
|
||||
from strix.telemetry.logging import set_scan_id, setup_scan_logging
|
||||
from strix.tools.output_store import (
|
||||
WORKSPACE_SPILL_DIR,
|
||||
@@ -59,8 +58,10 @@ if TYPE_CHECKING:
|
||||
|
||||
from strix.runtime.status import StatusSink
|
||||
from strix.tools.mcp import (
|
||||
ConnectedMcpServer,
|
||||
McpConnectionRequest,
|
||||
McpRegistry,
|
||||
SupervisedMcpSession,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,8 +70,8 @@ logger = logging.getLogger(__name__)
|
||||
StreamEventSink = Callable[[str, Any], None]
|
||||
|
||||
# Receives the run's MCP connection roster as a list of non-secret status dicts
|
||||
# ({"name", "provider", "tool_count", "dead", "state"}), once when the connections
|
||||
# are registered and again on lifecycle transitions. An interface
|
||||
# ({"name", "provider", "tool_count", "dead"}), once when the connections are
|
||||
# established and again each time a connection transitions to dead. An interface
|
||||
# can persist it, render it, or forward it on as connection status. Kept as a
|
||||
# snapshot of the whole roster (not a per-
|
||||
# connection delta) so every call carries a consistent, current picture.
|
||||
@@ -78,21 +79,30 @@ McpStatusSink = Callable[[list[dict[str, Any]]], None]
|
||||
|
||||
|
||||
def _mcp_roster_payload(registry: McpRegistry) -> list[dict[str, Any]]:
|
||||
"""The run's MCP roster as non-secret lifecycle status dicts."""
|
||||
"""The run's MCP roster as non-secret status dicts (name/provider/tool_count/dead)."""
|
||||
return [
|
||||
{
|
||||
"name": status.name,
|
||||
"provider": status.provider,
|
||||
"tool_count": status.tool_count,
|
||||
"dead": status.dead,
|
||||
"state": status.state,
|
||||
}
|
||||
for status in registry.statuses()
|
||||
]
|
||||
|
||||
|
||||
def _record_mcp_connections(connection_names: list[str]) -> None:
|
||||
"""Record which MCP servers this run configured, for the interfaces.
|
||||
def _mcp_startup_summary(connections: list[ConnectedMcpServer]) -> str:
|
||||
"""One user-facing line summarizing the MCP servers that connected."""
|
||||
server_count = len(connections)
|
||||
tool_count = sum(c.tool_count for c in connections)
|
||||
servers_word = "server" if server_count == 1 else "servers"
|
||||
tools_word = "tool" if tool_count == 1 else "tools"
|
||||
names = ", ".join(c.name for c in connections)
|
||||
return f"MCP: connected {server_count} {servers_word} ({tool_count} {tools_word}): {names}"
|
||||
|
||||
|
||||
def _record_mcp_connections(connections: list[ConnectedMcpServer]) -> None:
|
||||
"""Record which MCP servers this run connected, for the interfaces.
|
||||
|
||||
A server's tools are offered to the model under a name built from the
|
||||
connection name and the tool's own name, which cannot be split back apart, so
|
||||
@@ -103,14 +113,7 @@ def _record_mcp_connections(connection_names: list[str]) -> None:
|
||||
report_state = get_global_report_state()
|
||||
if report_state is None:
|
||||
return
|
||||
report_state.record_mcp_connections(connection_names)
|
||||
|
||||
|
||||
def _note_exit_reason(reason: str) -> None:
|
||||
"""Record why the scan stopped so the end-of-scan beacon reports it."""
|
||||
report_state = get_global_report_state()
|
||||
if report_state is not None and report_state.scan_ended_exit_reason is None:
|
||||
report_state.scan_ended_exit_reason = reason
|
||||
report_state.record_mcp_connections([connection.name for connection in connections])
|
||||
|
||||
|
||||
def _persist_mcp_status(roster: list[dict[str, Any]]) -> None:
|
||||
@@ -310,7 +313,6 @@ async def run_strix_scan(
|
||||
root_id = uuid.uuid4().hex[:8]
|
||||
|
||||
logger.info("Bringing up sandbox session for scan %s", scan_id)
|
||||
set_scan_phase("sandbox_init")
|
||||
bundle = await session_manager.create_or_reuse(
|
||||
scan_id,
|
||||
image=image,
|
||||
@@ -320,7 +322,6 @@ async def run_strix_scan(
|
||||
)
|
||||
report("Waiting for the first model response")
|
||||
logger.info("Sandbox ready for scan %s", scan_id)
|
||||
set_scan_phase("agent_setup")
|
||||
|
||||
sandbox_session = bundle["session"]
|
||||
|
||||
@@ -337,7 +338,7 @@ async def run_strix_scan(
|
||||
configure_spill_writer(_spill_to_workspace)
|
||||
|
||||
sessions_to_close: list[SQLiteSession] = []
|
||||
mcp_registry: McpRegistry | None = None
|
||||
mcp_sessions: list[SupervisedMcpSession] = []
|
||||
|
||||
try:
|
||||
targets = scan_config.get("targets") or []
|
||||
@@ -389,6 +390,7 @@ async def run_strix_scan(
|
||||
from strix.tools.mcp import (
|
||||
McpConnectionRequest,
|
||||
McpRegistry,
|
||||
attach_mcp_requests,
|
||||
load_user_mcp_configs,
|
||||
)
|
||||
|
||||
@@ -404,37 +406,55 @@ async def run_strix_scan(
|
||||
else:
|
||||
mcp_requests = mcp_connection_requests
|
||||
if mcp_requests:
|
||||
for request in mcp_requests:
|
||||
mcp_registry.register(request)
|
||||
_record_mcp_connections(mcp_registry.names())
|
||||
report(
|
||||
f"MCP: configured {len(mcp_registry)} connection(s); "
|
||||
"warming them in the background"
|
||||
)
|
||||
scope_context["mcp_available"] = True
|
||||
scope_context["mcp_connections"] = [
|
||||
{
|
||||
"name": summary.name,
|
||||
"purpose": summary.purpose,
|
||||
"tool_count": summary.tool_count,
|
||||
}
|
||||
for summary in mcp_registry.summaries()
|
||||
]
|
||||
connections = await attach_mcp_requests(mcp_requests, mcp_registry)
|
||||
mcp_sessions = [c.session for c in connections]
|
||||
# Recorded even when nothing connected, so a resumed run does not
|
||||
# keep attributing tool calls to servers it no longer has.
|
||||
_record_mcp_connections(connections)
|
||||
if connections:
|
||||
report(_mcp_startup_summary(connections))
|
||||
# Name the connected servers in the prompt so every agent
|
||||
# (root and children, both deriving from scope_context) sees
|
||||
# what is available at the start; they can still re-list or
|
||||
# inspect them at run time via list_mcps / describe_mcp. Set
|
||||
# only when a connection exists, so a run with no MCP leaves
|
||||
# the prompt context unchanged.
|
||||
scope_context["mcp_available"] = bool(mcp_registry)
|
||||
scope_context["mcp_connections"] = [
|
||||
{
|
||||
"name": summary.name,
|
||||
"purpose": summary.purpose,
|
||||
"tool_count": summary.tool_count,
|
||||
}
|
||||
for summary in mcp_registry.summaries()
|
||||
]
|
||||
# Feed a non-secret connection roster (name / provider /
|
||||
# tool_count / dead) to two consumers: once now (all
|
||||
# currently healthy) and again whenever a connection later
|
||||
# dies. It is always persisted to run.json so the viewer,
|
||||
# which re-reads the run's files from disk, can render the
|
||||
# MCP connections panel and health without an in-memory
|
||||
# sink. When an interface sink is attached (the TUI backend,
|
||||
# or pro forwarding into the app's event stream) it also
|
||||
# receives the same snapshot. In-use is derived separately by
|
||||
# each interface from the connection-tagged tool-call events,
|
||||
# so it is not carried here.
|
||||
def _emit_mcp_status() -> None:
|
||||
roster = _mcp_roster_payload(mcp_registry)
|
||||
_persist_mcp_status(roster)
|
||||
if mcp_status_sink is not None:
|
||||
try:
|
||||
mcp_status_sink(roster)
|
||||
except Exception:
|
||||
logger.exception("MCP status sink failed")
|
||||
|
||||
def _emit_mcp_status() -> None:
|
||||
roster = _mcp_roster_payload(mcp_registry)
|
||||
_persist_mcp_status(roster)
|
||||
if mcp_status_sink is not None:
|
||||
try:
|
||||
mcp_status_sink(roster)
|
||||
except Exception:
|
||||
logger.exception("MCP status sink failed")
|
||||
|
||||
mcp_registry.set_status_sink(_emit_mcp_status)
|
||||
_emit_mcp_status()
|
||||
mcp_registry.start_warmup(max_concurrency=6)
|
||||
for connection_name in mcp_registry.names():
|
||||
entry = mcp_registry.get(connection_name)
|
||||
if entry is not None:
|
||||
entry.session.set_on_dead(_emit_mcp_status)
|
||||
_emit_mcp_status()
|
||||
except Exception:
|
||||
logger.exception("Failed to configure user MCP servers; continuing without them")
|
||||
logger.exception("Failed to connect user MCP servers; continuing without them")
|
||||
|
||||
root_context = _merge_root_prompt_context(scope_context, extra_system_prompt_context)
|
||||
root_instructions = _compose_root_instructions_override(
|
||||
@@ -552,7 +572,6 @@ async def run_strix_scan(
|
||||
async with coordinator._lock:
|
||||
root_status = coordinator.statuses.get(root_id)
|
||||
|
||||
set_scan_phase("agent_loop")
|
||||
result = await run_agent_loop(
|
||||
agent=root_agent,
|
||||
initial_input=initial_input,
|
||||
@@ -590,7 +609,6 @@ async def run_strix_scan(
|
||||
return result # noqa: TRY300
|
||||
except BudgetExceededError as exc:
|
||||
logger.info("Scan %s stopped: %s", scan_id, exc)
|
||||
_note_exit_reason("budget_exceeded")
|
||||
if root_id is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator.set_status(root_id, "stopped")
|
||||
@@ -603,7 +621,6 @@ async def run_strix_scan(
|
||||
exc,
|
||||
scan_id,
|
||||
)
|
||||
_note_exit_reason("rate_limited")
|
||||
if root_id is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator.set_status(root_id, "stopped")
|
||||
@@ -630,9 +647,9 @@ async def run_strix_scan(
|
||||
for s in sessions_to_close:
|
||||
with contextlib.suppress(Exception):
|
||||
s.close()
|
||||
if mcp_registry is not None:
|
||||
for mcp_session in mcp_sessions:
|
||||
with contextlib.suppress(Exception):
|
||||
await mcp_registry.close()
|
||||
await mcp_session.aclose()
|
||||
with contextlib.suppress(Exception):
|
||||
await coordinator._maybe_snapshot()
|
||||
if cleanup_on_exit:
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Verified fix preparation contracts and runtime."""
|
||||
|
||||
from strix.fix.contracts import (
|
||||
BlockerKind,
|
||||
CandidateLocation,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CommandSpec,
|
||||
FileManifestEntry,
|
||||
FixCandidateV1,
|
||||
FixEdit,
|
||||
FixPreparationAttempt,
|
||||
FixPreparationRequestV1,
|
||||
FixPreparationResultV1,
|
||||
PreparationBlocker,
|
||||
PreparationState,
|
||||
RepairOutcome,
|
||||
RepairStatus,
|
||||
ReproductionSpec,
|
||||
SourceIdentity,
|
||||
SourceIdentityKind,
|
||||
VerificationDecision,
|
||||
VerificationTarget,
|
||||
VerifierResult,
|
||||
candidate_from_legacy_report,
|
||||
)
|
||||
from strix.fix.prepare import (
|
||||
PreparationCancelledError,
|
||||
PreparationContext,
|
||||
PreparationPolicy,
|
||||
build_git_manifest,
|
||||
prepare_fix,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BlockerKind",
|
||||
"CandidateLocation",
|
||||
"CheckResult",
|
||||
"CheckStatus",
|
||||
"CommandSpec",
|
||||
"FileManifestEntry",
|
||||
"FixCandidateV1",
|
||||
"FixEdit",
|
||||
"FixPreparationAttempt",
|
||||
"FixPreparationRequestV1",
|
||||
"FixPreparationResultV1",
|
||||
"PreparationBlocker",
|
||||
"PreparationCancelledError",
|
||||
"PreparationContext",
|
||||
"PreparationPolicy",
|
||||
"PreparationState",
|
||||
"RepairOutcome",
|
||||
"RepairStatus",
|
||||
"ReproductionSpec",
|
||||
"SourceIdentity",
|
||||
"SourceIdentityKind",
|
||||
"VerificationDecision",
|
||||
"VerificationTarget",
|
||||
"VerifierResult",
|
||||
"build_git_manifest",
|
||||
"candidate_from_legacy_report",
|
||||
"prepare_fix",
|
||||
]
|
||||
@@ -1,331 +0,0 @@
|
||||
"""Versioned contracts for verified fix preparation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from enum import StrEnum
|
||||
from pathlib import PurePosixPath
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
class ContractModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class SourceIdentityKind(StrEnum):
|
||||
COMMIT = "commit"
|
||||
ARCHIVE = "archive"
|
||||
|
||||
|
||||
class PreparationState(StrEnum):
|
||||
PREPARING = "preparing"
|
||||
READY = "ready"
|
||||
BLOCKED = "blocked"
|
||||
FAILED = "failed"
|
||||
STALE = "stale"
|
||||
|
||||
|
||||
class CheckStatus(StrEnum):
|
||||
PASSED = "passed"
|
||||
FAILED = "failed"
|
||||
UNAVAILABLE = "unavailable"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class VerificationTarget(StrEnum):
|
||||
BASE = "base"
|
||||
PATCHED = "patched"
|
||||
|
||||
|
||||
class VerificationDecision(StrEnum):
|
||||
VERIFIED = "verified"
|
||||
REJECTED = "rejected"
|
||||
INCONCLUSIVE = "inconclusive"
|
||||
|
||||
|
||||
class RepairStatus(StrEnum):
|
||||
COMPLETE = "complete"
|
||||
BLOCKED = "blocked"
|
||||
BUDGET_EXHAUSTED = "budget_exhausted"
|
||||
INCOMPLETE = "incomplete"
|
||||
|
||||
|
||||
class BlockerKind(StrEnum):
|
||||
SOURCE = "source"
|
||||
ENVIRONMENT = "environment"
|
||||
REPOSITORY_BASELINE = "repository_baseline"
|
||||
CREDENTIAL = "credential"
|
||||
EXTERNAL_CONFIGURATION = "external_configuration"
|
||||
SECURITY_EVIDENCE = "security_evidence"
|
||||
|
||||
|
||||
class PreparationBlocker(ContractModel):
|
||||
kind: BlockerKind
|
||||
summary: str = Field(min_length=1)
|
||||
user_action: str = Field(min_length=1)
|
||||
details: list[str] = []
|
||||
|
||||
|
||||
class SourceIdentity(ContractModel):
|
||||
kind: SourceIdentityKind
|
||||
value: str = Field(min_length=1)
|
||||
repository: str | None = None
|
||||
|
||||
@field_validator("value")
|
||||
@classmethod
|
||||
def validate_value(cls, value: str) -> str:
|
||||
normalized = value.strip().lower()
|
||||
if not normalized or any(char not in "0123456789abcdef" for char in normalized):
|
||||
raise ValueError("source identity must be a hexadecimal digest")
|
||||
return normalized
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_digest_length(self) -> SourceIdentity:
|
||||
valid_lengths = {40, 64} if self.kind is SourceIdentityKind.COMMIT else {64}
|
||||
if len(self.value) not in valid_lengths:
|
||||
expected = "40 or 64" if self.kind is SourceIdentityKind.COMMIT else "64"
|
||||
raise ValueError(f"{self.kind} identity must contain {expected} hexadecimal characters")
|
||||
return self
|
||||
|
||||
|
||||
def _validate_relative_path(value: str) -> str:
|
||||
path = value.strip()
|
||||
parsed = PurePosixPath(path)
|
||||
if not path or parsed.is_absolute() or ".." in parsed.parts or path.startswith("./"):
|
||||
raise ValueError("path must be relative to the repository root")
|
||||
return path
|
||||
|
||||
|
||||
class CandidateLocation(ContractModel):
|
||||
file: str
|
||||
start_line: int = Field(ge=1)
|
||||
end_line: int = Field(ge=1)
|
||||
snippet: str | None = None
|
||||
label: str | None = None
|
||||
|
||||
_relative_file = field_validator("file")(_validate_relative_path)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_range(self) -> CandidateLocation:
|
||||
if self.end_line < self.start_line:
|
||||
raise ValueError("end_line must be greater than or equal to start_line")
|
||||
return self
|
||||
|
||||
|
||||
class FixEdit(CandidateLocation):
|
||||
before: str = Field(min_length=1)
|
||||
after: str
|
||||
original_sha256: str | None = None
|
||||
|
||||
|
||||
class CommandSpec(ContractModel):
|
||||
name: str = Field(min_length=1, max_length=120)
|
||||
argv: list[str] = Field(min_length=1)
|
||||
required: bool = True
|
||||
timeout_seconds: int = Field(default=300, ge=1, le=3600)
|
||||
cwd: str = "."
|
||||
|
||||
_relative_cwd = field_validator("cwd")(_validate_relative_path)
|
||||
|
||||
@field_validator("argv")
|
||||
@classmethod
|
||||
def validate_argv(cls, value: list[str]) -> list[str]:
|
||||
if any(not argument or "\x00" in argument for argument in value):
|
||||
raise ValueError("command arguments must be non-empty and cannot contain NUL")
|
||||
return value
|
||||
|
||||
|
||||
class ReproductionSpec(ContractModel):
|
||||
instructions: str = Field(min_length=1)
|
||||
command: CommandSpec | None = None
|
||||
|
||||
|
||||
class ReportedCheck(ContractModel):
|
||||
name: str
|
||||
result: str
|
||||
executed: bool = False
|
||||
|
||||
|
||||
class FixCandidateV1(ContractModel):
|
||||
version: Literal["1"] = "1"
|
||||
source_identity: SourceIdentity | None = None
|
||||
security_invariant: str = Field(min_length=1)
|
||||
finding_locations: list[CandidateLocation] = []
|
||||
draft_edits: list[FixEdit] = []
|
||||
reproduction: ReproductionSpec | None = None
|
||||
reported_checks: list[ReportedCheck] = []
|
||||
known_gaps: list[str] = []
|
||||
|
||||
def digest(self) -> str:
|
||||
payload = json.dumps(
|
||||
self.model_dump(mode="json"),
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
class FixPreparationRequestV1(ContractModel):
|
||||
version: Literal["1"] = "1"
|
||||
organization_id: str | None = None
|
||||
scan_id: str
|
||||
finding_id: str
|
||||
repository_id: str | None = None
|
||||
candidate: FixCandidateV1
|
||||
checks: list[CommandSpec] = []
|
||||
max_repair_attempts: int = Field(default=2, ge=1, le=2)
|
||||
timeout_seconds: int = Field(default=1800, ge=30, le=14400)
|
||||
network_allowed: bool = False
|
||||
credentials_allowed: list[str] = []
|
||||
|
||||
|
||||
class CheckResult(ContractModel):
|
||||
name: str
|
||||
argv: list[str]
|
||||
status: CheckStatus
|
||||
exit_code: int | None = None
|
||||
duration_seconds: float = Field(ge=0)
|
||||
output: str = ""
|
||||
required: bool = True
|
||||
target: VerificationTarget | None = None
|
||||
baseline_status: CheckStatus | None = None
|
||||
baseline_output: str | None = None
|
||||
|
||||
|
||||
class VerifierResult(ContractModel):
|
||||
decision: VerificationDecision
|
||||
summary: str
|
||||
security_invariant_closed: bool = False
|
||||
reproduction_executed: bool = False
|
||||
reproduction_summary: str | None = None
|
||||
sibling_paths_reviewed: list[str] = []
|
||||
preserved_behaviors: list[str] = []
|
||||
gaps: list[str] = []
|
||||
security_tests: list[CheckResult] = []
|
||||
repairable: bool = False
|
||||
blocker: PreparationBlocker | None = None
|
||||
|
||||
|
||||
class RepairOutcome(ContractModel):
|
||||
status: RepairStatus
|
||||
summary: str = Field(min_length=1)
|
||||
gaps: list[str] = []
|
||||
reproduction_command: CommandSpec | None = None
|
||||
turns_used: int = Field(default=0, ge=0)
|
||||
blocker: PreparationBlocker | None = None
|
||||
|
||||
|
||||
class FixPreparationAttempt(ContractModel):
|
||||
attempt: int = Field(ge=1)
|
||||
repair: RepairOutcome
|
||||
checks: list[CheckResult] = []
|
||||
security_reproduction: CheckResult | None = None
|
||||
verifier: VerifierResult | None = None
|
||||
workspace_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class FileManifestEntry(ContractModel):
|
||||
path: str
|
||||
operation: Literal["add", "modify", "delete"]
|
||||
original_sha256: str | None = None
|
||||
resulting_sha256: str | None = None
|
||||
artifact_ref: str | None = None
|
||||
|
||||
_relative_path = field_validator("path")(_validate_relative_path)
|
||||
|
||||
|
||||
class FixPreparationResultV1(ContractModel):
|
||||
version: Literal["1"] = "1"
|
||||
state: PreparationState
|
||||
stop_reason: str
|
||||
source_identity: SourceIdentity | None
|
||||
candidate: FixCandidateV1
|
||||
candidate_digest: str
|
||||
final_file_manifest: list[FileManifestEntry] = []
|
||||
artifact_ref: str | None = None
|
||||
changed_files: list[str] = []
|
||||
diff_summary: str = ""
|
||||
checks: list[CheckResult] = []
|
||||
security_reproduction: CheckResult | None = None
|
||||
verifier: VerifierResult | None = None
|
||||
attempt_history: list[FixPreparationAttempt] = []
|
||||
gaps: list[str] = []
|
||||
blocker: PreparationBlocker | None = None
|
||||
attempts: int = Field(default=0, ge=0)
|
||||
elapsed_seconds: float = Field(default=0, ge=0)
|
||||
cost_usd: float | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
def candidate_from_legacy_report(
|
||||
report: Mapping[str, object],
|
||||
*,
|
||||
source_identity: SourceIdentity | None = None,
|
||||
) -> FixCandidateV1 | None:
|
||||
raw_locations = report.get("code_locations")
|
||||
if not isinstance(raw_locations, list):
|
||||
return None
|
||||
location_values = cast("list[object]", raw_locations)
|
||||
|
||||
locations: list[CandidateLocation] = []
|
||||
edits: list[FixEdit] = []
|
||||
for raw_value in location_values:
|
||||
if not isinstance(raw_value, dict):
|
||||
continue
|
||||
raw = cast("dict[str, object]", raw_value)
|
||||
file_path = raw.get("file")
|
||||
start_line = raw.get("start_line")
|
||||
end_line = raw.get("end_line")
|
||||
if not isinstance(file_path, str) or type(start_line) is not int:
|
||||
continue
|
||||
if type(end_line) is not int:
|
||||
end_line = start_line
|
||||
common = {
|
||||
"file": file_path,
|
||||
"start_line": start_line,
|
||||
"end_line": end_line,
|
||||
"snippet": raw.get("snippet") if isinstance(raw.get("snippet"), str) else None,
|
||||
"label": raw.get("label") if isinstance(raw.get("label"), str) else None,
|
||||
}
|
||||
try:
|
||||
locations.append(CandidateLocation.model_validate(common))
|
||||
before = raw.get("fix_before")
|
||||
after = raw.get("fix_after")
|
||||
if isinstance(before, str) and isinstance(after, str):
|
||||
edits.append(FixEdit.model_validate({**common, "before": before, "after": after}))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if not locations:
|
||||
return None
|
||||
|
||||
invariant = str(
|
||||
report.get("remediation_steps") or report.get("technical_analysis") or ""
|
||||
).strip()
|
||||
if not invariant:
|
||||
invariant = "Resolve the reported security finding without changing legitimate behavior."
|
||||
|
||||
reported_checks: list[ReportedCheck] = []
|
||||
verification = str(report.get("fix_verification") or "").strip()
|
||||
if verification:
|
||||
reported_checks.append(
|
||||
ReportedCheck(name="reporting-agent verification", result=verification, executed=False)
|
||||
)
|
||||
|
||||
return FixCandidateV1(
|
||||
source_identity=source_identity,
|
||||
security_invariant=invariant,
|
||||
finding_locations=locations,
|
||||
draft_edits=edits,
|
||||
reproduction=ReproductionSpec(
|
||||
instructions=str(report.get("poc_description") or report.get("evidence") or invariant)
|
||||
),
|
||||
reported_checks=reported_checks,
|
||||
known_gaps=["The reporting-agent verification is not independent."],
|
||||
)
|
||||
@@ -1,109 +0,0 @@
|
||||
"""Deterministic source anchoring for finding locations and draft edits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from strix.fix.contracts import CandidateLocation, FixCandidateV1, FixEdit
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class AnchorStatus(StrEnum):
|
||||
UNIQUE = "unique"
|
||||
MISSING = "missing"
|
||||
AMBIGUOUS = "ambiguous"
|
||||
STALE = "stale"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AnchorResult:
|
||||
status: AnchorStatus
|
||||
location: CandidateLocation | FixEdit
|
||||
matches: tuple[int, ...] = ()
|
||||
|
||||
|
||||
def sha256_text(value: str) -> str:
|
||||
return hashlib.sha256(value.encode()).hexdigest()
|
||||
|
||||
|
||||
def _find_blocks(content: str, block: str) -> tuple[int, ...]:
|
||||
source_lines = content.splitlines()
|
||||
block_lines = block.splitlines()
|
||||
if not block_lines:
|
||||
return ()
|
||||
width = len(block_lines)
|
||||
return tuple(
|
||||
index + 1
|
||||
for index in range(len(source_lines) - width + 1)
|
||||
if source_lines[index : index + width] == block_lines
|
||||
)
|
||||
|
||||
|
||||
def _read_anchor_source(root: Path, file: str) -> str | None:
|
||||
"""Read a location's source, refusing symlinks that escape ``root`` and
|
||||
files that cannot be decoded as UTF-8."""
|
||||
resolved_root = root.resolve()
|
||||
file_path = (root / file).resolve()
|
||||
if not file_path.is_relative_to(resolved_root) or not file_path.is_file():
|
||||
return None
|
||||
try:
|
||||
return file_path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError):
|
||||
return None
|
||||
|
||||
|
||||
def anchor_location(
|
||||
root: Path,
|
||||
location: CandidateLocation | FixEdit,
|
||||
) -> AnchorResult:
|
||||
content = _read_anchor_source(root, location.file)
|
||||
if content is None:
|
||||
return AnchorResult(AnchorStatus.MISSING, location)
|
||||
block = location.before if isinstance(location, FixEdit) else location.snippet
|
||||
if not block:
|
||||
return AnchorResult(AnchorStatus.UNIQUE, location, (location.start_line,))
|
||||
|
||||
matches = _find_blocks(content, block)
|
||||
if not matches:
|
||||
if (
|
||||
isinstance(location, FixEdit)
|
||||
and location.original_sha256
|
||||
and sha256_text(content) != location.original_sha256
|
||||
):
|
||||
return AnchorResult(AnchorStatus.STALE, location)
|
||||
return AnchorResult(AnchorStatus.MISSING, location)
|
||||
if len(matches) > 1:
|
||||
return AnchorResult(AnchorStatus.AMBIGUOUS, location, matches)
|
||||
|
||||
start_line = next(iter(matches))
|
||||
end_line = start_line + len(block.splitlines()) - 1
|
||||
anchored = location.model_copy(update={"start_line": start_line, "end_line": end_line})
|
||||
return AnchorResult(AnchorStatus.UNIQUE, anchored, matches)
|
||||
|
||||
|
||||
def anchor_candidate(
|
||||
root: Path,
|
||||
candidate: FixCandidateV1,
|
||||
) -> tuple[FixCandidateV1, list[AnchorResult]]:
|
||||
results = [
|
||||
*(anchor_location(root, location) for location in candidate.finding_locations),
|
||||
*(anchor_location(root, edit) for edit in candidate.draft_edits),
|
||||
]
|
||||
finding_count = len(candidate.finding_locations)
|
||||
anchored_locations = [result.location for result in results[:finding_count]]
|
||||
anchored_edits = [result.location for result in results[finding_count:]]
|
||||
return (
|
||||
candidate.model_copy(
|
||||
update={
|
||||
"finding_locations": anchored_locations,
|
||||
"draft_edits": anchored_edits,
|
||||
}
|
||||
),
|
||||
results,
|
||||
)
|
||||
@@ -1,755 +0,0 @@
|
||||
"""Repository fix preparation engine."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from strix.fix.contracts import (
|
||||
BlockerKind,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CommandSpec,
|
||||
FileManifestEntry,
|
||||
FixCandidateV1,
|
||||
FixPreparationAttempt,
|
||||
FixPreparationRequestV1,
|
||||
FixPreparationResultV1,
|
||||
PreparationBlocker,
|
||||
PreparationState,
|
||||
RepairOutcome,
|
||||
RepairStatus,
|
||||
VerificationDecision,
|
||||
VerificationTarget,
|
||||
VerifierResult,
|
||||
)
|
||||
from strix.fix.locations import AnchorStatus, anchor_location
|
||||
|
||||
|
||||
class PreparationCancelledError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
CommandRunner = Callable[[Path, CommandSpec], Awaitable[CheckResult]]
|
||||
ManifestBuilder = Callable[[Path], Awaitable[tuple[list[FileManifestEntry], str, str | None]]]
|
||||
CancellationCheck = Callable[[], bool]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreparationPolicy:
|
||||
max_repair_attempts: int = 2
|
||||
timeout_seconds: int = 1800
|
||||
max_output_chars: int = 20000
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 1 <= self.max_repair_attempts <= 2:
|
||||
raise ValueError("max_repair_attempts must be between 1 and 2")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreparationContext:
|
||||
request: FixPreparationRequestV1
|
||||
workspace: Path
|
||||
candidate: FixCandidateV1
|
||||
attempt: int = 0
|
||||
feedback: list[FixPreparationAttempt] = field(default_factory=list)
|
||||
|
||||
|
||||
RepairAgent = Callable[
|
||||
[PreparationContext, list[CheckResult]],
|
||||
Awaitable[RepairOutcome | None],
|
||||
]
|
||||
IndependentVerifier = Callable[
|
||||
[PreparationContext, list[CheckResult]],
|
||||
Awaitable[VerifierResult],
|
||||
]
|
||||
SourceVerifier = Callable[[PreparationContext], Awaitable[bool]]
|
||||
|
||||
|
||||
_COMMAND_ENV_ALLOWLIST = frozenset(
|
||||
{
|
||||
"HOME",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"PATH",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"SYSTEMROOT",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
"TMPDIR",
|
||||
"VIRTUAL_ENV",
|
||||
"SystemRoot",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _network_isolation_prefix() -> tuple[str, ...] | None:
|
||||
"""Return a working ``unshare`` prefix that creates an empty network
|
||||
namespace, or None when the platform cannot isolate egress."""
|
||||
for prefix in (("unshare", "-Urn"), ("unshare", "-n")):
|
||||
try:
|
||||
probe = subprocess.run( # noqa: S603
|
||||
[*prefix, "true"],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
continue
|
||||
if probe.returncode == 0:
|
||||
return prefix
|
||||
return None
|
||||
|
||||
|
||||
def _command_environment(credentials_allowed: Iterable[str]) -> dict[str, str]:
|
||||
allowed = _COMMAND_ENV_ALLOWLIST | set(credentials_allowed)
|
||||
env = {key: value for key, value in os.environ.items() if key in allowed}
|
||||
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
return env
|
||||
|
||||
|
||||
async def run_command(
|
||||
workspace: Path,
|
||||
command: CommandSpec,
|
||||
*,
|
||||
credentials_allowed: Iterable[str] = (),
|
||||
network_allowed: bool = False,
|
||||
) -> CheckResult:
|
||||
started = time.monotonic()
|
||||
cwd = (workspace / command.cwd).resolve()
|
||||
if not cwd.is_relative_to(workspace.resolve()) or not cwd.is_dir():
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.UNAVAILABLE,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
output="The command working directory is unavailable.",
|
||||
required=command.required,
|
||||
)
|
||||
argv = list(command.argv)
|
||||
if not network_allowed:
|
||||
prefix = _network_isolation_prefix()
|
||||
if prefix is None:
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.UNAVAILABLE,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
output="Network isolation is unavailable, so the command was not run.",
|
||||
required=command.required,
|
||||
)
|
||||
argv = [*prefix, *argv]
|
||||
process: asyncio.subprocess.Process | None = None
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*argv,
|
||||
cwd=cwd,
|
||||
env=_command_environment(credentials_allowed),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
)
|
||||
output, _ = await asyncio.wait_for(process.communicate(), command.timeout_seconds)
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.UNAVAILABLE,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
output=str(exc),
|
||||
required=command.required,
|
||||
)
|
||||
except TimeoutError:
|
||||
if process is not None:
|
||||
process.kill()
|
||||
await process.wait()
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.FAILED,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
output=f"Timed out after {command.timeout_seconds} seconds.",
|
||||
required=command.required,
|
||||
)
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.PASSED if process.returncode == 0 else CheckStatus.FAILED,
|
||||
exit_code=process.returncode,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
output=output.decode(errors="replace")[-20000:],
|
||||
required=command.required,
|
||||
)
|
||||
|
||||
|
||||
async def build_git_manifest(
|
||||
workspace: Path,
|
||||
) -> tuple[list[FileManifestEntry], str, str | None]:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"git",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--untracked-files=all",
|
||||
"-z",
|
||||
cwd=workspace,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
output, error = await process.communicate()
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(error.decode(errors="replace"))
|
||||
|
||||
entries: list[FileManifestEntry] = []
|
||||
changed_files: list[str] = []
|
||||
records = [record for record in output.decode(errors="replace").split("\0") if record]
|
||||
workspace_resolved = workspace.resolve()
|
||||
index = 0
|
||||
while index < len(records):
|
||||
record = records[index]
|
||||
status = record[:2]
|
||||
path_text = record[3:]
|
||||
if "R" in status or "C" in status:
|
||||
index += 1
|
||||
path = workspace / path_text
|
||||
changed_files.append(path_text)
|
||||
operation: Literal["add", "modify", "delete"]
|
||||
if status == "??" or "A" in status:
|
||||
operation = "add"
|
||||
elif "D" in status:
|
||||
operation = "delete"
|
||||
else:
|
||||
operation = "modify"
|
||||
resolved = path.resolve()
|
||||
contained = resolved.is_relative_to(workspace_resolved)
|
||||
if operation == "add" and contained and resolved.is_dir():
|
||||
for child in sorted(resolved.rglob("*")):
|
||||
child_resolved = child.resolve()
|
||||
if (
|
||||
not child_resolved.is_file()
|
||||
or not child_resolved.is_relative_to(workspace_resolved)
|
||||
or ".git" in child.relative_to(resolved).parts
|
||||
):
|
||||
continue
|
||||
entries.append(
|
||||
FileManifestEntry(
|
||||
path=child_resolved.relative_to(workspace_resolved).as_posix(),
|
||||
operation="add",
|
||||
resulting_sha256=hashlib.sha256(child_resolved.read_bytes()).hexdigest(),
|
||||
)
|
||||
)
|
||||
index += 1
|
||||
continue
|
||||
resulting = (
|
||||
hashlib.sha256(resolved.read_bytes()).hexdigest()
|
||||
if contained and resolved.is_file()
|
||||
else None
|
||||
)
|
||||
original: str | None = None
|
||||
if operation != "add":
|
||||
original_process = await asyncio.create_subprocess_exec(
|
||||
"git",
|
||||
"show",
|
||||
f"HEAD:{path_text}",
|
||||
cwd=workspace,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
original_bytes, _ = await original_process.communicate()
|
||||
if original_process.returncode == 0:
|
||||
original = hashlib.sha256(original_bytes).hexdigest()
|
||||
entries.append(
|
||||
FileManifestEntry(
|
||||
path=path_text,
|
||||
operation=operation,
|
||||
original_sha256=original,
|
||||
resulting_sha256=resulting,
|
||||
)
|
||||
)
|
||||
index += 1
|
||||
|
||||
summary_process = await asyncio.create_subprocess_exec(
|
||||
"git",
|
||||
"diff",
|
||||
"--stat",
|
||||
"--",
|
||||
cwd=workspace,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
summary, _ = await summary_process.communicate()
|
||||
return entries, summary.decode(errors="replace").strip(), None
|
||||
|
||||
|
||||
async def _workspace_digest(workspace: Path) -> str:
|
||||
manifest, _, _ = await build_git_manifest(workspace)
|
||||
payload = json.dumps(
|
||||
[entry.model_dump(mode="json") for entry in manifest],
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
async def _verify_source(context: PreparationContext) -> bool:
|
||||
identity = context.candidate.source_identity
|
||||
if identity is None:
|
||||
return False
|
||||
if identity.kind == "commit":
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"git",
|
||||
"rev-parse",
|
||||
"HEAD",
|
||||
cwd=context.workspace,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
output, _ = await process.communicate()
|
||||
if process.returncode != 0 or output.decode().strip().lower() != identity.value:
|
||||
return False
|
||||
status_process = await asyncio.create_subprocess_exec(
|
||||
"git",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"-z",
|
||||
cwd=context.workspace,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
status_output, _ = await status_process.communicate()
|
||||
return status_process.returncode == 0 and not status_output.strip(b"\x00")
|
||||
|
||||
|
||||
def _result(
|
||||
context: PreparationContext,
|
||||
*,
|
||||
state: PreparationState,
|
||||
reason: str,
|
||||
started: float,
|
||||
checks: list[CheckResult] | None = None,
|
||||
reproduction: CheckResult | None = None,
|
||||
verifier: VerifierResult | None = None,
|
||||
gaps: list[str] | None = None,
|
||||
manifest: list[FileManifestEntry] | None = None,
|
||||
diff_summary: str = "",
|
||||
artifact_ref: str | None = None,
|
||||
attempt_history: list[FixPreparationAttempt] | None = None,
|
||||
blocker: PreparationBlocker | None = None,
|
||||
) -> FixPreparationResultV1:
|
||||
return FixPreparationResultV1(
|
||||
state=state,
|
||||
stop_reason=reason,
|
||||
source_identity=context.candidate.source_identity,
|
||||
candidate=context.candidate,
|
||||
candidate_digest=context.candidate.digest(),
|
||||
final_file_manifest=manifest or [],
|
||||
artifact_ref=artifact_ref,
|
||||
changed_files=[entry.path for entry in manifest or []],
|
||||
diff_summary=diff_summary,
|
||||
checks=checks or [],
|
||||
security_reproduction=reproduction,
|
||||
verifier=verifier,
|
||||
attempt_history=attempt_history or [],
|
||||
gaps=gaps or [],
|
||||
blocker=blocker,
|
||||
attempts=context.attempt,
|
||||
elapsed_seconds=time.monotonic() - started,
|
||||
)
|
||||
|
||||
|
||||
def _repair_outcome(value: RepairOutcome | None) -> RepairOutcome:
|
||||
if value is not None:
|
||||
return value
|
||||
return RepairOutcome(
|
||||
status=RepairStatus.COMPLETE,
|
||||
summary="The repair implementation returned control for independent evaluation.",
|
||||
)
|
||||
|
||||
|
||||
def _required_checks_pass(checks: list[CheckResult]) -> bool:
|
||||
required = [result for result in checks if result.required]
|
||||
return bool(required) and all(result.status is CheckStatus.PASSED for result in required)
|
||||
|
||||
|
||||
def _verification_passes(
|
||||
checks: list[CheckResult],
|
||||
verifier: VerifierResult,
|
||||
) -> bool:
|
||||
return (
|
||||
_required_checks_pass(checks)
|
||||
and verifier.decision is VerificationDecision.VERIFIED
|
||||
and verifier.security_invariant_closed
|
||||
and verifier.reproduction_executed
|
||||
and any(
|
||||
result.target is VerificationTarget.PATCHED and result.status is CheckStatus.PASSED
|
||||
for result in verifier.security_tests
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _verification_gaps(
|
||||
repair_outcome: RepairOutcome,
|
||||
checks: list[CheckResult],
|
||||
verifier: VerifierResult,
|
||||
) -> list[str]:
|
||||
gaps = list(repair_outcome.gaps)
|
||||
required = [result for result in checks if result.required]
|
||||
if not required:
|
||||
gaps.append("No required repository check was configured.")
|
||||
gaps.extend(
|
||||
f"{result.name}: required check {result.status}"
|
||||
for result in required
|
||||
if result.status is not CheckStatus.PASSED
|
||||
)
|
||||
gaps.extend(
|
||||
f"{result.name}: optional check {result.status}"
|
||||
for result in checks
|
||||
if not result.required and result.status is not CheckStatus.PASSED
|
||||
)
|
||||
if not verifier.security_tests:
|
||||
gaps.append("The independent verifier did not execute a security test.")
|
||||
elif not any(
|
||||
result.target is VerificationTarget.PATCHED and result.status is CheckStatus.PASSED
|
||||
for result in verifier.security_tests
|
||||
):
|
||||
gaps.append("The independent verifier did not pass a security test on the patch.")
|
||||
if not verifier.reproduction_executed:
|
||||
gaps.append("The independent verifier did not execute the security reproduction.")
|
||||
if not verifier.security_invariant_closed:
|
||||
gaps.append("The independent verifier did not prove the security invariant.")
|
||||
gaps.extend(verifier.gaps)
|
||||
return list(dict.fromkeys(gaps))
|
||||
|
||||
|
||||
def _matches_baseline_failure(result: CheckResult) -> bool:
|
||||
if result.baseline_status is not CheckStatus.FAILED:
|
||||
return False
|
||||
candidate_output = " ".join(result.output.split())
|
||||
baseline_output = " ".join((result.baseline_output or "").split())
|
||||
return bool(candidate_output and candidate_output == baseline_output)
|
||||
|
||||
|
||||
async def prepare_fix( # noqa: PLR0915
|
||||
request: FixPreparationRequestV1,
|
||||
workspace: Path,
|
||||
*,
|
||||
repair: RepairAgent,
|
||||
verify: IndependentVerifier,
|
||||
command_runner: CommandRunner = run_command,
|
||||
manifest_builder: ManifestBuilder = build_git_manifest,
|
||||
source_verifier: SourceVerifier = _verify_source,
|
||||
cancelled: CancellationCheck = lambda: False,
|
||||
policy: PreparationPolicy | None = None,
|
||||
) -> FixPreparationResultV1:
|
||||
started = time.monotonic()
|
||||
resolved_policy = policy or PreparationPolicy(
|
||||
max_repair_attempts=request.max_repair_attempts,
|
||||
timeout_seconds=request.timeout_seconds,
|
||||
)
|
||||
context = PreparationContext(request=request, workspace=workspace, candidate=request.candidate)
|
||||
runner: CommandRunner = command_runner
|
||||
if runner is run_command:
|
||||
runner = functools.partial(
|
||||
run_command,
|
||||
credentials_allowed=request.credentials_allowed,
|
||||
network_allowed=request.network_allowed,
|
||||
)
|
||||
|
||||
async def execute() -> FixPreparationResultV1: # noqa: PLR0911, PLR0912, PLR0915
|
||||
if cancelled():
|
||||
raise PreparationCancelledError
|
||||
if not await source_verifier(context):
|
||||
blocker = PreparationBlocker(
|
||||
kind=BlockerKind.SOURCE,
|
||||
summary="The repository no longer matches the finding source.",
|
||||
user_action="Refresh the finding against the current repository revision.",
|
||||
)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.STALE,
|
||||
reason=blocker.summary,
|
||||
blocker=blocker,
|
||||
started=started,
|
||||
)
|
||||
|
||||
anchors = [
|
||||
anchor_location(workspace, location) for location in context.candidate.finding_locations
|
||||
]
|
||||
if any(result.status is not AnchorStatus.UNIQUE for result in anchors):
|
||||
details = [
|
||||
f"{result.location.file}: {result.status}"
|
||||
for result in anchors
|
||||
if result.status is not AnchorStatus.UNIQUE
|
||||
]
|
||||
blocker = PreparationBlocker(
|
||||
kind=BlockerKind.SOURCE,
|
||||
summary="The reported finding locations could not be resolved uniquely.",
|
||||
user_action="Refresh the finding or identify the affected source location.",
|
||||
details=details,
|
||||
)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.BLOCKED,
|
||||
reason=blocker.summary,
|
||||
gaps=details,
|
||||
blocker=blocker,
|
||||
started=started,
|
||||
)
|
||||
context.candidate = context.candidate.model_copy(
|
||||
update={"finding_locations": [result.location for result in anchors]}
|
||||
)
|
||||
|
||||
checks: list[CheckResult] = []
|
||||
verifier: VerifierResult | None = None
|
||||
attempt_history: list[FixPreparationAttempt] = []
|
||||
for attempt in range(1, resolved_policy.max_repair_attempts + 1):
|
||||
context.attempt = attempt
|
||||
if cancelled():
|
||||
raise PreparationCancelledError
|
||||
repair_outcome = _repair_outcome(await repair(context, checks))
|
||||
|
||||
if repair_outcome.status is RepairStatus.BLOCKED:
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
blocker = repair_outcome.blocker or PreparationBlocker(
|
||||
kind=BlockerKind.EXTERNAL_CONFIGURATION,
|
||||
summary=repair_outcome.summary,
|
||||
user_action=(
|
||||
"Provide the missing repository, credential, or production "
|
||||
"configuration prerequisite."
|
||||
),
|
||||
details=repair_outcome.gaps,
|
||||
)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.BLOCKED,
|
||||
reason=blocker.summary,
|
||||
gaps=repair_outcome.gaps,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
blocker=blocker,
|
||||
started=started,
|
||||
)
|
||||
|
||||
if repair_outcome.status is not RepairStatus.COMPLETE:
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason=repair_outcome.summary,
|
||||
gaps=repair_outcome.gaps,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
started=started,
|
||||
)
|
||||
|
||||
checks = [await runner(workspace, check) for check in request.checks]
|
||||
workspace_digest = await _workspace_digest(workspace)
|
||||
attempt_record = FixPreparationAttempt(
|
||||
attempt=attempt,
|
||||
repair=repair_outcome,
|
||||
checks=checks,
|
||||
workspace_digest=workspace_digest,
|
||||
)
|
||||
attempt_history.append(attempt_record)
|
||||
context.feedback = list(attempt_history)
|
||||
|
||||
required = [result for result in checks if result.required]
|
||||
unavailable = [
|
||||
result for result in required if result.status is CheckStatus.UNAVAILABLE
|
||||
]
|
||||
if not required or unavailable:
|
||||
details = (
|
||||
["No required repository quality check was configured."]
|
||||
if not required
|
||||
else [f"{result.name}: {result.output}" for result in unavailable]
|
||||
)
|
||||
blocker = PreparationBlocker(
|
||||
kind=BlockerKind.ENVIRONMENT,
|
||||
summary="The repository quality gate could not run.",
|
||||
user_action=(
|
||||
"Provide the missing tool or repository setup needed to run "
|
||||
"the required checks."
|
||||
),
|
||||
details=details,
|
||||
)
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.BLOCKED,
|
||||
reason=blocker.summary,
|
||||
checks=checks,
|
||||
gaps=blocker.details,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
blocker=blocker,
|
||||
started=started,
|
||||
)
|
||||
|
||||
failed = [result for result in required if result.status is CheckStatus.FAILED]
|
||||
baseline_failures = [result for result in failed if _matches_baseline_failure(result)]
|
||||
if baseline_failures:
|
||||
blocker = PreparationBlocker(
|
||||
kind=BlockerKind.REPOSITORY_BASELINE,
|
||||
summary="Required checks also fail on the unchanged repository.",
|
||||
user_action=(
|
||||
"Repair the repository baseline or identify authoritative "
|
||||
"replacement checks."
|
||||
),
|
||||
details=[result.name for result in baseline_failures],
|
||||
)
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.BLOCKED,
|
||||
reason=blocker.summary,
|
||||
checks=checks,
|
||||
gaps=blocker.details,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
blocker=blocker,
|
||||
started=started,
|
||||
)
|
||||
if failed:
|
||||
if attempt < resolved_policy.max_repair_attempts:
|
||||
continue
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason="The repair did not pass the repository quality gate.",
|
||||
checks=checks,
|
||||
gaps=[f"{result.name}: {result.output}" for result in failed],
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
started=started,
|
||||
)
|
||||
|
||||
verifier = await verify(context, checks)
|
||||
attempt_record.verifier = verifier
|
||||
attempt_record.security_reproduction = next(
|
||||
(
|
||||
result
|
||||
for result in verifier.security_tests
|
||||
if result.target is VerificationTarget.PATCHED
|
||||
),
|
||||
None,
|
||||
)
|
||||
context.feedback = list(attempt_history)
|
||||
gaps = _verification_gaps(repair_outcome, checks, verifier)
|
||||
|
||||
if verifier.blocker is not None:
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.BLOCKED,
|
||||
reason=verifier.blocker.summary,
|
||||
checks=checks,
|
||||
reproduction=attempt_record.security_reproduction,
|
||||
verifier=verifier,
|
||||
gaps=gaps,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
blocker=verifier.blocker,
|
||||
started=started,
|
||||
)
|
||||
|
||||
if _verification_passes(checks, verifier):
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
if not manifest:
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason="The repair did not change repository source.",
|
||||
checks=checks,
|
||||
verifier=verifier,
|
||||
gaps=["No prepared source change was produced."],
|
||||
attempt_history=attempt_history,
|
||||
started=started,
|
||||
)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.READY,
|
||||
reason="The fix passed repository checks and security verification.",
|
||||
checks=checks,
|
||||
reproduction=attempt_record.security_reproduction,
|
||||
verifier=verifier,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
started=started,
|
||||
)
|
||||
|
||||
if verifier.repairable and attempt < resolved_policy.max_repair_attempts:
|
||||
continue
|
||||
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason="The independent security verifier did not approve the repair.",
|
||||
checks=checks,
|
||||
reproduction=attempt_record.security_reproduction,
|
||||
verifier=verifier,
|
||||
gaps=gaps,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
started=started,
|
||||
)
|
||||
|
||||
manifest, summary, artifact_ref = await manifest_builder(workspace)
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason="The repair did not satisfy the required gates.",
|
||||
checks=checks,
|
||||
verifier=verifier,
|
||||
manifest=manifest,
|
||||
diff_summary=summary,
|
||||
artifact_ref=artifact_ref,
|
||||
attempt_history=attempt_history,
|
||||
started=started,
|
||||
)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(resolved_policy.timeout_seconds):
|
||||
return await execute()
|
||||
except PreparationCancelledError:
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason="Fix preparation was cancelled.",
|
||||
attempt_history=context.feedback,
|
||||
started=started,
|
||||
)
|
||||
except TimeoutError:
|
||||
return _result(
|
||||
context,
|
||||
state=PreparationState.FAILED,
|
||||
reason="Fix preparation exceeded its time limit.",
|
||||
attempt_history=context.feedback,
|
||||
started=started,
|
||||
)
|
||||
@@ -122,32 +122,10 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
||||
console.print(vuln_panel)
|
||||
console.print()
|
||||
|
||||
def display_vulnerability_deleted(report: dict[str, Any]) -> None:
|
||||
report_id = str(report.get("id", "unknown"))
|
||||
deletion = report.get("deletion")
|
||||
deletion = deletion if isinstance(deletion, dict) else {}
|
||||
deleted_by = deletion.get("agent_name") or deletion.get("agent_id") or "agent"
|
||||
text = Text()
|
||||
text.append("Withdrawn: ", style="bold")
|
||||
text.append(f"{report.get('title', '')}\n\n")
|
||||
text.append(f"Deleted by {deleted_by}. ", style="dim")
|
||||
text.append(str(deletion.get("reason") or ""))
|
||||
console.print(
|
||||
Panel(
|
||||
text,
|
||||
title=f"[bold yellow]{report_id.upper()} (withdrawn)",
|
||||
title_align="left",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
report_state.vulnerability_found_callback = display_vulnerability
|
||||
report_state.vulnerability_updated_callback = lambda report: display_vulnerability(
|
||||
report, updated=True
|
||||
)
|
||||
report_state.vulnerability_deleted_callback = display_vulnerability_deleted
|
||||
|
||||
def cleanup_on_exit() -> None:
|
||||
report_state.cleanup()
|
||||
|
||||
@@ -98,14 +98,6 @@ Examples:
|
||||
# Extra files placed in the sandbox workspace
|
||||
strix --target ./my-project --workspace-file ./wordlist.txt
|
||||
strix --target https://app.com --workspace-file ./openapi.yaml:specs/openapi.yaml
|
||||
|
||||
Strix Cloud:
|
||||
strix cloud login
|
||||
strix cloud scans start --source . --yes --wait
|
||||
strix cloud # list every cloud resource
|
||||
|
||||
Run a pentest in Strix Cloud https://app.strix.ai
|
||||
Try Strix Enterprise https://strix.ai/demo
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -171,9 +163,8 @@ Strix Cloud:
|
||||
help="Place a file from this machine into the sandbox workspace before the scan "
|
||||
"starts, for example a wordlist, an API specification, or notes. Repeat the option "
|
||||
"for more files. DEST is the path inside /workspace and defaults to the file name "
|
||||
"(for example '--workspace-file ./wordlist.txt:lists/wordlist.txt'). Strix copies "
|
||||
"the file into the sandbox, outside every target directory. The agent can edit the "
|
||||
"copy. The file on this machine does not change.",
|
||||
"(for example '--workspace-file ./wordlist.txt:lists/wordlist.txt'). The file is "
|
||||
"read-only inside the sandbox and lands outside every target directory.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
|
||||
@@ -14,7 +14,7 @@ import sys
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
|
||||
from strix.interface.cloud import http
|
||||
import strix.interface.cloud.http as http # noqa: PLR0402
|
||||
from strix.interface.cloud.render import json_mode
|
||||
from strix.interface.cloud.runner import resolve, run
|
||||
from strix.interface.cloud.session import run_session
|
||||
|
||||
@@ -34,30 +34,13 @@ EXIT_AUTH = 4
|
||||
EXIT_PAYMENT = 5
|
||||
|
||||
|
||||
TOPUP_COMMAND = "strix cloud billing topup --credits <count>"
|
||||
BALANCE_COMMAND = "strix cloud billing credits"
|
||||
|
||||
|
||||
class CloudError(Exception):
|
||||
"""A failed cloud command. Carries the process exit code.
|
||||
"""A failed cloud command. Carries the process exit code."""
|
||||
|
||||
`next_step` is a short recovery instruction that the runner prints on its
|
||||
own line after the error, so a person or an agent can act without reading
|
||||
the docs.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
exit_code: int = EXIT_ERROR,
|
||||
payload: Any = None,
|
||||
next_step: str | None = None,
|
||||
) -> None:
|
||||
def __init__(self, message: str, *, exit_code: int = EXIT_ERROR, payload: Any = None) -> None:
|
||||
super().__init__(message)
|
||||
self.exit_code = exit_code
|
||||
self.payload = payload
|
||||
self.next_step = next_step
|
||||
|
||||
|
||||
class CloudTransportError(CloudError):
|
||||
@@ -366,43 +349,13 @@ def check(response: requests.Response) -> Any:
|
||||
error_code = error_code or str(nested.get("code") or "")
|
||||
detail = str(nested.get("message") or detail)
|
||||
message = detail or f"HTTP {response.status_code}"
|
||||
if error_code == "scan_credit_limit_reached" or response.status_code == 402:
|
||||
raise payment_required_error(data, detail=detail)
|
||||
if error_code == "scan_credit_limit_reached":
|
||||
raise CloudError(message, exit_code=EXIT_PAYMENT, payload=data)
|
||||
if response.status_code in (401, 403):
|
||||
raise CloudError(message, exit_code=EXIT_AUTH, payload=data)
|
||||
if response.status_code == 402:
|
||||
hint = detail or (
|
||||
"not enough credits. Run `strix cloud billing topup --credits N` to buy credits."
|
||||
)
|
||||
raise CloudError(hint, exit_code=EXIT_PAYMENT, payload=data)
|
||||
raise CloudError(message, exit_code=EXIT_ERROR, payload=data)
|
||||
|
||||
|
||||
def topup_url() -> str:
|
||||
return f"{app_url()}/settings/billing"
|
||||
|
||||
|
||||
def topup_next_step(url: str | None = None) -> str:
|
||||
return (
|
||||
f"Buy credits with `{TOPUP_COMMAND}` or at {url or topup_url()}. "
|
||||
f"Run `{BALANCE_COMMAND}` to see the balance. Then retry this command."
|
||||
)
|
||||
|
||||
|
||||
def payment_required_error(data: Any, *, detail: str = "") -> CloudError:
|
||||
"""Build the error for an exhausted credit balance.
|
||||
|
||||
The platform sends the recovery instruction in `hint` and repeats it inside
|
||||
`detail`. The CLI shows the instruction once, on its own line, and adds its
|
||||
own instruction when the platform sends none.
|
||||
"""
|
||||
server_hint = ""
|
||||
server_url: str | None = None
|
||||
if isinstance(data, dict):
|
||||
raw = cast("dict[str, Any]", data)
|
||||
server_hint = str(raw.get("hint") or "").strip()
|
||||
raw_url = raw.get("topup_url")
|
||||
if isinstance(raw_url, str) and raw_url.startswith("https://"):
|
||||
server_url = raw_url
|
||||
message = detail.strip()
|
||||
if server_hint and message.endswith(server_hint):
|
||||
message = message[: -len(server_hint)].strip()
|
||||
if not message:
|
||||
message = "Not enough credits to run this command."
|
||||
next_step = server_hint or topup_next_step(server_url)
|
||||
return CloudError(message, exit_code=EXIT_PAYMENT, payload=data, next_step=next_step)
|
||||
@@ -1035,14 +1035,10 @@ def _emit_error(
|
||||
payload = {"error": str(exc)}
|
||||
if exc.payload is not None:
|
||||
payload["detail"] = exc.payload
|
||||
if exc.next_step:
|
||||
payload["next_step"] = exc.next_step
|
||||
sys.stdout.write(json.dumps(payload, indent=2, default=str) + "\n")
|
||||
return
|
||||
target = Console(stderr=True) if to_stderr else console
|
||||
target.print(f"[red]Error:[/] {escape(sanitize_terminal_text(exc))}")
|
||||
if exc.next_step:
|
||||
target.print(f"[yellow]Next step:[/] {escape(sanitize_terminal_text(exc.next_step))}")
|
||||
|
||||
|
||||
def _emit_interrupted(console: Console, *, as_json: bool, to_stderr: bool) -> None:
|
||||
|
||||
@@ -203,17 +203,6 @@ def prepare_source(
|
||||
"""Select safe source files and build a bounded temporary ZIP archive."""
|
||||
source = Path(value).expanduser().resolve()
|
||||
if not source.is_dir():
|
||||
if source.is_file() and (
|
||||
source.name.lower().endswith(_ARCHIVE_SUFFIXES) or _has_archive_magic(source)
|
||||
):
|
||||
raise http.CloudError(
|
||||
f"--source must be a directory, not an archive: {source}",
|
||||
next_step=(
|
||||
"Extract the archive and pass the directory to --source. Strix packs the "
|
||||
"directory and excludes dependencies, build output, and secret-like files. "
|
||||
"Add --dry-run --show-files to review the selection first."
|
||||
),
|
||||
)
|
||||
raise http.CloudError(f"--source must be a directory: {source}")
|
||||
manifest = select_source(
|
||||
source,
|
||||
@@ -235,34 +224,14 @@ def prepare_source(
|
||||
archive_bytes = archive_path.stat().st_size
|
||||
if archive_bytes > MAX_ARCHIVE_BYTES:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
raise _archive_too_large_error(manifest, archive_bytes)
|
||||
raise http.CloudError(
|
||||
"source archive is larger than the 50 MB upload limit; narrow --source or "
|
||||
"add --exclude patterns."
|
||||
)
|
||||
digest = _sha256(archive_path)
|
||||
return SourceBundle(manifest, archive_path, archive_bytes, digest)
|
||||
|
||||
|
||||
_LARGEST_FILES_SHOWN = 5
|
||||
|
||||
|
||||
def _format_mib(size: int) -> str:
|
||||
return f"{size / (1024 * 1024):.1f} MiB"
|
||||
|
||||
|
||||
def _archive_too_large_error(manifest: SourceManifest, archive_bytes: int) -> http.CloudError:
|
||||
"""Name the largest selected files so the user knows what to exclude."""
|
||||
largest = sorted(manifest.files, key=lambda item: item.size, reverse=True)
|
||||
listed = ", ".join(
|
||||
f"{item.archive_name} ({_format_mib(item.size)})" for item in largest[:_LARGEST_FILES_SHOWN]
|
||||
)
|
||||
return http.CloudError(
|
||||
f"the source archive is {_format_mib(archive_bytes)}, larger than the "
|
||||
f"{_format_mib(MAX_ARCHIVE_BYTES)} upload limit. Largest files: {listed}.",
|
||||
next_step=(
|
||||
"Add --exclude patterns for large files or directories, or point --source at a "
|
||||
"smaller directory. Run with --dry-run --show-files to review the selection."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def select_source(
|
||||
source: Path,
|
||||
*,
|
||||
|
||||
@@ -1066,8 +1066,7 @@ SPEC: dict[str, dict[str, Cmd]] = {
|
||||
"request": Cmd(
|
||||
"POST",
|
||||
"/uploads/request",
|
||||
"Request an upload URL. To scan local source, prefer `strix cloud scans start "
|
||||
"--source DIR`, which packs, uploads, and starts the scan in one step.",
|
||||
"Request an upload URL.",
|
||||
body=(
|
||||
P("file_name", required=True, help="File name."),
|
||||
P("file_size", "int", required=True, help="File size in bytes."),
|
||||
|
||||
@@ -8,29 +8,17 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.config import IntegrationSettings, codex, load_settings
|
||||
from strix.config import codex, load_settings
|
||||
from strix.interface.utils import (
|
||||
check_docker_connection,
|
||||
image_exists,
|
||||
process_pull_line,
|
||||
)
|
||||
from strix.telemetry import report_error
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _missing_web_search_vars(integrations: IntegrationSettings) -> list[str]:
|
||||
"""Mirror the web_search provider rules: which key(s) the selected provider needs."""
|
||||
if integrations.web_search_provider == "exa":
|
||||
return [] if integrations.exa_api_key else ["EXA_API_KEY"]
|
||||
if integrations.web_search_provider == "perplexity":
|
||||
return [] if integrations.perplexity_api_key else ["PERPLEXITY_API_KEY"]
|
||||
if integrations.exa_api_key or integrations.perplexity_api_key:
|
||||
return []
|
||||
return ["EXA_API_KEY", "PERPLEXITY_API_KEY"]
|
||||
|
||||
|
||||
def validate_environment() -> None:
|
||||
logger.info("Validating environment")
|
||||
console = Console()
|
||||
@@ -45,7 +33,6 @@ def validate_environment() -> None:
|
||||
f"[red]STRIX_LLM={settings.llm.model} uses your ChatGPT subscription, "
|
||||
"but you're not signed in.[/] Run [cyan]strix auth login chatgpt[/] first."
|
||||
)
|
||||
report_error("subscription_not_signed_in")
|
||||
sys.exit(1)
|
||||
logger.info("Environment OK (ChatGPT subscription)")
|
||||
return
|
||||
@@ -59,7 +46,8 @@ def validate_environment() -> None:
|
||||
if not settings.llm.api_base:
|
||||
missing_optional_vars.append("LLM_API_BASE")
|
||||
|
||||
missing_optional_vars.extend(_missing_web_search_vars(settings.integrations))
|
||||
if not settings.integrations.perplexity_api_key:
|
||||
missing_optional_vars.append("PERPLEXITY_API_KEY")
|
||||
|
||||
if missing_required_vars:
|
||||
error_text = Text()
|
||||
@@ -82,7 +70,7 @@ def validate_environment() -> None:
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("STRIX_LLM", style="bold cyan")
|
||||
error_text.append(
|
||||
" - Model name to use (e.g., 'openrouter/z-ai/glm-5.3' or "
|
||||
" - Model name to use (e.g., 'openai/gpt-5.4' or "
|
||||
"'anthropic/claude-opus-4-7')\n",
|
||||
style="white",
|
||||
)
|
||||
@@ -101,14 +89,7 @@ def validate_environment() -> None:
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
|
||||
error_text.append(
|
||||
" - API key for Perplexity AI web search (alternative to Exa)\n",
|
||||
style="white",
|
||||
)
|
||||
elif var == "EXA_API_KEY":
|
||||
error_text.append("• ", style="white")
|
||||
error_text.append("EXA_API_KEY", style="bold cyan")
|
||||
error_text.append(
|
||||
" - API key for Exa web search (enables real-time research)\n",
|
||||
" - API key for Perplexity AI web search (enables real-time research)\n",
|
||||
style="white",
|
||||
)
|
||||
elif var == "STRIX_REASONING_EFFORT":
|
||||
@@ -121,7 +102,7 @@ def validate_environment() -> None:
|
||||
)
|
||||
|
||||
error_text.append("\nExample setup:\n", style="white")
|
||||
error_text.append("export STRIX_LLM='openrouter/z-ai/glm-5.3'\n", style="dim white")
|
||||
error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white")
|
||||
|
||||
if missing_optional_vars:
|
||||
for var in missing_optional_vars:
|
||||
@@ -135,8 +116,6 @@ def validate_environment() -> None:
|
||||
error_text.append(
|
||||
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
|
||||
)
|
||||
elif var == "EXA_API_KEY":
|
||||
error_text.append("export EXA_API_KEY='your-exa-key-here'\n", style="dim white")
|
||||
elif var == "STRIX_REASONING_EFFORT":
|
||||
error_text.append(
|
||||
"export STRIX_REASONING_EFFORT='high'\n",
|
||||
@@ -155,7 +134,6 @@ def validate_environment() -> None:
|
||||
console.print("\n")
|
||||
console.print(panel)
|
||||
console.print()
|
||||
report_error("missing_required_config")
|
||||
sys.exit(1)
|
||||
logger.info(
|
||||
"Environment OK (optional missing: %s)",
|
||||
@@ -183,7 +161,6 @@ def check_docker_installed() -> None:
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n", panel, "\n")
|
||||
report_error("docker_not_installed")
|
||||
sys.exit(1)
|
||||
logger.debug("Docker CLI present")
|
||||
|
||||
@@ -231,7 +208,6 @@ def pull_docker_image() -> None:
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel, "\n")
|
||||
report_error("image_pull_failed", e)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Docker image %s ready", image)
|
||||
|
||||
+15
-25
@@ -41,8 +41,7 @@ from strix.interface.update_check import (
|
||||
from strix.interface.utils import (
|
||||
build_final_stats_text,
|
||||
)
|
||||
from strix.llm.warmup import start_import_warmup, wait_for_import_warmup
|
||||
from strix.telemetry import posthog, report_error, scarf, set_scan_phase
|
||||
from strix.telemetry import posthog, scarf
|
||||
from strix.telemetry.logging import configure_dependency_logging
|
||||
|
||||
|
||||
@@ -333,11 +332,6 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
||||
"[#60a5fa]docs.strix.ai[/] [dim]·[/] "
|
||||
"[#60a5fa]discord.gg/strix-ai[/]"
|
||||
)
|
||||
if not args.non_interactive:
|
||||
console.print(
|
||||
"[dim]Run a pentest in Strix Cloud[/] [#60a5fa]app.strix.ai[/] [dim]·[/] "
|
||||
"[dim]Enterprise[/] [#60a5fa]strix.ai/demo[/]"
|
||||
)
|
||||
console.print()
|
||||
if not args.non_interactive:
|
||||
notify_update(console)
|
||||
@@ -397,22 +391,22 @@ def _print_model_connection_error(exc: BaseException, model_name: str) -> None:
|
||||
def _bootstrap_scan(args: argparse.Namespace) -> None:
|
||||
"""Warm up the model and prepare the run for a non-interactive scan.
|
||||
|
||||
Interactive launches skip this: the model preflight and run preparation
|
||||
happen inside the TUI so the interface paints immediately instead of
|
||||
waiting on a model round trip.
|
||||
Interactive launches only validate the environment here; the model
|
||||
preflight and run preparation happen inside the TUI so the interface
|
||||
paints immediately instead of waiting on a model round trip.
|
||||
"""
|
||||
set_scan_phase("preflight")
|
||||
validate_environment()
|
||||
if not args.non_interactive:
|
||||
return
|
||||
try:
|
||||
asyncio.run(warm_up_llm(show_model_warning=True))
|
||||
except ModelConnectionError as exc:
|
||||
report_error("model_connection_failed", exc)
|
||||
_print_model_connection_error(exc, exc.model_name)
|
||||
sys.exit(1)
|
||||
persist_current()
|
||||
try:
|
||||
prepare_run(args)
|
||||
except ValueError as e:
|
||||
report_error("scan_preparation_failed", e)
|
||||
_print_error_panel("SCAN PREPARATION FAILED", str(e))
|
||||
sys.exit(1)
|
||||
telemetry_start(args)
|
||||
@@ -459,6 +453,8 @@ def main() -> None:
|
||||
|
||||
sys.exit(run_cloud(sys.argv[2:]))
|
||||
|
||||
from strix.llm.warmup import start_import_warmup
|
||||
|
||||
start_import_warmup()
|
||||
|
||||
args = parse_arguments()
|
||||
@@ -471,12 +467,10 @@ def main() -> None:
|
||||
|
||||
check_docker_installed()
|
||||
pull_docker_image()
|
||||
validate_environment()
|
||||
|
||||
# Everything below imports the scan engine; do not race the warm-up thread.
|
||||
wait_for_import_warmup()
|
||||
|
||||
if args.non_interactive:
|
||||
# In setup mode the TUI collects the target, then runs prepare_run(),
|
||||
# warm-up, and telemetry itself once the user starts the scan.
|
||||
if not args.needs_setup:
|
||||
_bootstrap_scan(args)
|
||||
|
||||
from strix.report.state import get_global_report_state
|
||||
@@ -487,21 +481,18 @@ def main() -> None:
|
||||
from strix.interface.cli import run_cli
|
||||
|
||||
asyncio.run(run_cli(args))
|
||||
# Headless runs have no user to quit: the agent either finished
|
||||
# (already beaconed as finished_by_tool) or stopped on its own.
|
||||
exit_reason = "agent_stopped"
|
||||
else:
|
||||
asyncio.run(run_tui(args))
|
||||
except InteractiveSetupUnavailableError as exc:
|
||||
exit_reason = "error"
|
||||
report_error("interactive_setup_unavailable", exc)
|
||||
_print_error_panel("INTERACTIVE SETUP UNAVAILABLE", str(exc))
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
exit_reason = "interrupted"
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
exit_reason = "error"
|
||||
report_error("unhandled_exception", exc)
|
||||
posthog.error("unhandled_exception")
|
||||
scarf.error("unhandled_exception")
|
||||
raise
|
||||
finally:
|
||||
report_state = get_global_report_state()
|
||||
@@ -520,7 +511,6 @@ def main() -> None:
|
||||
|
||||
if not args.run_name:
|
||||
# Setup mode where the user quit before starting a scan: nothing ran.
|
||||
notify_update(Console())
|
||||
return
|
||||
|
||||
results_path = run_dir_for(args.run_name)
|
||||
|
||||
@@ -36,8 +36,7 @@ if TYPE_CHECKING:
|
||||
_STOPPABLE_AGENT_STATUSES = frozenset({"running", "waiting", "budget_paused"})
|
||||
|
||||
ChangeCallback = Callable[[], None]
|
||||
StartCallback = Callable[[], Awaitable[None]]
|
||||
VerifyCallback = Callable[[], Awaitable[None]]
|
||||
StartCallback = Callable[[bool], Awaitable[None]]
|
||||
QuitCallback = Callable[[], Awaitable[None]]
|
||||
|
||||
|
||||
@@ -52,7 +51,6 @@ class TuiController:
|
||||
coordinator: Any = None,
|
||||
report_state: ReportState | None = None,
|
||||
on_start: StartCallback | None = None,
|
||||
on_verify: VerifyCallback | None = None,
|
||||
on_quit: QuitCallback | None = None,
|
||||
on_change: ChangeCallback | None = None,
|
||||
) -> None:
|
||||
@@ -101,6 +99,7 @@ class TuiController:
|
||||
# A target-less launch enters the live view and asks there before
|
||||
# anything is prepared; this holds the directory awaiting that answer.
|
||||
self.pending_workspace_mount: str | None = None
|
||||
self._pending_verify = True
|
||||
self.messages: list[dict[str, str]] = []
|
||||
self._next_message_id = 1
|
||||
self.error: str | None = None
|
||||
@@ -113,7 +112,6 @@ class TuiController:
|
||||
self.viewer_url: str | None = None
|
||||
self._viewer_httpd: Any = None
|
||||
self._on_start = on_start
|
||||
self._on_verify = on_verify
|
||||
self._on_quit = on_quit
|
||||
self._on_change = on_change
|
||||
|
||||
@@ -192,7 +190,7 @@ class TuiController:
|
||||
model_warning = ""
|
||||
if model and not is_recommended_or_frontier_model(model):
|
||||
model_warning = (
|
||||
f"{model} is not a recommended frontier model. Pentest quality could be degraded."
|
||||
f"{model} is not a recommended frontier model; pentest quality could be degraded"
|
||||
)
|
||||
state = {
|
||||
"setup_mode": self.setup_mode,
|
||||
@@ -330,6 +328,12 @@ class TuiController:
|
||||
async def _start(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
if self.scan_started or self._start_in_progress:
|
||||
raise RuntimeError("Scan is already starting or running")
|
||||
# A bare prompt launches optimistically, like a coding agent: it skips
|
||||
# the network model preflight and surfaces any model error live. A named
|
||||
# target keeps the preflight so a real scan does not commit blind.
|
||||
verify = payload.get("verify", True)
|
||||
if not isinstance(verify, bool):
|
||||
raise TypeError("verify must be a boolean")
|
||||
# Launching with no target mounts the working directory, so it requires
|
||||
# the user's explicit confirmation rather than happening silently.
|
||||
mount_working_dir = payload.get("mount_working_dir", False)
|
||||
@@ -340,44 +344,27 @@ class TuiController:
|
||||
raise ValueError("No model configured. Set STRIX_LLM first.")
|
||||
if self._on_start is None:
|
||||
raise RuntimeError("Scan start is unavailable")
|
||||
if not self.targets and not mount_working_dir:
|
||||
raise ValueError("No target set. Add a target first.")
|
||||
# The model check runs while still on the start screen, for a bare
|
||||
# prompt as much as for a named target, so a failure lands in the setup
|
||||
# log where the user can fix it and retry rather than in a dead run.
|
||||
await self._verify_model()
|
||||
if not self.targets:
|
||||
if not mount_working_dir:
|
||||
raise ValueError("No target set. Add a target first.")
|
||||
# Mounting the working directory needs the user's confirmation, and
|
||||
# that is asked in the live view. Enter it now and prepare nothing
|
||||
# until the answer arrives, so declining leaves no run behind.
|
||||
self.pending_workspace_mount = str(Path.cwd())
|
||||
self._pending_verify = verify
|
||||
self.setup_mode = False
|
||||
self.scan_started = True
|
||||
self.scan_state = "preparing"
|
||||
return {"started": True}
|
||||
await self._begin_scan()
|
||||
await self._begin_scan(verify)
|
||||
return {"started": True}
|
||||
|
||||
async def _verify_model(self) -> None:
|
||||
if self._on_verify is None:
|
||||
return
|
||||
self._start_in_progress = True
|
||||
try:
|
||||
await self._on_verify()
|
||||
finally:
|
||||
self._start_in_progress = False
|
||||
|
||||
async def _begin_scan(self) -> None:
|
||||
async def _begin_scan(self, verify: bool) -> None:
|
||||
if self._on_start is None:
|
||||
raise RuntimeError("Scan start is unavailable")
|
||||
self._start_in_progress = True
|
||||
try:
|
||||
await self._on_start()
|
||||
except Exception as exc:
|
||||
if not self.setup_mode:
|
||||
# The live view is already up, so the failure has to show there.
|
||||
self.fail_preparation(str(exc))
|
||||
raise
|
||||
await self._on_start(verify)
|
||||
finally:
|
||||
self._start_in_progress = False
|
||||
self.setup_mode = False
|
||||
@@ -397,7 +384,7 @@ class TuiController:
|
||||
# the whole of the input either way; the working directory is only an
|
||||
# extra the agent may look at, so the run goes ahead without one.
|
||||
self.workspace_mount = mount if approved else None
|
||||
await self._begin_scan()
|
||||
await self._begin_scan(self._pending_verify)
|
||||
return {"approved": approved}
|
||||
|
||||
async def _send_message(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
@@ -370,17 +370,6 @@ func TestStartedSnapshotTransitionsToLiveView(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplashModelWarningRendersTheBackendSentenceOnce(t *testing.T) {
|
||||
warning := "openai/glm-5.3 is not a recommended frontier model. Pentest quality could be degraded."
|
||||
got := ansi.Strip(splashModelWarning("openai/glm-5.3", warning))
|
||||
if got != "⚠ "+warning {
|
||||
t.Fatalf("splash warning = %q, want %q", got, "⚠ "+warning)
|
||||
}
|
||||
if got := ansi.Strip(splashModelWarning("other/model", warning)); got != "⚠ "+warning {
|
||||
t.Fatalf("splash warning with unrelated model = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupStartScreenFitsNarrowTerminal(t *testing.T) {
|
||||
model := New(nil)
|
||||
model.width, model.height = 40, 18
|
||||
|
||||
@@ -45,20 +45,23 @@ func (m *Model) submitSetupPrompt(value string) (tea.Model, tea.Cmd) {
|
||||
if len(fields) > targets {
|
||||
commands = append(commands, send(m.client, "setup.set_instruction", map[string]any{"instruction": value}))
|
||||
}
|
||||
// The backend verifies the model connection before either kind of launch
|
||||
// and reports on it through the setup log. A bare prompt mounts the working
|
||||
// directory - the backend asks about that from the live view, so the prompt
|
||||
// is held here in case it is declined.
|
||||
payload := map[string]any{}
|
||||
if targets == 0 && len(m.snapshot.Targets) == 0 {
|
||||
// With a target, verify the model connection before the scan commits to it.
|
||||
// A bare prompt launches optimistically, like a coding agent, and mounts the
|
||||
// working directory - the backend asks about that from the live view, so the
|
||||
// prompt is held here in case it is declined.
|
||||
verify := targets > 0 || len(m.snapshot.Targets) > 0
|
||||
payload := map[string]any{"verify": verify}
|
||||
if verify {
|
||||
m.setupMsg("Verifying model connection...", render.Col(amber))
|
||||
} else {
|
||||
m.pendingPrompt = value
|
||||
payload["mount_working_dir"] = true
|
||||
}
|
||||
commands = append(commands, send(m.client, "setup.start", payload))
|
||||
// Ordered, not batched: setup.start leaves setup mode, so it must be the
|
||||
// last command to reach the backend. Batched sends race, and if setup.start
|
||||
// wins the target and instruction commands land after the guard closes and
|
||||
// fail with a red error.
|
||||
// last command to reach the backend. Batched sends race, and once the
|
||||
// preflight is skipped setup.start wins, making the target and instruction
|
||||
// commands land after the guard closes and fail with a red error.
|
||||
return *m, tea.Sequence(commands...)
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,25 @@ func commandTypes(envelopes []protocol.Envelope) []string {
|
||||
return types
|
||||
}
|
||||
|
||||
// startVerify returns the verify flag on the setup.start command, and whether
|
||||
// a setup.start command was present at all.
|
||||
func startVerify(t *testing.T, envelopes []protocol.Envelope) (verify, found bool) {
|
||||
t.Helper()
|
||||
for _, envelope := range envelopes {
|
||||
if envelope.Type != "setup.start" {
|
||||
continue
|
||||
}
|
||||
var payload struct {
|
||||
Verify bool `json:"verify"`
|
||||
}
|
||||
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return payload.Verify, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func contains(values []string, want string) bool {
|
||||
for _, value := range values {
|
||||
if value == want {
|
||||
@@ -141,6 +160,10 @@ func TestSetupPromptWithoutTargetLaunchesAndRequestsMount(t *testing.T) {
|
||||
if mount, found := startPayloadFlag(t, envelopes, "mount_working_dir"); !found || !mount {
|
||||
t.Fatalf("mount was not requested: mount_working_dir=%v found=%v", mount, found)
|
||||
}
|
||||
// A bare prompt launches optimistically: no model preflight.
|
||||
if verify, found := startVerify(t, envelopes); !found || verify {
|
||||
t.Fatalf("bare prompt should launch with verify=false, got verify=%v found=%v", verify, found)
|
||||
}
|
||||
// setup.start leaves setup mode, so it must be the last command sent.
|
||||
if start, instr := firstIndex(types, "setup.start"), lastIndex(types, "setup.set_instruction"); start < instr {
|
||||
t.Fatalf("setup.start (%d) must come after setup.set_instruction (%d): %v", start, instr, types)
|
||||
@@ -250,8 +273,9 @@ func TestSetupPromptWithTargetLaunches(t *testing.T) {
|
||||
t.Fatalf("missing %s in %v", want, types)
|
||||
}
|
||||
}
|
||||
if _, found := startPayloadFlag(t, envelopes, "mount_working_dir"); found {
|
||||
t.Fatalf("a targeted prompt must not ask to mount the working directory: %v", types)
|
||||
// A named target keeps the upfront model check.
|
||||
if verify, found := startVerify(t, envelopes); !found || !verify {
|
||||
t.Fatalf("targeted prompt should launch with verify=true, got verify=%v found=%v", verify, found)
|
||||
}
|
||||
// The target and instruction must reach the backend before setup.start
|
||||
// closes the setup guard.
|
||||
|
||||
@@ -411,7 +411,7 @@ func (m Model) splashView() string {
|
||||
welcome + "\n" + version + "\n" + tagline + "\n\n" +
|
||||
start.String() + "\n\n" + url
|
||||
if warn := m.snapshot.ModelWarning; warn != "" {
|
||||
content += "\n\n" + splashModelWarning(m.snapshot.Model, warn)
|
||||
content += "\n\n" + splashModelWarning(warn)
|
||||
}
|
||||
panel := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(green).Padding(1, 6).Align(lipgloss.Center).Render(content)
|
||||
// #splash_screen background is solid black.
|
||||
@@ -419,16 +419,12 @@ func (m Model) splashView() string {
|
||||
lipgloss.WithWhitespaceBackground(black))
|
||||
}
|
||||
|
||||
// splashModelWarning renders the backend's full warning sentence, with the
|
||||
// model name highlighted when the sentence leads with it.
|
||||
func splashModelWarning(model, warning string) string {
|
||||
// splashModelWarning ports SplashScreen._build_model_warning_text.
|
||||
func splashModelWarning(model string) string {
|
||||
yellow := lipgloss.Color("#eab308")
|
||||
out := lipgloss.NewStyle().Bold(true).Foreground(yellow).Render("⚠ ")
|
||||
if model != "" && strings.HasPrefix(warning, model) {
|
||||
out += lipgloss.NewStyle().Bold(true).Foreground(render.Cyan).Render(model)
|
||||
warning = strings.TrimPrefix(warning, model)
|
||||
}
|
||||
return out + lipgloss.NewStyle().Foreground(yellow).Render(warning)
|
||||
return lipgloss.NewStyle().Bold(true).Foreground(yellow).Render("⚠ ") +
|
||||
lipgloss.NewStyle().Bold(true).Foreground(render.Cyan).Render(model) +
|
||||
lipgloss.NewStyle().Foreground(yellow).Render(" is not a recommended frontier model - pentest quality could be degraded")
|
||||
}
|
||||
|
||||
// chatPaneKey identifies everything the bordered trace depends on.
|
||||
|
||||
@@ -86,8 +86,6 @@ func Tool(data map[string]any) string {
|
||||
return renderVulnerabilityReport(args, result)
|
||||
case "update_vulnerability_report":
|
||||
return renderVulnerabilityReportUpdate(args, result)
|
||||
case "delete_vulnerability_report":
|
||||
return renderVulnerabilityReportDelete(args, result)
|
||||
case "create_dependency_report":
|
||||
return renderDependencyReport(args, result)
|
||||
case "list_reports":
|
||||
|
||||
@@ -21,36 +21,6 @@ func renderVulnerabilityReportUpdate(args map[string]any, result any) string {
|
||||
return renderReport(args, result, "Vulnerability Report Updated", "Updating report...")
|
||||
}
|
||||
|
||||
// A deletion names the report it withdraws and the reason; the result carries
|
||||
// the title the report had, so the reader knows what left the scan.
|
||||
func renderVulnerabilityReportDelete(args map[string]any, result any) string {
|
||||
resultMap, _ := result.(map[string]any)
|
||||
var b strings.Builder
|
||||
b.WriteString("🐞 " + Bold(ReportHdr).Render("Vulnerability Report Deleted"))
|
||||
|
||||
reportID := StringValue(args["report_id"])
|
||||
if reportID != "" {
|
||||
b.WriteString("\n\n" + Bold(Field).Render("Report: ") + reportID)
|
||||
}
|
||||
if title := StringValue(resultMap["title"]); title != "" {
|
||||
b.WriteString("\n\n" + Bold(Field).Render("Title: ") + title)
|
||||
}
|
||||
if sev := StringValue(resultMap["severity"]); sev != "" {
|
||||
b.WriteString("\n\n" + Bold(Field).Render("Severity: ") +
|
||||
lipgloss.NewStyle().Bold(true).Foreground(SeverityColor(sev)).Render(strings.ToUpper(sev)))
|
||||
}
|
||||
if reason := StringValue(args["delete_reason"]); reason != "" {
|
||||
b.WriteString("\n\n" + Bold(Field).Render("Reason") + "\n" + reason)
|
||||
}
|
||||
if errMsg := StringValue(resultMap["error"]); errMsg != "" {
|
||||
b.WriteString("\n\n" + Col(SevHigh).Render(errMsg))
|
||||
}
|
||||
if reportID == "" {
|
||||
b.WriteString("\n " + Dim().Render("Deleting report..."))
|
||||
}
|
||||
return "\n\n" + b.String() + "\n\n"
|
||||
}
|
||||
|
||||
func renderReport(args map[string]any, result any, heading, pending string) string {
|
||||
resultMap, _ := result.(map[string]any)
|
||||
var b strings.Builder
|
||||
|
||||
@@ -37,7 +37,6 @@ from strix.interface.tui.sidecar import (
|
||||
)
|
||||
from strix.interface.utils import read_workspace_files
|
||||
from strix.report.state import ReportState, set_global_report_state
|
||||
from strix.telemetry import report_error, set_scan_phase
|
||||
from strix.utils.resource_paths import get_strix_resource_path
|
||||
|
||||
|
||||
@@ -49,11 +48,6 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _revision_count(report: dict[str, Any]) -> int:
|
||||
history = report.get("update_history")
|
||||
return len(history) if isinstance(history, list) else 0
|
||||
|
||||
|
||||
class GoTuiPreActivationError(RuntimeError):
|
||||
"""A sidecar failure raised before the Go TUI activates."""
|
||||
|
||||
@@ -69,14 +63,11 @@ class GoTuiRuntime:
|
||||
self.scan_error: BaseException | None = None
|
||||
self._last_sync_fingerprint = ""
|
||||
self._error_noted_agents: set[str] = set()
|
||||
self.model_verified = False
|
||||
self._setup_preflight: asyncio.Task[None] | None = None
|
||||
self.controller = TuiController(
|
||||
args,
|
||||
live_view=self.live_view,
|
||||
coordinator=self.coordinator,
|
||||
on_start=self.start_from_setup,
|
||||
on_verify=self.ensure_model_verified,
|
||||
on_quit=self.quit,
|
||||
)
|
||||
self.server = TuiBackendServer(self.controller)
|
||||
@@ -114,58 +105,9 @@ class GoTuiRuntime:
|
||||
self.report_state.vulnerability_updated_callback = lambda _report: (
|
||||
self.controller.notify_changed()
|
||||
)
|
||||
self.report_state.vulnerability_deleted_callback = lambda _report: (
|
||||
self.controller.notify_changed()
|
||||
)
|
||||
self.controller.notify_changed()
|
||||
|
||||
async def check_setup_model(self) -> None:
|
||||
"""Verify the model route as soon as the start screen is up.
|
||||
|
||||
The same round trip a direct launch makes in prepare_and_start, run in
|
||||
the background so the screen paints first and the outcome lands in the
|
||||
setup log before the user has finished typing.
|
||||
"""
|
||||
if not (load_settings().llm.model or "").strip():
|
||||
return
|
||||
try:
|
||||
await self._preflight_model()
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI setup model preflight failed")
|
||||
self.controller.add_message(f"Model connection failed: {exc}", "error")
|
||||
return
|
||||
self.controller.add_message("Model connection verified")
|
||||
|
||||
async def ensure_model_verified(self) -> None:
|
||||
"""Hold a setup launch until the model has answered once."""
|
||||
preflight = self._setup_preflight
|
||||
if preflight is not None and not preflight.done():
|
||||
await asyncio.shield(preflight)
|
||||
if self.model_verified:
|
||||
return
|
||||
try:
|
||||
await self._preflight_model()
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI setup model preflight failed")
|
||||
report_error("model_connection_failed", exc)
|
||||
raise RuntimeError(f"Model connection failed: {exc}") from exc
|
||||
|
||||
async def _preflight_model(self) -> None:
|
||||
model = (load_settings().llm.model or "").strip()
|
||||
self.controller.add_message("Verifying model connection...")
|
||||
set_scan_phase("preflight")
|
||||
await preflight_model_connection(model)
|
||||
self.model_verified = True
|
||||
|
||||
def _start_preparation(self) -> asyncio.Task[None]:
|
||||
"""Kick off the work that runs behind the freshly painted TUI."""
|
||||
if self.controller.setup_mode:
|
||||
self._setup_preflight = asyncio.create_task(self.check_setup_model())
|
||||
return self._setup_preflight
|
||||
self.controller.begin_preparation()
|
||||
return asyncio.create_task(self.prepare_and_start())
|
||||
|
||||
async def start_from_setup(self) -> None:
|
||||
async def start_from_setup(self, verify: bool = True) -> None:
|
||||
candidate = deepcopy(self.args)
|
||||
candidate.scan_mode = self.controller.scan_mode
|
||||
candidate.instruction = self.controller.instruction
|
||||
@@ -182,7 +124,16 @@ class GoTuiRuntime:
|
||||
if isinstance(target, dict) and target.get("original")
|
||||
]
|
||||
targets_changed = self.controller.targets != existing_targets
|
||||
persist_current()
|
||||
model = (load_settings().llm.model or "").strip()
|
||||
# A bare prompt launches optimistically: it skips the network preflight
|
||||
# and lets any model error surface once the agent starts, like a coding
|
||||
# agent. A named target keeps the upfront check.
|
||||
if verify:
|
||||
try:
|
||||
await preflight_model_connection(model)
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI setup model preflight failed")
|
||||
raise RuntimeError(f"Model connection failed: {exc}") from exc
|
||||
# A confirmed target-less launch mounts the working directory for the
|
||||
# agent to work in, without making it a scan target.
|
||||
candidate.workspace_mount = self.controller.workspace_mount
|
||||
@@ -192,11 +143,7 @@ class GoTuiRuntime:
|
||||
candidate.target = list(self.controller.targets)
|
||||
candidate.target_list = []
|
||||
build_targets_info(candidate)
|
||||
try:
|
||||
prepare_run(candidate)
|
||||
except Exception as exc:
|
||||
report_error("scan_preparation_failed", exc)
|
||||
raise
|
||||
prepare_run(candidate)
|
||||
telemetry_start(candidate)
|
||||
|
||||
vars(self.args).update(vars(candidate))
|
||||
@@ -210,21 +157,13 @@ class GoTuiRuntime:
|
||||
launch so the interface appears immediately.
|
||||
"""
|
||||
model = (load_settings().llm.model or "").strip()
|
||||
set_scan_phase("preflight")
|
||||
try:
|
||||
await preflight_model_connection(model)
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI scan preparation failed")
|
||||
report_error("model_connection_failed", exc)
|
||||
self.controller.fail_preparation(str(exc))
|
||||
return
|
||||
try:
|
||||
persist_current()
|
||||
prepare_run(self.args)
|
||||
telemetry_start(self.args)
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI scan preparation failed")
|
||||
report_error("scan_preparation_failed", exc)
|
||||
self.controller.fail_preparation(str(exc))
|
||||
return
|
||||
self.controller.scan_state = "running"
|
||||
@@ -263,9 +202,6 @@ class GoTuiRuntime:
|
||||
self.controller.scan_state = "completed" if report_status == "completed" else "stopped"
|
||||
except Exception as exc:
|
||||
logger.exception("Go TUI scan failed")
|
||||
report_error("unhandled_exception", exc)
|
||||
if self.report_state is not None and self.report_state.scan_ended_exit_reason is None:
|
||||
self.report_state.scan_ended_exit_reason = "error"
|
||||
self.scan_error = exc
|
||||
self.controller.error = str(exc)
|
||||
self.controller.scan_state = "failed"
|
||||
@@ -347,9 +283,7 @@ class GoTuiRuntime:
|
||||
if self.report_state is not None:
|
||||
usage = dict(self.report_state.get_total_llm_usage())
|
||||
vulnerabilities = [
|
||||
(report.get("id", index), _revision_count(report))
|
||||
if isinstance(report, dict)
|
||||
else index
|
||||
report.get("id", index) if isinstance(report, dict) else index
|
||||
for index, report in enumerate(self.report_state.vulnerability_reports)
|
||||
]
|
||||
return json.dumps(
|
||||
@@ -442,7 +376,9 @@ class GoTuiRuntime:
|
||||
)
|
||||
process, backend_socket = await launch_tui_process(command, env, cwd)
|
||||
await self.server.start(backend_socket)
|
||||
prepare_task = self._start_preparation()
|
||||
if not self.controller.setup_mode:
|
||||
self.controller.begin_preparation()
|
||||
prepare_task = asyncio.create_task(self.prepare_and_start())
|
||||
sync_task = asyncio.create_task(self.sync_state())
|
||||
return_code = await wait_process(process)
|
||||
check_return_code(return_code)
|
||||
|
||||
@@ -19,7 +19,6 @@ from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.config import load_settings
|
||||
from strix.telemetry import report_error
|
||||
from strix.utils.api_spec import detect_spec_format
|
||||
|
||||
|
||||
@@ -209,9 +208,7 @@ def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR091
|
||||
text.append("\n ")
|
||||
text.append(loc["snippet"], style="dim")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
preparation = report.get("fix_preparation")
|
||||
prepared = isinstance(preparation, dict) and preparation.get("state") == "ready"
|
||||
text.append("\n Prepared fix:" if prepared else "\n Draft fix candidate:")
|
||||
text.append("\n Fix:")
|
||||
if loc.get("fix_before"):
|
||||
text.append("\n - ", style="dim")
|
||||
text.append(loc["fix_before"], style="dim")
|
||||
@@ -1325,7 +1322,6 @@ def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str,
|
||||
"source_path": details["target_path"],
|
||||
"workspace_subdir": workspace_subdir,
|
||||
"protect_metadata": True,
|
||||
"read_only": bool(details.get("read_only")),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1606,8 +1602,7 @@ def check_docker_connection() -> Any:
|
||||
|
||||
try:
|
||||
return docker.from_env()
|
||||
except DockerException as exc:
|
||||
report_error("docker_unavailable", exc)
|
||||
except DockerException:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
||||
|
||||
@@ -87,7 +87,7 @@ def run_view(argv: list[str]) -> None:
|
||||
|
||||
posthog.viewer_opened(source="cli", live=live)
|
||||
|
||||
state_label = _state_label(summary)
|
||||
state_label = "[#eab308]live[/]" if live else "[#22c55e]finished[/]"
|
||||
console.print()
|
||||
console.print(f"Serving [bold white]{run_name}[/] ({state_label}) at:")
|
||||
# Print the URL alone on its own line with soft_wrap so Rich never inserts a
|
||||
@@ -107,18 +107,6 @@ def run_view(argv: list[str]) -> None:
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
def _state_label(summary: dict[str, object]) -> str:
|
||||
if not summary.get("finished", False):
|
||||
return "[#eab308]live[/]"
|
||||
|
||||
status = summary.get("status")
|
||||
if status == "failed":
|
||||
return "[#ef4444]failed[/]"
|
||||
if status in {"stopped", "interrupted"}:
|
||||
return f"[#eab308]{status}[/]"
|
||||
return "[#22c55e]finished[/]"
|
||||
|
||||
|
||||
def _resolve_run_dir(run: str | None, console: Console) -> Path:
|
||||
if run:
|
||||
run_dir = run_dir_for(run)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Radar,
|
||||
Rocket,
|
||||
ArrowUpRight,
|
||||
Building2,
|
||||
History,
|
||||
} from "lucide-react";
|
||||
import type { Vulnerability, VulnerabilitySeverity } from "@/types/issues";
|
||||
@@ -36,7 +35,7 @@ import {
|
||||
type LoadedRun,
|
||||
type RunsPayload,
|
||||
} from "@/data/serverSource";
|
||||
import { SIGNUP_URL, DEMO_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
import { runTitle } from "@/lib/target-utils";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import PastRunsView from "@/components/PastRunsView";
|
||||
@@ -707,31 +706,6 @@ function OverviewTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{finished && (
|
||||
<div className="animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
|
||||
<p className="text-sm font-semibold text-white">Strix Cloud</p>
|
||||
<p className="mt-0.5 text-xs text-[#666]">Run your next pentest in Strix Cloud.</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2.5">
|
||||
<ProInlineCta
|
||||
label="Run a pentest in Strix Cloud"
|
||||
desc="Validated findings, autofix, and PR reviews."
|
||||
slug="overview_cloud"
|
||||
surface="overview"
|
||||
icon={Rocket}
|
||||
primary
|
||||
/>
|
||||
<ProInlineCta
|
||||
label="Try Strix Enterprise"
|
||||
desc="SSO, compliance-ready reports, VPC or self-hosted deployment."
|
||||
slug="book_demo"
|
||||
surface="overview"
|
||||
icon={Building2}
|
||||
href={DEMO_URL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sections.length > 0 ? (
|
||||
<div className="animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5 space-y-8">
|
||||
{sections.map((s) => (
|
||||
@@ -815,23 +789,14 @@ function AgentsTab({ run, canSteer }: { run: LoadedRun; canSteer: boolean }) {
|
||||
{/* Re-run always routes to Strix Cloud. */}
|
||||
<div className="rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5">
|
||||
<p className="text-sm font-semibold text-white">Run this pentest with more depth</p>
|
||||
<p className="mt-0.5 text-xs text-[#666]">Run this pentest again in Strix Cloud.</p>
|
||||
<p className="mt-0.5 text-xs text-[#666]">Re-run this pentest on managed infra in the cloud.</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2.5">
|
||||
<ProInlineCta
|
||||
label="Re-run in Strix Pro with more depth"
|
||||
desc="More depth, validated findings, and autofix."
|
||||
desc="Run this pentest on managed infra with more depth."
|
||||
slug="live_scan"
|
||||
surface="agents"
|
||||
icon={Rocket}
|
||||
primary
|
||||
/>
|
||||
<ProInlineCta
|
||||
label="Try Strix Enterprise"
|
||||
desc="SSO, compliance-ready reports, VPC or self-hosted deployment."
|
||||
slug="book_demo"
|
||||
surface="agents"
|
||||
icon={Building2}
|
||||
href={DEMO_URL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,40 +48,23 @@ export function ProInlineCta({
|
||||
slug,
|
||||
icon: Icon,
|
||||
surface,
|
||||
href = SIGNUP_URL,
|
||||
primary = false,
|
||||
}: {
|
||||
label: string;
|
||||
desc: string;
|
||||
slug: string;
|
||||
icon: React.ElementType;
|
||||
surface?: string;
|
||||
/** Destination before attribution params. Defaults to cloud sign-up. */
|
||||
href?: string;
|
||||
/** Solid white button instead of the outlined default. */
|
||||
primary?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip text={desc}>
|
||||
<a
|
||||
href={ctaUrl(href, slug)}
|
||||
href={ctaUrl(SIGNUP_URL, slug)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackCta(slug, surface)}
|
||||
className={
|
||||
primary
|
||||
? "group inline-flex items-center gap-2 rounded-lg border border-white bg-white px-3 py-2 text-sm font-semibold text-black transition-colors hover:bg-[#e5e5e5]"
|
||||
: "group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white"
|
||||
}
|
||||
className="group inline-flex items-center gap-2 rounded-lg border border-[#222] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-sm text-[#aaa] transition-colors hover:border-[#444] hover:text-white"
|
||||
>
|
||||
<Icon
|
||||
className={
|
||||
primary
|
||||
? "h-4 w-4 text-black"
|
||||
: "h-4 w-4 text-[#888] transition-colors group-hover:text-white"
|
||||
}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon className="h-4 w-4 text-[#888] transition-colors group-hover:text-white" aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
WandSparkles,
|
||||
Plug,
|
||||
} from "lucide-react";
|
||||
import { SIGNUP_URL, PRICING_URL, DEMO_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
import { SIGNUP_URL, PRICING_URL, ctaUrl, trackCta } from "@/lib/cta";
|
||||
|
||||
/**
|
||||
* Dialog shown when a platform feature is clicked in the sidebar: a short
|
||||
@@ -141,19 +141,6 @@ export function UpgradeModal({
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-[#666]">
|
||||
SSO, compliance reports, or a private deployment?{" "}
|
||||
<a
|
||||
href={ctaUrl(DEMO_URL, "upgrade_book_demo")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackCta("upgrade_book_demo", source)}
|
||||
className="whitespace-nowrap text-[#aaa] underline underline-offset-2 transition-colors hover:text-white"
|
||||
>
|
||||
Try Strix Enterprise
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ToolRendererProps } from "@/types/events";
|
||||
import { TruncatedText } from "./ToolCard";
|
||||
|
||||
const SEVERITY_COLORS: Record<string, string> = {
|
||||
critical: "text-red-400", high: "text-orange-400", medium: "text-yellow-400",
|
||||
low: "text-blue-400", info: "text-cyan-400",
|
||||
};
|
||||
|
||||
/** A withdrawn finding: which report went, and what disproved it. */
|
||||
export default function VulnReportDeleteRenderer({ args, result, status }: ToolRendererProps) {
|
||||
const reportId = (args.report_id as string) ?? "";
|
||||
const reason = (args.delete_reason as string) ?? "";
|
||||
|
||||
const res = result && typeof result === "object" ? (result as Record<string, unknown>) : null;
|
||||
const ok = res?.success === true;
|
||||
const error = typeof res?.error === "string" ? res.error : "";
|
||||
const title = typeof res?.title === "string" ? res.title : "";
|
||||
const severity = typeof res?.severity === "string" ? res.severity.toLowerCase() : "";
|
||||
const failed = res != null ? !ok : status === "failed" || status === "error";
|
||||
const pending = res == null && !failed;
|
||||
const label = pending ? "withdrawing report\u2026" : failed ? "report not withdrawn" : "report withdrawn";
|
||||
const labelColor = pending ? "text-[#888]" : failed ? "text-red-400/80" : "text-yellow-400/80";
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`font-semibold text-sm ${labelColor}`} aria-live="polite">
|
||||
{label}
|
||||
</span>
|
||||
{reportId && <span className="text-[#555] font-mono text-[13px]">{reportId}</span>}
|
||||
{severity && (
|
||||
<span className={`text-[13px] line-through ${SEVERITY_COLORS[severity] ?? "text-[#888]"}`}>
|
||||
{severity.toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{title && <div className="text-[15px] text-white/60 line-through">{title}</div>}
|
||||
{reason && (
|
||||
<div>
|
||||
<span className="text-emerald-400/60 text-sm font-semibold">Why</span>
|
||||
<div className="mt-1"><TruncatedText text={reason} maxLines={10} /></div>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="text-red-400/80 text-[13px]">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import FileEditRenderer from "./FileEditRenderer";
|
||||
import ApplyPatchRenderer from "./ApplyPatchRenderer";
|
||||
import ViewImageRenderer from "./ViewImageRenderer";
|
||||
import VulnReportRenderer from "./VulnReportRenderer";
|
||||
import VulnReportDeleteRenderer from "./VulnReportDeleteRenderer";
|
||||
import ReportListRenderer from "./ReportListRenderer";
|
||||
import ProxyRenderer from "./ProxyRenderer";
|
||||
import ThinkRenderer from "./ThinkRenderer";
|
||||
@@ -117,7 +116,7 @@ const CATEGORY_TOOLS: Record<ToolCategory, readonly string[]> = {
|
||||
filesystem: ["apply_patch", "view_image", "str_replace_editor", "list_files", "search_files"],
|
||||
// Caido proxy tools (legacy: send_request)
|
||||
proxy: ["list_requests", "view_request", "repeat_request", "list_sitemap", "view_sitemap_entry", "scope_rules", "send_request"],
|
||||
reporting: ["create_vulnerability_report", "update_vulnerability_report", "delete_vulnerability_report", "list_reports", "get_report"],
|
||||
reporting: ["create_vulnerability_report", "update_vulnerability_report", "list_reports", "get_report"],
|
||||
thinking: ["think"],
|
||||
agents: ["create_agent", "agent_finish", "send_message_to_agent", "wait_for_agents", "view_agent_graph", "stop_agent"],
|
||||
search: ["web_search"],
|
||||
@@ -152,7 +151,6 @@ const RENDERER_OVERRIDES: Partial<Record<string, ComponentType<ToolRendererProps
|
||||
view_image: ViewImageRenderer,
|
||||
list_reports: ReportListRenderer,
|
||||
get_report: ReportListRenderer,
|
||||
delete_vulnerability_report: VulnReportDeleteRenderer,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+133
-138
@@ -1,4 +1,4 @@
|
||||
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))a(s);new MutationObserver(s=>{for(const o of s)if(o.type==="childList")for(const c of o.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&a(c)}).observe(document,{childList:!0,subtree:!0});function r(s){const o={};return s.integrity&&(o.integrity=s.integrity),s.referrerPolicy&&(o.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?o.credentials="include":s.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function a(s){if(s.ep)return;s.ep=!0;const o=r(s);fetch(s.href,o)}})();function Mo(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var hh={exports:{}},Xl={};/**
|
||||
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))a(s);new MutationObserver(s=>{for(const o of s)if(o.type==="childList")for(const c of o.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&a(c)}).observe(document,{childList:!0,subtree:!0});function r(s){const o={};return s.integrity&&(o.integrity=s.integrity),s.referrerPolicy&&(o.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?o.credentials="include":s.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function a(s){if(s.ep)return;s.ep=!0;const o=r(s);fetch(s.href,o)}})();function Ao(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var fh={exports:{}},Xl={};/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.js
|
||||
*
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var J0;function jk(){if(J0)return Xl;J0=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function r(a,s,o){var c=null;if(o!==void 0&&(c=""+o),s.key!==void 0&&(c=""+s.key),"key"in s){o={};for(var d in s)d!=="key"&&(o[d]=s[d])}else o=s;return s=o.ref,{$$typeof:e,type:a,key:c,ref:s!==void 0?s:null,props:o}}return Xl.Fragment=t,Xl.jsx=r,Xl.jsxs=r,Xl}var ey;function Dk(){return ey||(ey=1,hh.exports=jk()),hh.exports}var m=Dk(),mh={exports:{}},Ve={};/**
|
||||
*/var Q0;function Mk(){if(Q0)return Xl;Q0=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function r(a,s,o){var c=null;if(o!==void 0&&(c=""+o),s.key!==void 0&&(c=""+s.key),"key"in s){o={};for(var d in s)d!=="key"&&(o[d]=s[d])}else o=s;return s=o.ref,{$$typeof:e,type:a,key:c,ref:s!==void 0?s:null,props:o}}return Xl.Fragment=t,Xl.jsx=r,Xl.jsxs=r,Xl}var W0;function Ok(){return W0||(W0=1,fh.exports=Mk()),fh.exports}var m=Ok(),hh={exports:{}},Ve={};/**
|
||||
* @license React
|
||||
* react.production.js
|
||||
*
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var ty;function Lk(){if(ty)return Ve;ty=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),s=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),f=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),b=Symbol.iterator;function y(L){return L===null||typeof L!="object"?null:(L=b&&L[b]||L["@@iterator"],typeof L=="function"?L:null)}var _={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},N=Object.assign,S={};function w(L,F,D){this.props=L,this.context=F,this.refs=S,this.updater=D||_}w.prototype.isReactComponent={},w.prototype.setState=function(L,F){if(typeof L!="object"&&typeof L!="function"&&L!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,L,F,"setState")},w.prototype.forceUpdate=function(L){this.updater.enqueueForceUpdate(this,L,"forceUpdate")};function C(){}C.prototype=w.prototype;function E(L,F,D){this.props=L,this.context=F,this.refs=S,this.updater=D||_}var A=E.prototype=new C;A.constructor=E,N(A,w.prototype),A.isPureReactComponent=!0;var B=Array.isArray;function R(){}var H={H:null,A:null,T:null,S:null},z=Object.prototype.hasOwnProperty;function Y(L,F,D){var V=D.ref;return{$$typeof:e,type:L,key:F,ref:V!==void 0?V:null,props:D}}function j(L,F){return Y(L.type,F,L.props)}function I(L){return typeof L=="object"&&L!==null&&L.$$typeof===e}function Z(L){var F={"=":"=0",":":"=2"};return"$"+L.replace(/[=:]/g,function(D){return F[D]})}var P=/\/+/g;function k(L,F){return typeof L=="object"&&L!==null&&L.key!=null?Z(""+L.key):F.toString(36)}function $(L){switch(L.status){case"fulfilled":return L.value;case"rejected":throw L.reason;default:switch(typeof L.status=="string"?L.then(R,R):(L.status="pending",L.then(function(F){L.status==="pending"&&(L.status="fulfilled",L.value=F)},function(F){L.status==="pending"&&(L.status="rejected",L.reason=F)})),L.status){case"fulfilled":return L.value;case"rejected":throw L.reason}}throw L}function O(L,F,D,V,q){var Q=typeof L;(Q==="undefined"||Q==="boolean")&&(L=null);var J=!1;if(L===null)J=!0;else switch(Q){case"bigint":case"string":case"number":J=!0;break;case"object":switch(L.$$typeof){case e:case t:J=!0;break;case p:return J=L._init,O(J(L._payload),F,D,V,q)}}if(J)return q=q(L),J=V===""?"."+k(L,0):V,B(q)?(D="",J!=null&&(D=J.replace(P,"$&/")+"/"),O(q,F,D,"",function(oe){return oe})):q!=null&&(I(q)&&(q=j(q,D+(q.key==null||L&&L.key===q.key?"":(""+q.key).replace(P,"$&/")+"/")+J)),F.push(q)),1;J=0;var W=V===""?".":V+":";if(B(L))for(var te=0;te<L.length;te++)V=L[te],Q=W+k(V,te),J+=O(V,F,D,Q,q);else if(te=y(L),typeof te=="function")for(L=te.call(L),te=0;!(V=L.next()).done;)V=V.value,Q=W+k(V,te++),J+=O(V,F,D,Q,q);else if(Q==="object"){if(typeof L.then=="function")return O($(L),F,D,V,q);throw F=String(L),Error("Objects are not valid as a React child (found: "+(F==="[object Object]"?"object with keys {"+Object.keys(L).join(", ")+"}":F)+"). If you meant to render a collection of children, use an array instead.")}return J}function U(L,F,D){if(L==null)return L;var V=[],q=0;return O(L,V,"","",function(Q){return F.call(D,Q,q++)}),V}function K(L){if(L._status===-1){var F=L._result;F=F(),F.then(function(D){(L._status===0||L._status===-1)&&(L._status=1,L._result=D)},function(D){(L._status===0||L._status===-1)&&(L._status=2,L._result=D)}),L._status===-1&&(L._status=0,L._result=F)}if(L._status===1)return L._result.default;throw L._result}var X=typeof reportError=="function"?reportError:function(L){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var F=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof L=="object"&&L!==null&&typeof L.message=="string"?String(L.message):String(L),error:L});if(!window.dispatchEvent(F))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",L);return}console.error(L)},M={map:U,forEach:function(L,F,D){U(L,function(){F.apply(this,arguments)},D)},count:function(L){var F=0;return U(L,function(){F++}),F},toArray:function(L){return U(L,function(F){return F})||[]},only:function(L){if(!I(L))throw Error("React.Children.only expected to receive a single React element child.");return L}};return Ve.Activity=g,Ve.Children=M,Ve.Component=w,Ve.Fragment=r,Ve.Profiler=s,Ve.PureComponent=E,Ve.StrictMode=a,Ve.Suspense=h,Ve.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=H,Ve.__COMPILER_RUNTIME={__proto__:null,c:function(L){return H.H.useMemoCache(L)}},Ve.cache=function(L){return function(){return L.apply(null,arguments)}},Ve.cacheSignal=function(){return null},Ve.cloneElement=function(L,F,D){if(L==null)throw Error("The argument must be a React element, but you passed "+L+".");var
Line truncated
|
||||
*/var J0;function Rk(){if(J0)return Ve;J0=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),s=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),c=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),f=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),g=Symbol.for("react.activity"),b=Symbol.iterator;function y(L){return L===null||typeof L!="object"?null:(L=b&&L[b]||L["@@iterator"],typeof L=="function"?L:null)}var _={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},N=Object.assign,S={};function w(L,F,D){this.props=L,this.context=F,this.refs=S,this.updater=D||_}w.prototype.isReactComponent={},w.prototype.setState=function(L,F){if(typeof L!="object"&&typeof L!="function"&&L!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,L,F,"setState")},w.prototype.forceUpdate=function(L){this.updater.enqueueForceUpdate(this,L,"forceUpdate")};function C(){}C.prototype=w.prototype;function E(L,F,D){this.props=L,this.context=F,this.refs=S,this.updater=D||_}var A=E.prototype=new C;A.constructor=E,N(A,w.prototype),A.isPureReactComponent=!0;var B=Array.isArray;function R(){}var H={H:null,A:null,T:null,S:null},z=Object.prototype.hasOwnProperty;function Y(L,F,D){var V=D.ref;return{$$typeof:e,type:L,key:F,ref:V!==void 0?V:null,props:D}}function j(L,F){return Y(L.type,F,L.props)}function I(L){return typeof L=="object"&&L!==null&&L.$$typeof===e}function Z(L){var F={"=":"=0",":":"=2"};return"$"+L.replace(/[=:]/g,function(D){return F[D]})}var P=/\/+/g;function k(L,F){return typeof L=="object"&&L!==null&&L.key!=null?Z(""+L.key):F.toString(36)}function $(L){switch(L.status){case"fulfilled":return L.value;case"rejected":throw L.reason;default:switch(typeof L.status=="string"?L.then(R,R):(L.status="pending",L.then(function(F){L.status==="pending"&&(L.status="fulfilled",L.value=F)},function(F){L.status==="pending"&&(L.status="rejected",L.reason=F)})),L.status){case"fulfilled":return L.value;case"rejected":throw L.reason}}throw L}function O(L,F,D,V,q){var Q=typeof L;(Q==="undefined"||Q==="boolean")&&(L=null);var J=!1;if(L===null)J=!0;else switch(Q){case"bigint":case"string":case"number":J=!0;break;case"object":switch(L.$$typeof){case e:case t:J=!0;break;case p:return J=L._init,O(J(L._payload),F,D,V,q)}}if(J)return q=q(L),J=V===""?"."+k(L,0):V,B(q)?(D="",J!=null&&(D=J.replace(P,"$&/")+"/"),O(q,F,D,"",function(oe){return oe})):q!=null&&(I(q)&&(q=j(q,D+(q.key==null||L&&L.key===q.key?"":(""+q.key).replace(P,"$&/")+"/")+J)),F.push(q)),1;J=0;var W=V===""?".":V+":";if(B(L))for(var te=0;te<L.length;te++)V=L[te],Q=W+k(V,te),J+=O(V,F,D,Q,q);else if(te=y(L),typeof te=="function")for(L=te.call(L),te=0;!(V=L.next()).done;)V=V.value,Q=W+k(V,te++),J+=O(V,F,D,Q,q);else if(Q==="object"){if(typeof L.then=="function")return O($(L),F,D,V,q);throw F=String(L),Error("Objects are not valid as a React child (found: "+(F==="[object Object]"?"object with keys {"+Object.keys(L).join(", ")+"}":F)+"). If you meant to render a collection of children, use an array instead.")}return J}function U(L,F,D){if(L==null)return L;var V=[],q=0;return O(L,V,"","",function(Q){return F.call(D,Q,q++)}),V}function K(L){if(L._status===-1){var F=L._result;F=F(),F.then(function(D){(L._status===0||L._status===-1)&&(L._status=1,L._result=D)},function(D){(L._status===0||L._status===-1)&&(L._status=2,L._result=D)}),L._status===-1&&(L._status=0,L._result=F)}if(L._status===1)return L._result.default;throw L._result}var X=typeof reportError=="function"?reportError:function(L){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var F=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof L=="object"&&L!==null&&typeof L.message=="string"?String(L.message):String(L),error:L});if(!window.dispatchEvent(F))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",L);return}console.error(L)},M={map:U,forEach:function(L,F,D){U(L,function(){F.apply(this,arguments)},D)},count:function(L){var F=0;return U(L,function(){F++}),F},toArray:function(L){return U(L,function(F){return F})||[]},only:function(L){if(!I(L))throw Error("React.Children.only expected to receive a single React element child.");return L}};return Ve.Activity=g,Ve.Children=M,Ve.Component=w,Ve.Fragment=r,Ve.Profiler=s,Ve.PureComponent=E,Ve.StrictMode=a,Ve.Suspense=h,Ve.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=H,Ve.__COMPILER_RUNTIME={__proto__:null,c:function(L){return H.H.useMemoCache(L)}},Ve.cache=function(L){return function(){return L.apply(null,arguments)}},Ve.cacheSignal=function(){return null},Ve.cloneElement=function(L,F,D){if(L==null)throw Error("The argument must be a React element, but you passed "+L+".");var
Line truncated
|
||||
* @license React
|
||||
* scheduler.production.js
|
||||
*
|
||||
@@ -22,7 +22,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var ry;function zk(){return ry||(ry=1,(function(e){function t(O,U){var K=O.length;O.push(U);e:for(;0<K;){var X=K-1>>>1,M=O[X];if(0<s(M,U))O[X]=U,O[K]=M,K=X;else break e}}function r(O){return O.length===0?null:O[0]}function a(O){if(O.length===0)return null;var U=O[0],K=O.pop();if(K!==U){O[0]=K;e:for(var X=0,M=O.length,L=M>>>1;X<L;){var F=2*(X+1)-1,D=O[F],V=F+1,q=O[V];if(0>s(D,K))V<M&&0>s(q,D)?(O[X]=q,O[V]=K,X=V):(O[X]=D,O[F]=K,X=F);else if(V<M&&0>s(q,K))O[X]=q,O[V]=K,X=V;else break e}}return U}function s(O,U){var K=O.sortIndex-U.sortIndex;return K!==0?K:O.id-U.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var c=Date,d=c.now();e.unstable_now=function(){return c.now()-d}}var h=[],f=[],p=1,g=null,b=3,y=!1,_=!1,N=!1,S=!1,w=typeof setTimeout=="function"?setTimeout:null,C=typeof clearTimeout=="function"?clearTimeout:null,E=typeof setImmediate<"u"?setImmediate:null;function A(O){for(var U=r(f);U!==null;){if(U.callback===null)a(f);else if(U.startTime<=O)a(f),U.sortIndex=U.expirationTime,t(h,U);else break;U=r(f)}}function B(O){if(N=!1,A(O),!_)if(r(h)!==null)_=!0,R||(R=!0,Z());else{var U=r(f);U!==null&&$(B,U.startTime-O)}}var R=!1,H=-1,z=5,Y=-1;function j(){return S?!0:!(e.unstable_now()-Y<z)}function I(){if(S=!1,R){var O=e.unstable_now();Y=O;var U=!0;try{e:{_=!1,N&&(N=!1,C(H),H=-1),y=!0;var K=b;try{t:{for(A(O),g=r(h);g!==null&&!(g.expirationTime>O&&j());){var X=g.callback;if(typeof X=="function"){g.callback=null,b=g.priorityLevel;var M=X(g.expirationTime<=O);if(O=e.unstable_now(),typeof M=="function"){g.callback=M,A(O),U=!0;break t}g===r(h)&&a(h),A(O)}else a(h);g=r(h)}if(g!==null)U=!0;else{var L=r(f);L!==null&&$(B,L.startTime-O),U=!1}}break e}finally{g=null,b=K,y=!1}U=void 0}}finally{U?Z():R=!1}}}var Z;if(typeof E=="function")Z=function(){E(I)};else if(typeof MessageChannel<"u"){var P=new MessageChannel,k=P.port2;P.port1.onmessage=I,Z=function(){k.postMessage(null)}}else Z=function(){w(I,0)};function $(O,U){H=w(function(){O(e.unstable_now())},U)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(O){O.callback=null},e.unstable_forceFrameRate=function(O){0>O||125<O?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):z=0<O?Math.floor(1e3/O):5},e.unstable_getCurrentPriorityLevel=function(){return b},e.unstable_next=function(O){switch(b){case 1:case 2:case 3:var U=3;break;default:U=b}var K=b;b=U;try{return O()}finally{b=K}},e.unstable_requestPaint=function(){S=!0},e.unstable_runWithPriority=function(O,U){switch(O){case 1:case 2:case 3:case 4:case 5:break;default:O=3}var K=b;b=O;try{return U()}finally{b=K}},e.unstable_scheduleCallback=function(O,U,K){var X=e.unstable_now();switch(typeof K=="object"&&K!==null?(K=K.delay,K=typeof K=="number"&&0<K?X+K:X):K=X,O){case 1:var M=-1;break;case 2:M=250;break;case 5:M=1073741823;break;case 4:M=1e4;break;default:M=5e3}return M=K+M,O={id:p++,callback:U,priorityLevel:O,startTime:K,expirationTime:M,sortIndex:-1},K>X?(O.sortIndex=K,t(f,O),r(h)===null&&O===r(f)&&(N?(C(H),H=-1):N=!0,$(B,K-X))):(O.sortIndex=M,t(h,O),_||y||(_=!0,R||(R=!0,Z()))),O},e.unstable_shouldYield=j,e.unstable_wrapCallback=function(O){var U=b;return function(){var K=b;b=U;try{return O.apply(this,arguments)}finally{b=K}}}})(xh)),xh}var iy;function Ik(){return iy||(iy=1,gh.exports=zk()),gh.exports}var bh={exports:{}},Tn={};/**
|
||||
*/var ty;function jk(){return ty||(ty=1,(function(e){function t(O,U){var K=O.length;O.push(U);e:for(;0<K;){var X=K-1>>>1,M=O[X];if(0<s(M,U))O[X]=U,O[K]=M,K=X;else break e}}function r(O){return O.length===0?null:O[0]}function a(O){if(O.length===0)return null;var U=O[0],K=O.pop();if(K!==U){O[0]=K;e:for(var X=0,M=O.length,L=M>>>1;X<L;){var F=2*(X+1)-1,D=O[F],V=F+1,q=O[V];if(0>s(D,K))V<M&&0>s(q,D)?(O[X]=q,O[V]=K,X=V):(O[X]=D,O[F]=K,X=F);else if(V<M&&0>s(q,K))O[X]=q,O[V]=K,X=V;else break e}}return U}function s(O,U){var K=O.sortIndex-U.sortIndex;return K!==0?K:O.id-U.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var c=Date,d=c.now();e.unstable_now=function(){return c.now()-d}}var h=[],f=[],p=1,g=null,b=3,y=!1,_=!1,N=!1,S=!1,w=typeof setTimeout=="function"?setTimeout:null,C=typeof clearTimeout=="function"?clearTimeout:null,E=typeof setImmediate<"u"?setImmediate:null;function A(O){for(var U=r(f);U!==null;){if(U.callback===null)a(f);else if(U.startTime<=O)a(f),U.sortIndex=U.expirationTime,t(h,U);else break;U=r(f)}}function B(O){if(N=!1,A(O),!_)if(r(h)!==null)_=!0,R||(R=!0,Z());else{var U=r(f);U!==null&&$(B,U.startTime-O)}}var R=!1,H=-1,z=5,Y=-1;function j(){return S?!0:!(e.unstable_now()-Y<z)}function I(){if(S=!1,R){var O=e.unstable_now();Y=O;var U=!0;try{e:{_=!1,N&&(N=!1,C(H),H=-1),y=!0;var K=b;try{t:{for(A(O),g=r(h);g!==null&&!(g.expirationTime>O&&j());){var X=g.callback;if(typeof X=="function"){g.callback=null,b=g.priorityLevel;var M=X(g.expirationTime<=O);if(O=e.unstable_now(),typeof M=="function"){g.callback=M,A(O),U=!0;break t}g===r(h)&&a(h),A(O)}else a(h);g=r(h)}if(g!==null)U=!0;else{var L=r(f);L!==null&&$(B,L.startTime-O),U=!1}}break e}finally{g=null,b=K,y=!1}U=void 0}}finally{U?Z():R=!1}}}var Z;if(typeof E=="function")Z=function(){E(I)};else if(typeof MessageChannel<"u"){var P=new MessageChannel,k=P.port2;P.port1.onmessage=I,Z=function(){k.postMessage(null)}}else Z=function(){w(I,0)};function $(O,U){H=w(function(){O(e.unstable_now())},U)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(O){O.callback=null},e.unstable_forceFrameRate=function(O){0>O||125<O?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):z=0<O?Math.floor(1e3/O):5},e.unstable_getCurrentPriorityLevel=function(){return b},e.unstable_next=function(O){switch(b){case 1:case 2:case 3:var U=3;break;default:U=b}var K=b;b=U;try{return O()}finally{b=K}},e.unstable_requestPaint=function(){S=!0},e.unstable_runWithPriority=function(O,U){switch(O){case 1:case 2:case 3:case 4:case 5:break;default:O=3}var K=b;b=O;try{return U()}finally{b=K}},e.unstable_scheduleCallback=function(O,U,K){var X=e.unstable_now();switch(typeof K=="object"&&K!==null?(K=K.delay,K=typeof K=="number"&&0<K?X+K:X):K=X,O){case 1:var M=-1;break;case 2:M=250;break;case 5:M=1073741823;break;case 4:M=1e4;break;default:M=5e3}return M=K+M,O={id:p++,callback:U,priorityLevel:O,startTime:K,expirationTime:M,sortIndex:-1},K>X?(O.sortIndex=K,t(f,O),r(h)===null&&O===r(f)&&(N?(C(H),H=-1):N=!0,$(B,K-X))):(O.sortIndex=M,t(h,O),_||y||(_=!0,R||(R=!0,Z()))),O},e.unstable_shouldYield=j,e.unstable_wrapCallback=function(O){var U=b;return function(){var K=b;b=U;try{return O.apply(this,arguments)}finally{b=K}}}})(gh)),gh}var ny;function Dk(){return ny||(ny=1,ph.exports=jk()),ph.exports}var xh={exports:{}},Tn={};/**
|
||||
* @license React
|
||||
* react-dom.production.js
|
||||
*
|
||||
@@ -30,7 +30,7 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var ay;function Bk(){if(ay)return Tn;ay=1;var e=Oo();function t(h){var f="https://react.dev/errors/"+h;if(1<arguments.length){f+="?args[]="+encodeURIComponent(arguments[1]);for(var p=2;p<arguments.length;p++)f+="&args[]="+encodeURIComponent(arguments[p])}return"Minified React error #"+h+"; visit "+f+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function r(){}var a={d:{f:r,r:function(){throw Error(t(522))},D:r,C:r,L:r,m:r,X:r,S:r,M:r},p:0,findDOMNode:null},s=Symbol.for("react.portal");function o(h,f,p){var g=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:s,key:g==null?null:""+g,children:h,containerInfo:f,implementation:p}}var c=e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function d(h,f){if(h==="font")return"";if(typeof f=="string")return f==="use-credentials"?f:""}return Tn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,Tn.createPortal=function(h,f){var p=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!f||f.nodeType!==1&&f.nodeType!==9&&f.nodeType!==11)throw Error(t(299));return o(h,f,null,p)},Tn.flushSync=function(h){var f=c.T,p=a.p;try{if(c.T=null,a.p=2,h)return h()}finally{c.T=f,a.p=p,a.d.f()}},Tn.preconnect=function(h,f){typeof h=="string"&&(f?(f=f.crossOrigin,f=typeof f=="string"?f==="use-credentials"?f:"":void 0):f=null,a.d.C(h,f))},Tn.prefetchDNS=function(h){typeof h=="string"&&a.d.D(h)},Tn.preinit=function(h,f){if(typeof h=="string"&&f&&typeof f.as=="string"){var p=f.as,g=d(p,f.crossOrigin),b=typeof f.integrity=="string"?f.integrity:void 0,y=typeof f.fetchPriority=="string"?f.fetchPriority:void 0;p==="style"?a.d.S(h,typeof f.precedence=="string"?f.precedence:void 0,{crossOrigin:g,integrity:b,fetchPriority:y}):p==="script"&&a.d.X(h,{crossOrigin:g,integrity:b,fetchPriority:y,nonce:typeof f.nonce=="string"?f.nonce:void 0})}},Tn.preinitModule=function(h,f){if(typeof h=="string")if(typeof f=="object"&&f!==null){if(f.as==null||f.as==="script"){var p=d(f.as,f.crossOrigin);a.d.M(h,{crossOrigin:p,integrity:typeof f.integrity=="string"?f.integrity:void 0,nonce:typeof f.nonce=="string"?f.nonce:void 0})}}else f==null&&a.d.M(h)},Tn.preload=function(h,f){if(typeof h=="string"&&typeof f=="object"&&f!==null&&typeof f.as=="string"){var p=f.as,g=d(p,f.crossOrigin);a.d.L(h,p,{crossOrigin:g,integrity:typeof f.integrity=="string"?f.integrity:void 0,nonce:typeof f.nonce=="string"?f.nonce:void 0,type:typeof f.type=="string"?f.type:void 0,fetchPriority:typeof f.fetchPriority=="string"?f.fetchPriority:void 0,referrerPolicy:typeof f.referrerPolicy=="string"?f.referrerPolicy:void 0,imageSrcSet:typeof f.imageSrcSet=="string"?f.imageSrcSet:void 0,imageSizes:typeof f.imageSizes=="string"?f.imageSizes:void 0,media:typeof f.media=="string"?f.media:void 0})}},Tn.preloadModule=function(h,f){if(typeof h=="string")if(f){var p=d(f.as,f.crossOrigin);a.d.m(h,{as:typeof f.as=="string"&&f.as!=="script"?f.as:void 0,crossOrigin:p,integrity:typeof f.integrity=="string"?f.integrity:void 0})}else a.d.m(h)},Tn.requestFormReset=function(h){a.d.r(h)},Tn.unstable_batchedUpdates=function(h,f){return h(f)},Tn.useFormState=function(h,f,p){return c.H.useFormState(h,f,p)},Tn.useFormStatus=function(){return c.H.useHostTransitionStatus()},Tn.version="19.2.7",Tn}var sy;function z_(){if(sy)return bh.exports;sy=1;function e(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),bh.exports=Bk(),bh.exports}/**
|
||||
*/var ry;function Lk(){if(ry)return Tn;ry=1;var e=Mo();function t(h){var f="https://react.dev/errors/"+h;if(1<arguments.length){f+="?args[]="+encodeURIComponent(arguments[1]);for(var p=2;p<arguments.length;p++)f+="&args[]="+encodeURIComponent(arguments[p])}return"Minified React error #"+h+"; visit "+f+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function r(){}var a={d:{f:r,r:function(){throw Error(t(522))},D:r,C:r,L:r,m:r,X:r,S:r,M:r},p:0,findDOMNode:null},s=Symbol.for("react.portal");function o(h,f,p){var g=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:s,key:g==null?null:""+g,children:h,containerInfo:f,implementation:p}}var c=e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function d(h,f){if(h==="font")return"";if(typeof f=="string")return f==="use-credentials"?f:""}return Tn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,Tn.createPortal=function(h,f){var p=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!f||f.nodeType!==1&&f.nodeType!==9&&f.nodeType!==11)throw Error(t(299));return o(h,f,null,p)},Tn.flushSync=function(h){var f=c.T,p=a.p;try{if(c.T=null,a.p=2,h)return h()}finally{c.T=f,a.p=p,a.d.f()}},Tn.preconnect=function(h,f){typeof h=="string"&&(f?(f=f.crossOrigin,f=typeof f=="string"?f==="use-credentials"?f:"":void 0):f=null,a.d.C(h,f))},Tn.prefetchDNS=function(h){typeof h=="string"&&a.d.D(h)},Tn.preinit=function(h,f){if(typeof h=="string"&&f&&typeof f.as=="string"){var p=f.as,g=d(p,f.crossOrigin),b=typeof f.integrity=="string"?f.integrity:void 0,y=typeof f.fetchPriority=="string"?f.fetchPriority:void 0;p==="style"?a.d.S(h,typeof f.precedence=="string"?f.precedence:void 0,{crossOrigin:g,integrity:b,fetchPriority:y}):p==="script"&&a.d.X(h,{crossOrigin:g,integrity:b,fetchPriority:y,nonce:typeof f.nonce=="string"?f.nonce:void 0})}},Tn.preinitModule=function(h,f){if(typeof h=="string")if(typeof f=="object"&&f!==null){if(f.as==null||f.as==="script"){var p=d(f.as,f.crossOrigin);a.d.M(h,{crossOrigin:p,integrity:typeof f.integrity=="string"?f.integrity:void 0,nonce:typeof f.nonce=="string"?f.nonce:void 0})}}else f==null&&a.d.M(h)},Tn.preload=function(h,f){if(typeof h=="string"&&typeof f=="object"&&f!==null&&typeof f.as=="string"){var p=f.as,g=d(p,f.crossOrigin);a.d.L(h,p,{crossOrigin:g,integrity:typeof f.integrity=="string"?f.integrity:void 0,nonce:typeof f.nonce=="string"?f.nonce:void 0,type:typeof f.type=="string"?f.type:void 0,fetchPriority:typeof f.fetchPriority=="string"?f.fetchPriority:void 0,referrerPolicy:typeof f.referrerPolicy=="string"?f.referrerPolicy:void 0,imageSrcSet:typeof f.imageSrcSet=="string"?f.imageSrcSet:void 0,imageSizes:typeof f.imageSizes=="string"?f.imageSizes:void 0,media:typeof f.media=="string"?f.media:void 0})}},Tn.preloadModule=function(h,f){if(typeof h=="string")if(f){var p=d(f.as,f.crossOrigin);a.d.m(h,{as:typeof f.as=="string"&&f.as!=="script"?f.as:void 0,crossOrigin:p,integrity:typeof f.integrity=="string"?f.integrity:void 0})}else a.d.m(h)},Tn.requestFormReset=function(h){a.d.r(h)},Tn.unstable_batchedUpdates=function(h,f){return h(f)},Tn.useFormState=function(h,f,p){return c.H.useFormState(h,f,p)},Tn.useFormStatus=function(){return c.H.useHostTransitionStatus()},Tn.version="19.2.7",Tn}var iy;function D_(){if(iy)return xh.exports;iy=1;function e(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),xh.exports=Lk(),xh.exports}/**
|
||||
* @license React
|
||||
* react-dom-client.production.js
|
||||
*
|
||||
@@ -38,432 +38,427 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var ly;function Uk(){if(ly)return Kl;ly=1;var e=Ik(),t=Oo(),r=z_();function a(n){var i="https://react.dev/errors/"+n;if(1<arguments.length){i+="?args[]="+encodeURIComponent(arguments[1]);for(var l=2;l<arguments.length;l++)i+="&args[]="+encodeURIComponent(arguments[l])}return"Minified React error #"+n+"; visit "+i+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function s(n){return!(!n||n.nodeType!==1&&n.nodeType!==9&&n.nodeType!==11)}function o(n){var i=n,l=n;if(n.alternate)for(;i.return;)i=i.return;else{n=i;do i=n,(i.flags&4098)!==0&&(l=i.return),n=i.return;while(n)}return i.tag===3?l:null}function c(n){if(n.tag===13){var i=n.memoizedState;if(i===null&&(n=n.alternate,n!==null&&(i=n.memoizedState)),i!==null)return i.dehydrated}return null}function d(n){if(n.tag===31){var i=n.memoizedState;if(i===null&&(n=n.alternate,n!==null&&(i=n.memoizedState)),i!==null)return i.dehydrated}return null}function h(n){if(o(n)!==n)throw Error(a(188))}function f(n){var i=n.alternate;if(!i){if(i=o(n),i===null)throw Error(a(188));return i!==n?null:n}for(var l=n,u=i;;){var x=l.return;if(x===null)break;var v=x.alternate;if(v===null){if(u=x.return,u!==null){l=u;continue}break}if(x.child===v.child){for(v=x.child;v;){if(v===l)return h(x),n;if(v===u)return h(x),i;v=v.sibling}throw Error(a(188))}if(l.return!==u.return)l=x,u=v;else{for(var T=!1,G=x.child;G;){if(G===l){T=!0,l=x,u=v;break}if(G===u){T=!0,u=x,l=v;break}G=G.sibling}if(!T){for(G=v.child;G;){if(G===l){T=!0,l=v,u=x;break}if(G===u){T=!0,u=v,l=x;break}G=G.sibling}if(!T)throw Error(a(189))}}if(l.alternate!==u)throw Error(a(190))}if(l.tag!==3)throw Error(a(188));return l.stateNode.current===l?n:i}function p(n){var i=n.tag;if(i===5||i===26||i===27||i===6)return n;for(n=n.child;n!==null;){if(i=p(n),i!==null)return i;n=n.sibling}return null}var g=Object.assign,b=Symbol.for("react.element"),y=Symbol.for("react.transitional.element"),_=Symbol.for("react.portal"),N=Symbol.for("react.fragment"),S=Symbol.for("react.strict_mode"),w=Symbol.for("react.profiler"),C=Symbol.for("react.consumer"),E=Symbol.for("react.context"),A=Symbol.for("react.forward_ref"),B=Symbol.for("react.suspense"),R=Symbol.for("react.suspense_list"),H=Symbol.for("react.memo"),z=Symbol.for("react.lazy"),Y=Symbol.for("react.activity"),j=Symbol.for("react.memo_cache_sentinel"),I=Symbol.iterator;function Z(n){return n===null||typeof n!="object"?null:(n=I&&n[I]||n["@@iterator"],typeof n=="function"?n:null)}var P=Symbol.for("react.client.reference");function k(n){if(n==null)return null;if(typeof n=="function")return n.$$typeof===P?null:n.displayName||n.name||null;if(typeof n=="string")return n;switch(n){case N:return"Fragment";case w:return"Profiler";case S:return"StrictMode";case B:return"Suspense";case R:return"SuspenseList";case Y:return"Activity"}if(typeof n=="object")switch(n.$$typeof){case _:return"Portal";case E:return n.displayName||"Context";case C:return(n._context.displayName||"Context")+".Consumer";case A:var i=n.render;return n=n.displayName,n||(n=i.displayName||i.name||"",n=n!==""?"ForwardRef("+n+")":"ForwardRef"),n;case H:return i=n.displayName||null,i!==null?i:k(n.type)||"Memo";case z:i=n._payload,n=n._init;try{return k(n(i))}catch{}}return null}var $=Array.isArray,O=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,U=r.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,K={pending:!1,data:null,method:null,action:null},X=[],M=-1;function L(n){return{current:n}}function F(n){0>M||(n.current=X[M],X[M]=null,M--)}function D(n,i){M++,X[M]=n.current,n.current=i}var V=L(null),q=L(null),Q=L(null),J=L(null);function W(n,i){switch(D(Q,i),D(q,n),D(V,null),i.nodeType){case 9:case 11:n=(n=i.documentElement)&&(n=n.namespaceURI)?w0(n):0;break;default:if(n=i.tagName,i=i.namespaceURI)i=w0(i),n=E0(i,n);else switch(n){case"svg":n=1;break;case"math":n=2;break;default:n=0}}F(V),D(V,n)}function te(){F(V),F(q),F(Q)}function oe(n){n.memoizedState!==null&&D(J,n);var i=V.current,l=E0(i,n.type);i!==l&&(D(q,n),D(V,l))}function fe(n){q.current===n&&(F(V),F(q)),J.current===n&&(F(J),Fl._currentValue=K)}var xe,we;function Ne(n){if(xe===void 0)try{throw Error()}catch(l){var i=l.stack.trim().match(/\n( *(at )?)/);xe=i&&i[1]||"",we=-1<l.stack.indexOf(`
|
||||
*/var ay;function zk(){if(ay)return Kl;ay=1;var e=Dk(),t=Mo(),r=D_();function a(n){var i="https://react.dev/errors/"+n;if(1<arguments.length){i+="?args[]="+encodeURIComponent(arguments[1]);for(var l=2;l<arguments.length;l++)i+="&args[]="+encodeURIComponent(arguments[l])}return"Minified React error #"+n+"; visit "+i+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function s(n){return!(!n||n.nodeType!==1&&n.nodeType!==9&&n.nodeType!==11)}function o(n){var i=n,l=n;if(n.alternate)for(;i.return;)i=i.return;else{n=i;do i=n,(i.flags&4098)!==0&&(l=i.return),n=i.return;while(n)}return i.tag===3?l:null}function c(n){if(n.tag===13){var i=n.memoizedState;if(i===null&&(n=n.alternate,n!==null&&(i=n.memoizedState)),i!==null)return i.dehydrated}return null}function d(n){if(n.tag===31){var i=n.memoizedState;if(i===null&&(n=n.alternate,n!==null&&(i=n.memoizedState)),i!==null)return i.dehydrated}return null}function h(n){if(o(n)!==n)throw Error(a(188))}function f(n){var i=n.alternate;if(!i){if(i=o(n),i===null)throw Error(a(188));return i!==n?null:n}for(var l=n,u=i;;){var x=l.return;if(x===null)break;var v=x.alternate;if(v===null){if(u=x.return,u!==null){l=u;continue}break}if(x.child===v.child){for(v=x.child;v;){if(v===l)return h(x),n;if(v===u)return h(x),i;v=v.sibling}throw Error(a(188))}if(l.return!==u.return)l=x,u=v;else{for(var T=!1,G=x.child;G;){if(G===l){T=!0,l=x,u=v;break}if(G===u){T=!0,u=x,l=v;break}G=G.sibling}if(!T){for(G=v.child;G;){if(G===l){T=!0,l=v,u=x;break}if(G===u){T=!0,u=v,l=x;break}G=G.sibling}if(!T)throw Error(a(189))}}if(l.alternate!==u)throw Error(a(190))}if(l.tag!==3)throw Error(a(188));return l.stateNode.current===l?n:i}function p(n){var i=n.tag;if(i===5||i===26||i===27||i===6)return n;for(n=n.child;n!==null;){if(i=p(n),i!==null)return i;n=n.sibling}return null}var g=Object.assign,b=Symbol.for("react.element"),y=Symbol.for("react.transitional.element"),_=Symbol.for("react.portal"),N=Symbol.for("react.fragment"),S=Symbol.for("react.strict_mode"),w=Symbol.for("react.profiler"),C=Symbol.for("react.consumer"),E=Symbol.for("react.context"),A=Symbol.for("react.forward_ref"),B=Symbol.for("react.suspense"),R=Symbol.for("react.suspense_list"),H=Symbol.for("react.memo"),z=Symbol.for("react.lazy"),Y=Symbol.for("react.activity"),j=Symbol.for("react.memo_cache_sentinel"),I=Symbol.iterator;function Z(n){return n===null||typeof n!="object"?null:(n=I&&n[I]||n["@@iterator"],typeof n=="function"?n:null)}var P=Symbol.for("react.client.reference");function k(n){if(n==null)return null;if(typeof n=="function")return n.$$typeof===P?null:n.displayName||n.name||null;if(typeof n=="string")return n;switch(n){case N:return"Fragment";case w:return"Profiler";case S:return"StrictMode";case B:return"Suspense";case R:return"SuspenseList";case Y:return"Activity"}if(typeof n=="object")switch(n.$$typeof){case _:return"Portal";case E:return n.displayName||"Context";case C:return(n._context.displayName||"Context")+".Consumer";case A:var i=n.render;return n=n.displayName,n||(n=i.displayName||i.name||"",n=n!==""?"ForwardRef("+n+")":"ForwardRef"),n;case H:return i=n.displayName||null,i!==null?i:k(n.type)||"Memo";case z:i=n._payload,n=n._init;try{return k(n(i))}catch{}}return null}var $=Array.isArray,O=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,U=r.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,K={pending:!1,data:null,method:null,action:null},X=[],M=-1;function L(n){return{current:n}}function F(n){0>M||(n.current=X[M],X[M]=null,M--)}function D(n,i){M++,X[M]=n.current,n.current=i}var V=L(null),q=L(null),Q=L(null),J=L(null);function W(n,i){switch(D(Q,i),D(q,n),D(V,null),i.nodeType){case 9:case 11:n=(n=i.documentElement)&&(n=n.namespaceURI)?v0(n):0;break;default:if(n=i.tagName,i=i.namespaceURI)i=v0(i),n=_0(i,n);else switch(n){case"svg":n=1;break;case"math":n=2;break;default:n=0}}F(V),D(V,n)}function te(){F(V),F(q),F(Q)}function oe(n){n.memoizedState!==null&&D(J,n);var i=V.current,l=_0(i,n.type);i!==l&&(D(q,n),D(V,l))}function fe(n){q.current===n&&(F(V),F(q)),J.current===n&&(F(J),Fl._currentValue=K)}var xe,we;function Ne(n){if(xe===void 0)try{throw Error()}catch(l){var i=l.stack.trim().match(/\n( *(at )?)/);xe=i&&i[1]||"",we=-1<l.stack.indexOf(`
|
||||
at`)?" (<anonymous>)":-1<l.stack.indexOf("@")?"@unknown:0:0":""}return`
|
||||
`+xe+n+we}var De=!1;function $e(n,i){if(!n||De)return"";De=!0;var l=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var u={DetermineComponentFrameRoot:function(){try{if(i){var ge=function(){throw Error()};if(Object.defineProperty(ge.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ge,[])}catch(de){var ce=de}Reflect.construct(n,[],ge)}else{try{ge.call()}catch(de){ce=de}n.call(ge.prototype)}}else{try{throw Error()}catch(de){ce=de}(ge=n())&&typeof ge.catch=="function"&&ge.catch(function(){})}}catch(de){if(de&&ce&&typeof de.stack=="string")return[de.stack,ce.stack]}return[null,null]}};u.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var x=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");x&&x.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var v=u.DetermineComponentFrameRoot(),T=v[0],G=v[1];if(T&&G){var ne=T.split(`
|
||||
`),le=G.split(`
|
||||
`);for(x=u=0;u<ne.length&&!ne[u].includes("DetermineComponentFrameRoot");)u++;for(;x<le.length&&!le[x].includes("DetermineComponentFrameRoot");)x++;if(u===ne.length||x===le.length)for(u=ne.length-1,x=le.length-1;1<=u&&0<=x&&ne[u]!==le[x];)x--;for(;1<=u&&0<=x;u--,x--)if(ne[u]!==le[x]){if(u!==1||x!==1)do if(u--,x--,0>x||ne[u]!==le[x]){var he=`
|
||||
`+ne[u].replace(" at new "," at ");return n.displayName&&he.includes("<anonymous>")&&(he=he.replace("<anonymous>",n.displayName)),he}while(1<=u&&0<=x);break}}}finally{De=!1,Error.prepareStackTrace=l}return(l=n?n.displayName||n.name:"")?Ne(l):""}function st(n,i){switch(n.tag){case 26:case 27:case 5:return Ne(n.type);case 16:return Ne("Lazy");case 13:return n.child!==i&&i!==null?Ne("Suspense Fallback"):Ne("Suspense");case 19:return Ne("SuspenseList");case 0:case 15:return $e(n.type,!1);case 11:return $e(n.type.render,!1);case 1:return $e(n.type,!0);case 31:return Ne("Activity");default:return""}}function Rt(n){try{var i="",l=null;do i+=st(n,l),l=n,n=n.return;while(n);return i}catch(u){return`
|
||||
Error generating stack: `+u.message+`
|
||||
`+u.stack}}var Xt=Object.prototype.hasOwnProperty,Ft=e.unstable_scheduleCallback,Kt=e.unstable_cancelCallback,Yn=e.unstable_shouldYield,Nn=e.unstable_requestPaint,ct=e.unstable_now,It=e.unstable_getCurrentPriorityLevel,ue=e.unstable_ImmediatePriority,be=e.unstable_UserBlockingPriority,Oe=e.unstable_NormalPriority,Fe=e.unstable_LowPriority,Ze=e.unstable_IdlePriority,cn=e.log,Sn=e.unstable_setDisableYieldValue,Zt=null,At=null;function Jt(n){if(typeof cn=="function"&&Sn(n),At&&typeof At.setStrictMode=="function")try{At.setStrictMode(Zt,n)}catch{}}var ut=Math.clz32?Math.clz32:Si,In=Math.log,un=Math.LN2;function Si(n){return n>>>=0,n===0?32:31-(In(n)/un|0)|0}var nt=256,Xn=262144,On=4194304;function mn(n){var i=n&42;if(i!==0)return i;switch(n&-n){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return n&261888;case 262144:case 524288:case 1048576:case 2097152:return n&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return n&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return n}}function re(n,i,l){var u=n.pendingLanes;if(u===0)return 0;var x=0,v=n.suspendedLanes,T=n.pingedLanes;n=n.warmLanes;var G=u&134217727;return G!==0?(u=G&~v,u!==0?x=mn(u):(T&=G,T!==0?x=mn(T):l||(l=G&~n,l!==0&&(x=mn(l))))):(G=u&~v,G!==0?x=mn(G):T!==0?x=mn(T):l||(l=u&~n,l!==0&&(x=mn(l)))),x===0?0:i!==0&&i!==x&&(i&v)===0&&(v=x&-x,l=i&-i,v>=l||v===32&&(l&4194048)!==0)?i:x}function me(n,i){return(n.pendingLanes&~(n.suspendedLanes&~n.pingedLanes)&i)===0}function Ee(n,i){switch(n){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Pe(){var n=On;return On<<=1,(On&62914560)===0&&(On=4194304),n}function St(n){for(var i=[],l=0;31>l;l++)i.push(n);return i}function gt(n,i){n.pendingLanes|=i,i!==268435456&&(n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0)}function Me(n,i,l,u,x,v){var T=n.pendingLanes;n.pendingLanes=l,n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0,n.expiredLanes&=l,n.entangledLanes&=l,n.errorRecoveryDisabledLanes&=l,n.shellSuspendCounter=0;var G=n.entanglements,ne=n.expirationTimes,le=n.hiddenUpdates;for(l=T&~l;0<l;){var he=31-ut(l),ge=1<<he;G[he]=0,ne[he]=-1;var ce=le[he];if(ce!==null)for(le[he]=null,he=0;he<ce.length;he++){var de=ce[he];de!==null&&(de.lane&=-536870913)}l&=~ge}u!==0&&Se(n,u,0),v!==0&&x===0&&n.tag!==0&&(n.suspendedLanes|=v&~(T&~i))}function Se(n,i,l){n.pendingLanes|=i,n.suspendedLanes&=~i;var u=31-ut(i);n.entangledLanes|=i,n.entanglements[u]=n.entanglements[u]|1073741824|l&261930}function Ue(n,i){var l=n.entangledLanes|=i;for(n=n.entanglements;l;){var u=31-ut(l),x=1<<u;x&i|n[u]&i&&(n[u]|=i),l&=~x}}function Bt(n,i){var l=i&-i;return l=(l&42)!==0?1:Mt(l),(l&(n.suspendedLanes|i))!==0?0:l}function Mt(n){switch(n){case 2:n=1;break;case 8:n=4;break;case 32:n=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:n=128;break;case 268435456:n=134217728;break;default:n=0}return n}function vr(n){return n&=-n,2<n?8<n?(n&134217727)!==0?32:268435456:8:2}function ki(){var n=U.p;return n!==0?n:(n=window.event,n===void 0?32:V0(n.type))}function Ci(n,i){var l=U.p;try{return U.p=n,i()}finally{U.p=l}}var lr=Math.random().toString(36).slice(2),Ut="__reactFiber$"+lr,pn="__reactProps$"+lr,_r="__reactContainer$"+lr,Ti="__reactEvents$"+lr,ga="__reactListeners$"+lr,Ai="__reactHandles$"+lr,ts="__reactResources$"+lr,Wr="__reactMarker$"+lr;function xa(n){delete n[Ut],delete n[pn],delete n[Ti],delete n[ga],delete n[Ai]}function bn(n){var i=n[Ut];if(i)return i;for(var l=n.parentNode;l;){if(i=l[_r]||l[Ut]){if(l=i.alternate,i.child!==null||l!==null&&l.child!==null)for(n=M0(n);n!==null;){if(l=n[Ut])return l;n=M0(n)}return i}n=l,l=n.parentNode}return null}function wr(n){if(n=n[Ut]||n[_r]){var i=n.tag;if(i===5||i===6||i===13||i===31||i===26||i===27||i===3)return n}return null}function Er(n){var i=n.tag;if(i===5||i===26||i===27||i===6)return n.stateNode;throw Error(a(33))}function Br(n){var i=n[ts];return i||(i=n[ts]={hoistableStyles:new Map,hoistableScripts:new Map}),i}function Gt(n){n[Wr]=!0}var ns=new Set,Jr={};function Nr(n,i){ye(n,i),ye(n+"Capture",i)}function ye(n,i){for(Jr[n]=i,n=0;n<i.length;n++)ns.add(i[n])}var Le=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u2
Line truncated
|
||||
`).replace(tk,"")}function v0(n,i){return i=y0(i),y0(n)===i}function Et(n,i,l,u,x,v){switch(l){case"children":typeof u=="string"?i==="body"||i==="textarea"&&u===""||qe(n,u):(typeof u=="number"||typeof u=="bigint")&&i!=="body"&&qe(n,""+u);break;case"className":Rn(n,"class",u);break;case"tabIndex":Rn(n,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":Rn(n,l,u);break;case"style":xt(n,u,v);break;case"data":if(i!=="object"){Rn(n,"data",u);break}case"src":case"href":if(u===""&&(i!=="a"||l!=="href")){n.removeAttribute(l);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){n.removeAttribute(l);break}u=Di(""+u),n.setAttribute(l,u);break;case"action":case"formAction":if(typeof u=="function"){n.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof v=="function"&&(l==="formAction"?(i!=="input"&&Et(n,i,"name",x.name,x,null),Et(n,i,"formEncType",x.formEncType,x,null),Et(n,i,"formMethod",x.formMethod,x,null),Et(n,i,"formTarget",x.formTarget,x,null)):(Et(n,i,"encType",x.encType,x,null),Et(n,i,"method",x.method,x,null),Et(n,i,"target",x.target,x,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){n.removeAttribute(l);break}u=Di(""+u),n.setAttribute(l,u);break;case"onClick":u!=null&&(n.onclick=_e);break;case"onScroll":u!=null&&et("scroll",n);break;case"onScrollEnd":u!=null&&et("scrollend",n);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(l=u.__html,l!=null){if(x.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"multiple":n.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":n.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){n.removeAttribute("xlink:href");break}l=Di(""+u),n.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?n.setAttribute(l,""+u):n.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?n.setAttribute(l,""):n.removeAttribute(l);break;case"capture":case"download":u===!0?n.setAttribute(l,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?n.setAttribute(l,u):n.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?n.setAttribute(l,u):n.removeAttribute(l);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?n.removeAttribute(l):n.setAttribute(l,u);break;case"popover":et("beforetoggle",n),et("toggle",n),gn(n,"popover",u);break;case"xlinkActuate":kn(n,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":kn(n,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":kn(n,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":kn(n,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":kn(n,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":kn(n,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":kn(n,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":kn(n,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":kn(n,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":gn(n,"is",u);break;case"innerText":case"textContent":break;default:(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")&&(l=ei.get(l)||l,gn(n,l,u))}}function Kf(n,i,l,u,x,v){switch(l){case"style":xt(n,u,v);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(l=u.__html,l!=null){if(x.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"children":typeof u=="string"?qe(n,u):(typeof u=="number"||typeof u=="bigint")&&qe(n,""+u);break;case"onScroll":u!=null&&et("scroll",n);break;case"onScrollEnd":u!=null&&et("scrollend",n);break;case"onClick":u!=null&&(n.onclick=_e);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
`+u.stack}}var Xt=Object.prototype.hasOwnProperty,Pt=e.unstable_scheduleCallback,Kt=e.unstable_cancelCallback,Yn=e.unstable_shouldYield,Nn=e.unstable_requestPaint,ct=e.unstable_now,It=e.unstable_getCurrentPriorityLevel,ue=e.unstable_ImmediatePriority,be=e.unstable_UserBlockingPriority,Oe=e.unstable_NormalPriority,Fe=e.unstable_LowPriority,Ze=e.unstable_IdlePriority,cn=e.log,Sn=e.unstable_setDisableYieldValue,Zt=null,At=null;function Jt(n){if(typeof cn=="function"&&Sn(n),At&&typeof At.setStrictMode=="function")try{At.setStrictMode(Zt,n)}catch{}}var ut=Math.clz32?Math.clz32:Ni,In=Math.log,un=Math.LN2;function Ni(n){return n>>>=0,n===0?32:31-(In(n)/un|0)|0}var nt=256,Xn=262144,On=4194304;function mn(n){var i=n&42;if(i!==0)return i;switch(n&-n){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return n&261888;case 262144:case 524288:case 1048576:case 2097152:return n&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return n&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return n}}function re(n,i,l){var u=n.pendingLanes;if(u===0)return 0;var x=0,v=n.suspendedLanes,T=n.pingedLanes;n=n.warmLanes;var G=u&134217727;return G!==0?(u=G&~v,u!==0?x=mn(u):(T&=G,T!==0?x=mn(T):l||(l=G&~n,l!==0&&(x=mn(l))))):(G=u&~v,G!==0?x=mn(G):T!==0?x=mn(T):l||(l=u&~n,l!==0&&(x=mn(l)))),x===0?0:i!==0&&i!==x&&(i&v)===0&&(v=x&-x,l=i&-i,v>=l||v===32&&(l&4194048)!==0)?i:x}function me(n,i){return(n.pendingLanes&~(n.suspendedLanes&~n.pingedLanes)&i)===0}function Ee(n,i){switch(n){case 1:case 2:case 4:case 8:case 64:return i+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Pe(){var n=On;return On<<=1,(On&62914560)===0&&(On=4194304),n}function St(n){for(var i=[],l=0;31>l;l++)i.push(n);return i}function gt(n,i){n.pendingLanes|=i,i!==268435456&&(n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0)}function Me(n,i,l,u,x,v){var T=n.pendingLanes;n.pendingLanes=l,n.suspendedLanes=0,n.pingedLanes=0,n.warmLanes=0,n.expiredLanes&=l,n.entangledLanes&=l,n.errorRecoveryDisabledLanes&=l,n.shellSuspendCounter=0;var G=n.entanglements,ne=n.expirationTimes,le=n.hiddenUpdates;for(l=T&~l;0<l;){var he=31-ut(l),ge=1<<he;G[he]=0,ne[he]=-1;var ce=le[he];if(ce!==null)for(le[he]=null,he=0;he<ce.length;he++){var de=ce[he];de!==null&&(de.lane&=-536870913)}l&=~ge}u!==0&&Se(n,u,0),v!==0&&x===0&&n.tag!==0&&(n.suspendedLanes|=v&~(T&~i))}function Se(n,i,l){n.pendingLanes|=i,n.suspendedLanes&=~i;var u=31-ut(i);n.entangledLanes|=i,n.entanglements[u]=n.entanglements[u]|1073741824|l&261930}function Ue(n,i){var l=n.entangledLanes|=i;for(n=n.entanglements;l;){var u=31-ut(l),x=1<<u;x&i|n[u]&i&&(n[u]|=i),l&=~x}}function Bt(n,i){var l=i&-i;return l=(l&42)!==0?1:Mt(l),(l&(n.suspendedLanes|i))!==0?0:l}function Mt(n){switch(n){case 2:n=1;break;case 8:n=4;break;case 32:n=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:n=128;break;case 268435456:n=134217728;break;default:n=0}return n}function yr(n){return n&=-n,2<n?8<n?(n&134217727)!==0?32:268435456:8:2}function Si(){var n=U.p;return n!==0?n:(n=window.event,n===void 0?32:F0(n.type))}function ki(n,i){var l=U.p;try{return U.p=n,i()}finally{U.p=l}}var lr=Math.random().toString(36).slice(2),Ut="__reactFiber$"+lr,pn="__reactProps$"+lr,vr="__reactContainer$"+lr,Ci="__reactEvents$"+lr,ga="__reactListeners$"+lr,Ti="__reactHandles$"+lr,ts="__reactResources$"+lr,Wr="__reactMarker$"+lr;function xa(n){delete n[Ut],delete n[pn],delete n[Ci],delete n[ga],delete n[Ti]}function bn(n){var i=n[Ut];if(i)return i;for(var l=n.parentNode;l;){if(i=l[vr]||l[Ut]){if(l=i.alternate,i.child!==null||l!==null&&l.child!==null)for(n=T0(n);n!==null;){if(l=n[Ut])return l;n=T0(n)}return i}n=l,l=n.parentNode}return null}function _r(n){if(n=n[Ut]||n[vr]){var i=n.tag;if(i===5||i===6||i===13||i===31||i===26||i===27||i===3)return n}return null}function wr(n){var i=n.tag;if(i===5||i===26||i===27||i===6)return n.stateNode;throw Error(a(33))}function Br(n){var i=n[ts];return i||(i=n[ts]={hoistableStyles:new Map,hoistableScripts:new Map}),i}function Ft(n){n[Wr]=!0}var ns=new Set,Jr={};function Er(n,i){ye(n,i),ye(n+"Capture",i)}function ye(n,i){for(Jr[n]=i,n=0;n<i.length;n++)ns.add(i[n])}var Le=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u2
Line truncated
|
||||
`).replace(WS,"")}function b0(n,i){return i=x0(i),x0(n)===i}function Et(n,i,l,u,x,v){switch(l){case"children":typeof u=="string"?i==="body"||i==="textarea"&&u===""||qe(n,u):(typeof u=="number"||typeof u=="bigint")&&i!=="body"&&qe(n,""+u);break;case"className":Rn(n,"class",u);break;case"tabIndex":Rn(n,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":Rn(n,l,u);break;case"style":xt(n,u,v);break;case"data":if(i!=="object"){Rn(n,"data",u);break}case"src":case"href":if(u===""&&(i!=="a"||l!=="href")){n.removeAttribute(l);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){n.removeAttribute(l);break}u=ji(""+u),n.setAttribute(l,u);break;case"action":case"formAction":if(typeof u=="function"){n.setAttribute(l,"javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')");break}else typeof v=="function"&&(l==="formAction"?(i!=="input"&&Et(n,i,"name",x.name,x,null),Et(n,i,"formEncType",x.formEncType,x,null),Et(n,i,"formMethod",x.formMethod,x,null),Et(n,i,"formTarget",x.formTarget,x,null)):(Et(n,i,"encType",x.encType,x,null),Et(n,i,"method",x.method,x,null),Et(n,i,"target",x.target,x,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){n.removeAttribute(l);break}u=ji(""+u),n.setAttribute(l,u);break;case"onClick":u!=null&&(n.onclick=_e);break;case"onScroll":u!=null&&et("scroll",n);break;case"onScrollEnd":u!=null&&et("scrollend",n);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(l=u.__html,l!=null){if(x.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"multiple":n.multiple=u&&typeof u!="function"&&typeof u!="symbol";break;case"muted":n.muted=u&&typeof u!="function"&&typeof u!="symbol";break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"defaultValue":case"defaultChecked":case"innerHTML":case"ref":break;case"autoFocus":break;case"xlinkHref":if(u==null||typeof u=="function"||typeof u=="boolean"||typeof u=="symbol"){n.removeAttribute("xlink:href");break}l=ji(""+u),n.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",l);break;case"contentEditable":case"spellCheck":case"draggable":case"value":case"autoReverse":case"externalResourcesRequired":case"focusable":case"preserveAlpha":u!=null&&typeof u!="function"&&typeof u!="symbol"?n.setAttribute(l,""+u):n.removeAttribute(l);break;case"inert":case"allowFullScreen":case"async":case"autoPlay":case"controls":case"default":case"defer":case"disabled":case"disablePictureInPicture":case"disableRemotePlayback":case"formNoValidate":case"hidden":case"loop":case"noModule":case"noValidate":case"open":case"playsInline":case"readOnly":case"required":case"reversed":case"scoped":case"seamless":case"itemScope":u&&typeof u!="function"&&typeof u!="symbol"?n.setAttribute(l,""):n.removeAttribute(l);break;case"capture":case"download":u===!0?n.setAttribute(l,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?n.setAttribute(l,u):n.removeAttribute(l);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?n.setAttribute(l,u):n.removeAttribute(l);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?n.removeAttribute(l):n.setAttribute(l,u);break;case"popover":et("beforetoggle",n),et("toggle",n),gn(n,"popover",u);break;case"xlinkActuate":kn(n,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":kn(n,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":kn(n,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":kn(n,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":kn(n,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":kn(n,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":kn(n,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":kn(n,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":kn(n,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":gn(n,"is",u);break;case"innerText":case"textContent":break;default:(!(2<l.length)||l[0]!=="o"&&l[0]!=="O"||l[1]!=="n"&&l[1]!=="N")&&(l=ei.get(l)||l,gn(n,l,u))}}function Xf(n,i,l,u,x,v){switch(l){case"style":xt(n,u,v);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(l=u.__html,l!=null){if(x.children!=null)throw Error(a(60));n.innerHTML=l}}break;case"children":typeof u=="string"?qe(n,u):(typeof u=="number"||typeof u=="bigint")&&qe(n,""+u);break;case"onScroll":u!=null&&et("scroll",n);break;case"onScrollEnd":u!=null&&et("scrollend",n);break;case"onClick":u!=null&&(n.onclick=_e);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"inne
Line truncated
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const I_=(...e)=>e.filter((t,r,a)=>!!t&&t.trim()!==""&&a.indexOf(t)===r).join(" ").trim();/**
|
||||
*/const L_=(...e)=>e.filter((t,r,a)=>!!t&&t.trim()!==""&&a.indexOf(t)===r).join(" ").trim();/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const qk=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/**
|
||||
*/const Uk=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Pk=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,a)=>a?a.toUpperCase():r.toLowerCase());/**
|
||||
*/const Hk=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,a)=>a?a.toUpperCase():r.toLowerCase());/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const cy=e=>{const t=Pk(e);return t.charAt(0).toUpperCase()+t.slice(1)};/**
|
||||
*/const ly=e=>{const t=Hk(e);return t.charAt(0).toUpperCase()+t.slice(1)};/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/var Fk={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
|
||||
*/var $k={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Gk=e=>{for(const t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0;return!1};/**
|
||||
*/const qk=e=>{for(const t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0;return!1};/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Vk=ee.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:a,className:s="",children:o,iconNode:c,...d},h)=>ee.createElement("svg",{ref:h,...Fk,width:t,height:t,stroke:e,strokeWidth:a?Number(r)*24/Number(t):r,className:I_("lucide",s),...!o&&!Gk(d)&&{"aria-hidden":"true"},...d},[...c.map(([f,p])=>ee.createElement(f,p)),...Array.isArray(o)?o:[o]]));/**
|
||||
*/const Pk=ee.forwardRef(({color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:a,className:s="",children:o,iconNode:c,...d},h)=>ee.createElement("svg",{ref:h,...$k,width:t,height:t,stroke:e,strokeWidth:a?Number(r)*24/Number(t):r,className:L_("lucide",s),...!o&&!qk(d)&&{"aria-hidden":"true"},...d},[...c.map(([f,p])=>ee.createElement(f,p)),...Array.isArray(o)?o:[o]]));/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Ce=(e,t)=>{const r=ee.forwardRef(({className:a,...s},o)=>ee.createElement(Vk,{ref:o,iconNode:t,className:I_(`lucide-${qk(cy(e))}`,`lucide-${e}`,a),...s}));return r.displayName=cy(e),r};/**
|
||||
*/const Te=(e,t)=>{const r=ee.forwardRef(({className:a,...s},o)=>ee.createElement(Pk,{ref:o,iconNode:t,className:L_(`lucide-${Uk(ly(e))}`,`lucide-${e}`,a),...s}));return r.displayName=ly(e),r};/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Yk=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],Ep=Ce("arrow-left",Yk);/**
|
||||
*/const Fk=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],wp=Te("arrow-left",Fk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Xk=[["path",{d:"M7 7h10v10",key:"1tivn9"}],["path",{d:"M7 17 17 7",key:"1vkiza"}]],B_=Ce("arrow-up-right",Xk);/**
|
||||
*/const Gk=[["path",{d:"M7 7h10v10",key:"1tivn9"}],["path",{d:"M7 17 17 7",key:"1vkiza"}]],z_=Te("arrow-up-right",Gk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Kk=[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]],Zk=Ce("arrow-up",Kk);/**
|
||||
*/const Vk=[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]],Yk=Te("arrow-up",Vk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Qk=[["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],U_=Ce("ban",Qk);/**
|
||||
*/const Xk=[["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],I_=Te("ban",Xk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Wk=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0",key:"vwvbt9"}],["path",{d:"M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742",key:"178tsu"}],["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05",key:"1hqiys"}]],Jk=Ce("bell-off",Wk);/**
|
||||
*/const Kk=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0",key:"vwvbt9"}],["path",{d:"M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742",key:"178tsu"}],["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05",key:"1hqiys"}]],Zk=Te("bell-off",Kk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const eC=[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]],Ro=Ce("bot",eC);/**
|
||||
*/const Qk=[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]],Oo=Te("bot",Qk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const tC=[["path",{d:"M12 18V5",key:"adv99a"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4",key:"1e3is1"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5",key:"1gqd8o"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77",key:"iwvgf7"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464",key:"efp6ie"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517",key:"1gq6am"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464",key:"k1g0md"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77",key:"q97ue3"}]],H_=Ce("brain",tC);/**
|
||||
*/const Wk=[["path",{d:"M12 18V5",key:"adv99a"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4",key:"1e3is1"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5",key:"1gqd8o"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77",key:"iwvgf7"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464",key:"efp6ie"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517",key:"1gq6am"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464",key:"k1g0md"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77",key:"q97ue3"}]],B_=Te("brain",Wk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const nC=[["path",{d:"M10 12h4",key:"a56b0p"}],["path",{d:"M10 8h4",key:"1sr2af"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3",key:"1rgiei"}],["path",{d:"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2",key:"secmi2"}],["path",{d:"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16",key:"16ra0t"}]],$_=Ce("building-2",nC);/**
|
||||
*/const Jk=[["path",{d:"M16 14v2.2l1.6 1",key:"fo4ql5"}],["path",{d:"M16 2v4",key:"4m81vk"}],["path",{d:"M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5",key:"1osxxc"}],["path",{d:"M3 10h5",key:"r794hk"}],["path",{d:"M8 2v4",key:"1cmpym"}],["circle",{cx:"16",cy:"16",r:"6",key:"qoo3c4"}]],eC=Te("calendar-clock",Jk);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const rC=[["path",{d:"M16 14v2.2l1.6 1",key:"fo4ql5"}],["path",{d:"M16 2v4",key:"4m81vk"}],["path",{d:"M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5",key:"1osxxc"}],["path",{d:"M3 10h5",key:"r794hk"}],["path",{d:"M8 2v4",key:"1cmpym"}],["circle",{cx:"16",cy:"16",r:"6",key:"qoo3c4"}]],iC=Ce("calendar-clock",rC);/**
|
||||
*/const tC=[["path",{d:"M18 6 7 17l-5-5",key:"116fxf"}],["path",{d:"m22 10-7.5 7.5L13 16",key:"ke71qq"}]],nC=Te("check-check",tC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const aC=[["path",{d:"M18 6 7 17l-5-5",key:"116fxf"}],["path",{d:"m22 10-7.5 7.5L13 16",key:"ke71qq"}]],sC=Ce("check-check",aC);/**
|
||||
*/const rC=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Vs=Te("check",rC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const lC=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Vs=Ce("check",lC);/**
|
||||
*/const iC=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],po=Te("chevron-down",iC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const oC=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],po=Ce("chevron-down",oC);/**
|
||||
*/const aC=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],sC=Te("chevron-right",aC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const cC=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],uC=Ce("chevron-right",cC);/**
|
||||
*/const lC=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],U_=Te("chevron-up",lC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const dC=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],q_=Ce("chevron-up",dC);/**
|
||||
*/const oC=[["path",{d:"m7 15 5 5 5-5",key:"1hf1tw"}],["path",{d:"m7 9 5-5 5 5",key:"sgt6xg"}]],cC=Te("chevrons-up-down",oC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const fC=[["path",{d:"m7 15 5 5 5-5",key:"1hf1tw"}],["path",{d:"m7 9 5-5 5 5",key:"sgt6xg"}]],hC=Ce("chevrons-up-down",fC);/**
|
||||
*/const uC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],Fu=Te("circle-alert",uC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const mC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],Gu=Ce("circle-alert",mC);/**
|
||||
*/const dC=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]],H_=Te("circle-check-big",dC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const pC=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]],P_=Ce("circle-check-big",pC);/**
|
||||
*/const fC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],Eu=Te("circle-check",fC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const gC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],Nu=Ce("circle-check",gC);/**
|
||||
*/const hC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],mC=Te("circle-dot",hC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const xC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],bC=Ce("circle-dot",xC);/**
|
||||
*/const pC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],gC=Te("circle-question-mark",pC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const yC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],vC=Ce("circle-question-mark",yC);/**
|
||||
*/const xC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9",key:"1dfufj"}]],bC=Te("circle-slash",xC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const _C=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9",key:"1dfufj"}]],wC=Ce("circle-slash",_C);/**
|
||||
*/const yC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],$_=Te("circle",yC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const EC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],F_=Ce("circle",EC);/**
|
||||
*/const vC=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1",key:"tgr4d6"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",key:"116196"}],["path",{d:"M12 11h4",key:"1jrz19"}],["path",{d:"M12 16h4",key:"n85exb"}],["path",{d:"M8 11h.01",key:"1dfujw"}],["path",{d:"M8 16h.01",key:"18s6g9"}]],q_=Te("clipboard-list",vC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const NC=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1",key:"tgr4d6"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",key:"116196"}],["path",{d:"M12 11h4",key:"1jrz19"}],["path",{d:"M12 16h4",key:"n85exb"}],["path",{d:"M8 11h.01",key:"1dfujw"}],["path",{d:"M8 16h.01",key:"18s6g9"}]],G_=Ce("clipboard-list",NC);/**
|
||||
*/const _C=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],P_=Te("clock",_C);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const SC=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],V_=Ce("clock",SC);/**
|
||||
*/const wC=[["path",{d:"m16 18 6-6-6-6",key:"eg8j8"}],["path",{d:"m8 6-6 6 6 6",key:"ppft3o"}]],EC=Te("code",wC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const kC=[["path",{d:"m16 18 6-6-6-6",key:"eg8j8"}],["path",{d:"m8 6-6 6 6 6",key:"ppft3o"}]],CC=Ce("code",kC);/**
|
||||
*/const NC=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],go=Te("copy",NC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const TC=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],go=Ce("copy",TC);/**
|
||||
*/const SC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]],Gu=Te("crosshair",SC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const AC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]],Vu=Ce("crosshair",AC);/**
|
||||
*/const kC=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],oy=Te("external-link",kC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const MC=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],uy=Ce("external-link",MC);/**
|
||||
*/const CC=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],TC=Te("eye",CC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const OC=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],RC=Ce("eye",OC);/**
|
||||
*/const AC=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],MC=Te("file-text",AC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const jC=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],DC=Ce("file-text",jC);/**
|
||||
*/const OC=[["path",{d:"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528",key:"1jaruq"}]],F_=Te("flag",OC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const LC=[["path",{d:"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528",key:"1jaruq"}]],Y_=Ce("flag",LC);/**
|
||||
*/const RC=[["circle",{cx:"18",cy:"18",r:"3",key:"1xkwt0"}],["circle",{cx:"6",cy:"6",r:"3",key:"1lh9wr"}],["path",{d:"M6 21V9a9 9 0 0 0 9 9",key:"7kw0sc"}]],jC=Te("git-merge",RC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const zC=[["circle",{cx:"18",cy:"18",r:"3",key:"1xkwt0"}],["circle",{cx:"6",cy:"6",r:"3",key:"1lh9wr"}],["path",{d:"M6 21V9a9 9 0 0 0 9 9",key:"7kw0sc"}]],IC=Ce("git-merge",zC);/**
|
||||
*/const DC=[["circle",{cx:"18",cy:"18",r:"3",key:"1xkwt0"}],["circle",{cx:"6",cy:"6",r:"3",key:"1lh9wr"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7",key:"1yeb86"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21",key:"rroup"}]],LC=Te("git-pull-request",DC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const BC=[["circle",{cx:"18",cy:"18",r:"3",key:"1xkwt0"}],["circle",{cx:"6",cy:"6",r:"3",key:"1lh9wr"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7",key:"1yeb86"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21",key:"rroup"}]],UC=Ce("git-pull-request",BC);/**
|
||||
*/const zC=[["path",{d:"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4",key:"tonef"}],["path",{d:"M9 18c-4.51 2-5-2-7-2",key:"9comsn"}]],IC=Te("github",zC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const HC=[["path",{d:"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4",key:"tonef"}],["path",{d:"M9 18c-4.51 2-5-2-7-2",key:"9comsn"}]],$C=Ce("github",HC);/**
|
||||
*/const BC=[["path",{d:"m22 13.29-3.33-10a.42.42 0 0 0-.14-.18.38.38 0 0 0-.22-.11.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18l-2.26 6.67H8.32L6.1 3.26a.42.42 0 0 0-.1-.18.38.38 0 0 0-.26-.08.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18L2 13.29a.74.74 0 0 0 .27.83L12 21l9.69-6.88a.71.71 0 0 0 .31-.83Z",key:"148pdi"}]],UC=Te("gitlab",BC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const qC=[["path",{d:"m22 13.29-3.33-10a.42.42 0 0 0-.14-.18.38.38 0 0 0-.22-.11.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18l-2.26 6.67H8.32L6.1 3.26a.42.42 0 0 0-.1-.18.38.38 0 0 0-.26-.08.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18L2 13.29a.74.74 0 0 0 .27.83L12 21l9.69-6.88a.71.71 0 0 0 .31-.83Z",key:"148pdi"}]],PC=Ce("gitlab",qC);/**
|
||||
*/const HC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]],G_=Te("globe",HC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const FC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]],X_=Ce("globe",FC);/**
|
||||
*/const $C=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]],Ys=Te("history",$C);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const GC=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]],Ys=Ce("history",GC);/**
|
||||
*/const qC=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]],PC=Te("image",qC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const VC=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]],YC=Ce("image",VC);/**
|
||||
*/const FC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],GC=Te("info",FC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const XC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],KC=Ce("info",XC);/**
|
||||
*/const VC=[["path",{d:"M13 5h8",key:"a7qcls"}],["path",{d:"M13 12h8",key:"h98zly"}],["path",{d:"M13 19h8",key:"c3s6r1"}],["path",{d:"m3 17 2 2 4-4",key:"1jhpwq"}],["rect",{x:"3",y:"4",width:"6",height:"6",rx:"1",key:"cif1o7"}]],YC=Te("list-todo",VC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const ZC=[["path",{d:"M13 5h8",key:"a7qcls"}],["path",{d:"M13 12h8",key:"h98zly"}],["path",{d:"M13 19h8",key:"c3s6r1"}],["path",{d:"m3 17 2 2 4-4",key:"1jhpwq"}],["rect",{x:"3",y:"4",width:"6",height:"6",rx:"1",key:"cif1o7"}]],QC=Ce("list-todo",ZC);/**
|
||||
*/const XC=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],Ps=Te("loader-circle",XC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const WC=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],Ps=Ce("loader-circle",WC);/**
|
||||
*/const KC=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]],ZC=Te("lock",KC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const JC=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]],eT=Ce("lock",JC);/**
|
||||
*/const QC=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],WC=Te("log-out",QC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const tT=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],nT=Ce("log-out",tT);/**
|
||||
*/const JC=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7",key:"132q7q"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2",key:"izxlao"}]],Ep=Te("mail",JC);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const rT=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7",key:"132q7q"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2",key:"izxlao"}]],Np=Ce("mail",rT);/**
|
||||
*/const eT=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719",key:"1sd12s"}]],bh=Te("message-circle",eT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const iT=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719",key:"1sd12s"}]],yh=Ce("message-circle",iT);/**
|
||||
*/const tT=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]],nT=Te("pencil",tT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const aT=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]],sT=Ce("pencil",aT);/**
|
||||
*/const rT=[["path",{d:"M12 22v-5",key:"1ega77"}],["path",{d:"M15 8V2",key:"18g5xt"}],["path",{d:"M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z",key:"1xoxul"}],["path",{d:"M9 8V2",key:"14iosj"}]],V_=Te("plug",rT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const lT=[["path",{d:"M12 22v-5",key:"1ega77"}],["path",{d:"M15 8V2",key:"18g5xt"}],["path",{d:"M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z",key:"1xoxul"}],["path",{d:"M9 8V2",key:"14iosj"}]],K_=Ce("plug",lT);/**
|
||||
*/const iT=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Y_=Te("plus",iT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const oT=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Z_=Ce("plus",oT);/**
|
||||
*/const aT=[["path",{d:"M19.07 4.93A10 10 0 0 0 6.99 3.34",key:"z3du51"}],["path",{d:"M4 6h.01",key:"oypzma"}],["path",{d:"M2.29 9.62A10 10 0 1 0 21.31 8.35",key:"qzzz0"}],["path",{d:"M16.24 7.76A6 6 0 1 0 8.23 16.67",key:"1yjesh"}],["path",{d:"M12 18h.01",key:"mhygvu"}],["path",{d:"M17.99 11.66A6 6 0 0 1 15.77 16.67",key:"1u2y91"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["path",{d:"m13.41 10.59 5.66-5.66",key:"mhq4k0"}]],sT=Te("radar",aT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const cT=[["path",{d:"M19.07 4.93A10 10 0 0 0 6.99 3.34",key:"z3du51"}],["path",{d:"M4 6h.01",key:"oypzma"}],["path",{d:"M2.29 9.62A10 10 0 1 0 21.31 8.35",key:"qzzz0"}],["path",{d:"M16.24 7.76A6 6 0 1 0 8.23 16.67",key:"1yjesh"}],["path",{d:"M12 18h.01",key:"mhygvu"}],["path",{d:"M17.99 11.66A6 6 0 0 1 15.77 16.67",key:"1u2y91"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}],["path",{d:"m13.41 10.59 5.66-5.66",key:"mhq4k0"}]],uT=Ce("radar",cT);/**
|
||||
*/const lT=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],oT=Te("refresh-cw",lT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const dT=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],fT=Ce("refresh-cw",dT);/**
|
||||
*/const cT=[["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z",key:"m3kijz"}],["path",{d:"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z",key:"1fmvmk"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0",key:"1f8sc4"}],["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5",key:"qeys4"}]],uT=Te("rocket",cT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const hT=[["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z",key:"m3kijz"}],["path",{d:"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z",key:"1fmvmk"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0",key:"1f8sc4"}],["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5",key:"qeys4"}]],Q_=Ce("rocket",hT);/**
|
||||
*/const dT=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]],fT=Te("rotate-ccw",dT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const mT=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]],pT=Ce("rotate-ccw",mT);/**
|
||||
*/const hT=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]],mT=Te("save",hT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const gT=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]],xT=Ce("save",gT);/**
|
||||
*/const pT=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],gT=Te("search",pT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const bT=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],yT=Ce("search",bT);/**
|
||||
*/const xT=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"M12 8v4",key:"1got3b"}],["path",{d:"M12 16h.01",key:"1drbdi"}]],bT=Te("shield-alert",xT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const vT=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"M12 8v4",key:"1got3b"}],["path",{d:"M12 16h.01",key:"1drbdi"}]],_T=Ce("shield-alert",vT);/**
|
||||
*/const yT=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],X_=Te("shield-check",yT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const wT=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],W_=Ce("shield-check",wT);/**
|
||||
*/const vT=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]],_T=Te("shield",vT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const ET=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]],NT=Ce("shield",ET);/**
|
||||
*/const wT=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",key:"1s2grr"}],["path",{d:"M20 2v4",key:"1rf3ol"}],["path",{d:"M22 4h-4",key:"gwowj6"}],["circle",{cx:"4",cy:"20",r:"2",key:"6kqj1y"}]],Pm=Te("sparkles",wT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const ST=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",key:"1s2grr"}],["path",{d:"M20 2v4",key:"1rf3ol"}],["path",{d:"M22 4h-4",key:"gwowj6"}],["circle",{cx:"4",cy:"20",r:"2",key:"6kqj1y"}]],Fm=Ce("sparkles",ST);/**
|
||||
*/const ET=[["path",{d:"M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",key:"1dfntj"}],["path",{d:"M15 3v5a1 1 0 0 0 1 1h5",key:"6s6qgf"}]],NT=Te("sticky-note",ET);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const kT=[["path",{d:"M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z",key:"1dfntj"}],["path",{d:"M15 3v5a1 1 0 0 0 1 1h5",key:"6s6qgf"}]],CT=Ce("sticky-note",kT);/**
|
||||
*/const ST=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],K_=Te("terminal",ST);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const TT=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],J_=Ce("terminal",TT);/**
|
||||
*/const kT=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],CT=Te("trash-2",kT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const AT=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],MT=Ce("trash-2",AT);/**
|
||||
*/const TT=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],Np=Te("triangle-alert",TT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const OT=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],Sp=Ce("triangle-alert",OT);/**
|
||||
*/const AT=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744",key:"16gr8j"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]],MT=Te("users",AT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const RT=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744",key:"16gr8j"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]],jT=Ce("users",RT);/**
|
||||
*/const OT=[["path",{d:"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72",key:"ul74o6"}],["path",{d:"m14 7 3 3",key:"1r5n42"}],["path",{d:"M5 6v4",key:"ilb8ba"}],["path",{d:"M19 14v4",key:"blhpug"}],["path",{d:"M10 2v2",key:"7u0qdc"}],["path",{d:"M7 8H3",key:"zfb6yr"}],["path",{d:"M21 16h-4",key:"1cnmox"}],["path",{d:"M11 3H9",key:"1obp7u"}]],RT=Te("wand-sparkles",OT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const DT=[["path",{d:"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72",key:"ul74o6"}],["path",{d:"m14 7 3 3",key:"1r5n42"}],["path",{d:"M5 6v4",key:"ilb8ba"}],["path",{d:"M19 14v4",key:"blhpug"}],["path",{d:"M10 2v2",key:"7u0qdc"}],["path",{d:"M7 8H3",key:"zfb6yr"}],["path",{d:"M21 16h-4",key:"1cnmox"}],["path",{d:"M11 3H9",key:"1obp7u"}]],LT=Ce("wand-sparkles",DT);/**
|
||||
*/const jT=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]],Fm=Te("wrench",jT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const zT=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]],Gm=Ce("wrench",zT);/**
|
||||
*/const DT=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Sp=Te("x",DT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const IT=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],kp=Ce("x",IT);/**
|
||||
* @license lucide-react v0.563.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const BT=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],UT=Ce("zap",BT),HT={open:{label:"Open",color:"bg-red-500/10 text-red-400 border-red-500/20",dotColor:"bg-red-500",description:"Newly discovered, awaiting triage"},in_progress:{label:"In Progress",color:"bg-blue-500/10 text-blue-400 border-blue-500/20",dotColor:"bg-blue-500",description:"Someone is working on this"},snoozed:{label:"Snoozed",color:"bg-purple-500/10 text-purple-400 border-purple-500/20",dotColor:"bg-purple-500",description:"Temporarily hidden until a follow-up date"},fixed:{label:"Fixed",color:"bg-emerald-500/10 text-emerald-400 border-emerald-500/20",dotColor:"bg-emerald-500",description:"This vulnerability has been fixed"},ignored:{label:"Ignored",color:"bg-gray-500/10 text-gray-400 border-gray-500/20",dotColor:"bg-gray-500",description:"Acknowledged but accepted"}},$T={trivial:{label:"Trivial",color:"bg-emerald-500/10 text-emerald-400 border-emerald-500/20"},low:{label:"Low",color:"bg-blue-500/10 text-blue-400 border-blue-500/20"},medium:{label:"Medium",color:"bg-yellow-500/10 text-yellow-400 border-yellow-500/20"},high:{label:"High",color:"bg-orange-500/10 text-orange-400 border-orange-500/20"}},ew={critical:"bg-red-500/20 text-red-500 border-red-500/30",high:"bg-orange-500/20 text-orange-500 border-orange-500/30",medium:"bg-yellow-500/20 text-yellow-500 border-yellow-500/30",low:"bg-blue-500/20 text-blue-500 border-blue-500/30"};function Jc(e){return e.original_severity!=null&&e.original_severity!==e.severity}const qT={js:"javascript",ts:"typescript",tsx:"typescript",jsx:"javascript",py:"python",rb:"ruby",go:"go",rs:"rust",java:"java",php:"php",cs:"csharp",cpp:"cpp",c:"c",sh:"bash",bash:"bash",sql:"sql",html:"html",css:"css",json:"json",yaml:"yaml",yml:"yaml",xml:"xml"};function PT(e){var r;if(!e)return null;const t=(r=e.split(".").pop())==null?void 0:r.toLowerCase();return t&&qT[t]||null}function Cp(e){switch(e){case"critical":return"bg-red-500";case"high":return"bg-orange-500";case"medium":return"bg-yellow-500";default:return"bg-blue-500"}}async function Tp(e){try{await navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea");t.value=e,t.style.position="absolute",t.style.left="-9999px",document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}}const Yu="https://app.strix.ai/api/auth/signup",Ap="https://strix.ai/demo",FT="https://strix.ai/pricing",GT="ref=oss_viewer&utm_source=oss_viewer&utm_medium=local_viewer&utm_campaign=oss_viewer";function _i(e,t){const r=e.includes("?")?"&":"?";return`${e}${r}${GT}&utm_content=${encodeURIComponent(t)}`}function Mr(e,t={}){try{const r={event:e};for(const[s,o]of Object.entries(t))o!==void 0&&(r[s]=o);const a=JSON.stringify(r);typeof navigator<"u"&&navigator.sendBeacon?navigator.sendBeacon("/api/event",a):fetch("/api/event",{method:"POST",body:a,keepalive:!0})}catch{}}function yr(e,t){Mr("cta_clicked",{cta:e,surface:t})}function tw(e){var t,r,a="";if(typeof e=="string"||typeof e=="number")a+=e;else if(typeof e=="object")if(Array.isArray(e)){var s=e.length;for(t=0;t<s;t++)e[t]&&(r=tw(e[t]))&&(a&&(a+=" "),a+=r)}else for(r in e)e[r]&&(a&&(a+=" "),a+=r);return a}function VT(){for(var e,t,r=0,a="",s=arguments.length;r<s;r++)(e=arguments[r])&&(t=tw(e))&&(a&&(a+=" "),a+=t);return a}const YT=(e,t)=>{const r=new Array(e.length+t.length);for(let a=0;a<e.length;a++)r[a]=e[a];for(let a=0;a<t.length;a++)r[e.length+a]=t[a];return r},XT=(e,t)=>({classGroupId:e,validator:t}),nw=(e=new Map,t=null,r)=>({nextPart:e,validators:t,classGroupId:r}),Su="-",dy=[],KT="arbitrary..",ZT=e=>{const t=WT(e),{conflictingClassGroups:r,conflictingClassGroupModifiers:a}=e;return{getClassGroupId:c=>{if(c.startsWith("[")&&c.endsWith("]"))return QT(c);const d=c.split(Su),h=d[0]===""&&d.length>1?1:0;return rw(d,h,t)},getConflictingClassGroupIds:(c,d)=>{if(d){const h=a[c],f=r[c];return h?f?YT(f,h):h:f||dy}return r[c]||dy}}},rw=(e,t,r)=>{if(e.length-t===0)return r.classGroupId;const s=e[t],o=r.nextPart.get(s);if(o){const f=rw(e,t+1,o);if(f)return f}const c=r.validators;if(c===null)return;const d=t===0?e.join(Su):e.slice(t).join(Su),h=c.length;for(let f=0;f<h;f++){const p=c[f];if(p.validator(d))return p.classGroupId}},QT=e=>e.slice(1,-1).indexOf(":")===-1?void 0:(()=>{const t=e.slice(1,-1),r=t.indexOf(":"),a=t.slice(0,r);return a?KT+a:void 0})(),WT=e=>{const{theme:t,classGroups:r}=e;return JT(r,t)},JT=(e,t)=>{const r=nw();for(const a in e){const s=e[a];Mp(s,r,a,t)}return r},Mp=(e,t,r,a)=>{const s=e.length;for(let o=0;o<s;o++){const c=e[o];eA(c,t,r,a)}},eA=(e,t,r,a)=>{if(typeof e=="string"){tA(e,t,r);return}if(typeof e=="function"){nA(e,t,r,a);return}rA(e,t,r,a)},tA=(e,t,r)=>{const a=e===""?t:iw(t,e);a.classGroupId=r},nA=(e,t,r,a)=>{if(iA(e)){Mp(e(a),t,r,a);return}t.validators===null&&(t.validators=[]),t.validators.push(XT(r,e))},rA=(eLine truncated
|
||||
`,f="/",p="*",g="",b="comment",y="declaration";function _(S,w){if(typeof S!="string")throw new TypeError("First argument must be a string");if(!S)return[];w=w||{};var C=1,E=1;function A(k){var $=k.match(t);$&&(C+=$.length);var O=k.lastIndexOf(h);E=~O?k.length-O:E+k.length}function B(){var k={line:C,column:E};return function($){return $.position=new R(k),Y(),$}}function R(k){this.start=k,this.end={line:C,column:E},this.source=w.source}R.prototype.content=S;function H(k){var $=new Error(w.source+":"+C+":"+E+": "+k);if($.reason=k,$.filename=w.source,$.line=C,$.column=E,$.source=S,!w.silent)throw $}function z(k){var $=k.exec(S);if($){var O=$[0];return A(O),S=S.slice(O.length),$}}function Y(){z(r)}function j(k){var $;for(k=k||[];$=I();)$!==!1&&k.push($);return k}function I(){var k=B();if(!(f!=S.charAt(0)||p!=S.charAt(1))){for(var $=2;g!=S.charAt($)&&(p!=S.charAt($)||f!=S.charAt($+1));)++$;if($+=2,g===S.charAt($-1))return H("End of comment missing");var O=S.slice(2,$-2);return E+=2,A(O),S=S.slice($),E+=2,k({type:b,comment:O})}}function Z(){var k=B(),$=z(a);if($){if(I(),!z(s))return H("property missing ':'");var O=z(o),U=k({type:y,property:N($[0].replace(e,g)),value:O?N(O[0].replace(e,g)):g});return z(c),U}}function P(){var k=[];j(k);for(var $;$=Z();)$!==!1&&(k.push($),j(k));return k}return Y(),P()}function N(S){return S?S.replace(d,g):g}return wh=_,wh}var wy;function iM(){if(wy)return zs;wy=1;var e=zs&&zs.__importDefault||function(a){return a&&a.__esModule?a:{default:a}};Object.defineProperty(zs,"__esModule",{value:!0}),zs.default=r;const t=e(rM());function r(a,s){let o=null;if(!a||typeof a!="string")return o;const c=(0,t.default)(a),d=typeof s=="function";return c.forEach(h=>{if(h.type!=="declaration")return;const{property:f,value:p}=h;d?s(f,p,h):p&&(o=o||{},o[f]=p)}),o}return zs}var Ql={},Ey;function aM(){if(Ey)return Ql;Ey=1,Object.defineProperty(Ql,"__esModule",{value:!0}),Ql.camelCase=void 0;var e=/^--[a-zA-Z0-9_-]+$/,t=/-([a-z])/g,r=/^[^-]+$/,a=/^-(webkit|moz|ms|o|khtml)-/,s=/^-(ms)-/,o=function(f){return!f||r.test(f)||e.test(f)},c=function(f,p){return p.toUpperCase()},d=function(f,p){return"".concat(p,"-")},h=function(f,p){return p===void 0&&(p={}),o(f)?f:(f=f.toLowerCase(),p.reactCompat?f=f.replace(s,d):f=f.replace(a,d),f.replace(t,c))};return Ql.camelCase=h,Ql}var Wl,Ny;function sM(){if(Ny)return Wl;Ny=1;var e=Wl&&Wl.__importDefault||function(s){return s&&s.__esModule?s:{default:s}},t=e(iM()),r=aM();function a(s,o){var c={};return!s||typeof s!="string"||(0,t.default)(s,function(d,h){d&&h&&(c[(0,r.camelCase)(d,o)]=h)}),c}return a.default=a,Wl=a,Wl}var lM=sM();const oM=Mo(lM),Ew=Nw("end"),Dp=Nw("start");function Nw(e){return t;function t(r){const a=r&&r.position&&r.position[e]||{};if(typeof a.line=="number"&&a.line>0&&typeof a.column=="number"&&a.column>0)return{line:a.line,column:a.column,offset:typeof a.offset=="number"&&a.offset>-1?a.offset:void 0}}}function cM(e){const t=Dp(e),r=Ew(e);if(t&&r)return{start:t,end:r}}function oo(e){return!e||typeof e!="object"?"":"position"in e||"type"in e?Sy(e.position):"start"in e||"end"in e?Sy(e):"line"in e||"column"in e?Qm(e):""}function Qm(e){return ky(e&&e.line)+":"+ky(e&&e.column)}function Sy(e){return Qm(e&&e.start)+"-"+Qm(e&&e.end)}function ky(e){return e&&typeof e=="number"?e:1}class Mn extends Error{constructor(t,r,a){super(),typeof r=="string"&&(a=r,r=void 0);let s="",o={},c=!1;if(r&&("line"in r&&"column"in r?o={place:r}:"start"in r&&"end"in r?o={place:r}:"type"in r?o={ancestors:[r],place:r.position}:o={...r}),typeof t=="string"?s=t:!o.cause&&t&&(c=!0,s=t.message,o.cause=t),!o.ruleId&&!o.source&&typeof a=="string"){const h=a.indexOf(":");h===-1?o.ruleId=a:(o.source=a.slice(0,h),o.ruleId=a.slice(h+1))}if(!o.place&&o.ancestors&&o.ancestors){const h=o.ancestors[o.ancestors.length-1];h&&(o.place=h.position)}const d=o.place&&"start"in o.place?o.place.start:o.place;this.ancestors=o.ancestors||void 0,this.cause=o.cause||void 0,this.column=d?d.column:void 0,this.fatal=void 0,this.file="",this.message=s,this.line=d?d.line:void 0,this.name=oo(o.place)||"1:1",this.place=o.place||void 0,this.reason=this.message,this.ruleId=o.ruleId||void 0,this.source=o.source||void 0,this.stack=c&&o.cause&&typeof o.cause.stack=="string"?o.cause.stack:"",this.actual=void 0,this.expected=void 0,this.note=void 0,this.url=void 0}}Mn.prototype.file="";Mn.prototype.name="";Mn.prototype.reason="";Mn.prototype.message="";Mn.prototype.stack="";Mn.prototype.column=void 0;Mn.prototype.line=void 0;Mn.prototype.ancestors=void 0;Mn.prototype.cause=void 0;Mn.prototype.fatal=void 0;Mn.prototype.place=void 0;Mn.prototype.ruleId=void 0;Mn.prototype.source=void 0;const Lp={}.hasOwnProperty,uM=new Map,dM=/[A-Z]/g,fM=new Set(["table","tbody","thead","tfoot","tr"]),hM=new Set(["td","th"]),Sw="https://github.com/syntax-tree/hast-util-to-jsx-runtime";function mM(e,t){if(!t||t.Fragment===void 0)throw new TypeError("Expected `Fragment` in options");const r=t.filePath||void 0;let a;if(t.development){if(typeof t.j
Line truncated
|
||||
*/const LT=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],zT=Te("zap",LT),IT={open:{label:"Open",color:"bg-red-500/10 text-red-400 border-red-500/20",dotColor:"bg-red-500",description:"Newly discovered, awaiting triage"},in_progress:{label:"In Progress",color:"bg-blue-500/10 text-blue-400 border-blue-500/20",dotColor:"bg-blue-500",description:"Someone is working on this"},snoozed:{label:"Snoozed",color:"bg-purple-500/10 text-purple-400 border-purple-500/20",dotColor:"bg-purple-500",description:"Temporarily hidden until a follow-up date"},fixed:{label:"Fixed",color:"bg-emerald-500/10 text-emerald-400 border-emerald-500/20",dotColor:"bg-emerald-500",description:"This vulnerability has been fixed"},ignored:{label:"Ignored",color:"bg-gray-500/10 text-gray-400 border-gray-500/20",dotColor:"bg-gray-500",description:"Acknowledged but accepted"}},BT={trivial:{label:"Trivial",color:"bg-emerald-500/10 text-emerald-400 border-emerald-500/20"},low:{label:"Low",color:"bg-blue-500/10 text-blue-400 border-blue-500/20"},medium:{label:"Medium",color:"bg-yellow-500/10 text-yellow-400 border-yellow-500/20"},high:{label:"High",color:"bg-orange-500/10 text-orange-400 border-orange-500/20"}},Z_={critical:"bg-red-500/20 text-red-500 border-red-500/30",high:"bg-orange-500/20 text-orange-500 border-orange-500/30",medium:"bg-yellow-500/20 text-yellow-500 border-yellow-500/30",low:"bg-blue-500/20 text-blue-500 border-blue-500/30"};function Wc(e){return e.original_severity!=null&&e.original_severity!==e.severity}const UT={js:"javascript",ts:"typescript",tsx:"typescript",jsx:"javascript",py:"python",rb:"ruby",go:"go",rs:"rust",java:"java",php:"php",cs:"csharp",cpp:"cpp",c:"c",sh:"bash",bash:"bash",sql:"sql",html:"html",css:"css",json:"json",yaml:"yaml",yml:"yaml",xml:"xml"};function HT(e){var r;if(!e)return null;const t=(r=e.split(".").pop())==null?void 0:r.toLowerCase();return t&&UT[t]||null}function kp(e){switch(e){case"critical":return"bg-red-500";case"high":return"bg-orange-500";case"medium":return"bg-yellow-500";default:return"bg-blue-500"}}async function Cp(e){try{await navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea");t.value=e,t.style.position="absolute",t.style.left="-9999px",document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}}const Vu="https://app.strix.ai/api/auth/signup",$T="https://strix.ai/pricing",qT="ref=oss_viewer&utm_source=oss_viewer&utm_medium=local_viewer&utm_campaign=oss_viewer";function ha(e,t){const r=e.includes("?")?"&":"?";return`${e}${r}${qT}&utm_content=${encodeURIComponent(t)}`}function Ar(e,t={}){try{const r={event:e};for(const[s,o]of Object.entries(t))o!==void 0&&(r[s]=o);const a=JSON.stringify(r);typeof navigator<"u"&&navigator.sendBeacon?navigator.sendBeacon("/api/event",a):fetch("/api/event",{method:"POST",body:a,keepalive:!0})}catch{}}function jr(e,t){Ar("cta_clicked",{cta:e,surface:t})}function Q_(e){var t,r,a="";if(typeof e=="string"||typeof e=="number")a+=e;else if(typeof e=="object")if(Array.isArray(e)){var s=e.length;for(t=0;t<s;t++)e[t]&&(r=Q_(e[t]))&&(a&&(a+=" "),a+=r)}else for(r in e)e[r]&&(a&&(a+=" "),a+=r);return a}function PT(){for(var e,t,r=0,a="",s=arguments.length;r<s;r++)(e=arguments[r])&&(t=Q_(e))&&(a&&(a+=" "),a+=t);return a}const FT=(e,t)=>{const r=new Array(e.length+t.length);for(let a=0;a<e.length;a++)r[a]=e[a];for(let a=0;a<t.length;a++)r[e.length+a]=t[a];return r},GT=(e,t)=>({classGroupId:e,validator:t}),W_=(e=new Map,t=null,r)=>({nextPart:e,validators:t,classGroupId:r}),Nu="-",cy=[],VT="arbitrary..",YT=e=>{const t=KT(e),{conflictingClassGroups:r,conflictingClassGroupModifiers:a}=e;return{getClassGroupId:c=>{if(c.startsWith("[")&&c.endsWith("]"))return XT(c);const d=c.split(Nu),h=d[0]===""&&d.length>1?1:0;return J_(d,h,t)},getConflictingClassGroupIds:(c,d)=>{if(d){const h=a[c],f=r[c];return h?f?FT(f,h):h:f||cy}return r[c]||cy}}},J_=(e,t,r)=>{if(e.length-t===0)return r.classGroupId;const s=e[t],o=r.nextPart.get(s);if(o){const f=J_(e,t+1,o);if(f)return f}const c=r.validators;if(c===null)return;const d=t===0?e.join(Nu):e.slice(t).join(Nu),h=c.length;for(let f=0;f<h;f++){const p=c[f];if(p.validator(d))return p.classGroupId}},XT=e=>e.slice(1,-1).indexOf(":")===-1?void 0:(()=>{const t=e.slice(1,-1),r=t.indexOf(":"),a=t.slice(0,r);return a?VT+a:void 0})(),KT=e=>{const{theme:t,classGroups:r}=e;return ZT(r,t)},ZT=(e,t)=>{const r=W_();for(const a in e){const s=e[a];Tp(s,r,a,t)}return r},Tp=(e,t,r,a)=>{const s=e.length;for(let o=0;o<s;o++){const c=e[o];QT(c,t,r,a)}},QT=(e,t,r,a)=>{if(typeof e=="string"){WT(e,t,r);return}if(typeof e=="function"){JT(e,t,r,a);return}eA(e,t,r,a)},WT=(e,t,r)=>{const a=e===""?t:ew(t,e);a.classGroupId=r},JT=(e,t,r,a)=>{if(tA(e)){Tp(e(a),t,r,a);return}t.validators===null&&(t.validators=[]),t.validators.push(GT(r,e))},eA=(e,t,r,a)=>{const s=Object.enLine truncated
|
||||
`,f="/",p="*",g="",b="comment",y="declaration";function _(S,w){if(typeof S!="string")throw new TypeError("First argument must be a string");if(!S)return[];w=w||{};var C=1,E=1;function A(k){var $=k.match(t);$&&(C+=$.length);var O=k.lastIndexOf(h);E=~O?k.length-O:E+k.length}function B(){var k={line:C,column:E};return function($){return $.position=new R(k),Y(),$}}function R(k){this.start=k,this.end={line:C,column:E},this.source=w.source}R.prototype.content=S;function H(k){var $=new Error(w.source+":"+C+":"+E+": "+k);if($.reason=k,$.filename=w.source,$.line=C,$.column=E,$.source=S,!w.silent)throw $}function z(k){var $=k.exec(S);if($){var O=$[0];return A(O),S=S.slice(O.length),$}}function Y(){z(r)}function j(k){var $;for(k=k||[];$=I();)$!==!1&&k.push($);return k}function I(){var k=B();if(!(f!=S.charAt(0)||p!=S.charAt(1))){for(var $=2;g!=S.charAt($)&&(p!=S.charAt($)||f!=S.charAt($+1));)++$;if($+=2,g===S.charAt($-1))return H("End of comment missing");var O=S.slice(2,$-2);return E+=2,A(O),S=S.slice($),E+=2,k({type:b,comment:O})}}function Z(){var k=B(),$=z(a);if($){if(I(),!z(s))return H("property missing ':'");var O=z(o),U=k({type:y,property:N($[0].replace(e,g)),value:O?N(O[0].replace(e,g)):g});return z(c),U}}function P(){var k=[];j(k);for(var $;$=Z();)$!==!1&&(k.push($),j(k));return k}return Y(),P()}function N(S){return S?S.replace(d,g):g}return _h=_,_h}var vy;function tM(){if(vy)return zs;vy=1;var e=zs&&zs.__importDefault||function(a){return a&&a.__esModule?a:{default:a}};Object.defineProperty(zs,"__esModule",{value:!0}),zs.default=r;const t=e(eM());function r(a,s){let o=null;if(!a||typeof a!="string")return o;const c=(0,t.default)(a),d=typeof s=="function";return c.forEach(h=>{if(h.type!=="declaration")return;const{property:f,value:p}=h;d?s(f,p,h):p&&(o=o||{},o[f]=p)}),o}return zs}var Ql={},_y;function nM(){if(_y)return Ql;_y=1,Object.defineProperty(Ql,"__esModule",{value:!0}),Ql.camelCase=void 0;var e=/^--[a-zA-Z0-9_-]+$/,t=/-([a-z])/g,r=/^[^-]+$/,a=/^-(webkit|moz|ms|o|khtml)-/,s=/^-(ms)-/,o=function(f){return!f||r.test(f)||e.test(f)},c=function(f,p){return p.toUpperCase()},d=function(f,p){return"".concat(p,"-")},h=function(f,p){return p===void 0&&(p={}),o(f)?f:(f=f.toLowerCase(),p.reactCompat?f=f.replace(s,d):f=f.replace(a,d),f.replace(t,c))};return Ql.camelCase=h,Ql}var Wl,wy;function rM(){if(wy)return Wl;wy=1;var e=Wl&&Wl.__importDefault||function(s){return s&&s.__esModule?s:{default:s}},t=e(tM()),r=nM();function a(s,o){var c={};return!s||typeof s!="string"||(0,t.default)(s,function(d,h){d&&h&&(c[(0,r.camelCase)(d,o)]=h)}),c}return a.default=a,Wl=a,Wl}var iM=rM();const aM=Ao(iM),yw=vw("end"),Rp=vw("start");function vw(e){return t;function t(r){const a=r&&r.position&&r.position[e]||{};if(typeof a.line=="number"&&a.line>0&&typeof a.column=="number"&&a.column>0)return{line:a.line,column:a.column,offset:typeof a.offset=="number"&&a.offset>-1?a.offset:void 0}}}function sM(e){const t=Rp(e),r=yw(e);if(t&&r)return{start:t,end:r}}function oo(e){return!e||typeof e!="object"?"":"position"in e||"type"in e?Ey(e.position):"start"in e||"end"in e?Ey(e):"line"in e||"column"in e?Zm(e):""}function Zm(e){return Ny(e&&e.line)+":"+Ny(e&&e.column)}function Ey(e){return Zm(e&&e.start)+"-"+Zm(e&&e.end)}function Ny(e){return e&&typeof e=="number"?e:1}class Mn extends Error{constructor(t,r,a){super(),typeof r=="string"&&(a=r,r=void 0);let s="",o={},c=!1;if(r&&("line"in r&&"column"in r?o={place:r}:"start"in r&&"end"in r?o={place:r}:"type"in r?o={ancestors:[r],place:r.position}:o={...r}),typeof t=="string"?s=t:!o.cause&&t&&(c=!0,s=t.message,o.cause=t),!o.ruleId&&!o.source&&typeof a=="string"){const h=a.indexOf(":");h===-1?o.ruleId=a:(o.source=a.slice(0,h),o.ruleId=a.slice(h+1))}if(!o.place&&o.ancestors&&o.ancestors){const h=o.ancestors[o.ancestors.length-1];h&&(o.place=h.position)}const d=o.place&&"start"in o.place?o.place.start:o.place;this.ancestors=o.ancestors||void 0,this.cause=o.cause||void 0,this.column=d?d.column:void 0,this.fatal=void 0,this.file="",this.message=s,this.line=d?d.line:void 0,this.name=oo(o.place)||"1:1",this.place=o.place||void 0,this.reason=this.message,this.ruleId=o.ruleId||void 0,this.source=o.source||void 0,this.stack=c&&o.cause&&typeof o.cause.stack=="string"?o.cause.stack:"",this.actual=void 0,this.expected=void 0,this.note=void 0,this.url=void 0}}Mn.prototype.file="";Mn.prototype.name="";Mn.prototype.reason="";Mn.prototype.message="";Mn.prototype.stack="";Mn.prototype.column=void 0;Mn.prototype.line=void 0;Mn.prototype.ancestors=void 0;Mn.prototype.cause=void 0;Mn.prototype.fatal=void 0;Mn.prototype.place=void 0;Mn.prototype.ruleId=void 0;Mn.prototype.source=void 0;const jp={}.hasOwnProperty,lM=new Map,oM=/[A-Z]/g,cM=new Set(["table","tbody","thead","tfoot","tr"]),uM=new Set(["td","th"]),_w="https://github.com/syntax-tree/hast-util-to-jsx-runtime";function dM(e,t){if(!t||t.Fragment===void 0)throw new TypeError("Expected `Fragment` in options");const r=t.filePath||void 0;let a;if(t.development){if(typeof t.j
Line truncated
|
||||
`;break}case-3:{c=`\r
|
||||
`;break}case-2:{c=t?" ":" ";break}case-1:{if(!t&&s)continue;c=" ";break}default:c=String.fromCharCode(o)}s=o===-2,a.push(c)}return a.join("")}function wO(e){const a={constructs:Mw([bO,...(e||{}).extensions||[]]),content:s(IM),defined:[],document:s(UM),flow:s(rO),lazy:{},string:s(sO),text:s(lO)};return a;function s(o){return c;function c(d){return yO(a,o,d)}}}function EO(e){for(;!Lw(e););return e}const zy=/[\0\t\n\r]/g;function NO(){let e=1,t="",r=!0,a;return s;function s(o,c,d){const h=[];let f,p,g,b,y;for(o=t+(typeof o=="string"?o.toString():new TextDecoder(c||void 0).decode(o)),g=0,t="",r&&(o.charCodeAt(0)===65279&&g++,r=void 0);g<o.length;){if(zy.lastIndex=g,f=zy.exec(o),b=f&&f.index!==void 0?f.index:o.length,y=o.charCodeAt(b),!f){t=o.slice(g);break}if(y===10&&g===b&&a)h.push(-3),a=void 0;else switch(a&&(h.push(-5),a=void 0),g<b&&(h.push(o.slice(g,b)),e+=b-g),y){case 0:{h.push(65533),e++;break}case 9:{for(p=Math.ceil(e/4)*4,h.push(-2);e++<p;)h.push(-1);break}case 10:{h.push(-4),e=1;break}default:a=!0,e=1}g=b+1}return d&&(a&&h.push(-5),t&&h.push(t),h.push(null)),h}}const SO=/\\([!-/:-@[-`{-~])|&(#(?:\d{1,7}|x[\da-f]{1,6})|[\da-z]{1,31});/gi;function kO(e){return e.replace(SO,CO)}function CO(e,t,r){if(t)return t;if(r.charCodeAt(0)===35){const s=r.charCodeAt(1),o=s===120||s===88;return Ow(r.slice(o?2:1),o?16:10)}return Up(r)||e}const $w={}.hasOwnProperty;function TO(e,t,r){return t&&typeof t=="object"&&(r=t,t=void 0),AO(r)(EO(wO(r).document().write(NO()(e,t,!0))))}function AO(e){const t={transforms:[],canContainEols:["emphasis","fragment","heading","paragraph","strong"],enter:{autolink:o(Xt),autolinkProtocol:Y,autolinkEmail:Y,atxHeading:o(De),blockQuote:o(oe),characterEscape:Y,characterReference:Y,codeFenced:o(fe),codeFencedFenceInfo:c,codeFencedFenceMeta:c,codeIndented:o(fe,c),codeText:o(xe,c),codeTextData:Y,data:Y,codeFlowValue:Y,definition:o(we),definitionDestinationString:c,definitionLabelString:c,definitionTitleString:c,emphasis:o(Ne),hardBreakEscape:o($e),hardBreakTrailing:o($e),htmlFlow:o(st,c),htmlFlowData:Y,htmlText:o(st,c),htmlTextData:Y,image:o(Rt),label:c,link:o(Xt),listItem:o(Kt),listItemValue:b,listOrdered:o(Ft,g),listUnordered:o(Ft),paragraph:o(Yn),reference:D,referenceString:c,resourceDestinationString:c,resourceTitleString:c,setextHeading:o(De),strong:o(Nn),thematicBreak:o(It)},exit:{atxHeading:h(),atxHeadingSequence:B,autolink:h(),autolinkEmail:te,autolinkProtocol:W,blockQuote:h(),characterEscapeValue:j,characterReferenceMarkerHexadecimal:q,characterReferenceMarkerNumeric:q,characterReferenceValue:Q,characterReference:J,codeFenced:h(S),codeFencedFence:N,codeFencedFenceInfo:y,codeFencedFenceMeta:_,codeFlowValue:j,codeIndented:h(w),codeText:h($),codeTextData:j,data:j,definition:h(),definitionDestinationString:A,definitionLabelString:C,definitionTitleString:E,emphasis:h(),hardBreakEscape:h(Z),hardBreakTrailing:h(Z),htmlFlow:h(P),htmlFlowData:j,htmlText:h(k),htmlTextData:j,image:h(U),label:X,labelText:K,lineEnding:I,link:h(O),listItem:h(),listOrdered:h(),listUnordered:h(),paragraph:h(),referenceString:V,resourceDestinationString:M,resourceTitleString:L,resource:F,setextHeading:h(z),setextHeadingLineSequence:H,setextHeadingText:R,strong:h(),thematicBreak:h()}};qw(t,(e||{}).mdastExtensions||[]);const r={};return a;function a(ue){let be={type:"root",children:[]};const Oe={stack:[be],tokenStack:[],config:t,enter:d,exit:f,buffer:c,resume:p,data:r},Fe=[];let Ze=-1;for(;++Ze<ue.length;)if(ue[Ze][1].type==="listOrdered"||ue[Ze][1].type==="listUnordered")if(ue[Ze][0]==="enter")Fe.push(Ze);else{const cn=Fe.pop();Ze=s(ue,cn,Ze)}for(Ze=-1;++Ze<ue.length;){const cn=t[ue[Ze][0]];$w.call(cn,ue[Ze][1].type)&&cn[ue[Ze][1].type].call(Object.assign({sliceSerialize:ue[Ze][2].sliceSerialize},Oe),ue[Ze][1])}if(Oe.tokenStack.length>0){const cn=Oe.tokenStack[Oe.tokenStack.length-1];(cn[1]||Iy).call(Oe,void 0,cn[0])}for(be.position={start:aa(ue.length>0?ue[0][1].start:{line:1,column:1,offset:0}),end:aa(ue.length>0?ue[ue.length-2][1].end:{line:1,column:1,offset:0})},Ze=-1;++Ze<t.transforms.length;)be=t.transforms[Ze](be)||be;return be}function s(ue,be,Oe){let Fe=be-1,Ze=-1,cn=!1,Sn,Zt,At,Jt;for(;++Fe<=Oe;){const ut=ue[Fe];switch(ut[1].type){case"listUnordered":case"listOrdered":case"blockQuote":{ut[0]==="enter"?Ze++:Ze--,Jt=void 0;break}case"lineEndingBlank":{ut[0]==="enter"&&(Sn&&!Jt&&!Ze&&!At&&(At=Fe),Jt=void 0);break}case"linePrefix":case"listItemValue":case"listItemMarker":case"listItemPrefix":case"listItemPrefixWhitespace":break;default:Jt=void 0}if(!Ze&&ut[0]==="enter"&&ut[1].type==="listItemPrefix"||Ze===-1&&ut[0]==="exit"&&(ut[1].type==="listUnordered"||ut[1].type==="listOrdered")){if(Sn){let In=Fe;for(Zt=void 0;In--;){const un=ue[In];if(un[1].type==="lineEnding"||un[1].type==="lineEndingBlank"){if(un[0]==="exit")continue;Zt&&(ue[Zt][1].type="lineEndingBlank",cn=!0),un[1].type="lineEnding",Zt=In}else if(!(un[1].type==="linePrefix"||un[1].type==="blockQuotePrefix"||un[1].type==="blockQuotePrefixWhitespace"||un[Line truncated
|
||||
`}]}function DO(e,t){const r=t.value?t.value+`
|
||||
`:"",a={},s=t.lang?t.lang.split(/\s+/):[];s.length>0&&(a.className=["language-"+s[0]]);let o={type:"element",tagName:"code",properties:a,children:[{type:"text",value:r}]};return t.meta&&(o.data={meta:t.meta}),e.patch(t,o),o=e.applyData(t,o),o={type:"element",tagName:"pre",properties:{},children:[o]},e.patch(t,o),o}function LO(e,t){const r={type:"element",tagName:"del",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function zO(e,t){const r={type:"element",tagName:"em",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function IO(e,t){const r=typeof e.options.clobberPrefix=="string"?e.options.clobberPrefix:"user-content-",a=String(t.identifier).toUpperCase(),s=rl(a.toLowerCase()),o=e.footnoteOrder.indexOf(a);let c,d=e.footnoteCounts.get(a);d===void 0?(d=0,e.footnoteOrder.push(a),c=e.footnoteOrder.length):c=o+1,d+=1,e.footnoteCounts.set(a,d);const h={type:"element",tagName:"a",properties:{href:"#"+r+"fn-"+s,id:r+"fnref-"+s+(d>1?"-"+d:""),dataFootnoteRef:!0,ariaDescribedBy:["footnote-label"]},children:[{type:"text",value:String(c)}]};e.patch(t,h);const f={type:"element",tagName:"sup",properties:{},children:[h]};return e.patch(t,f),e.applyData(t,f)}function BO(e,t){const r={type:"element",tagName:"h"+t.depth,properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function UO(e,t){if(e.options.allowDangerousHtml){const r={type:"raw",value:t.value};return e.patch(t,r),e.applyData(t,r)}}function Pw(e,t){const r=t.referenceType;let a="]";if(r==="collapsed"?a+="[]":r==="full"&&(a+="["+(t.label||t.identifier)+"]"),t.type==="imageReference")return[{type:"text",value:"!["+t.alt+a}];const s=e.all(t),o=s[0];o&&o.type==="text"?o.value="["+o.value:s.unshift({type:"text",value:"["});const c=s[s.length-1];return c&&c.type==="text"?c.value+=a:s.push({type:"text",value:a}),s}function HO(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return Pw(e,t);const s={src:rl(a.url||""),alt:t.alt};a.title!==null&&a.title!==void 0&&(s.title=a.title);const o={type:"element",tagName:"img",properties:s,children:[]};return e.patch(t,o),e.applyData(t,o)}function $O(e,t){const r={src:rl(t.url)};t.alt!==null&&t.alt!==void 0&&(r.alt=t.alt),t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"img",properties:r,children:[]};return e.patch(t,a),e.applyData(t,a)}function qO(e,t){const r={type:"text",value:t.value.replace(/\r?\n|\r/g," ")};e.patch(t,r);const a={type:"element",tagName:"code",properties:{},children:[r]};return e.patch(t,a),e.applyData(t,a)}function PO(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return Pw(e,t);const s={href:rl(a.url||"")};a.title!==null&&a.title!==void 0&&(s.title=a.title);const o={type:"element",tagName:"a",properties:s,children:e.all(t)};return e.patch(t,o),e.applyData(t,o)}function FO(e,t){const r={href:rl(t.url)};t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"a",properties:r,children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function GO(e,t,r){const a=e.all(t),s=r?VO(r):Fw(t),o={},c=[];if(typeof t.checked=="boolean"){const p=a[0];let g;p&&p.type==="element"&&p.tagName==="p"?g=p:(g={type:"element",tagName:"p",properties:{},children:[]},a.unshift(g)),g.children.length>0&&g.children.unshift({type:"text",value:" "}),g.children.unshift({type:"element",tagName:"input",properties:{type:"checkbox",checked:t.checked,disabled:!0},children:[]}),o.className=["task-list-item"]}let d=-1;for(;++d<a.length;){const p=a[d];(s||d!==0||p.type!=="element"||p.tagName!=="p")&&c.push({type:"text",value:`
|
||||
`;break}case-2:{c=t?" ":" ";break}case-1:{if(!t&&s)continue;c=" ";break}default:c=String.fromCharCode(o)}s=o===-2,a.push(c)}return a.join("")}function yO(e){const a={constructs:kw([pO,...(e||{}).extensions||[]]),content:s(DM),defined:[],document:s(zM),flow:s(eO),lazy:{},string:s(rO),text:s(iO)};return a;function s(o){return c;function c(d){return gO(a,o,d)}}}function vO(e){for(;!Ow(e););return e}const Dy=/[\0\t\n\r]/g;function _O(){let e=1,t="",r=!0,a;return s;function s(o,c,d){const h=[];let f,p,g,b,y;for(o=t+(typeof o=="string"?o.toString():new TextDecoder(c||void 0).decode(o)),g=0,t="",r&&(o.charCodeAt(0)===65279&&g++,r=void 0);g<o.length;){if(Dy.lastIndex=g,f=Dy.exec(o),b=f&&f.index!==void 0?f.index:o.length,y=o.charCodeAt(b),!f){t=o.slice(g);break}if(y===10&&g===b&&a)h.push(-3),a=void 0;else switch(a&&(h.push(-5),a=void 0),g<b&&(h.push(o.slice(g,b)),e+=b-g),y){case 0:{h.push(65533),e++;break}case 9:{for(p=Math.ceil(e/4)*4,h.push(-2);e++<p;)h.push(-1);break}case 10:{h.push(-4),e=1;break}default:a=!0,e=1}g=b+1}return d&&(a&&h.push(-5),t&&h.push(t),h.push(null)),h}}const wO=/\\([!-/:-@[-`{-~])|&(#(?:\d{1,7}|x[\da-f]{1,6})|[\da-z]{1,31});/gi;function EO(e){return e.replace(wO,NO)}function NO(e,t,r){if(t)return t;if(r.charCodeAt(0)===35){const s=r.charCodeAt(1),o=s===120||s===88;return Cw(r.slice(o?2:1),o?16:10)}return Ip(r)||e}const Iw={}.hasOwnProperty;function SO(e,t,r){return t&&typeof t=="object"&&(r=t,t=void 0),kO(r)(vO(yO(r).document().write(_O()(e,t,!0))))}function kO(e){const t={transforms:[],canContainEols:["emphasis","fragment","heading","paragraph","strong"],enter:{autolink:o(Xt),autolinkProtocol:Y,autolinkEmail:Y,atxHeading:o(De),blockQuote:o(oe),characterEscape:Y,characterReference:Y,codeFenced:o(fe),codeFencedFenceInfo:c,codeFencedFenceMeta:c,codeIndented:o(fe,c),codeText:o(xe,c),codeTextData:Y,data:Y,codeFlowValue:Y,definition:o(we),definitionDestinationString:c,definitionLabelString:c,definitionTitleString:c,emphasis:o(Ne),hardBreakEscape:o($e),hardBreakTrailing:o($e),htmlFlow:o(st,c),htmlFlowData:Y,htmlText:o(st,c),htmlTextData:Y,image:o(Rt),label:c,link:o(Xt),listItem:o(Kt),listItemValue:b,listOrdered:o(Pt,g),listUnordered:o(Pt),paragraph:o(Yn),reference:D,referenceString:c,resourceDestinationString:c,resourceTitleString:c,setextHeading:o(De),strong:o(Nn),thematicBreak:o(It)},exit:{atxHeading:h(),atxHeadingSequence:B,autolink:h(),autolinkEmail:te,autolinkProtocol:W,blockQuote:h(),characterEscapeValue:j,characterReferenceMarkerHexadecimal:q,characterReferenceMarkerNumeric:q,characterReferenceValue:Q,characterReference:J,codeFenced:h(S),codeFencedFence:N,codeFencedFenceInfo:y,codeFencedFenceMeta:_,codeFlowValue:j,codeIndented:h(w),codeText:h($),codeTextData:j,data:j,definition:h(),definitionDestinationString:A,definitionLabelString:C,definitionTitleString:E,emphasis:h(),hardBreakEscape:h(Z),hardBreakTrailing:h(Z),htmlFlow:h(P),htmlFlowData:j,htmlText:h(k),htmlTextData:j,image:h(U),label:X,labelText:K,lineEnding:I,link:h(O),listItem:h(),listOrdered:h(),listUnordered:h(),paragraph:h(),referenceString:V,resourceDestinationString:M,resourceTitleString:L,resource:F,setextHeading:h(z),setextHeadingLineSequence:H,setextHeadingText:R,strong:h(),thematicBreak:h()}};Bw(t,(e||{}).mdastExtensions||[]);const r={};return a;function a(ue){let be={type:"root",children:[]};const Oe={stack:[be],tokenStack:[],config:t,enter:d,exit:f,buffer:c,resume:p,data:r},Fe=[];let Ze=-1;for(;++Ze<ue.length;)if(ue[Ze][1].type==="listOrdered"||ue[Ze][1].type==="listUnordered")if(ue[Ze][0]==="enter")Fe.push(Ze);else{const cn=Fe.pop();Ze=s(ue,cn,Ze)}for(Ze=-1;++Ze<ue.length;){const cn=t[ue[Ze][0]];Iw.call(cn,ue[Ze][1].type)&&cn[ue[Ze][1].type].call(Object.assign({sliceSerialize:ue[Ze][2].sliceSerialize},Oe),ue[Ze][1])}if(Oe.tokenStack.length>0){const cn=Oe.tokenStack[Oe.tokenStack.length-1];(cn[1]||Ly).call(Oe,void 0,cn[0])}for(be.position={start:ia(ue.length>0?ue[0][1].start:{line:1,column:1,offset:0}),end:ia(ue.length>0?ue[ue.length-2][1].end:{line:1,column:1,offset:0})},Ze=-1;++Ze<t.transforms.length;)be=t.transforms[Ze](be)||be;return be}function s(ue,be,Oe){let Fe=be-1,Ze=-1,cn=!1,Sn,Zt,At,Jt;for(;++Fe<=Oe;){const ut=ue[Fe];switch(ut[1].type){case"listUnordered":case"listOrdered":case"blockQuote":{ut[0]==="enter"?Ze++:Ze--,Jt=void 0;break}case"lineEndingBlank":{ut[0]==="enter"&&(Sn&&!Jt&&!Ze&&!At&&(At=Fe),Jt=void 0);break}case"linePrefix":case"listItemValue":case"listItemMarker":case"listItemPrefix":case"listItemPrefixWhitespace":break;default:Jt=void 0}if(!Ze&&ut[0]==="enter"&&ut[1].type==="listItemPrefix"||Ze===-1&&ut[0]==="exit"&&(ut[1].type==="listUnordered"||ut[1].type==="listOrdered")){if(Sn){let In=Fe;for(Zt=void 0;In--;){const un=ue[In];if(un[1].type==="lineEnding"||un[1].type==="lineEndingBlank"){if(un[0]==="exit")continue;Zt&&(ue[Zt][1].type="lineEndingBlank",cn=!0),un[1].type="lineEnding",Zt=In}else if(!(un[1].type==="linePrefix"||un[1].type==="blockQuotePrefix"||un[1].type==="blockQuotePrefixWhitespace"||un[Line truncated
|
||||
`}]}function OO(e,t){const r=t.value?t.value+`
|
||||
`:"",a={},s=t.lang?t.lang.split(/\s+/):[];s.length>0&&(a.className=["language-"+s[0]]);let o={type:"element",tagName:"code",properties:a,children:[{type:"text",value:r}]};return t.meta&&(o.data={meta:t.meta}),e.patch(t,o),o=e.applyData(t,o),o={type:"element",tagName:"pre",properties:{},children:[o]},e.patch(t,o),o}function RO(e,t){const r={type:"element",tagName:"del",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function jO(e,t){const r={type:"element",tagName:"em",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function DO(e,t){const r=typeof e.options.clobberPrefix=="string"?e.options.clobberPrefix:"user-content-",a=String(t.identifier).toUpperCase(),s=rl(a.toLowerCase()),o=e.footnoteOrder.indexOf(a);let c,d=e.footnoteCounts.get(a);d===void 0?(d=0,e.footnoteOrder.push(a),c=e.footnoteOrder.length):c=o+1,d+=1,e.footnoteCounts.set(a,d);const h={type:"element",tagName:"a",properties:{href:"#"+r+"fn-"+s,id:r+"fnref-"+s+(d>1?"-"+d:""),dataFootnoteRef:!0,ariaDescribedBy:["footnote-label"]},children:[{type:"text",value:String(c)}]};e.patch(t,h);const f={type:"element",tagName:"sup",properties:{},children:[h]};return e.patch(t,f),e.applyData(t,f)}function LO(e,t){const r={type:"element",tagName:"h"+t.depth,properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function zO(e,t){if(e.options.allowDangerousHtml){const r={type:"raw",value:t.value};return e.patch(t,r),e.applyData(t,r)}}function Uw(e,t){const r=t.referenceType;let a="]";if(r==="collapsed"?a+="[]":r==="full"&&(a+="["+(t.label||t.identifier)+"]"),t.type==="imageReference")return[{type:"text",value:"!["+t.alt+a}];const s=e.all(t),o=s[0];o&&o.type==="text"?o.value="["+o.value:s.unshift({type:"text",value:"["});const c=s[s.length-1];return c&&c.type==="text"?c.value+=a:s.push({type:"text",value:a}),s}function IO(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return Uw(e,t);const s={src:rl(a.url||""),alt:t.alt};a.title!==null&&a.title!==void 0&&(s.title=a.title);const o={type:"element",tagName:"img",properties:s,children:[]};return e.patch(t,o),e.applyData(t,o)}function BO(e,t){const r={src:rl(t.url)};t.alt!==null&&t.alt!==void 0&&(r.alt=t.alt),t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"img",properties:r,children:[]};return e.patch(t,a),e.applyData(t,a)}function UO(e,t){const r={type:"text",value:t.value.replace(/\r?\n|\r/g," ")};e.patch(t,r);const a={type:"element",tagName:"code",properties:{},children:[r]};return e.patch(t,a),e.applyData(t,a)}function HO(e,t){const r=String(t.identifier).toUpperCase(),a=e.definitionById.get(r);if(!a)return Uw(e,t);const s={href:rl(a.url||"")};a.title!==null&&a.title!==void 0&&(s.title=a.title);const o={type:"element",tagName:"a",properties:s,children:e.all(t)};return e.patch(t,o),e.applyData(t,o)}function $O(e,t){const r={href:rl(t.url)};t.title!==null&&t.title!==void 0&&(r.title=t.title);const a={type:"element",tagName:"a",properties:r,children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function qO(e,t,r){const a=e.all(t),s=r?PO(r):Hw(t),o={},c=[];if(typeof t.checked=="boolean"){const p=a[0];let g;p&&p.type==="element"&&p.tagName==="p"?g=p:(g={type:"element",tagName:"p",properties:{},children:[]},a.unshift(g)),g.children.length>0&&g.children.unshift({type:"text",value:" "}),g.children.unshift({type:"element",tagName:"input",properties:{type:"checkbox",checked:t.checked,disabled:!0},children:[]}),o.className=["task-list-item"]}let d=-1;for(;++d<a.length;){const p=a[d];(s||d!==0||p.type!=="element"||p.tagName!=="p")&&c.push({type:"text",value:`
|
||||
`}),p.type==="element"&&p.tagName==="p"&&!s?c.push(...p.children):c.push(p)}const h=a[a.length-1];h&&(s||h.type!=="element"||h.tagName!=="p")&&c.push({type:"text",value:`
|
||||
`});const f={type:"element",tagName:"li",properties:o,children:c};return e.patch(t,f),e.applyData(t,f)}function VO(e){let t=!1;if(e.type==="list"){t=e.spread||!1;const r=e.children;let a=-1;for(;!t&&++a<r.length;)t=Fw(r[a])}return t}function Fw(e){const t=e.spread;return t??e.children.length>1}function YO(e,t){const r={},a=e.all(t);let s=-1;for(typeof t.start=="number"&&t.start!==1&&(r.start=t.start);++s<a.length;){const c=a[s];if(c.type==="element"&&c.tagName==="li"&&c.properties&&Array.isArray(c.properties.className)&&c.properties.className.includes("task-list-item")){r.className=["contains-task-list"];break}}const o={type:"element",tagName:t.ordered?"ol":"ul",properties:r,children:e.wrap(a,!0)};return e.patch(t,o),e.applyData(t,o)}function XO(e,t){const r={type:"element",tagName:"p",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function KO(e,t){const r={type:"root",children:e.wrap(e.all(t))};return e.patch(t,r),e.applyData(t,r)}function ZO(e,t){const r={type:"element",tagName:"strong",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function QO(e,t){const r=e.all(t),a=r.shift(),s=[];if(a){const c={type:"element",tagName:"thead",properties:{},children:e.wrap([a],!0)};e.patch(t.children[0],c),s.push(c)}if(r.length>0){const c={type:"element",tagName:"tbody",properties:{},children:e.wrap(r,!0)},d=Dp(t.children[1]),h=Ew(t.children[t.children.length-1]);d&&h&&(c.position={start:d,end:h}),s.push(c)}const o={type:"element",tagName:"table",properties:{},children:e.wrap(s,!0)};return e.patch(t,o),e.applyData(t,o)}function WO(e,t,r){const a=r?r.children:void 0,o=(a?a.indexOf(t):1)===0?"th":"td",c=r&&r.type==="table"?r.align:void 0,d=c?c.length:t.children.length;let h=-1;const f=[];for(;++h<d;){const g=t.children[h],b={},y=c?c[h]:void 0;y&&(b.align=y);let _={type:"element",tagName:o,properties:b,children:[]};g&&(_.children=e.all(g),e.patch(g,_),_=e.applyData(g,_)),f.push(_)}const p={type:"element",tagName:"tr",properties:{},children:e.wrap(f,!0)};return e.patch(t,p),e.applyData(t,p)}function JO(e,t){const r={type:"element",tagName:"td",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}const By=9,Uy=32;function eR(e){const t=String(e),r=/\r?\n|\r/g;let a=r.exec(t),s=0;const o=[];for(;a;)o.push(Hy(t.slice(s,a.index),s>0,!0),a[0]),s=a.index+a[0].length,a=r.exec(t);return o.push(Hy(t.slice(s),s>0,!1)),o.join("")}function Hy(e,t,r){let a=0,s=e.length;if(t){let o=e.codePointAt(a);for(;o===By||o===Uy;)a++,o=e.codePointAt(a)}if(r){let o=e.codePointAt(s-1);for(;o===By||o===Uy;)s--,o=e.codePointAt(s-1)}return s>a?e.slice(a,s):""}function tR(e,t){const r={type:"text",value:eR(String(t.value))};return e.patch(t,r),e.applyData(t,r)}function nR(e,t){const r={type:"element",tagName:"hr",properties:{},children:[]};return e.patch(t,r),e.applyData(t,r)}const rR={blockquote:RO,break:jO,code:DO,delete:LO,emphasis:zO,footnoteReference:IO,heading:BO,html:UO,imageReference:HO,image:$O,inlineCode:qO,linkReference:PO,link:FO,listItem:GO,list:YO,paragraph:XO,root:KO,strong:ZO,table:QO,tableCell:JO,tableRow:WO,text:tR,thematicBreak:nR,toml:nu,yaml:nu,definition:nu,footnoteDefinition:nu};function nu(){}const Gw=-1,Zu=0,uo=1,Cu=2,$p=3,qp=4,Pp=5,Fp=6,Vw=7,Yw=8,Xw=typeof self=="object"?self:globalThis,$y=(e,t)=>{switch(e){case"Function":case"SharedWorker":case"Worker":case"eval":case"setInterval":case"setTimeout":throw new TypeError("unable to deserialize "+e)}return new Xw[e](t)},iR=(e,t)=>{const r=(s,o)=>(e.set(o,s),s),a=s=>{if(e.has(s))return e.get(s);const[o,c]=t[s];switch(o){case Zu:case Gw:return r(c,s);case uo:{const d=r([],s);for(const h of c)d.push(a(h));return d}case Cu:{const d=r({},s);for(const[h,f]of c)d[a(h)]=a(f);return d}case $p:return r(new Date(c),s);case qp:{const{source:d,flags:h}=c;return r(new RegExp(d,h),s)}case Pp:{const d=r(new Map,s);for(const[h,f]of c)d.set(a(h),a(f));return d}case Fp:{const d=r(new Set,s);for(const h of c)d.add(a(h));return d}case Vw:{const{name:d,message:h}=c;return r(typeof Xw[d]=="function"?$y(d,h):new Error(h),s)}case Yw:return r(BigInt(c),s);case"BigInt":return r(Object(BigInt(c)),s);case"ArrayBuffer":return r(new Uint8Array(c).buffer,c);case"DataView":{const{buffer:d}=new Uint8Array(c);return r(new DataView(d),c)}}return r($y(o,c),s)};return a},qy=e=>iR(new Map,e)(0),Ua="",{toString:aR}={},{keys:sR}=Object,eo=e=>{const t=typeof e;if(t!=="object"||!e)return[Zu,t];const r=aR.call(e).slice(8,-1);switch(r){case"Array":return[uo,Ua];case"Object":return[Cu,Ua];case"Date":return[$p,Ua];case"RegExp":return[qp,Ua];case"Map":return[Pp,Ua];case"Set":return[Fp,Ua];case"DataView":return[uo,r]}return r.includes("Array")?[uo,r]:e instanceof Error?[Vw,e.name||"Error"]:[Cu,r]},ru=([e,t])=>e===Zu&&(t==="function"||t==="symbol"),lR=(e,t,r,a)=>{const s=(c,d)=>{const h=a.push(c)-1;return r.set(d,h),h},o=c=>{if(r.has(c))return r.get(c);let[d,h]=eo(c);switch(d){case Zu:{let p=c;switch(h){case"bigint":d=Yw,p=c.toString();break;case"function":case"symbol":
Line truncated
|
||||
`});const f={type:"element",tagName:"li",properties:o,children:c};return e.patch(t,f),e.applyData(t,f)}function PO(e){let t=!1;if(e.type==="list"){t=e.spread||!1;const r=e.children;let a=-1;for(;!t&&++a<r.length;)t=Hw(r[a])}return t}function Hw(e){const t=e.spread;return t??e.children.length>1}function FO(e,t){const r={},a=e.all(t);let s=-1;for(typeof t.start=="number"&&t.start!==1&&(r.start=t.start);++s<a.length;){const c=a[s];if(c.type==="element"&&c.tagName==="li"&&c.properties&&Array.isArray(c.properties.className)&&c.properties.className.includes("task-list-item")){r.className=["contains-task-list"];break}}const o={type:"element",tagName:t.ordered?"ol":"ul",properties:r,children:e.wrap(a,!0)};return e.patch(t,o),e.applyData(t,o)}function GO(e,t){const r={type:"element",tagName:"p",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function VO(e,t){const r={type:"root",children:e.wrap(e.all(t))};return e.patch(t,r),e.applyData(t,r)}function YO(e,t){const r={type:"element",tagName:"strong",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}function XO(e,t){const r=e.all(t),a=r.shift(),s=[];if(a){const c={type:"element",tagName:"thead",properties:{},children:e.wrap([a],!0)};e.patch(t.children[0],c),s.push(c)}if(r.length>0){const c={type:"element",tagName:"tbody",properties:{},children:e.wrap(r,!0)},d=Rp(t.children[1]),h=yw(t.children[t.children.length-1]);d&&h&&(c.position={start:d,end:h}),s.push(c)}const o={type:"element",tagName:"table",properties:{},children:e.wrap(s,!0)};return e.patch(t,o),e.applyData(t,o)}function KO(e,t,r){const a=r?r.children:void 0,o=(a?a.indexOf(t):1)===0?"th":"td",c=r&&r.type==="table"?r.align:void 0,d=c?c.length:t.children.length;let h=-1;const f=[];for(;++h<d;){const g=t.children[h],b={},y=c?c[h]:void 0;y&&(b.align=y);let _={type:"element",tagName:o,properties:b,children:[]};g&&(_.children=e.all(g),e.patch(g,_),_=e.applyData(g,_)),f.push(_)}const p={type:"element",tagName:"tr",properties:{},children:e.wrap(f,!0)};return e.patch(t,p),e.applyData(t,p)}function ZO(e,t){const r={type:"element",tagName:"td",properties:{},children:e.all(t)};return e.patch(t,r),e.applyData(t,r)}const zy=9,Iy=32;function QO(e){const t=String(e),r=/\r?\n|\r/g;let a=r.exec(t),s=0;const o=[];for(;a;)o.push(By(t.slice(s,a.index),s>0,!0),a[0]),s=a.index+a[0].length,a=r.exec(t);return o.push(By(t.slice(s),s>0,!1)),o.join("")}function By(e,t,r){let a=0,s=e.length;if(t){let o=e.codePointAt(a);for(;o===zy||o===Iy;)a++,o=e.codePointAt(a)}if(r){let o=e.codePointAt(s-1);for(;o===zy||o===Iy;)s--,o=e.codePointAt(s-1)}return s>a?e.slice(a,s):""}function WO(e,t){const r={type:"text",value:QO(String(t.value))};return e.patch(t,r),e.applyData(t,r)}function JO(e,t){const r={type:"element",tagName:"hr",properties:{},children:[]};return e.patch(t,r),e.applyData(t,r)}const eR={blockquote:AO,break:MO,code:OO,delete:RO,emphasis:jO,footnoteReference:DO,heading:LO,html:zO,imageReference:IO,image:BO,inlineCode:UO,linkReference:HO,link:$O,listItem:qO,list:FO,paragraph:GO,root:VO,strong:YO,table:XO,tableCell:ZO,tableRow:KO,text:WO,thematicBreak:JO,toml:tu,yaml:tu,definition:tu,footnoteDefinition:tu};function tu(){}const $w=-1,Ku=0,uo=1,ku=2,Up=3,Hp=4,$p=5,qp=6,qw=7,Pw=8,Fw=typeof self=="object"?self:globalThis,Uy=(e,t)=>{switch(e){case"Function":case"SharedWorker":case"Worker":case"eval":case"setInterval":case"setTimeout":throw new TypeError("unable to deserialize "+e)}return new Fw[e](t)},tR=(e,t)=>{const r=(s,o)=>(e.set(o,s),s),a=s=>{if(e.has(s))return e.get(s);const[o,c]=t[s];switch(o){case Ku:case $w:return r(c,s);case uo:{const d=r([],s);for(const h of c)d.push(a(h));return d}case ku:{const d=r({},s);for(const[h,f]of c)d[a(h)]=a(f);return d}case Up:return r(new Date(c),s);case Hp:{const{source:d,flags:h}=c;return r(new RegExp(d,h),s)}case $p:{const d=r(new Map,s);for(const[h,f]of c)d.set(a(h),a(f));return d}case qp:{const d=r(new Set,s);for(const h of c)d.add(a(h));return d}case qw:{const{name:d,message:h}=c;return r(typeof Fw[d]=="function"?Uy(d,h):new Error(h),s)}case Pw:return r(BigInt(c),s);case"BigInt":return r(Object(BigInt(c)),s);case"ArrayBuffer":return r(new Uint8Array(c).buffer,c);case"DataView":{const{buffer:d}=new Uint8Array(c);return r(new DataView(d),c)}}return r(Uy(o,c),s)};return a},Hy=e=>tR(new Map,e)(0),Ua="",{toString:nR}={},{keys:rR}=Object,eo=e=>{const t=typeof e;if(t!=="object"||!e)return[Ku,t];const r=nR.call(e).slice(8,-1);switch(r){case"Array":return[uo,Ua];case"Object":return[ku,Ua];case"Date":return[Up,Ua];case"RegExp":return[Hp,Ua];case"Map":return[$p,Ua];case"Set":return[qp,Ua];case"DataView":return[uo,r]}return r.includes("Array")?[uo,r]:e instanceof Error?[qw,e.name||"Error"]:[ku,r]},nu=([e,t])=>e===Ku&&(t==="function"||t==="symbol"),iR=(e,t,r,a)=>{const s=(c,d)=>{const h=a.push(c)-1;return r.set(d,h),h},o=c=>{if(r.has(c))return r.get(c);let[d,h]=eo(c);switch(d){case Ku:{let p=c;switch(h){case"bigint":d=Pw,p=c.toString();break;case"function":case"symbol":
Line truncated
|
||||
`},{type:"element",tagName:"ol",properties:{},children:e.wrap(d,!0)},{type:"text",value:`
|
||||
`}]}}const Qu=(function(e){if(e==null)return mR;if(typeof e=="function")return Wu(e);if(typeof e=="object")return Array.isArray(e)?dR(e):fR(e);if(typeof e=="string")return hR(e);throw new Error("Expected function, string, or object as test")});function dR(e){const t=[];let r=-1;for(;++r<e.length;)t[r]=Qu(e[r]);return Wu(a);function a(...s){let o=-1;for(;++o<t.length;)if(t[o].apply(this,s))return!0;return!1}}function fR(e){const t=e;return Wu(r);function r(a){const s=a;let o;for(o in e)if(s[o]!==t[o])return!1;return!0}}function hR(e){return Wu(t);function t(r){return r&&r.type===e}}function Wu(e){return t;function t(r,a,s){return!!(pR(r)&&e.call(this,r,typeof a=="number"?a:void 0,s||void 0))}}function mR(){return!0}function pR(e){return e!==null&&typeof e=="object"&&"type"in e}const Kw=[],gR=!0,ep=!1,xR="skip";function Zw(e,t,r,a){let s;typeof t=="function"&&typeof r!="function"?(a=r,r=t):s=t;const o=Qu(s),c=a?-1:1;d(e,void 0,[])();function d(h,f,p){const g=h&&typeof h=="object"?h:{};if(typeof g.type=="string"){const y=typeof g.tagName=="string"?g.tagName:typeof g.name=="string"?g.name:void 0;Object.defineProperty(b,"name",{value:"node ("+(h.type+(y?"<"+y+">":""))+")"})}return b;function b(){let y=Kw,_,N,S;if((!t||o(h,f,p[p.length-1]||void 0))&&(y=bR(r(h,p)),y[0]===ep))return y;if("children"in h&&h.children){const w=h;if(w.children&&y[0]!==xR)for(N=(a?w.children.length:-1)+c,S=p.concat(w);N>-1&&N<w.children.length;){const C=w.children[N];if(_=d(C,N,S)(),_[0]===ep)return _;N=typeof _[1]=="number"?_[1]:N+c}}return y}}}function bR(e){return Array.isArray(e)?e:typeof e=="number"?[gR,e]:e==null?Kw:[e]}function Gp(e,t,r,a){let s,o,c;typeof t=="function"&&typeof r!="function"?(o=void 0,c=t,s=r):(o=t,c=r,s=a),Zw(e,o,d,s);function d(h,f){const p=f[f.length-1],g=p?p.children.indexOf(h):void 0;return c(h,g,p)}}const tp={}.hasOwnProperty,yR={};function vR(e,t){const r=t||yR,a=new Map,s=new Map,o=new Map,c={...rR,...r.handlers},d={all:f,applyData:wR,definitionById:a,footnoteById:s,footnoteCounts:o,footnoteOrder:[],handlers:c,one:h,options:r,patch:_R,wrap:NR};return Gp(e,function(p){if(p.type==="definition"||p.type==="footnoteDefinition"){const g=p.type==="definition"?a:s,b=String(p.identifier).toUpperCase();g.has(b)||g.set(b,p)}}),d;function h(p,g){const b=p.type,y=d.handlers[b];if(tp.call(d.handlers,b)&&y)return y(d,p,g);if(d.options.passThrough&&d.options.passThrough.includes(b)){if("children"in p){const{children:N,...S}=p,w=Tu(S);return w.children=d.all(p),w}return Tu(p)}return(d.options.unknownHandler||ER)(d,p,g)}function f(p){const g=[];if("children"in p){const b=p.children;let y=-1;for(;++y<b.length;){const _=d.one(b[y],p);if(_){if(y&&b[y-1].type==="break"&&(!Array.isArray(_)&&_.type==="text"&&(_.value=Fy(_.value)),!Array.isArray(_)&&_.type==="element")){const N=_.children[0];N&&N.type==="text"&&(N.value=Fy(N.value))}Array.isArray(_)?g.push(..._):g.push(_)}}}return g}}function _R(e,t){e.position&&(t.position=cM(e))}function wR(e,t){let r=t;if(e&&e.data){const a=e.data.hName,s=e.data.hChildren,o=e.data.hProperties;if(typeof a=="string")if(r.type==="element")r.tagName=a;else{const c="children"in r?r.children:[r];r={type:"element",tagName:a,properties:{},children:c}}r.type==="element"&&o&&Object.assign(r.properties,Tu(o)),"children"in r&&r.children&&s!==null&&s!==void 0&&(r.children=s)}return r}function ER(e,t){const r=t.data||{},a="value"in t&&!(tp.call(r,"hProperties")||tp.call(r,"hChildren"))?{type:"text",value:t.value}:{type:"element",tagName:"div",properties:{},children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function NR(e,t){const r=[];let a=-1;for(t&&r.push({type:"text",value:`
|
||||
`}]}}const Zu=(function(e){if(e==null)return dR;if(typeof e=="function")return Qu(e);if(typeof e=="object")return Array.isArray(e)?oR(e):cR(e);if(typeof e=="string")return uR(e);throw new Error("Expected function, string, or object as test")});function oR(e){const t=[];let r=-1;for(;++r<e.length;)t[r]=Zu(e[r]);return Qu(a);function a(...s){let o=-1;for(;++o<t.length;)if(t[o].apply(this,s))return!0;return!1}}function cR(e){const t=e;return Qu(r);function r(a){const s=a;let o;for(o in e)if(s[o]!==t[o])return!1;return!0}}function uR(e){return Qu(t);function t(r){return r&&r.type===e}}function Qu(e){return t;function t(r,a,s){return!!(fR(r)&&e.call(this,r,typeof a=="number"?a:void 0,s||void 0))}}function dR(){return!0}function fR(e){return e!==null&&typeof e=="object"&&"type"in e}const Gw=[],hR=!0,Jm=!1,mR="skip";function Vw(e,t,r,a){let s;typeof t=="function"&&typeof r!="function"?(a=r,r=t):s=t;const o=Zu(s),c=a?-1:1;d(e,void 0,[])();function d(h,f,p){const g=h&&typeof h=="object"?h:{};if(typeof g.type=="string"){const y=typeof g.tagName=="string"?g.tagName:typeof g.name=="string"?g.name:void 0;Object.defineProperty(b,"name",{value:"node ("+(h.type+(y?"<"+y+">":""))+")"})}return b;function b(){let y=Gw,_,N,S;if((!t||o(h,f,p[p.length-1]||void 0))&&(y=pR(r(h,p)),y[0]===Jm))return y;if("children"in h&&h.children){const w=h;if(w.children&&y[0]!==mR)for(N=(a?w.children.length:-1)+c,S=p.concat(w);N>-1&&N<w.children.length;){const C=w.children[N];if(_=d(C,N,S)(),_[0]===Jm)return _;N=typeof _[1]=="number"?_[1]:N+c}}return y}}}function pR(e){return Array.isArray(e)?e:typeof e=="number"?[hR,e]:e==null?Gw:[e]}function Pp(e,t,r,a){let s,o,c;typeof t=="function"&&typeof r!="function"?(o=void 0,c=t,s=r):(o=t,c=r,s=a),Vw(e,o,d,s);function d(h,f){const p=f[f.length-1],g=p?p.children.indexOf(h):void 0;return c(h,g,p)}}const ep={}.hasOwnProperty,gR={};function xR(e,t){const r=t||gR,a=new Map,s=new Map,o=new Map,c={...eR,...r.handlers},d={all:f,applyData:yR,definitionById:a,footnoteById:s,footnoteCounts:o,footnoteOrder:[],handlers:c,one:h,options:r,patch:bR,wrap:_R};return Pp(e,function(p){if(p.type==="definition"||p.type==="footnoteDefinition"){const g=p.type==="definition"?a:s,b=String(p.identifier).toUpperCase();g.has(b)||g.set(b,p)}}),d;function h(p,g){const b=p.type,y=d.handlers[b];if(ep.call(d.handlers,b)&&y)return y(d,p,g);if(d.options.passThrough&&d.options.passThrough.includes(b)){if("children"in p){const{children:N,...S}=p,w=Cu(S);return w.children=d.all(p),w}return Cu(p)}return(d.options.unknownHandler||vR)(d,p,g)}function f(p){const g=[];if("children"in p){const b=p.children;let y=-1;for(;++y<b.length;){const _=d.one(b[y],p);if(_){if(y&&b[y-1].type==="break"&&(!Array.isArray(_)&&_.type==="text"&&(_.value=qy(_.value)),!Array.isArray(_)&&_.type==="element")){const N=_.children[0];N&&N.type==="text"&&(N.value=qy(N.value))}Array.isArray(_)?g.push(..._):g.push(_)}}}return g}}function bR(e,t){e.position&&(t.position=sM(e))}function yR(e,t){let r=t;if(e&&e.data){const a=e.data.hName,s=e.data.hChildren,o=e.data.hProperties;if(typeof a=="string")if(r.type==="element")r.tagName=a;else{const c="children"in r?r.children:[r];r={type:"element",tagName:a,properties:{},children:c}}r.type==="element"&&o&&Object.assign(r.properties,Cu(o)),"children"in r&&r.children&&s!==null&&s!==void 0&&(r.children=s)}return r}function vR(e,t){const r=t.data||{},a="value"in t&&!(ep.call(r,"hProperties")||ep.call(r,"hChildren"))?{type:"text",value:t.value}:{type:"element",tagName:"div",properties:{},children:e.all(t)};return e.patch(t,a),e.applyData(t,a)}function _R(e,t){const r=[];let a=-1;for(t&&r.push({type:"text",value:`
|
||||
`});++a<e.length;)a&&r.push({type:"text",value:`
|
||||
`}),r.push(e[a]);return t&&e.length>0&&r.push({type:"text",value:`
|
||||
`}),r}function Fy(e){let t=0,r=e.charCodeAt(t);for(;r===9||r===32;)t++,r=e.charCodeAt(t);return e.slice(t)}function Gy(e,t){const r=vR(e,t),a=r.one(e,void 0),s=uR(r),o=Array.isArray(a)?{type:"root",children:a}:a||{type:"root",children:[]};return s&&o.children.push({type:"text",value:`
|
||||
`},s),o}function SR(e,t){return e&&"run"in e?async function(r,a){const s=Gy(r,{file:a,...t});await e.run(s,a)}:function(r,a){return Gy(r,{file:a,...e||t})}}function Vy(e){if(e)throw e}var kh,Yy;function kR(){if(Yy)return kh;Yy=1;var e=Object.prototype.hasOwnProperty,t=Object.prototype.toString,r=Object.defineProperty,a=Object.getOwnPropertyDescriptor,s=function(f){return typeof Array.isArray=="function"?Array.isArray(f):t.call(f)==="[object Array]"},o=function(f){if(!f||t.call(f)!=="[object Object]")return!1;var p=e.call(f,"constructor"),g=f.constructor&&f.constructor.prototype&&e.call(f.constructor.prototype,"isPrototypeOf");if(f.constructor&&!p&&!g)return!1;var b;for(b in f);return typeof b>"u"||e.call(f,b)},c=function(f,p){r&&p.name==="__proto__"?r(f,p.name,{enumerable:!0,configurable:!0,value:p.newValue,writable:!0}):f[p.name]=p.newValue},d=function(f,p){if(p==="__proto__")if(e.call(f,p)){if(a)return a(f,p).value}else return;return f[p]};return kh=function h(){var f,p,g,b,y,_,N=arguments[0],S=1,w=arguments.length,C=!1;for(typeof N=="boolean"&&(C=N,N=arguments[1]||{},S=2),(N==null||typeof N!="object"&&typeof N!="function")&&(N={});S<w;++S)if(f=arguments[S],f!=null)for(p in f)g=d(N,p),b=d(f,p),N!==b&&(C&&b&&(o(b)||(y=s(b)))?(y?(y=!1,_=g&&s(g)?g:[]):_=g&&o(g)?g:{},c(N,{name:p,newValue:h(C,_,b)})):typeof b<"u"&&c(N,{name:p,newValue:b}));return N},kh}var CR=kR();const Ch=Mo(CR);function np(e){if(typeof e!="object"||e===null)return!1;const t=Object.getPrototypeOf(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Symbol.toStringTag in e)&&!(Symbol.iterator in e)}function TR(){const e=[],t={run:r,use:a};return t;function r(...s){let o=-1;const c=s.pop();if(typeof c!="function")throw new TypeError("Expected function as last argument, not "+c);d(null,...s);function d(h,...f){const p=e[++o];let g=-1;if(h){c(h);return}for(;++g<s.length;)(f[g]===null||f[g]===void 0)&&(f[g]=s[g]);s=f,p?AR(p,d)(...f):c(null,...f)}}function a(s){if(typeof s!="function")throw new TypeError("Expected `middelware` to be a function, not "+s);return e.push(s),t}}function AR(e,t){let r;return a;function a(...c){const d=e.length>c.length;let h;d&&c.push(s);try{h=e.apply(this,c)}catch(f){const p=f;if(d&&r)throw p;return s(p)}d||(h&&h.then&&typeof h.then=="function"?h.then(o,s):h instanceof Error?s(h):o(h))}function s(c,...d){r||(r=!0,t(c,...d))}function o(c){s(null,c)}}const Gr={basename:MR,dirname:OR,extname:RR,join:jR,sep:"/"};function MR(e,t){if(t!==void 0&&typeof t!="string")throw new TypeError('"ext" argument must be a string');Lo(e);let r=0,a=-1,s=e.length,o;if(t===void 0||t.length===0||t.length>e.length){for(;s--;)if(e.codePointAt(s)===47){if(o){r=s+1;break}}else a<0&&(o=!0,a=s+1);return a<0?"":e.slice(r,a)}if(t===e)return"";let c=-1,d=t.length-1;for(;s--;)if(e.codePointAt(s)===47){if(o){r=s+1;break}}else c<0&&(o=!0,c=s+1),d>-1&&(e.codePointAt(s)===t.codePointAt(d--)?d<0&&(a=s):(d=-1,a=c));return r===a?a=c:a<0&&(a=e.length),e.slice(r,a)}function OR(e){if(Lo(e),e.length===0)return".";let t=-1,r=e.length,a;for(;--r;)if(e.codePointAt(r)===47){if(a){t=r;break}}else a||(a=!0);return t<0?e.codePointAt(0)===47?"/":".":t===1&&e.codePointAt(0)===47?"//":e.slice(0,t)}function RR(e){Lo(e);let t=e.length,r=-1,a=0,s=-1,o=0,c;for(;t--;){const d=e.codePointAt(t);if(d===47){if(c){a=t+1;break}continue}r<0&&(c=!0,r=t+1),d===46?s<0?s=t:o!==1&&(o=1):s>-1&&(o=-1)}return s<0||r<0||o===0||o===1&&s===r-1&&s===a+1?"":e.slice(s,r)}function jR(...e){let t=-1,r;for(;++t<e.length;)Lo(e[t]),e[t]&&(r=r===void 0?e[t]:r+"/"+e[t]);return r===void 0?".":DR(r)}function DR(e){Lo(e);const t=e.codePointAt(0)===47;let r=LR(e,!t);return r.length===0&&!t&&(r="."),r.length>0&&e.codePointAt(e.length-1)===47&&(r+="/"),t?"/"+r:r}function LR(e,t){let r="",a=0,s=-1,o=0,c=-1,d,h;for(;++c<=e.length;){if(c<e.length)d=e.codePointAt(c);else{if(d===47)break;d=47}if(d===47){if(!(s===c-1||o===1))if(s!==c-1&&o===2){if(r.length<2||a!==2||r.codePointAt(r.length-1)!==46||r.codePointAt(r.length-2)!==46){if(r.length>2){if(h=r.lastIndexOf("/"),h!==r.length-1){h<0?(r="",a=0):(r=r.slice(0,h),a=r.length-1-r.lastIndexOf("/")),s=c,o=0;continue}}else if(r.length>0){r="",a=0,s=c,o=0;continue}}t&&(r=r.length>0?r+"/..":"..",a=2)}else r.length>0?r+="/"+e.slice(s+1,c):r=e.slice(s+1,c),a=c-s-1;s=c,o=0}else d===46&&o>-1?o++:o=-1}return r}function Lo(e){if(typeof e!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}const zR={cwd:IR};function IR(){return"/"}function rp(e){return!!(e!==null&&typeof e=="object"&&"href"in e&&e.href&&"protocol"in e&&e.protocol&&e.auth===void 0)}function BR(e){if(typeof e=="string")e=new URL(e);else if(!rp(e)){const t=new TypeError('The "path" argument must be of type string or an instance of URL. Received `'+e+"`");throw t.code="ERR_INVALID_ARG_TYPE",t}if(e.protocol!=="file:"){const t=new TypeError("The URL must be of scheme file");throw t.code="ERR_INVALID_URL_SCHEME",t}return UR(e)}function UR(e){if(e.hostname!==""){consLine truncated
|
||||
`:" ")+o.indentLines(o.containerFlow(a,d.current()),t?eE:T3))),f(),h}}function T3(e,t,r){return t===0?e:eE(e,t,r)}function eE(e,t,r){return(r?"":" ")+e}const A3=["autolink","destinationLiteral","destinationRaw","reference","titleQuote","titleApostrophe"];tE.peek=D3;function M3(){return{canContainEols:["delete"],enter:{strikethrough:R3},exit:{strikethrough:j3}}}function O3(){return{unsafe:[{character:"~",inConstruct:"phrasing",notInConstruct:A3}],handlers:{delete:tE}}}function R3(e){this.enter({type:"delete",children:[]},e)}function j3(e){this.exit(e)}function tE(e,t,r,a){const s=r.createTracker(a),o=r.enter("strikethrough");let c=s.move("~~");return c+=r.containerPhrasing(e,{...s.current(),before:c,after:"~"}),c+=s.move("~~"),o(),c}function D3(){return"~"}function L3(e){return e.length}function z3(e,t){const r=t||{},a=(r.align||[]).concat(),s=r.stringLength||L3,o=[],c=[],d=[],h=[];let f=0,p=-1;for(;++p<e.length;){const N=[],S=[];let w=-1;for(e[p].length>f&&(f=e[p].length);++w<e[p].length;){const C=I3(e[p][w]);if(r.alignDelimiters!==!1){const E=s(C);S[w]=E,(h[w]===void 0||E>h[w])&&(h[w]=E)}N.push(C)}c[p]=N,d[p]=S}let g=-1;if(typeof a=="object"&&"length"in a)for(;++g<f;)o[g]=ev(a[g]);else{const N=ev(a);for(;++g<f;)o[g]=N}g=-1;const b=[],y=[];for(;++g<f;){const N=o[g];let S="",w="";N===99?(S=":",w=":"):N===108?S=":":N===114&&(w=":");let C=r.alignDelimiters===!1?1:Math.max(1,h[g]-S.length-w.length);const E=S+"-".repeat(C)+w;r.alignDelimiters!==!1&&(C=S.length+C+w.length,C>h[g]&&(h[g]=C),y[g]=C),b[g]=E}c.splice(1,0,b),d.splice(1,0,y),p=-1;const _=[];for(;++p<c.length;){const N=c[p],S=d[p];g=-1;const w=[];for(;++g<f;){const C=N[g]||"";let E="",A="";if(r.alignDelimiters!==!1){const B=h[g]-(S[g]||0),R=o[g];R===114?E=" ".repeat(B):R===99?B%2?(E=" ".repeat(B/2+.5),A=" ".repeat(B/2-.5)):(E=" ".repeat(B/2),A=E):A=" ".repeat(B)}r.delimiterStart!==!1&&!g&&w.push("|"),r.padding!==!1&&!(r.alignDelimiters===!1&&C==="")&&(r.delimiterStart!==!1||g)&&w.push(" "),r.alignDelimiters!==!1&&w.push(E),w.push(C),r.alignDelimiters!==!1&&w.push(A),r.padding!==!1&&w.push(" "),(r.delimiterEnd!==!1||g!==f-1)&&w.push("|")}_.push(r.delimiterEnd===!1?w.join("").replace(/ +$/,""):w.join(""))}return _.join(`
|
||||
`)}function I3(e){return e==null?"":String(e)}function ev(e){const t=typeof e=="string"?e.codePointAt(0):0;return t===67||t===99?99:t===76||t===108?108:t===82||t===114?114:0}function B3(e,t,r,a){const s=r.enter("blockquote"),o=r.createTracker(a);o.move("> "),o.shift(2);const c=r.indentLines(r.containerFlow(e,o.current()),U3);return s(),c}function U3(e,t,r){return">"+(r?"":" ")+e}function H3(e,t){return tv(e,t.inConstruct,!0)&&!tv(e,t.notInConstruct,!1)}function tv(e,t,r){if(typeof t=="string"&&(t=[t]),!t||t.length===0)return r;let a=-1;for(;++a<t.length;)if(e.includes(t[a]))return!0;return!1}function nv(e,t,r,a){let s=-1;for(;++s<r.unsafe.length;)if(r.unsafe[s].character===`
|
||||
`&&H3(r.stack,r.unsafe[s]))return/[ \t]/.test(a.before)?"":" ";return`\\
|
||||
`}function $3(e,t){const r=String(e);let a=r.indexOf(t),s=a,o=0,c=0;if(typeof t!="string")throw new TypeError("Expected substring");for(;a!==-1;)a===s?++o>c&&(c=o):o=1,s=a+t.length,a=r.indexOf(t,s);return c}function q3(e,t){return!!(t.options.fences===!1&&e.value&&!e.lang&&/[^ \r\n]/.test(e.value)&&!/^[\t ]*(?:[\r\n]|$)|(?:^|[\r\n])[\t ]*$/.test(e.value))}function P3(e){const t=e.options.fence||"`";if(t!=="`"&&t!=="~")throw new Error("Cannot serialize code with `"+t+"` for `options.fence`, expected `` ` `` or `~`");return t}function F3(e,t,r,a){const s=P3(r),o=e.value||"",c=s==="`"?"GraveAccent":"Tilde";if(q3(e,r)){const g=r.enter("codeIndented"),b=r.indentLines(o,G3);return g(),b}const d=r.createTracker(a),h=s.repeat(Math.max($3(o,s)+1,3)),f=r.enter("codeFenced");let p=d.move(h);if(e.lang){const g=r.enter(`codeFencedLang${c}`);p+=d.move(r.safe(e.lang,{before:p,after:" ",encode:["`"],...d.current()})),g()}if(e.lang&&e.meta){const g=r.enter(`codeFencedMeta${c}`);p+=d.move(" "),p+=d.move(r.safe(e.meta,{before:p,after:`
|
||||
`}),r}function qy(e){let t=0,r=e.charCodeAt(t);for(;r===9||r===32;)t++,r=e.charCodeAt(t);return e.slice(t)}function Py(e,t){const r=xR(e,t),a=r.one(e,void 0),s=lR(r),o=Array.isArray(a)?{type:"root",children:a}:a||{type:"root",children:[]};return s&&o.children.push({type:"text",value:`
|
||||
`},s),o}function wR(e,t){return e&&"run"in e?async function(r,a){const s=Py(r,{file:a,...t});await e.run(s,a)}:function(r,a){return Py(r,{file:a,...e||t})}}function Fy(e){if(e)throw e}var Sh,Gy;function ER(){if(Gy)return Sh;Gy=1;var e=Object.prototype.hasOwnProperty,t=Object.prototype.toString,r=Object.defineProperty,a=Object.getOwnPropertyDescriptor,s=function(f){return typeof Array.isArray=="function"?Array.isArray(f):t.call(f)==="[object Array]"},o=function(f){if(!f||t.call(f)!=="[object Object]")return!1;var p=e.call(f,"constructor"),g=f.constructor&&f.constructor.prototype&&e.call(f.constructor.prototype,"isPrototypeOf");if(f.constructor&&!p&&!g)return!1;var b;for(b in f);return typeof b>"u"||e.call(f,b)},c=function(f,p){r&&p.name==="__proto__"?r(f,p.name,{enumerable:!0,configurable:!0,value:p.newValue,writable:!0}):f[p.name]=p.newValue},d=function(f,p){if(p==="__proto__")if(e.call(f,p)){if(a)return a(f,p).value}else return;return f[p]};return Sh=function h(){var f,p,g,b,y,_,N=arguments[0],S=1,w=arguments.length,C=!1;for(typeof N=="boolean"&&(C=N,N=arguments[1]||{},S=2),(N==null||typeof N!="object"&&typeof N!="function")&&(N={});S<w;++S)if(f=arguments[S],f!=null)for(p in f)g=d(N,p),b=d(f,p),N!==b&&(C&&b&&(o(b)||(y=s(b)))?(y?(y=!1,_=g&&s(g)?g:[]):_=g&&o(g)?g:{},c(N,{name:p,newValue:h(C,_,b)})):typeof b<"u"&&c(N,{name:p,newValue:b}));return N},Sh}var NR=ER();const kh=Ao(NR);function tp(e){if(typeof e!="object"||e===null)return!1;const t=Object.getPrototypeOf(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Symbol.toStringTag in e)&&!(Symbol.iterator in e)}function SR(){const e=[],t={run:r,use:a};return t;function r(...s){let o=-1;const c=s.pop();if(typeof c!="function")throw new TypeError("Expected function as last argument, not "+c);d(null,...s);function d(h,...f){const p=e[++o];let g=-1;if(h){c(h);return}for(;++g<s.length;)(f[g]===null||f[g]===void 0)&&(f[g]=s[g]);s=f,p?kR(p,d)(...f):c(null,...f)}}function a(s){if(typeof s!="function")throw new TypeError("Expected `middelware` to be a function, not "+s);return e.push(s),t}}function kR(e,t){let r;return a;function a(...c){const d=e.length>c.length;let h;d&&c.push(s);try{h=e.apply(this,c)}catch(f){const p=f;if(d&&r)throw p;return s(p)}d||(h&&h.then&&typeof h.then=="function"?h.then(o,s):h instanceof Error?s(h):o(h))}function s(c,...d){r||(r=!0,t(c,...d))}function o(c){s(null,c)}}const Gr={basename:CR,dirname:TR,extname:AR,join:MR,sep:"/"};function CR(e,t){if(t!==void 0&&typeof t!="string")throw new TypeError('"ext" argument must be a string');Do(e);let r=0,a=-1,s=e.length,o;if(t===void 0||t.length===0||t.length>e.length){for(;s--;)if(e.codePointAt(s)===47){if(o){r=s+1;break}}else a<0&&(o=!0,a=s+1);return a<0?"":e.slice(r,a)}if(t===e)return"";let c=-1,d=t.length-1;for(;s--;)if(e.codePointAt(s)===47){if(o){r=s+1;break}}else c<0&&(o=!0,c=s+1),d>-1&&(e.codePointAt(s)===t.codePointAt(d--)?d<0&&(a=s):(d=-1,a=c));return r===a?a=c:a<0&&(a=e.length),e.slice(r,a)}function TR(e){if(Do(e),e.length===0)return".";let t=-1,r=e.length,a;for(;--r;)if(e.codePointAt(r)===47){if(a){t=r;break}}else a||(a=!0);return t<0?e.codePointAt(0)===47?"/":".":t===1&&e.codePointAt(0)===47?"//":e.slice(0,t)}function AR(e){Do(e);let t=e.length,r=-1,a=0,s=-1,o=0,c;for(;t--;){const d=e.codePointAt(t);if(d===47){if(c){a=t+1;break}continue}r<0&&(c=!0,r=t+1),d===46?s<0?s=t:o!==1&&(o=1):s>-1&&(o=-1)}return s<0||r<0||o===0||o===1&&s===r-1&&s===a+1?"":e.slice(s,r)}function MR(...e){let t=-1,r;for(;++t<e.length;)Do(e[t]),e[t]&&(r=r===void 0?e[t]:r+"/"+e[t]);return r===void 0?".":OR(r)}function OR(e){Do(e);const t=e.codePointAt(0)===47;let r=RR(e,!t);return r.length===0&&!t&&(r="."),r.length>0&&e.codePointAt(e.length-1)===47&&(r+="/"),t?"/"+r:r}function RR(e,t){let r="",a=0,s=-1,o=0,c=-1,d,h;for(;++c<=e.length;){if(c<e.length)d=e.codePointAt(c);else{if(d===47)break;d=47}if(d===47){if(!(s===c-1||o===1))if(s!==c-1&&o===2){if(r.length<2||a!==2||r.codePointAt(r.length-1)!==46||r.codePointAt(r.length-2)!==46){if(r.length>2){if(h=r.lastIndexOf("/"),h!==r.length-1){h<0?(r="",a=0):(r=r.slice(0,h),a=r.length-1-r.lastIndexOf("/")),s=c,o=0;continue}}else if(r.length>0){r="",a=0,s=c,o=0;continue}}t&&(r=r.length>0?r+"/..":"..",a=2)}else r.length>0?r+="/"+e.slice(s+1,c):r=e.slice(s+1,c),a=c-s-1;s=c,o=0}else d===46&&o>-1?o++:o=-1}return r}function Do(e){if(typeof e!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}const jR={cwd:DR};function DR(){return"/"}function np(e){return!!(e!==null&&typeof e=="object"&&"href"in e&&e.href&&"protocol"in e&&e.protocol&&e.auth===void 0)}function LR(e){if(typeof e=="string")e=new URL(e);else if(!np(e)){const t=new TypeError('The "path" argument must be of type string or an instance of URL. Received `'+e+"`");throw t.code="ERR_INVALID_ARG_TYPE",t}if(e.protocol!=="file:"){const t=new TypeError("The URL must be of scheme file");throw t.code="ERR_INVALID_URL_SCHEME",t}return zR(e)}function zR(e){if(e.hostname!==""){consLine truncated
|
||||
`:" ")+o.indentLines(o.containerFlow(a,d.current()),t?Zw:S3))),f(),h}}function S3(e,t,r){return t===0?e:Zw(e,t,r)}function Zw(e,t,r){return(r?"":" ")+e}const k3=["autolink","destinationLiteral","destinationRaw","reference","titleQuote","titleApostrophe"];Qw.peek=O3;function C3(){return{canContainEols:["delete"],enter:{strikethrough:A3},exit:{strikethrough:M3}}}function T3(){return{unsafe:[{character:"~",inConstruct:"phrasing",notInConstruct:k3}],handlers:{delete:Qw}}}function A3(e){this.enter({type:"delete",children:[]},e)}function M3(e){this.exit(e)}function Qw(e,t,r,a){const s=r.createTracker(a),o=r.enter("strikethrough");let c=s.move("~~");return c+=r.containerPhrasing(e,{...s.current(),before:c,after:"~"}),c+=s.move("~~"),o(),c}function O3(){return"~"}function R3(e){return e.length}function j3(e,t){const r=t||{},a=(r.align||[]).concat(),s=r.stringLength||R3,o=[],c=[],d=[],h=[];let f=0,p=-1;for(;++p<e.length;){const N=[],S=[];let w=-1;for(e[p].length>f&&(f=e[p].length);++w<e[p].length;){const C=D3(e[p][w]);if(r.alignDelimiters!==!1){const E=s(C);S[w]=E,(h[w]===void 0||E>h[w])&&(h[w]=E)}N.push(C)}c[p]=N,d[p]=S}let g=-1;if(typeof a=="object"&&"length"in a)for(;++g<f;)o[g]=Wy(a[g]);else{const N=Wy(a);for(;++g<f;)o[g]=N}g=-1;const b=[],y=[];for(;++g<f;){const N=o[g];let S="",w="";N===99?(S=":",w=":"):N===108?S=":":N===114&&(w=":");let C=r.alignDelimiters===!1?1:Math.max(1,h[g]-S.length-w.length);const E=S+"-".repeat(C)+w;r.alignDelimiters!==!1&&(C=S.length+C+w.length,C>h[g]&&(h[g]=C),y[g]=C),b[g]=E}c.splice(1,0,b),d.splice(1,0,y),p=-1;const _=[];for(;++p<c.length;){const N=c[p],S=d[p];g=-1;const w=[];for(;++g<f;){const C=N[g]||"";let E="",A="";if(r.alignDelimiters!==!1){const B=h[g]-(S[g]||0),R=o[g];R===114?E=" ".repeat(B):R===99?B%2?(E=" ".repeat(B/2+.5),A=" ".repeat(B/2-.5)):(E=" ".repeat(B/2),A=E):A=" ".repeat(B)}r.delimiterStart!==!1&&!g&&w.push("|"),r.padding!==!1&&!(r.alignDelimiters===!1&&C==="")&&(r.delimiterStart!==!1||g)&&w.push(" "),r.alignDelimiters!==!1&&w.push(E),w.push(C),r.alignDelimiters!==!1&&w.push(A),r.padding!==!1&&w.push(" "),(r.delimiterEnd!==!1||g!==f-1)&&w.push("|")}_.push(r.delimiterEnd===!1?w.join("").replace(/ +$/,""):w.join(""))}return _.join(`
|
||||
`)}function D3(e){return e==null?"":String(e)}function Wy(e){const t=typeof e=="string"?e.codePointAt(0):0;return t===67||t===99?99:t===76||t===108?108:t===82||t===114?114:0}function L3(e,t,r,a){const s=r.enter("blockquote"),o=r.createTracker(a);o.move("> "),o.shift(2);const c=r.indentLines(r.containerFlow(e,o.current()),z3);return s(),c}function z3(e,t,r){return">"+(r?"":" ")+e}function I3(e,t){return Jy(e,t.inConstruct,!0)&&!Jy(e,t.notInConstruct,!1)}function Jy(e,t,r){if(typeof t=="string"&&(t=[t]),!t||t.length===0)return r;let a=-1;for(;++a<t.length;)if(e.includes(t[a]))return!0;return!1}function ev(e,t,r,a){let s=-1;for(;++s<r.unsafe.length;)if(r.unsafe[s].character===`
|
||||
`&&I3(r.stack,r.unsafe[s]))return/[ \t]/.test(a.before)?"":" ";return`\\
|
||||
`}function B3(e,t){const r=String(e);let a=r.indexOf(t),s=a,o=0,c=0;if(typeof t!="string")throw new TypeError("Expected substring");for(;a!==-1;)a===s?++o>c&&(c=o):o=1,s=a+t.length,a=r.indexOf(t,s);return c}function U3(e,t){return!!(t.options.fences===!1&&e.value&&!e.lang&&/[^ \r\n]/.test(e.value)&&!/^[\t ]*(?:[\r\n]|$)|(?:^|[\r\n])[\t ]*$/.test(e.value))}function H3(e){const t=e.options.fence||"`";if(t!=="`"&&t!=="~")throw new Error("Cannot serialize code with `"+t+"` for `options.fence`, expected `` ` `` or `~`");return t}function $3(e,t,r,a){const s=H3(r),o=e.value||"",c=s==="`"?"GraveAccent":"Tilde";if(U3(e,r)){const g=r.enter("codeIndented"),b=r.indentLines(o,q3);return g(),b}const d=r.createTracker(a),h=s.repeat(Math.max(B3(o,s)+1,3)),f=r.enter("codeFenced");let p=d.move(h);if(e.lang){const g=r.enter(`codeFencedLang${c}`);p+=d.move(r.safe(e.lang,{before:p,after:" ",encode:["`"],...d.current()})),g()}if(e.lang&&e.meta){const g=r.enter(`codeFencedMeta${c}`);p+=d.move(" "),p+=d.move(r.safe(e.meta,{before:p,after:`
|
||||
`,encode:["`"],...d.current()})),g()}return p+=d.move(`
|
||||
`),o&&(p+=d.move(o+`
|
||||
`)),p+=d.move(h),f(),p}function G3(e,t,r){return(r?"":" ")+e}function Xp(e){const t=e.options.quote||'"';if(t!=='"'&&t!=="'")throw new Error("Cannot serialize title with `"+t+"` for `options.quote`, expected `\"`, or `'`");return t}function V3(e,t,r,a){const s=Xp(r),o=s==='"'?"Quote":"Apostrophe",c=r.enter("definition");let d=r.enter("label");const h=r.createTracker(a);let f=h.move("[");return f+=h.move(r.safe(r.associationId(e),{before:f,after:"]",...h.current()})),f+=h.move("]: "),d(),!e.url||/[\0- \u007F]/.test(e.url)?(d=r.enter("destinationLiteral"),f+=h.move("<"),f+=h.move(r.safe(e.url,{before:f,after:">",...h.current()})),f+=h.move(">")):(d=r.enter("destinationRaw"),f+=h.move(r.safe(e.url,{before:f,after:e.title?" ":`
|
||||
`,...h.current()}))),d(),e.title&&(d=r.enter(`title${o}`),f+=h.move(" "+s),f+=h.move(r.safe(e.title,{before:f,after:s,...h.current()})),f+=h.move(s),d()),c(),f}function Y3(e){const t=e.options.emphasis||"*";if(t!=="*"&&t!=="_")throw new Error("Cannot serialize emphasis with `"+t+"` for `options.emphasis`, expected `*`, or `_`");return t}function bo(e){return"&#x"+e.toString(16).toUpperCase()+";"}function Au(e,t,r){const a=Xs(e),s=Xs(t);return a===void 0?s===void 0?r==="_"?{inside:!0,outside:!0}:{inside:!1,outside:!1}:s===1?{inside:!0,outside:!0}:{inside:!1,outside:!0}:a===1?s===void 0?{inside:!1,outside:!1}:s===1?{inside:!0,outside:!0}:{inside:!1,outside:!1}:s===void 0?{inside:!1,outside:!1}:s===1?{inside:!0,outside:!1}:{inside:!1,outside:!1}}nE.peek=X3;function nE(e,t,r,a){const s=Y3(r),o=r.enter("emphasis"),c=r.createTracker(a),d=c.move(s);let h=c.move(r.containerPhrasing(e,{after:s,before:d,...c.current()}));const f=h.charCodeAt(0),p=Au(a.before.charCodeAt(a.before.length-1),f,s);p.inside&&(h=bo(f)+h.slice(1));const g=h.charCodeAt(h.length-1),b=Au(a.after.charCodeAt(0),g,s);b.inside&&(h=h.slice(0,-1)+bo(g));const y=c.move(s);return o(),r.attentionEncodeSurroundingInfo={after:b.outside,before:p.outside},d+h+y}function X3(e,t,r){return r.options.emphasis||"*"}function K3(e,t){let r=!1;return Gp(e,function(a){if("value"in a&&/\r?\n|\r/.test(a.value)||a.type==="break")return r=!0,ep}),!!((!e.depth||e.depth<3)&&Bp(e)&&(t.options.setext||r))}function Z3(e,t,r,a){const s=Math.max(Math.min(6,e.depth||1),1),o=r.createTracker(a);if(K3(e,r)){const p=r.enter("headingSetext"),g=r.enter("phrasing"),b=r.containerPhrasing(e,{...o.current(),before:`
|
||||
`)),p+=d.move(h),f(),p}function q3(e,t,r){return(r?"":" ")+e}function Vp(e){const t=e.options.quote||'"';if(t!=='"'&&t!=="'")throw new Error("Cannot serialize title with `"+t+"` for `options.quote`, expected `\"`, or `'`");return t}function P3(e,t,r,a){const s=Vp(r),o=s==='"'?"Quote":"Apostrophe",c=r.enter("definition");let d=r.enter("label");const h=r.createTracker(a);let f=h.move("[");return f+=h.move(r.safe(r.associationId(e),{before:f,after:"]",...h.current()})),f+=h.move("]: "),d(),!e.url||/[\0- \u007F]/.test(e.url)?(d=r.enter("destinationLiteral"),f+=h.move("<"),f+=h.move(r.safe(e.url,{before:f,after:">",...h.current()})),f+=h.move(">")):(d=r.enter("destinationRaw"),f+=h.move(r.safe(e.url,{before:f,after:e.title?" ":`
|
||||
`,...h.current()}))),d(),e.title&&(d=r.enter(`title${o}`),f+=h.move(" "+s),f+=h.move(r.safe(e.title,{before:f,after:s,...h.current()})),f+=h.move(s),d()),c(),f}function F3(e){const t=e.options.emphasis||"*";if(t!=="*"&&t!=="_")throw new Error("Cannot serialize emphasis with `"+t+"` for `options.emphasis`, expected `*`, or `_`");return t}function bo(e){return"&#x"+e.toString(16).toUpperCase()+";"}function Tu(e,t,r){const a=Xs(e),s=Xs(t);return a===void 0?s===void 0?r==="_"?{inside:!0,outside:!0}:{inside:!1,outside:!1}:s===1?{inside:!0,outside:!0}:{inside:!1,outside:!0}:a===1?s===void 0?{inside:!1,outside:!1}:s===1?{inside:!0,outside:!0}:{inside:!1,outside:!1}:s===void 0?{inside:!1,outside:!1}:s===1?{inside:!0,outside:!1}:{inside:!1,outside:!1}}Ww.peek=G3;function Ww(e,t,r,a){const s=F3(r),o=r.enter("emphasis"),c=r.createTracker(a),d=c.move(s);let h=c.move(r.containerPhrasing(e,{after:s,before:d,...c.current()}));const f=h.charCodeAt(0),p=Tu(a.before.charCodeAt(a.before.length-1),f,s);p.inside&&(h=bo(f)+h.slice(1));const g=h.charCodeAt(h.length-1),b=Tu(a.after.charCodeAt(0),g,s);b.inside&&(h=h.slice(0,-1)+bo(g));const y=c.move(s);return o(),r.attentionEncodeSurroundingInfo={after:b.outside,before:p.outside},d+h+y}function G3(e,t,r){return r.options.emphasis||"*"}function V3(e,t){let r=!1;return Pp(e,function(a){if("value"in a&&/\r?\n|\r/.test(a.value)||a.type==="break")return r=!0,Jm}),!!((!e.depth||e.depth<3)&&zp(e)&&(t.options.setext||r))}function Y3(e,t,r,a){const s=Math.max(Math.min(6,e.depth||1),1),o=r.createTracker(a);if(V3(e,r)){const p=r.enter("headingSetext"),g=r.enter("phrasing"),b=r.containerPhrasing(e,{...o.current(),before:`
|
||||
`,after:`
|
||||
`});return g(),p(),b+`
|
||||
`+(s===1?"=":"-").repeat(b.length-(Math.max(b.lastIndexOf("\r"),b.lastIndexOf(`
|
||||
`))+1))}const c="#".repeat(s),d=r.enter("headingAtx"),h=r.enter("phrasing");o.move(c+" ");let f=r.containerPhrasing(e,{before:"# ",after:`
|
||||
`,...o.current()});return/^[\t ]/.test(f)&&(f=bo(f.charCodeAt(0))+f.slice(1)),f=f?c+" "+f:c,r.options.closeAtx&&(f+=" "+c),h(),d(),f}rE.peek=Q3;function rE(e){return e.value||""}function Q3(){return"<"}iE.peek=W3;function iE(e,t,r,a){const s=Xp(r),o=s==='"'?"Quote":"Apostrophe",c=r.enter("image");let d=r.enter("label");const h=r.createTracker(a);let f=h.move("![");return f+=h.move(r.safe(e.alt,{before:f,after:"]",...h.current()})),f+=h.move("]("),d(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(d=r.enter("destinationLiteral"),f+=h.move("<"),f+=h.move(r.safe(e.url,{before:f,after:">",...h.current()})),f+=h.move(">")):(d=r.enter("destinationRaw"),f+=h.move(r.safe(e.url,{before:f,after:e.title?" ":")",...h.current()}))),d(),e.title&&(d=r.enter(`title${o}`),f+=h.move(" "+s),f+=h.move(r.safe(e.title,{before:f,after:s,...h.current()})),f+=h.move(s),d()),f+=h.move(")"),c(),f}function W3(){return"!"}aE.peek=J3;function aE(e,t,r,a){const s=e.referenceType,o=r.enter("imageReference");let c=r.enter("label");const d=r.createTracker(a);let h=d.move("![");const f=r.safe(e.alt,{before:h,after:"]",...d.current()});h+=d.move(f+"]["),c();const p=r.stack;r.stack=[],c=r.enter("reference");const g=r.safe(r.associationId(e),{before:h,after:"]",...d.current()});return c(),r.stack=p,o(),s==="full"||!f||f!==g?h+=d.move(g+"]"):s==="shortcut"?h=h.slice(0,-1):h+=d.move("]"),h}function J3(){return"!"}sE.peek=e4;function sE(e,t,r){let a=e.value||"",s="`",o=-1;for(;new RegExp("(^|[^`])"+s+"([^`]|$)").test(a);)s+="`";for(/[^ \r\n]/.test(a)&&(/^[ \r\n]/.test(a)&&/[ \r\n]$/.test(a)||/^`|`$/.test(a))&&(a=" "+a+" ");++o<r.unsafe.length;){const c=r.unsafe[o],d=r.compilePattern(c);let h;if(c.atBreak)for(;h=d.exec(a);){let f=h.index;a.charCodeAt(f)===10&&a.charCodeAt(f-1)===13&&f--,a=a.slice(0,f)+" "+a.slice(h.index+1)}}return s+a+s}function e4(){return"`"}function lE(e,t){const r=Bp(e);return!!(!t.options.resourceLink&&e.url&&!e.title&&e.children&&e.children.length===1&&e.children[0].type==="text"&&(r===e.url||"mailto:"+r===e.url)&&/^[a-z][a-z+.-]+:/i.test(e.url)&&!/[\0- <>\u007F]/.test(e.url))}oE.peek=t4;function oE(e,t,r,a){const s=Xp(r),o=s==='"'?"Quote":"Apostrophe",c=r.createTracker(a);let d,h;if(lE(e,r)){const p=r.stack;r.stack=[],d=r.enter("autolink");let g=c.move("<");return g+=c.move(r.containerPhrasing(e,{before:g,after:">",...c.current()})),g+=c.move(">"),d(),r.stack=p,g}d=r.enter("link"),h=r.enter("label");let f=c.move("[");return f+=c.move(r.containerPhrasing(e,{before:f,after:"](",...c.current()})),f+=c.move("]("),h(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(h=r.enter("destinationLiteral"),f+=c.move("<"),f+=c.move(r.safe(e.url,{before:f,after:">",...c.current()})),f+=c.move(">")):(h=r.enter("destinationRaw"),f+=c.move(r.safe(e.url,{before:f,after:e.title?" ":")",...c.current()}))),h(),e.title&&(h=r.enter(`title${o}`),f+=c.move(" "+s),f+=c.move(r.safe(e.title,{before:f,after:s,...c.current()})),f+=c.move(s),h()),f+=c.move(")"),d(),f}function t4(e,t,r){return lE(e,r)?"<":"["}cE.peek=n4;function cE(e,t,r,a){const s=e.referenceType,o=r.enter("linkReference");let c=r.enter("label");const d=r.createTracker(a);let h=d.move("[");const f=r.containerPhrasing(e,{before:h,after:"]",...d.current()});h+=d.move(f+"]["),c();const p=r.stack;r.stack=[],c=r.enter("reference");const g=r.safe(r.associationId(e),{before:h,after:"]",...d.current()});return c(),r.stack=p,o(),s==="full"||!f||f!==g?h+=d.move(g+"]"):s==="shortcut"?h=h.slice(0,-1):h+=d.move("]"),h}function n4(){return"["}function Kp(e){const t=e.options.bullet||"*";if(t!=="*"&&t!=="+"&&t!=="-")throw new Error("Cannot serialize items with `"+t+"` for `options.bullet`, expected `*`, `+`, or `-`");return t}function r4(e){const t=Kp(e),r=e.options.bulletOther;if(!r)return t==="*"?"-":"*";if(r!=="*"&&r!=="+"&&r!=="-")throw new Error("Cannot serialize items with `"+r+"` for `options.bulletOther`, expected `*`, `+`, or `-`");if(r===t)throw new Error("Expected `bullet` (`"+t+"`) and `bulletOther` (`"+r+"`) to be different");return r}function i4(e){const t=e.options.bulletOrdered||".";if(t!=="."&&t!==")")throw new Error("Cannot serialize items with `"+t+"` for `options.bulletOrdered`, expected `.` or `)`");return t}function uE(e){const t=e.options.rule||"*";if(t!=="*"&&t!=="-"&&t!=="_")throw new Error("Cannot serialize rules with `"+t+"` for `options.rule`, expected `*`, `-`, or `_`");return t}function a4(e,t,r,a){const s=r.enter("list"),o=r.bulletCurrent;let c=e.ordered?i4(r):Kp(r);const d=e.ordered?c==="."?")":".":r4(r);let h=t&&r.bulletLastUsed?c===r.bulletLastUsed:!1;if(!e.ordered){const p=e.children?e.children[0]:void 0;if((c==="*"||c==="-")&&p&&(!p.children||!p.children[0])&&r.stack[r.stack.length-1]==="list"&&r.stack[r.stack.length-2]==="listItem"&&r.stack[r.stack.length-3]==="list"&&r.stack[r.stack.length-4]==="listItem"&&r.indexStack[r.indexStack.length-1]===0&&r.indexStack[r.indexStack.length-2]===0&&r.indexStack[r.indexStack.length-3]===0&&(h=!0),uE(r)===c&&p){let g=-1;for(;++g<e.chilLine truncated
|
||||
`,...o.current()});return/^[\t ]/.test(f)&&(f=bo(f.charCodeAt(0))+f.slice(1)),f=f?c+" "+f:c,r.options.closeAtx&&(f+=" "+c),h(),d(),f}Jw.peek=X3;function Jw(e){return e.value||""}function X3(){return"<"}eE.peek=K3;function eE(e,t,r,a){const s=Vp(r),o=s==='"'?"Quote":"Apostrophe",c=r.enter("image");let d=r.enter("label");const h=r.createTracker(a);let f=h.move("![");return f+=h.move(r.safe(e.alt,{before:f,after:"]",...h.current()})),f+=h.move("]("),d(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(d=r.enter("destinationLiteral"),f+=h.move("<"),f+=h.move(r.safe(e.url,{before:f,after:">",...h.current()})),f+=h.move(">")):(d=r.enter("destinationRaw"),f+=h.move(r.safe(e.url,{before:f,after:e.title?" ":")",...h.current()}))),d(),e.title&&(d=r.enter(`title${o}`),f+=h.move(" "+s),f+=h.move(r.safe(e.title,{before:f,after:s,...h.current()})),f+=h.move(s),d()),f+=h.move(")"),c(),f}function K3(){return"!"}tE.peek=Z3;function tE(e,t,r,a){const s=e.referenceType,o=r.enter("imageReference");let c=r.enter("label");const d=r.createTracker(a);let h=d.move("![");const f=r.safe(e.alt,{before:h,after:"]",...d.current()});h+=d.move(f+"]["),c();const p=r.stack;r.stack=[],c=r.enter("reference");const g=r.safe(r.associationId(e),{before:h,after:"]",...d.current()});return c(),r.stack=p,o(),s==="full"||!f||f!==g?h+=d.move(g+"]"):s==="shortcut"?h=h.slice(0,-1):h+=d.move("]"),h}function Z3(){return"!"}nE.peek=Q3;function nE(e,t,r){let a=e.value||"",s="`",o=-1;for(;new RegExp("(^|[^`])"+s+"([^`]|$)").test(a);)s+="`";for(/[^ \r\n]/.test(a)&&(/^[ \r\n]/.test(a)&&/[ \r\n]$/.test(a)||/^`|`$/.test(a))&&(a=" "+a+" ");++o<r.unsafe.length;){const c=r.unsafe[o],d=r.compilePattern(c);let h;if(c.atBreak)for(;h=d.exec(a);){let f=h.index;a.charCodeAt(f)===10&&a.charCodeAt(f-1)===13&&f--,a=a.slice(0,f)+" "+a.slice(h.index+1)}}return s+a+s}function Q3(){return"`"}function rE(e,t){const r=zp(e);return!!(!t.options.resourceLink&&e.url&&!e.title&&e.children&&e.children.length===1&&e.children[0].type==="text"&&(r===e.url||"mailto:"+r===e.url)&&/^[a-z][a-z+.-]+:/i.test(e.url)&&!/[\0- <>\u007F]/.test(e.url))}iE.peek=W3;function iE(e,t,r,a){const s=Vp(r),o=s==='"'?"Quote":"Apostrophe",c=r.createTracker(a);let d,h;if(rE(e,r)){const p=r.stack;r.stack=[],d=r.enter("autolink");let g=c.move("<");return g+=c.move(r.containerPhrasing(e,{before:g,after:">",...c.current()})),g+=c.move(">"),d(),r.stack=p,g}d=r.enter("link"),h=r.enter("label");let f=c.move("[");return f+=c.move(r.containerPhrasing(e,{before:f,after:"](",...c.current()})),f+=c.move("]("),h(),!e.url&&e.title||/[\0- \u007F]/.test(e.url)?(h=r.enter("destinationLiteral"),f+=c.move("<"),f+=c.move(r.safe(e.url,{before:f,after:">",...c.current()})),f+=c.move(">")):(h=r.enter("destinationRaw"),f+=c.move(r.safe(e.url,{before:f,after:e.title?" ":")",...c.current()}))),h(),e.title&&(h=r.enter(`title${o}`),f+=c.move(" "+s),f+=c.move(r.safe(e.title,{before:f,after:s,...c.current()})),f+=c.move(s),h()),f+=c.move(")"),d(),f}function W3(e,t,r){return rE(e,r)?"<":"["}aE.peek=J3;function aE(e,t,r,a){const s=e.referenceType,o=r.enter("linkReference");let c=r.enter("label");const d=r.createTracker(a);let h=d.move("[");const f=r.containerPhrasing(e,{before:h,after:"]",...d.current()});h+=d.move(f+"]["),c();const p=r.stack;r.stack=[],c=r.enter("reference");const g=r.safe(r.associationId(e),{before:h,after:"]",...d.current()});return c(),r.stack=p,o(),s==="full"||!f||f!==g?h+=d.move(g+"]"):s==="shortcut"?h=h.slice(0,-1):h+=d.move("]"),h}function J3(){return"["}function Yp(e){const t=e.options.bullet||"*";if(t!=="*"&&t!=="+"&&t!=="-")throw new Error("Cannot serialize items with `"+t+"` for `options.bullet`, expected `*`, `+`, or `-`");return t}function e4(e){const t=Yp(e),r=e.options.bulletOther;if(!r)return t==="*"?"-":"*";if(r!=="*"&&r!=="+"&&r!=="-")throw new Error("Cannot serialize items with `"+r+"` for `options.bulletOther`, expected `*`, `+`, or `-`");if(r===t)throw new Error("Expected `bullet` (`"+t+"`) and `bulletOther` (`"+r+"`) to be different");return r}function t4(e){const t=e.options.bulletOrdered||".";if(t!=="."&&t!==")")throw new Error("Cannot serialize items with `"+t+"` for `options.bulletOrdered`, expected `.` or `)`");return t}function sE(e){const t=e.options.rule||"*";if(t!=="*"&&t!=="-"&&t!=="_")throw new Error("Cannot serialize rules with `"+t+"` for `options.rule`, expected `*`, `-`, or `_`");return t}function n4(e,t,r,a){const s=r.enter("list"),o=r.bulletCurrent;let c=e.ordered?t4(r):Yp(r);const d=e.ordered?c==="."?")":".":e4(r);let h=t&&r.bulletLastUsed?c===r.bulletLastUsed:!1;if(!e.ordered){const p=e.children?e.children[0]:void 0;if((c==="*"||c==="-")&&p&&(!p.children||!p.children[0])&&r.stack[r.stack.length-1]==="list"&&r.stack[r.stack.length-2]==="listItem"&&r.stack[r.stack.length-3]==="list"&&r.stack[r.stack.length-4]==="listItem"&&r.indexStack[r.indexStack.length-1]===0&&r.indexStack[r.indexStack.length-2]===0&&r.indexStack[r.indexStack.length-3]===0&&(h=!0),sE(r)===c&&p){let g=-1;for(;++g<e.chilLine truncated
|
||||
`,inConstruct:"tableCell"},{atBreak:!0,character:"|",after:"[ :-]"},{character:"|",inConstruct:"tableCell"},{atBreak:!0,character:":",after:"-"},{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{inlineCode:b,table:c,tableCell:h,tableRow:d}};function c(y,_,N,S){return f(p(y,N,S),y.align)}function d(y,_,N,S){const w=g(y,N,S),C=f([w]);return C.slice(0,C.indexOf(`
|
||||
`))}function h(y,_,N,S){const w=N.enter("tableCell"),C=N.enter("phrasing"),E=N.containerPhrasing(y,{...S,before:o,after:o});return C(),w(),E}function f(y,_){return z3(y,{align:_,alignDelimiters:a,padding:r,stringLength:s})}function p(y,_,N){const S=y.children;let w=-1;const C=[],E=_.enter("table");for(;++w<S.length;)C[w]=g(S[w],_,N);return E(),C}function g(y,_,N){const S=y.children;let w=-1;const C=[],E=_.enter("tableRow");for(;++w<S.length;)C[w]=h(S[w],y,_,N);return E(),C}function b(y,_,N){let S=fE.inlineCode(y,_,N);return N.stack.includes("tableCell")&&(S=S.replace(/\|/g,"\\$&")),S}}function E4(){return{exit:{taskListCheckValueChecked:iv,taskListCheckValueUnchecked:iv,paragraph:S4}}}function N4(){return{unsafe:[{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{listItem:k4}}}function iv(e){const t=this.stack[this.stack.length-2];t.type,t.checked=e.type==="taskListCheckValueChecked"}function S4(e){const t=this.stack[this.stack.length-2];if(t&&t.type==="listItem"&&typeof t.checked=="boolean"){const r=this.stack[this.stack.length-1];r.type;const a=r.children[0];if(a&&a.type==="text"){const s=t.children;let o=-1,c;for(;++o<s.length;){const d=s[o];if(d.type==="paragraph"){c=d;break}}c===r&&(a.value=a.value.slice(1),a.value.length===0?r.children.shift():r.position&&a.position&&typeof a.position.start.offset=="number"&&(a.position.start.column++,a.position.start.offset++,r.position.start=Object.assign({},a.position.start)))}}this.exit(e)}function k4(e,t,r,a){const s=e.children[0],o=typeof e.checked=="boolean"&&s&&s.type==="paragraph",c="["+(e.checked?"x":" ")+"] ",d=r.createTracker(a);o&&d.move(c);let h=fE.listItem(e,t,r,{...a,...d.current()});return o&&(h=h.replace(/^(?:[*+-]|\d+\.)([\r\n]| {1,3})/,f)),h;function f(p){return p+c}}function C4(){return[a3(),k3(),M3(),g4(),E4()]}function T4(e){return{extensions:[s3(),C3(e),O3(),w4(e),N4()]}}const A4={tokenize:L4,partial:!0},hE={tokenize:z4,partial:!0},mE={tokenize:I4,partial:!0},pE={tokenize:B4,partial:!0},M4={tokenize:U4,partial:!0},gE={name:"wwwAutolink",tokenize:j4,previous:bE},xE={name:"protocolAutolink",tokenize:D4,previous:yE},Ni={name:"emailAutolink",tokenize:R4,previous:vE},Kr={};function O4(){return{text:Kr}}let Ia=48;for(;Ia<123;)Kr[Ia]=Ni,Ia++,Ia===58?Ia=65:Ia===91&&(Ia=97);Kr[43]=Ni;Kr[45]=Ni;Kr[46]=Ni;Kr[95]=Ni;Kr[72]=[Ni,xE];Kr[104]=[Ni,xE];Kr[87]=[Ni,gE];Kr[119]=[Ni,gE];function R4(e,t,r){const a=this;let s,o;return c;function c(g){return!ip(g)||!vE.call(a,a.previous)||Zp(a.events)?r(g):(e.enter("literalAutolink"),e.enter("literalAutolinkEmail"),d(g))}function d(g){return ip(g)?(e.consume(g),d):g===64?(e.consume(g),h):r(g)}function h(g){return g===46?e.check(M4,p,f)(g):g===45||g===95||An(g)?(o=!0,e.consume(g),h):p(g)}function f(g){return e.consume(g),s=!0,h}function p(g){return o&&s&&Ln(a.previous)?(e.exit("literalAutolinkEmail"),e.exit("literalAutolink"),t(g)):r(g)}}function j4(e,t,r){const a=this;return s;function s(c){return c!==87&&c!==119||!bE.call(a,a.previous)||Zp(a.events)?r(c):(e.enter("literalAutolink"),e.enter("literalAutolinkWww"),e.check(A4,e.attempt(hE,e.attempt(mE,o),r),r)(c))}function o(c){return e.exit("literalAutolinkWww"),e.exit("literalAutolink"),t(c)}}function D4(e,t,r){const a=this;let s="",o=!1;return c;function c(g){return(g===72||g===104)&&yE.call(a,a.previous)&&!Zp(a.events)?(e.enter("literalAutolink"),e.enter("literalAutolinkHttp"),s+=String.fromCodePoint(g),e.consume(g),d):r(g)}function d(g){if(Ln(g)&&s.length<5)return s+=String.fromCodePoint(g),e.consume(g),d;if(g===58){const b=s.toLowerCase();if(b==="http"||b==="https")return e.consume(g),h}return r(g)}function h(g){return g===47?(e.consume(g),o?f:(o=!0,h)):r(g)}function f(g){return g===null||ku(g)||Tt(g)||Va(g)||Xu(g)?r(g):e.attempt(hE,e.attempt(mE,p),r)(g)}function p(g){return e.exit("literalAutolinkHttp"),e.exit("literalAutolink"),t(g)}}function L4(e,t,r){let a=0;return s;function s(c){return(c===87||c===119)&&a<3?(a++,e.consume(c),s):c===46&&a===3?(e.consume(c),o):r(c)}function o(c){return c===null?r(c):t(c)}}function z4(e,t,r){let a,s,o;return c;function c(f){return f===46||f===95?e.check(pE,h,d)(f):f===null||Tt(f)||Va(f)||f!==45&&Xu(f)?h(f):(o=!0,e.consume(f),c)}function d(f){return f===95?a=!0:(s=a,a=void 0),e.consume(f),c}function h(f){return s||a||!o?r(f):t(f)}}function I4(e,t){let r=0,a=0;return s;function s(c){return c===40?(r++,e.consume(c),s):c===41&&a<r?o(c):c===33||c===34||c===38||c===39||c===41||c===42||c===44||c===46||c===58||c===59||c===60||c===63||c===93||c===95||c===126?e.check(pE,t,o)(c):c===null||Tt(c)||Va(c)?t(c):(e.consume(c),s)}function o(c){return c===41&&a++,e.consume(c),s}}function B4(e,t,r){return a;function a(d){return d===33||d===34||d===39||d===41||d===42||d===44||d===46||d===58||d===59||d===63||d===95||d===126?(e.consume(d),a):d===38?(e.consume(d),o):d===93?(e.consume(d),s):d===60||d===null||Tt(d)||Va(d)?t(d):r(d)}function s(d){return d===null||d===40||d===91||Tt(d)||Va(d)?t(d):a(d)}function o(d){return Ln(d)?c(d):r(d)}function c(d){re
Line truncated
|
||||
https://github.com/highlightjs/highlight.js/issues/2277`),Ht=ye,ft=Le),Qe===void 0&&(Qe=!0);const gn={code:ft,language:Ht};Jr("before:highlight",gn);const Rn=gn.result?gn.result:vr(gn.language,gn.code,Qe);return Rn.code=gn.code,Jr("after:highlight",Rn),Rn}function vr(ye,Le,Qe,ft){const Ht=Object.create(null);function gn(_e,Re){return _e.keywords[Re]}function Rn(){if(!qe.keywords){en.addText(xt);return}let _e=0;qe.keywordPatternRe.lastIndex=0;let Re=qe.keywordPatternRe.exec(xt),Ye="";for(;Re;){Ye+=xt.substring(_e,Re.index);const rt=dn.case_insensitive?Re[0].toLowerCase():Re[0],$t=gn(qe,rt);if($t){const[or,sl]=$t;if(en.addText(Ye),Ye="",Ht[rt]=(Ht[rt]||0)+1,Ht[rt]<=Xn&&(ji+=sl),or.startsWith("_"))Ye+=Re[0];else{const Fo=dn.classNameAliases[or]||or;jn(Re[0],Fo)}}else Ye+=Re[0];_e=qe.keywordPatternRe.lastIndex,Re=qe.keywordPatternRe.exec(xt)}Ye+=xt.substring(_e),en.addText(Ye)}function kn(){if(xt==="")return;let _e=null;if(typeof qe.subLanguage=="string"){if(!me[qe.subLanguage]){en.addText(xt);return}_e=vr(qe.subLanguage,xt,!0,Po[qe.subLanguage]),Po[qe.subLanguage]=_e._top}else _e=Ci(xt,qe.subLanguage.length?qe.subLanguage:null);qe.relevance>0&&(ji+=_e.relevance),en.__addSublanguage(_e._emitter,_e.language)}function _t(){qe.subLanguage!=null?kn():Rn(),xt=""}function jn(_e,Re){_e!==""&&(en.startScope(Re),en.addText(_e),en.endScope())}function rs(_e,Re){let Ye=1;const rt=Re.length-1;for(;Ye<=rt;){if(!_e._emit[Ye]){Ye++;continue}const $t=dn.classNameAliases[_e[Ye]]||_e[Ye],or=Re[Ye];$t?jn(or,$t):(xt=or,Rn(),xt=""),Ye++}}function Mi(_e,Re){return _e.scope&&typeof _e.scope=="string"&&en.openNode(dn.classNameAliases[_e.scope]||_e.scope),_e.beginScope&&(_e.beginScope._wrap?(jn(xt,dn.classNameAliases[_e.beginScope._wrap]||_e.beginScope._wrap),xt=""):_e.beginScope._multi&&(rs(_e.beginScope,Re),xt="")),qe=Object.create(_e,{parent:{value:qe}}),qe}function Ur(_e,Re,Ye){let rt=E(_e.endRe,Ye);if(rt){if(_e["on:end"]){const $t=new t(_e);_e["on:end"](Re,$t),$t.isMatchIgnored&&(rt=!1)}if(rt){for(;_e.endsParent&&_e.parent;)_e=_e.parent;return _e}}if(_e.endsWithParent)return Ur(_e.parent,Re,Ye)}function Oi(_e){return qe.matcher.regexIndex===0?(xt+=_e[0],1):(Di=!0,0)}function is(_e){const Re=_e[0],Ye=_e.rule,rt=new t(Ye),$t=[Ye.__beforeBegin,Ye["on:begin"]];for(const or of $t)if(or&&(or(_e,rt),rt.isMatchIgnored))return Oi(Re);return Ye.skip?xt+=Re:(Ye.excludeBegin&&(xt+=Re),_t(),!Ye.returnBegin&&!Ye.excludeBegin&&(xt=Re)),Mi(Ye,_e),Ye.returnBegin?0:Re.length}function Cn(_e){const Re=_e[0],Ye=Le.substring(_e.index),rt=Ur(qe,_e,Ye);if(!rt)return nt;const $t=qe;qe.endScope&&qe.endScope._wrap?(_t(),jn(Re,qe.endScope._wrap)):qe.endScope&&qe.endScope._multi?(_t(),rs(qe.endScope,_e)):$t.skip?xt+=Re:($t.returnEnd||$t.excludeEnd||(xt+=Re),_t(),$t.excludeEnd&&(xt=Re));do qe.scope&&en.closeNode(),!qe.skip&&!qe.subLanguage&&(ji+=qe.relevance),qe=qe.parent;while(qe!==rt.parent);return rt.starts&&Mi(rt.starts,_e),$t.returnEnd?0:Re.length}function ba(){const _e=[];for(let Re=qe;Re!==dn;Re=Re.parent)Re.scope&&_e.unshift(Re.scope);_e.forEach(Re=>en.openNode(Re))}let Sr={};function Ri(_e,Re){const Ye=Re&&Re[0];if(xt+=_e,Ye==null)return _t(),0;if(Sr.type==="begin"&&Re.type==="end"&&Sr.index===Re.index&&Ye===""){if(xt+=Le.slice(Re.index,Re.index+1),!St){const rt=new Error(`0 width match regex (${ye})`);throw rt.languageName=ye,rt.badRule=Sr.rule,rt}return 1}if(Sr=Re,Re.type==="begin")return is(Re);if(Re.type==="illegal"&&!Qe){const rt=new Error('Illegal lexeme "'+Ye+'" for mode "'+(qe.scope||"<unnamed>")+'"');throw rt.mode=qe,rt}else if(Re.type==="end"){const rt=Cn(Re);if(rt!==nt)return rt}if(Re.type==="illegal"&&Ye==="")return xt+=`
|
||||
`,1;if(al>1e5&&al>Re.index*3)throw new Error("potential infinite loop, way more iterations than matches");return xt+=Ye,Ye.length}const dn=bn(ye);if(!dn)throw ct(gt.replace("{}",ye)),new Error('Unknown language: "'+ye+'"');const ya=Zt(dn);let as="",qe=ft||ya;const Po={},en=new Se.__emitter(Se);ba();let xt="",ji=0,ei=0,al=0,Di=!1;try{if(dn.__emitTokens)dn.__emitTokens(Le,en);else{for(qe.matcher.considerAll();;){al++,Di?Di=!1:qe.matcher.considerAll(),qe.matcher.lastIndex=ei;const _e=qe.matcher.exec(Le);if(!_e)break;const Re=Le.substring(ei,_e.index),Ye=Ri(Re,_e);ei=_e.index+Ye}Ri(Le.substring(ei))}return en.finalize(),as=en.toHTML(),{language:ye,value:as,relevance:ji,illegal:!1,_emitter:en,_top:qe}}catch(_e){if(_e.message&&_e.message.includes("Illegal"))return{language:ye,value:un(Le),illegal:!0,relevance:0,_illegalBy:{message:_e.message,index:ei,context:Le.slice(ei-100,ei+100),mode:_e.mode,resultSoFar:as},_emitter:en};if(St)return{language:ye,value:un(Le),illegal:!1,relevance:0,errorRaised:_e,_emitter:en,_top:qe};throw _e}}function ki(ye){const Le={value:un(ye),illegal:!1,relevance:0,_top:Me,_emitter:new Se.__emitter(Se)};return Le._emitter.addText(ye),Le}function Ci(ye,Le){Le=Le||Se.languages||Object.keys(me);const Qe=ki(ye),ft=Le.filter(bn).filter(Er).map(_t=>vr(_t,ye,!1));ft.unshift(Qe);const Ht=ft.sort((_t,jn)=>{if(_t.relevance!==jn.relevance)return jn.relevance-_t.relevance;if(_t.language&&jn.language){if(bn(_t.language).supersetOf===jn.language)return 1;if(bn(jn.language).supersetOf===_t.language)return-1}return 0}),[gn,Rn]=Ht,kn=gn;return kn.secondBest=Rn,kn}function lr(ye,Le,Qe){const ft=Le&&Ee[Le]||Qe;ye.classList.add("hljs"),ye.classList.add(`language-${ft}`)}function Ut(ye){let Le=null;const Qe=Bt(ye);if(Ue(Qe))return;if(Jr("before:highlightElement",{el:ye,language:Qe}),ye.dataset.highlighted){console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",ye);return}if(ye.children.length>0&&(Se.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(ye)),Se.throwUnescapedHTML))throw new In("One of your code blocks includes unescaped HTML.",ye.innerHTML);Le=ye;const ft=Le.textContent,Ht=Qe?Mt(ft,{language:Qe,ignoreIllegals:!0}):Ci(ft);ye.innerHTML=Ht.value,ye.dataset.highlighted="yes",lr(ye,Qe,Ht.language),ye.result={language:Ht.language,re:Ht.relevance,relevance:Ht.relevance},Ht.secondBest&&(ye.secondBest={language:Ht.secondBest.language,relevance:Ht.secondBest.relevance}),Jr("after:highlightElement",{el:ye,result:Ht,text:ft})}function pn(ye){Se=Si(Se,ye)}const _r=()=>{Ai(),ue("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")};function Ti(){Ai(),ue("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")}let ga=!1;function Ai(){function ye(){Ai()}if(document.readyState==="loading"){ga||window.addEventListener("DOMContentLoaded",ye,!1),ga=!0;return}document.querySelectorAll(Se.cssSelector).forEach(Ut)}function ts(ye,Le){let Qe=null;try{Qe=Le(re)}catch(ft){if(ct("Language definition for '{}' could not be registered.".replace("{}",ye)),St)ct(ft);else throw ft;Qe=Me}Qe.name||(Qe.name=ye),me[ye]=Qe,Qe.rawDefinition=Le.bind(null,re),Qe.aliases&&wr(Qe.aliases,{languageName:ye})}function Wr(ye){delete me[ye];for(const Le of Object.keys(Ee))Ee[Le]===ye&&delete Ee[Le]}function xa(){return Object.keys(me)}function bn(ye){return ye=(ye||"").toLowerCase(),me[ye]||me[Ee[ye]]}function wr(ye,{languageName:Le}){typeof ye=="string"&&(ye=[ye]),ye.forEach(Qe=>{Ee[Qe.toLowerCase()]=Le})}function Er(ye){const Le=bn(ye);return Le&&!Le.disableAutodetect}function Br(ye){ye["before:highlightBlock"]&&!ye["before:highlightElement"]&&(ye["before:highlightElement"]=Le=>{ye["before:highlightBlock"](Object.assign({block:Le.el},Le))}),ye["after:highlightBlock"]&&!ye["after:highlightElement"]&&(ye["after:highlightElement"]=Le=>{ye["after:highlightBlock"](Object.assign({block:Le.el},Le))})}function Gt(ye){Br(ye),Pe.push(ye)}function ns(ye){const Le=Pe.indexOf(ye);Le!==-1&&Pe.splice(Le,1)}function Jr(ye,Le){const Qe=ye;Pe.forEach(function(ft){ft[Qe]&&ft[Qe](Le)})}function Nr(ye){return ue("10.7.0","highlightBlock will be removed entirely in v12.0"),ue("10.7.0","Please use highlightElement now."),Ut(ye)}Object.assign(re,{highlight:Mt,highlightAuto:Ci,highlightAll:Ai,highlightElement:Ut,highlightBlock:Nr,configure:pn,initHighlighting:_r,initHighlightingOnLoad:Ti,registerLanguage:ts,unregisterLanguage:Wr,listLanguages:xa,getLanguage:bn,registerAliases:wr,autoDetection:Er,inherit:Si,addPlugin:Gt,removePlugin:ns}),re.debugMode=function(){St=!1},re.safeMode=function(){St=!0},re.versionString=ut,re.regex={concat:N,lookahead:b,either:w,optional:_,anyNumberOfTimes:y};for(const ye in oe)typeof oe[ye]=="object"&&e(oe[ye]);return Object.assign(re,oe),re},mn=On({})
Line truncated
|
||||
`},_]}}return em=s,em}var tm,Ev;function Sj(){if(Ev)return tm;Ev=1;const e=p=>({IMPORTANT:{scope:"meta",begin:"!important"},BLOCK_COMMENT:p.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[p.APOS_STRING_MODE,p.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{scope:"number",begin:p.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/}}),t=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","optgroup","option","p","picture","q","quote","samp","section","select","source","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],r=["defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],a=[...t,...r],s=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),o=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),c=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),d=["accent-color","align-content","align-items","align-self","alignment-baseline","all","anchor-name","animation","animation-composition","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-range","animation-range-end","animation-range-start","animation-timeline","animation-timing-function","appearance","aspect-ratio","backdrop-filter","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-position-x","background-position-y","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-end-end-radius","border-end-start-radius","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-start-end-radius","border-start-start-radius","border-style","border-top","border-top
Line truncated
|
||||
]`,N={scope:"string",variants:[g,p,b,y]},S={scope:"number",variants:[{begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?"}],relevance:0},w=["false","null","true"],C=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],E=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],B={keyword:C,literal:($=>{const O=[];return $.forEach(U=>{O.push(U),U.toLowerCase()===U?O.push(U.toUpperCase()):O.push(U.toLowerCase())}),O})(w),built_in:E},R=$=>$.map(O=>O.replace(/\|\d+$/,"")),H={variants:[{match:[/new/,r.concat(_,"+"),r.concat("(?!",R(E).join("\\b|"),"\\b)"),o],scope:{1:"keyword",4:"title.class"}}]},z=r.concat(s,"\\b(?!\\()"),Y={variants:[{match:[r.concat(/::/,r.lookahead(/(?!class\b)/)),z],scope:{2:"variable.constant"}},{match:[/::/,/class/],scope:{2:"variable.language"}},{match:[o,r.concat(/::/,r.lookahead(/(?!class\b)/)),z],scope:{1:"title.class",3:"variable.constant"}},{match:[o,r.concat("::",r.lookahead(/(?!class\b)/))],scope:{1:"title.class"}},{match:[o,/::/,/class/],scope:{1:"title.class",3:"variable.language"}}]},j={scope:"attr",match:r.concat(s,r.lookahead(":"),r.lookahead(/(?!::)/))},I={relevance:0,begin:/\(/,end:/\)/,keywords:B,contains:[j,d,Y,t.C_BLOCK_COMMENT_MODE,N,S,H]},Z={relevance:0,match:[/\b/,r.concat("(?!fn\\b|function\\b|",R(C).join("\\b|"),"|",R(E).join("\\b|"),"\\b)"),s,r.concat(_,"*"),r.lookahead(/(?=\()/)],scope:{3:"title.function.invoke"},contains:[I]};I.contains.push(Z);const P=[j,Y,t.C_BLOCK_COMMENT_MODE,N,S,H],k={begin:r.concat(/#\[\s*\\?/,r.either(o,c)),beginScope:"meta",end:/]/,endScope:"meta",keywords:{literal:w,keyword:["new","array"]},contains:[{begin:/\[/,end:/]/,keywords:{literal:w,keyword:["new","array"]},contains:["self",...P]},...P,{scope:"meta",variants:[{match:o},{match:c}]}]};return{case_insensitive:!1,keywords:B,contains:[k,t.HASH_COMMENT_MODE,t.COMMENT("//","$"),t.COMMENT("/\\*","\\*/",{contains:[{scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/,keywords:"__halt_compiler",starts:{scope:"comment",end:t.MATCH_NOTHING_RE,contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},h,{scope:"variable.language",match:/\$this\b/},d,Z,Y,{match:[/const/,/\s/,s],scope:{1:"keyword",3:"variable.constant"}},H,{scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use"},t.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:B,contains:["self",k,d,Y,t.C_BLOCK_COMMENT_MODE,N,S]}]},{scope:"class",variants:[{beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"cl
Line truncated
|
||||
`)||c))return m.jsx("code",{className:`${e||""} bg-white/8 px-1.5 py-0.5 rounded text-[13px]`,children:t});const h=((w=r==null?void 0:r.data)==null?void 0:w.meta)||((C=r==null?void 0:r.properties)==null?void 0:C.metastring)||"",f=/title=["']?([^"'\s}]+)["']?/.exec(h),p=/startLineNumber=(\d+)/.exec(h),g=(f==null?void 0:f[1])||null,b=p?parseInt(p[1],10):1,y=g?p?`${g}:${b}`:g:null,N=_E(o,c==null?void 0:c[1]).split(`
|
||||
`),S=()=>{Tp(o),s(!0),setTimeout(()=>s(!1),2e3)};return m.jsxs("div",{className:"group/code relative rounded-md border border-[#2a2a2a] my-4 text-[#ddd] overflow-hidden",children:[y?m.jsxs("div",{className:"flex items-stretch",children:[m.jsxs("span",{className:"relative flex items-center text-[13px] text-[#999] font-mono px-4 py-2 border-r border-[#2a2a2a]",children:[y,m.jsx("span",{className:"absolute top-0 inset-x-0 h-0.5 bg-white/60 rounded-full"})]}),m.jsx("div",{className:"flex-1 border-b border-[#2a2a2a]"}),m.jsx("button",{onClick:S,className:"px-3 py-2 text-[#555] hover:text-white transition-colors border-b border-[#2a2a2a]","aria-label":"Copy code",children:a?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})})]}):m.jsx("button",{onClick:S,className:"absolute top-2 right-2 z-10 p-1 rounded text-[#444] hover:text-white opacity-0 group-hover/code:opacity-100 transition-opacity","aria-label":"Copy code",children:a?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})}),m.jsx("div",{className:"overflow-auto max-h-[400px]",children:m.jsx("table",{className:"w-full border-collapse font-mono text-[12px] leading-[22px] [font-variant-ligatures:none]",children:m.jsx("tbody",{children:N.map((E,A)=>m.jsxs("tr",{children:[m.jsx("td",{className:"select-none w-[1px] whitespace-nowrap px-4 text-right text-[#555] align-top text-[12px] leading-[22px] border-r border-[#2a2a2a]",children:b+A}),m.jsx("td",{className:"pl-4 pr-4 whitespace-pre",dangerouslySetInnerHTML:{__html:E||`
|
||||
`}})]},A))})})})]})}function Wp(){return e=>{const t=r=>{var a;if(r.type==="element"&&r.tagName==="pre"&&r.children){const s=r.children.find(o=>o.type==="element"&&o.tagName==="code");(a=s==null?void 0:s.data)!=null&&a.meta&&(s.properties=s.properties||{},s.properties.metastring=s.data.meta)}r.children&&r.children.forEach(s=>t(s))};t(e)}}const Jp={code:wE,pre:({children:e})=>m.jsx(m.Fragment,{children:e})};function ca({title:e,content:t,action:r}){return m.jsxs("section",{children:[(e||r)&&m.jsxs("div",{className:"flex items-center justify-between gap-3 mb-3",children:[e?m.jsx("h2",{className:"text-xl font-semibold text-white",children:e}):m.jsx("span",{}),r]}),m.jsx("div",{className:"prose-markdown",children:m.jsx(Yp,{remarkPlugins:[Qp],rehypePlugins:[Wp],components:Jp,children:t})})]})}class Wj{diff(t,r,a={}){let s;typeof a=="function"?(s=a,a={}):"callback"in a&&(s=a.callback);const o=this.castInput(t,a),c=this.castInput(r,a),d=this.removeEmpty(this.tokenize(o,a)),h=this.removeEmpty(this.tokenize(c,a));return this.diffWithOptionsObj(d,h,a,s)}diffWithOptionsObj(t,r,a,s){var o;const c=C=>{if(C=this.postProcess(C,a),s){setTimeout(function(){s(C)},0);return}else return C},d=r.length,h=t.length;let f=1,p=d+h;a.maxEditLength!=null&&(p=Math.min(p,a.maxEditLength));const g=(o=a.timeout)!==null&&o!==void 0?o:1/0,b=Date.now()+g,y=[{oldPos:-1,lastComponent:void 0}];let _=this.extractCommon(y[0],r,t,0,a);if(y[0].oldPos+1>=h&&_+1>=d)return c(this.buildValues(y[0].lastComponent,r,t));let N=-1/0,S=1/0;const w=()=>{for(let C=Math.max(N,-f);C<=Math.min(S,f);C+=2){let E;const A=y[C-1],B=y[C+1];A&&(y[C-1]=void 0);let R=!1;if(B){const z=B.oldPos-C;R=B&&0<=z&&z<d}const H=A&&A.oldPos+1<h;if(!R&&!H){y[C]=void 0;continue}if(!H||R&&A.oldPos<B.oldPos?E=this.addToPath(B,!0,!1,0,a):E=this.addToPath(A,!1,!0,1,a),_=this.extractCommon(E,r,t,C,a),E.oldPos+1>=h&&_+1>=d)return c(this.buildValues(E.lastComponent,r,t))||!0;y[C]=E,E.oldPos+1>=h&&(S=Math.min(S,C-1)),_+1>=d&&(N=Math.max(N,C+1))}f++};if(s)(function C(){setTimeout(function(){if(f>p||Date.now()>b)return s(void 0);w()||C()},0)})();else for(;f<=p&&Date.now()<=b;){const C=w();if(C)return C}}addToPath(t,r,a,s,o){const c=t.lastComponent;return c&&!o.oneChangePerToken&&c.added===r&&c.removed===a?{oldPos:t.oldPos+s,lastComponent:{count:c.count+1,added:r,removed:a,previousComponent:c.previousComponent}}:{oldPos:t.oldPos+s,lastComponent:{count:1,added:r,removed:a,previousComponent:c}}}extractCommon(t,r,a,s,o){const c=r.length,d=a.length;let h=t.oldPos,f=h-s,p=0;for(;f+1<c&&h+1<d&&this.equals(a[h+1],r[f+1],o);)f++,h++,p++,o.oneChangePerToken&&(t.lastComponent={count:1,previousComponent:t.lastComponent,added:!1,removed:!1});return p&&!o.oneChangePerToken&&(t.lastComponent={count:p,previousComponent:t.lastComponent,added:!1,removed:!1}),t.oldPos=h,f}equals(t,r,a){return a.comparator?a.comparator(t,r):t===r||!!a.ignoreCase&&t.toLowerCase()===r.toLowerCase()}removeEmpty(t){const r=[];for(let a=0;a<t.length;a++)t[a]&&r.push(t[a]);return r}castInput(t,r){return t}tokenize(t,r){return Array.from(t)}join(t){return t.join("")}postProcess(t,r){return t}get useLongestToken(){return!1}buildValues(t,r,a){const s=[];let o;for(;t;)s.push(t),o=t.previousComponent,delete t.previousComponent,t=o;s.reverse();const c=s.length;let d=0,h=0,f=0;for(;d<c;d++){const p=s[d];if(p.removed)p.value=this.join(a.slice(f,f+p.count)),f+=p.count;else{if(!p.added&&this.useLongestToken){let g=r.slice(h,h+p.count);g=g.map(function(b,y){const _=a[f+y];return _.length>b.length?_:b}),p.value=this.join(g)}else p.value=this.join(r.slice(h,h+p.count));h+=p.count,p.added||(f+=p.count)}}return s}}class Jj extends Wj{constructor(){super(...arguments),this.tokenize=nD}equals(t,r,a){return a.ignoreWhitespace?((!a.newlineIsToken||!t.includes(`
|
||||
`))}function h(y,_,N,S){const w=N.enter("tableCell"),C=N.enter("phrasing"),E=N.containerPhrasing(y,{...S,before:o,after:o});return C(),w(),E}function f(y,_){return j3(y,{align:_,alignDelimiters:a,padding:r,stringLength:s})}function p(y,_,N){const S=y.children;let w=-1;const C=[],E=_.enter("table");for(;++w<S.length;)C[w]=g(S[w],_,N);return E(),C}function g(y,_,N){const S=y.children;let w=-1;const C=[],E=_.enter("tableRow");for(;++w<S.length;)C[w]=h(S[w],y,_,N);return E(),C}function b(y,_,N){let S=oE.inlineCode(y,_,N);return N.stack.includes("tableCell")&&(S=S.replace(/\|/g,"\\$&")),S}}function v4(){return{exit:{taskListCheckValueChecked:nv,taskListCheckValueUnchecked:nv,paragraph:w4}}}function _4(){return{unsafe:[{atBreak:!0,character:"-",after:"[:|-]"}],handlers:{listItem:E4}}}function nv(e){const t=this.stack[this.stack.length-2];t.type,t.checked=e.type==="taskListCheckValueChecked"}function w4(e){const t=this.stack[this.stack.length-2];if(t&&t.type==="listItem"&&typeof t.checked=="boolean"){const r=this.stack[this.stack.length-1];r.type;const a=r.children[0];if(a&&a.type==="text"){const s=t.children;let o=-1,c;for(;++o<s.length;){const d=s[o];if(d.type==="paragraph"){c=d;break}}c===r&&(a.value=a.value.slice(1),a.value.length===0?r.children.shift():r.position&&a.position&&typeof a.position.start.offset=="number"&&(a.position.start.column++,a.position.start.offset++,r.position.start=Object.assign({},a.position.start)))}}this.exit(e)}function E4(e,t,r,a){const s=e.children[0],o=typeof e.checked=="boolean"&&s&&s.type==="paragraph",c="["+(e.checked?"x":" ")+"] ",d=r.createTracker(a);o&&d.move(c);let h=oE.listItem(e,t,r,{...a,...d.current()});return o&&(h=h.replace(/^(?:[*+-]|\d+\.)([\r\n]| {1,3})/,f)),h;function f(p){return p+c}}function N4(){return[n3(),E3(),C3(),h4(),v4()]}function S4(e){return{extensions:[r3(),N3(e),T3(),y4(e),_4()]}}const k4={tokenize:R4,partial:!0},cE={tokenize:j4,partial:!0},uE={tokenize:D4,partial:!0},dE={tokenize:L4,partial:!0},C4={tokenize:z4,partial:!0},fE={name:"wwwAutolink",tokenize:M4,previous:mE},hE={name:"protocolAutolink",tokenize:O4,previous:pE},Ei={name:"emailAutolink",tokenize:A4,previous:gE},Kr={};function T4(){return{text:Kr}}let Ia=48;for(;Ia<123;)Kr[Ia]=Ei,Ia++,Ia===58?Ia=65:Ia===91&&(Ia=97);Kr[43]=Ei;Kr[45]=Ei;Kr[46]=Ei;Kr[95]=Ei;Kr[72]=[Ei,hE];Kr[104]=[Ei,hE];Kr[87]=[Ei,fE];Kr[119]=[Ei,fE];function A4(e,t,r){const a=this;let s,o;return c;function c(g){return!rp(g)||!gE.call(a,a.previous)||Xp(a.events)?r(g):(e.enter("literalAutolink"),e.enter("literalAutolinkEmail"),d(g))}function d(g){return rp(g)?(e.consume(g),d):g===64?(e.consume(g),h):r(g)}function h(g){return g===46?e.check(C4,p,f)(g):g===45||g===95||An(g)?(o=!0,e.consume(g),h):p(g)}function f(g){return e.consume(g),s=!0,h}function p(g){return o&&s&&Ln(a.previous)?(e.exit("literalAutolinkEmail"),e.exit("literalAutolink"),t(g)):r(g)}}function M4(e,t,r){const a=this;return s;function s(c){return c!==87&&c!==119||!mE.call(a,a.previous)||Xp(a.events)?r(c):(e.enter("literalAutolink"),e.enter("literalAutolinkWww"),e.check(k4,e.attempt(cE,e.attempt(uE,o),r),r)(c))}function o(c){return e.exit("literalAutolinkWww"),e.exit("literalAutolink"),t(c)}}function O4(e,t,r){const a=this;let s="",o=!1;return c;function c(g){return(g===72||g===104)&&pE.call(a,a.previous)&&!Xp(a.events)?(e.enter("literalAutolink"),e.enter("literalAutolinkHttp"),s+=String.fromCodePoint(g),e.consume(g),d):r(g)}function d(g){if(Ln(g)&&s.length<5)return s+=String.fromCodePoint(g),e.consume(g),d;if(g===58){const b=s.toLowerCase();if(b==="http"||b==="https")return e.consume(g),h}return r(g)}function h(g){return g===47?(e.consume(g),o?f:(o=!0,h)):r(g)}function f(g){return g===null||Su(g)||Tt(g)||Va(g)||Yu(g)?r(g):e.attempt(cE,e.attempt(uE,p),r)(g)}function p(g){return e.exit("literalAutolinkHttp"),e.exit("literalAutolink"),t(g)}}function R4(e,t,r){let a=0;return s;function s(c){return(c===87||c===119)&&a<3?(a++,e.consume(c),s):c===46&&a===3?(e.consume(c),o):r(c)}function o(c){return c===null?r(c):t(c)}}function j4(e,t,r){let a,s,o;return c;function c(f){return f===46||f===95?e.check(dE,h,d)(f):f===null||Tt(f)||Va(f)||f!==45&&Yu(f)?h(f):(o=!0,e.consume(f),c)}function d(f){return f===95?a=!0:(s=a,a=void 0),e.consume(f),c}function h(f){return s||a||!o?r(f):t(f)}}function D4(e,t){let r=0,a=0;return s;function s(c){return c===40?(r++,e.consume(c),s):c===41&&a<r?o(c):c===33||c===34||c===38||c===39||c===41||c===42||c===44||c===46||c===58||c===59||c===60||c===63||c===93||c===95||c===126?e.check(dE,t,o)(c):c===null||Tt(c)||Va(c)?t(c):(e.consume(c),s)}function o(c){return c===41&&a++,e.consume(c),s}}function L4(e,t,r){return a;function a(d){return d===33||d===34||d===39||d===41||d===42||d===44||d===46||d===58||d===59||d===63||d===95||d===126?(e.consume(d),a):d===38?(e.consume(d),o):d===93?(e.consume(d),s):d===60||d===null||Tt(d)||Va(d)?t(d):r(d)}function s(d){return d===null||d===40||d===91||Tt(d)||Va(d)?t(d):a(d)}function o(d){return Ln(d)?c(d):r(d)}function c(d){re
Line truncated
|
||||
https://github.com/highlightjs/highlight.js/issues/2277`),Ht=ye,ft=Le),Qe===void 0&&(Qe=!0);const gn={code:ft,language:Ht};Jr("before:highlight",gn);const Rn=gn.result?gn.result:yr(gn.language,gn.code,Qe);return Rn.code=gn.code,Jr("after:highlight",Rn),Rn}function yr(ye,Le,Qe,ft){const Ht=Object.create(null);function gn(_e,Re){return _e.keywords[Re]}function Rn(){if(!qe.keywords){en.addText(xt);return}let _e=0;qe.keywordPatternRe.lastIndex=0;let Re=qe.keywordPatternRe.exec(xt),Ye="";for(;Re;){Ye+=xt.substring(_e,Re.index);const rt=dn.case_insensitive?Re[0].toLowerCase():Re[0],$t=gn(qe,rt);if($t){const[or,sl]=$t;if(en.addText(Ye),Ye="",Ht[rt]=(Ht[rt]||0)+1,Ht[rt]<=Xn&&(Ri+=sl),or.startsWith("_"))Ye+=Re[0];else{const Po=dn.classNameAliases[or]||or;jn(Re[0],Po)}}else Ye+=Re[0];_e=qe.keywordPatternRe.lastIndex,Re=qe.keywordPatternRe.exec(xt)}Ye+=xt.substring(_e),en.addText(Ye)}function kn(){if(xt==="")return;let _e=null;if(typeof qe.subLanguage=="string"){if(!me[qe.subLanguage]){en.addText(xt);return}_e=yr(qe.subLanguage,xt,!0,qo[qe.subLanguage]),qo[qe.subLanguage]=_e._top}else _e=ki(xt,qe.subLanguage.length?qe.subLanguage:null);qe.relevance>0&&(Ri+=_e.relevance),en.__addSublanguage(_e._emitter,_e.language)}function _t(){qe.subLanguage!=null?kn():Rn(),xt=""}function jn(_e,Re){_e!==""&&(en.startScope(Re),en.addText(_e),en.endScope())}function rs(_e,Re){let Ye=1;const rt=Re.length-1;for(;Ye<=rt;){if(!_e._emit[Ye]){Ye++;continue}const $t=dn.classNameAliases[_e[Ye]]||_e[Ye],or=Re[Ye];$t?jn(or,$t):(xt=or,Rn(),xt=""),Ye++}}function Ai(_e,Re){return _e.scope&&typeof _e.scope=="string"&&en.openNode(dn.classNameAliases[_e.scope]||_e.scope),_e.beginScope&&(_e.beginScope._wrap?(jn(xt,dn.classNameAliases[_e.beginScope._wrap]||_e.beginScope._wrap),xt=""):_e.beginScope._multi&&(rs(_e.beginScope,Re),xt="")),qe=Object.create(_e,{parent:{value:qe}}),qe}function Ur(_e,Re,Ye){let rt=E(_e.endRe,Ye);if(rt){if(_e["on:end"]){const $t=new t(_e);_e["on:end"](Re,$t),$t.isMatchIgnored&&(rt=!1)}if(rt){for(;_e.endsParent&&_e.parent;)_e=_e.parent;return _e}}if(_e.endsWithParent)return Ur(_e.parent,Re,Ye)}function Mi(_e){return qe.matcher.regexIndex===0?(xt+=_e[0],1):(ji=!0,0)}function is(_e){const Re=_e[0],Ye=_e.rule,rt=new t(Ye),$t=[Ye.__beforeBegin,Ye["on:begin"]];for(const or of $t)if(or&&(or(_e,rt),rt.isMatchIgnored))return Mi(Re);return Ye.skip?xt+=Re:(Ye.excludeBegin&&(xt+=Re),_t(),!Ye.returnBegin&&!Ye.excludeBegin&&(xt=Re)),Ai(Ye,_e),Ye.returnBegin?0:Re.length}function Cn(_e){const Re=_e[0],Ye=Le.substring(_e.index),rt=Ur(qe,_e,Ye);if(!rt)return nt;const $t=qe;qe.endScope&&qe.endScope._wrap?(_t(),jn(Re,qe.endScope._wrap)):qe.endScope&&qe.endScope._multi?(_t(),rs(qe.endScope,_e)):$t.skip?xt+=Re:($t.returnEnd||$t.excludeEnd||(xt+=Re),_t(),$t.excludeEnd&&(xt=Re));do qe.scope&&en.closeNode(),!qe.skip&&!qe.subLanguage&&(Ri+=qe.relevance),qe=qe.parent;while(qe!==rt.parent);return rt.starts&&Ai(rt.starts,_e),$t.returnEnd?0:Re.length}function ba(){const _e=[];for(let Re=qe;Re!==dn;Re=Re.parent)Re.scope&&_e.unshift(Re.scope);_e.forEach(Re=>en.openNode(Re))}let Nr={};function Oi(_e,Re){const Ye=Re&&Re[0];if(xt+=_e,Ye==null)return _t(),0;if(Nr.type==="begin"&&Re.type==="end"&&Nr.index===Re.index&&Ye===""){if(xt+=Le.slice(Re.index,Re.index+1),!St){const rt=new Error(`0 width match regex (${ye})`);throw rt.languageName=ye,rt.badRule=Nr.rule,rt}return 1}if(Nr=Re,Re.type==="begin")return is(Re);if(Re.type==="illegal"&&!Qe){const rt=new Error('Illegal lexeme "'+Ye+'" for mode "'+(qe.scope||"<unnamed>")+'"');throw rt.mode=qe,rt}else if(Re.type==="end"){const rt=Cn(Re);if(rt!==nt)return rt}if(Re.type==="illegal"&&Ye==="")return xt+=`
|
||||
`,1;if(al>1e5&&al>Re.index*3)throw new Error("potential infinite loop, way more iterations than matches");return xt+=Ye,Ye.length}const dn=bn(ye);if(!dn)throw ct(gt.replace("{}",ye)),new Error('Unknown language: "'+ye+'"');const ya=Zt(dn);let as="",qe=ft||ya;const qo={},en=new Se.__emitter(Se);ba();let xt="",Ri=0,ei=0,al=0,ji=!1;try{if(dn.__emitTokens)dn.__emitTokens(Le,en);else{for(qe.matcher.considerAll();;){al++,ji?ji=!1:qe.matcher.considerAll(),qe.matcher.lastIndex=ei;const _e=qe.matcher.exec(Le);if(!_e)break;const Re=Le.substring(ei,_e.index),Ye=Oi(Re,_e);ei=_e.index+Ye}Oi(Le.substring(ei))}return en.finalize(),as=en.toHTML(),{language:ye,value:as,relevance:Ri,illegal:!1,_emitter:en,_top:qe}}catch(_e){if(_e.message&&_e.message.includes("Illegal"))return{language:ye,value:un(Le),illegal:!0,relevance:0,_illegalBy:{message:_e.message,index:ei,context:Le.slice(ei-100,ei+100),mode:_e.mode,resultSoFar:as},_emitter:en};if(St)return{language:ye,value:un(Le),illegal:!1,relevance:0,errorRaised:_e,_emitter:en,_top:qe};throw _e}}function Si(ye){const Le={value:un(ye),illegal:!1,relevance:0,_top:Me,_emitter:new Se.__emitter(Se)};return Le._emitter.addText(ye),Le}function ki(ye,Le){Le=Le||Se.languages||Object.keys(me);const Qe=Si(ye),ft=Le.filter(bn).filter(wr).map(_t=>yr(_t,ye,!1));ft.unshift(Qe);const Ht=ft.sort((_t,jn)=>{if(_t.relevance!==jn.relevance)return jn.relevance-_t.relevance;if(_t.language&&jn.language){if(bn(_t.language).supersetOf===jn.language)return 1;if(bn(jn.language).supersetOf===_t.language)return-1}return 0}),[gn,Rn]=Ht,kn=gn;return kn.secondBest=Rn,kn}function lr(ye,Le,Qe){const ft=Le&&Ee[Le]||Qe;ye.classList.add("hljs"),ye.classList.add(`language-${ft}`)}function Ut(ye){let Le=null;const Qe=Bt(ye);if(Ue(Qe))return;if(Jr("before:highlightElement",{el:ye,language:Qe}),ye.dataset.highlighted){console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",ye);return}if(ye.children.length>0&&(Se.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(ye)),Se.throwUnescapedHTML))throw new In("One of your code blocks includes unescaped HTML.",ye.innerHTML);Le=ye;const ft=Le.textContent,Ht=Qe?Mt(ft,{language:Qe,ignoreIllegals:!0}):ki(ft);ye.innerHTML=Ht.value,ye.dataset.highlighted="yes",lr(ye,Qe,Ht.language),ye.result={language:Ht.language,re:Ht.relevance,relevance:Ht.relevance},Ht.secondBest&&(ye.secondBest={language:Ht.secondBest.language,relevance:Ht.secondBest.relevance}),Jr("after:highlightElement",{el:ye,result:Ht,text:ft})}function pn(ye){Se=Ni(Se,ye)}const vr=()=>{Ti(),ue("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")};function Ci(){Ti(),ue("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")}let ga=!1;function Ti(){function ye(){Ti()}if(document.readyState==="loading"){ga||window.addEventListener("DOMContentLoaded",ye,!1),ga=!0;return}document.querySelectorAll(Se.cssSelector).forEach(Ut)}function ts(ye,Le){let Qe=null;try{Qe=Le(re)}catch(ft){if(ct("Language definition for '{}' could not be registered.".replace("{}",ye)),St)ct(ft);else throw ft;Qe=Me}Qe.name||(Qe.name=ye),me[ye]=Qe,Qe.rawDefinition=Le.bind(null,re),Qe.aliases&&_r(Qe.aliases,{languageName:ye})}function Wr(ye){delete me[ye];for(const Le of Object.keys(Ee))Ee[Le]===ye&&delete Ee[Le]}function xa(){return Object.keys(me)}function bn(ye){return ye=(ye||"").toLowerCase(),me[ye]||me[Ee[ye]]}function _r(ye,{languageName:Le}){typeof ye=="string"&&(ye=[ye]),ye.forEach(Qe=>{Ee[Qe.toLowerCase()]=Le})}function wr(ye){const Le=bn(ye);return Le&&!Le.disableAutodetect}function Br(ye){ye["before:highlightBlock"]&&!ye["before:highlightElement"]&&(ye["before:highlightElement"]=Le=>{ye["before:highlightBlock"](Object.assign({block:Le.el},Le))}),ye["after:highlightBlock"]&&!ye["after:highlightElement"]&&(ye["after:highlightElement"]=Le=>{ye["after:highlightBlock"](Object.assign({block:Le.el},Le))})}function Ft(ye){Br(ye),Pe.push(ye)}function ns(ye){const Le=Pe.indexOf(ye);Le!==-1&&Pe.splice(Le,1)}function Jr(ye,Le){const Qe=ye;Pe.forEach(function(ft){ft[Qe]&&ft[Qe](Le)})}function Er(ye){return ue("10.7.0","highlightBlock will be removed entirely in v12.0"),ue("10.7.0","Please use highlightElement now."),Ut(ye)}Object.assign(re,{highlight:Mt,highlightAuto:ki,highlightAll:Ti,highlightElement:Ut,highlightBlock:Er,configure:pn,initHighlighting:vr,initHighlightingOnLoad:Ci,registerLanguage:ts,unregisterLanguage:Wr,listLanguages:xa,getLanguage:bn,registerAliases:_r,autoDetection:wr,inherit:Ni,addPlugin:Ft,removePlugin:ns}),re.debugMode=function(){St=!1},re.safeMode=function(){St=!0},re.versionString=ut,re.regex={concat:N,lookahead:b,either:w,optional:_,anyNumberOfTimes:y};for(const ye in oe)typeof oe[ye]=="object"&&e(oe[ye]);return Object.assign(re,oe),re},mn=On({})
Line truncated
|
||||
`},_]}}return Jh=s,Jh}var em,_v;function wj(){if(_v)return em;_v=1;const e=p=>({IMPORTANT:{scope:"meta",begin:"!important"},BLOCK_COMMENT:p.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[p.APOS_STRING_MODE,p.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{scope:"number",begin:p.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/}}),t=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","optgroup","option","p","picture","q","quote","samp","section","select","source","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],r=["defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],a=[...t,...r],s=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),o=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),c=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),d=["accent-color","align-content","align-items","align-self","alignment-baseline","all","anchor-name","animation","animation-composition","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-range","animation-range-end","animation-range-start","animation-timeline","animation-timing-function","appearance","aspect-ratio","backdrop-filter","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-position-x","background-position-y","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-end-end-radius","border-end-start-radius","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-start-end-radius","border-start-start-radius","border-style","border-top","border-top
Line truncated
|
||||
]`,N={scope:"string",variants:[g,p,b,y]},S={scope:"number",variants:[{begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?"}],relevance:0},w=["false","null","true"],C=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],E=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],B={keyword:C,literal:($=>{const O=[];return $.forEach(U=>{O.push(U),U.toLowerCase()===U?O.push(U.toUpperCase()):O.push(U.toLowerCase())}),O})(w),built_in:E},R=$=>$.map(O=>O.replace(/\|\d+$/,"")),H={variants:[{match:[/new/,r.concat(_,"+"),r.concat("(?!",R(E).join("\\b|"),"\\b)"),o],scope:{1:"keyword",4:"title.class"}}]},z=r.concat(s,"\\b(?!\\()"),Y={variants:[{match:[r.concat(/::/,r.lookahead(/(?!class\b)/)),z],scope:{2:"variable.constant"}},{match:[/::/,/class/],scope:{2:"variable.language"}},{match:[o,r.concat(/::/,r.lookahead(/(?!class\b)/)),z],scope:{1:"title.class",3:"variable.constant"}},{match:[o,r.concat("::",r.lookahead(/(?!class\b)/))],scope:{1:"title.class"}},{match:[o,/::/,/class/],scope:{1:"title.class",3:"variable.language"}}]},j={scope:"attr",match:r.concat(s,r.lookahead(":"),r.lookahead(/(?!::)/))},I={relevance:0,begin:/\(/,end:/\)/,keywords:B,contains:[j,d,Y,t.C_BLOCK_COMMENT_MODE,N,S,H]},Z={relevance:0,match:[/\b/,r.concat("(?!fn\\b|function\\b|",R(C).join("\\b|"),"|",R(E).join("\\b|"),"\\b)"),s,r.concat(_,"*"),r.lookahead(/(?=\()/)],scope:{3:"title.function.invoke"},contains:[I]};I.contains.push(Z);const P=[j,Y,t.C_BLOCK_COMMENT_MODE,N,S,H],k={begin:r.concat(/#\[\s*\\?/,r.either(o,c)),beginScope:"meta",end:/]/,endScope:"meta",keywords:{literal:w,keyword:["new","array"]},contains:[{begin:/\[/,end:/]/,keywords:{literal:w,keyword:["new","array"]},contains:["self",...P]},...P,{scope:"meta",variants:[{match:o},{match:c}]}]};return{case_insensitive:!1,keywords:B,contains:[k,t.HASH_COMMENT_MODE,t.COMMENT("//","$"),t.COMMENT("/\\*","\\*/",{contains:[{scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/,keywords:"__halt_compiler",starts:{scope:"comment",end:t.MATCH_NOTHING_RE,contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},h,{scope:"variable.language",match:/\$this\b/},d,Z,Y,{match:[/const/,/\s/,s],scope:{1:"keyword",3:"variable.constant"}},H,{scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use"},t.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:B,contains:["self",k,d,Y,t.C_BLOCK_COMMENT_MODE,N,S]}]},{scope:"class",variants:[{beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"cl
Line truncated
|
||||
`)||c))return m.jsx("code",{className:`${e||""} bg-white/8 px-1.5 py-0.5 rounded text-[13px]`,children:t});const h=((w=r==null?void 0:r.data)==null?void 0:w.meta)||((C=r==null?void 0:r.properties)==null?void 0:C.metastring)||"",f=/title=["']?([^"'\s}]+)["']?/.exec(h),p=/startLineNumber=(\d+)/.exec(h),g=(f==null?void 0:f[1])||null,b=p?parseInt(p[1],10):1,y=g?p?`${g}:${b}`:g:null,N=xE(o,c==null?void 0:c[1]).split(`
|
||||
`),S=()=>{Cp(o),s(!0),setTimeout(()=>s(!1),2e3)};return m.jsxs("div",{className:"group/code relative rounded-md border border-[#2a2a2a] my-4 text-[#ddd] overflow-hidden",children:[y?m.jsxs("div",{className:"flex items-stretch",children:[m.jsxs("span",{className:"relative flex items-center text-[13px] text-[#999] font-mono px-4 py-2 border-r border-[#2a2a2a]",children:[y,m.jsx("span",{className:"absolute top-0 inset-x-0 h-0.5 bg-white/60 rounded-full"})]}),m.jsx("div",{className:"flex-1 border-b border-[#2a2a2a]"}),m.jsx("button",{onClick:S,className:"px-3 py-2 text-[#555] hover:text-white transition-colors border-b border-[#2a2a2a]","aria-label":"Copy code",children:a?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})})]}):m.jsx("button",{onClick:S,className:"absolute top-2 right-2 z-10 p-1 rounded text-[#444] hover:text-white opacity-0 group-hover/code:opacity-100 transition-opacity","aria-label":"Copy code",children:a?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})}),m.jsx("div",{className:"overflow-auto max-h-[400px]",children:m.jsx("table",{className:"w-full border-collapse font-mono text-[12px] leading-[22px] [font-variant-ligatures:none]",children:m.jsx("tbody",{children:N.map((E,A)=>m.jsxs("tr",{children:[m.jsx("td",{className:"select-none w-[1px] whitespace-nowrap px-4 text-right text-[#555] align-top text-[12px] leading-[22px] border-r border-[#2a2a2a]",children:b+A}),m.jsx("td",{className:"pl-4 pr-4 whitespace-pre",dangerouslySetInnerHTML:{__html:E||`
|
||||
`}})]},A))})})})]})}function Zp(){return e=>{const t=r=>{var a;if(r.type==="element"&&r.tagName==="pre"&&r.children){const s=r.children.find(o=>o.type==="element"&&o.tagName==="code");(a=s==null?void 0:s.data)!=null&&a.meta&&(s.properties=s.properties||{},s.properties.metastring=s.data.meta)}r.children&&r.children.forEach(s=>t(s))};t(e)}}const Qp={code:bE,pre:({children:e})=>m.jsx(m.Fragment,{children:e})};function oa({title:e,content:t,action:r}){return m.jsxs("section",{children:[(e||r)&&m.jsxs("div",{className:"flex items-center justify-between gap-3 mb-3",children:[e?m.jsx("h2",{className:"text-xl font-semibold text-white",children:e}):m.jsx("span",{}),r]}),m.jsx("div",{className:"prose-markdown",children:m.jsx(Gp,{remarkPlugins:[Kp],rehypePlugins:[Zp],components:Qp,children:t})})]})}class Kj{diff(t,r,a={}){let s;typeof a=="function"?(s=a,a={}):"callback"in a&&(s=a.callback);const o=this.castInput(t,a),c=this.castInput(r,a),d=this.removeEmpty(this.tokenize(o,a)),h=this.removeEmpty(this.tokenize(c,a));return this.diffWithOptionsObj(d,h,a,s)}diffWithOptionsObj(t,r,a,s){var o;const c=C=>{if(C=this.postProcess(C,a),s){setTimeout(function(){s(C)},0);return}else return C},d=r.length,h=t.length;let f=1,p=d+h;a.maxEditLength!=null&&(p=Math.min(p,a.maxEditLength));const g=(o=a.timeout)!==null&&o!==void 0?o:1/0,b=Date.now()+g,y=[{oldPos:-1,lastComponent:void 0}];let _=this.extractCommon(y[0],r,t,0,a);if(y[0].oldPos+1>=h&&_+1>=d)return c(this.buildValues(y[0].lastComponent,r,t));let N=-1/0,S=1/0;const w=()=>{for(let C=Math.max(N,-f);C<=Math.min(S,f);C+=2){let E;const A=y[C-1],B=y[C+1];A&&(y[C-1]=void 0);let R=!1;if(B){const z=B.oldPos-C;R=B&&0<=z&&z<d}const H=A&&A.oldPos+1<h;if(!R&&!H){y[C]=void 0;continue}if(!H||R&&A.oldPos<B.oldPos?E=this.addToPath(B,!0,!1,0,a):E=this.addToPath(A,!1,!0,1,a),_=this.extractCommon(E,r,t,C,a),E.oldPos+1>=h&&_+1>=d)return c(this.buildValues(E.lastComponent,r,t))||!0;y[C]=E,E.oldPos+1>=h&&(S=Math.min(S,C-1)),_+1>=d&&(N=Math.max(N,C+1))}f++};if(s)(function C(){setTimeout(function(){if(f>p||Date.now()>b)return s(void 0);w()||C()},0)})();else for(;f<=p&&Date.now()<=b;){const C=w();if(C)return C}}addToPath(t,r,a,s,o){const c=t.lastComponent;return c&&!o.oneChangePerToken&&c.added===r&&c.removed===a?{oldPos:t.oldPos+s,lastComponent:{count:c.count+1,added:r,removed:a,previousComponent:c.previousComponent}}:{oldPos:t.oldPos+s,lastComponent:{count:1,added:r,removed:a,previousComponent:c}}}extractCommon(t,r,a,s,o){const c=r.length,d=a.length;let h=t.oldPos,f=h-s,p=0;for(;f+1<c&&h+1<d&&this.equals(a[h+1],r[f+1],o);)f++,h++,p++,o.oneChangePerToken&&(t.lastComponent={count:1,previousComponent:t.lastComponent,added:!1,removed:!1});return p&&!o.oneChangePerToken&&(t.lastComponent={count:p,previousComponent:t.lastComponent,added:!1,removed:!1}),t.oldPos=h,f}equals(t,r,a){return a.comparator?a.comparator(t,r):t===r||!!a.ignoreCase&&t.toLowerCase()===r.toLowerCase()}removeEmpty(t){const r=[];for(let a=0;a<t.length;a++)t[a]&&r.push(t[a]);return r}castInput(t,r){return t}tokenize(t,r){return Array.from(t)}join(t){return t.join("")}postProcess(t,r){return t}get useLongestToken(){return!1}buildValues(t,r,a){const s=[];let o;for(;t;)s.push(t),o=t.previousComponent,delete t.previousComponent,t=o;s.reverse();const c=s.length;let d=0,h=0,f=0;for(;d<c;d++){const p=s[d];if(p.removed)p.value=this.join(a.slice(f,f+p.count)),f+=p.count;else{if(!p.added&&this.useLongestToken){let g=r.slice(h,h+p.count);g=g.map(function(b,y){const _=a[f+y];return _.length>b.length?_:b}),p.value=this.join(g)}else p.value=this.join(r.slice(h,h+p.count));h+=p.count,p.added||(f+=p.count)}}return s}}class Zj extends Kj{constructor(){super(...arguments),this.tokenize=Jj}equals(t,r,a){return a.ignoreWhitespace?((!a.newlineIsToken||!t.includes(`
|
||||
`))&&(t=t.trim()),(!a.newlineIsToken||!r.includes(`
|
||||
`))&&(r=r.trim())):a.ignoreNewlineAtEof&&!a.newlineIsToken&&(t.endsWith(`
|
||||
`)&&(t=t.slice(0,-1)),r.endsWith(`
|
||||
`)&&(r=r.slice(0,-1))),super.equals(t,r,a)}}const eD=new Jj;function tD(e,t,r){return eD.diff(e,t,r)}function nD(e,t){t.stripTrailingCr&&(e=e.replace(/\r\n/g,`
|
||||
`));const r=[],a=e.split(/(\n|\r\n)/);a[a.length-1]||a.pop();for(let s=0;s<a.length;s++){const o=a[s];s%2&&!t.newlineIsToken?r[r.length-1]+=o:r.push(o)}return r}function rD(e,t){try{return zn.highlight(e,{language:t,ignoreIllegals:!0}).value}catch{return zn.highlightAuto(e).value}}function iD({file:e,startLine:t,endLine:r,before:a,after:s,onCopy:o}){const[c,d]=ee.useState(!1),h=r&&r!==t?`${t}-${r}`:`${t}`,f=PT(e)||"text",p=tD(a,s);let g=t,b=t;const y=p.flatMap(N=>N.value.replace(/\n$/,"").split(`
|
||||
`)&&(r=r.slice(0,-1))),super.equals(t,r,a)}}const Qj=new Zj;function Wj(e,t,r){return Qj.diff(e,t,r)}function Jj(e,t){t.stripTrailingCr&&(e=e.replace(/\r\n/g,`
|
||||
`));const r=[],a=e.split(/(\n|\r\n)/);a[a.length-1]||a.pop();for(let s=0;s<a.length;s++){const o=a[s];s%2&&!t.newlineIsToken?r[r.length-1]+=o:r.push(o)}return r}function eD(e,t){try{return zn.highlight(e,{language:t,ignoreIllegals:!0}).value}catch{return zn.highlightAuto(e).value}}function tD({file:e,startLine:t,endLine:r,before:a,after:s,onCopy:o}){const[c,d]=ee.useState(!1),h=r&&r!==t?`${t}-${r}`:`${t}`,f=HT(e)||"text",p=Wj(a,s);let g=t,b=t;const y=p.flatMap(N=>N.value.replace(/\n$/,"").split(`
|
||||
`).map(S=>{const w=S===""?`
|
||||
`:f!=="text"?rD(S,f):S.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");let C="",E="";return N.removed?C=String(g++):(N.added||(C=String(g++)),E=String(b++)),{highlighted:w,added:!!N.added,removed:!!N.removed,leftNo:C,rightNo:E}})),_=()=>{Tp(s),d(!0),setTimeout(()=>d(!1),2e3),o==null||o()};return m.jsxs("div",{className:"rounded-md border border-[#2a2a2a] overflow-hidden",children:[m.jsxs("div",{className:"flex items-stretch",children:[m.jsxs("span",{className:"relative flex items-center text-[13px] text-[#999] font-mono px-4 py-2 border-r border-[#2a2a2a] break-all",children:[e,":",h,m.jsx("span",{className:"absolute top-0 inset-x-0 h-0.5 bg-white/60 rounded-full"})]}),m.jsx("div",{className:"flex-1 border-b border-[#2a2a2a]"}),m.jsx("button",{onClick:_,className:"px-3 py-2 text-[#555] hover:text-white transition-colors flex-shrink-0 border-b border-[#2a2a2a]","aria-label":"Copy fixed code",children:c?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})})]}),m.jsx("div",{className:"overflow-auto max-h-[400px]",children:m.jsx("table",{className:"w-full border-collapse font-mono text-[12px] leading-[22px] [font-variant-ligatures:none]",children:m.jsx("tbody",{children:y.map((N,S)=>m.jsxs("tr",{className:N.added?"bg-blue-500/[0.12]":N.removed?"bg-red-500/[0.12]":"",children:[m.jsx("td",{className:"select-none w-[1px] whitespace-nowrap pl-4 pr-1.5 text-right text-[#555] align-top text-[12px] leading-[22px]",children:N.leftNo}),m.jsx("td",{className:"select-none w-[1px] whitespace-nowrap pl-1.5 pr-4 text-right text-[#555] align-top text-[12px] leading-[22px] border-r border-[#2a2a2a]",children:N.rightNo}),m.jsx("td",{className:"pl-4 pr-4 whitespace-pre",dangerouslySetInnerHTML:{__html:N.highlighted}})]},S))})})})]})}const aD=/^```([^\n`]*)\r?\n([\s\S]*?)\r?\n?```$/;function EE(e){if(!e)return{code:""};const t=aD.exec(e.trim());if(!t)return{code:e};const r=t[1].trim();return{language:(r?r.split(/\s+/)[0]:void 0)||void 0,code:t[2]}}function sD({description:e,scriptCode:t,onCopy:r}){const[a,s]=ee.useState(!1);if(!e&&!t)return null;const{language:o,code:c}=EE(t),d=_E(c,o),h=()=>{c&&(Tp(c),s(!0),setTimeout(()=>s(!1),2e3),r==null||r())};return m.jsxs("section",{children:[m.jsx("h2",{className:"text-xl font-semibold text-white mb-3",children:"Proof of Concept"}),m.jsxs("div",{className:"space-y-4",children:[e&&m.jsx("div",{className:"prose-markdown",children:m.jsx(Yp,{remarkPlugins:[Qp],rehypePlugins:[Wp],components:Jp,children:e})}),c&&m.jsxs("div",{className:"group/poc relative rounded-md border border-[#2a2a2a] overflow-hidden",children:[m.jsxs("div",{className:"flex items-stretch",children:[m.jsxs("span",{className:"relative flex items-center text-[13px] text-[#999] font-mono px-4 py-2 border-r border-[#2a2a2a]",children:["PoC Script",m.jsx("span",{className:"absolute top-0 inset-x-0 h-0.5 bg-white/60 rounded-full"})]}),m.jsx("div",{className:"flex-1 border-b border-[#2a2a2a]"}),m.jsx("button",{onClick:h,className:"px-3 py-2 text-[#555] hover:text-white transition-colors flex-shrink-0 border-b border-[#2a2a2a]","aria-label":"Copy PoC code",children:a?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})})]}),m.jsx("div",{className:"overflow-auto max-h-[400px] px-4 py-3",children:m.jsx("pre",{className:"font-mono text-[12px] leading-[22px] whitespace-pre [font-variant-ligatures:none]",children:m.jsx("code",{dangerouslySetInnerHTML:{__html:d}})})})]})]})]})}function NE(e){const t=e.match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^\s/]+\/[^\s/]+)/);if(t){const s=t[1].replace(/\.git$/,"");return{display:s,href:`https://github.com/${s}`,provider:"github"}}const r=e.match(/(?:https?:\/\/)?(?:www\.)?gitlab\.com\/([^\s/]+\/[^\s/]+)/);if(r){const s=r[1].replace(/\.git$/,"");return{display:s,href:`https://gitlab.com/${s}`,provider:"gitlab"}}const a=e.match(/(?:https?:\/\/)?(?:www\.)?bitbucket\.org\/([^\s/]+\/[^\s/]+)/);if(a){const s=a[1].replace(/\.git$/,"");return{display:s,href:`https://bitbucket.org/${s}`,provider:"bitbucket"}}return/^https?:\/\//i.test(e)?{display:e.replace(/^https?:\/\/(www\.)?/,""),href:e,provider:null}:/^[a-zA-Z0-9][\w.-]*\.[a-zA-Z]{2,}/.test(e)?{display:e,href:`https://${e}`,provider:null}:{display:e,href:null,provider:null}}function yo(e,t){return e?NE(e).display.replace(/\/$/,""):t||"Untitled pentest"}function lD({className:e}){return m.jsx("svg",{viewBox:"0 0 24 24",fill:"currentColor",className:e,"aria-hidden":"true",children:m.jsx("path",{d:"M2.65 3a.72.72 0 0 0-.72.83l2.86 17.39a.98.98 0 0 0 .96.82h13.72a.72.72 0 0 0 .72-.6l2.86-17.4A.72.72 0 0 0 22.3 3H2.65Zm12.1 12.53H9.3L8.06 8.9h7.8l-1.11 6.63Z"})})}function oD({provider:e,className:t}){const r=t??"w-4 h-4";return e==="gitlab"?m.jsx(PC,{className:`${r} text-orange-400`}):e==="bitbucket"?m.jsx(lD,{className:`${r} text-blue-400`}):m.jsx($C,{className:`${r} text-white`})}const cD={attack_vector:{N:"Remotely exploitable",A:"Adjacent network",L:"Local access
Line truncated
|
||||
`:f!=="text"?eD(S,f):S.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");let C="",E="";return N.removed?C=String(g++):(N.added||(C=String(g++)),E=String(b++)),{highlighted:w,added:!!N.added,removed:!!N.removed,leftNo:C,rightNo:E}})),_=()=>{Cp(s),d(!0),setTimeout(()=>d(!1),2e3),o==null||o()};return m.jsxs("div",{className:"rounded-md border border-[#2a2a2a] overflow-hidden",children:[m.jsxs("div",{className:"flex items-stretch",children:[m.jsxs("span",{className:"relative flex items-center text-[13px] text-[#999] font-mono px-4 py-2 border-r border-[#2a2a2a] break-all",children:[e,":",h,m.jsx("span",{className:"absolute top-0 inset-x-0 h-0.5 bg-white/60 rounded-full"})]}),m.jsx("div",{className:"flex-1 border-b border-[#2a2a2a]"}),m.jsx("button",{onClick:_,className:"px-3 py-2 text-[#555] hover:text-white transition-colors flex-shrink-0 border-b border-[#2a2a2a]","aria-label":"Copy fixed code",children:c?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})})]}),m.jsx("div",{className:"overflow-auto max-h-[400px]",children:m.jsx("table",{className:"w-full border-collapse font-mono text-[12px] leading-[22px] [font-variant-ligatures:none]",children:m.jsx("tbody",{children:y.map((N,S)=>m.jsxs("tr",{className:N.added?"bg-blue-500/[0.12]":N.removed?"bg-red-500/[0.12]":"",children:[m.jsx("td",{className:"select-none w-[1px] whitespace-nowrap pl-4 pr-1.5 text-right text-[#555] align-top text-[12px] leading-[22px]",children:N.leftNo}),m.jsx("td",{className:"select-none w-[1px] whitespace-nowrap pl-1.5 pr-4 text-right text-[#555] align-top text-[12px] leading-[22px] border-r border-[#2a2a2a]",children:N.rightNo}),m.jsx("td",{className:"pl-4 pr-4 whitespace-pre",dangerouslySetInnerHTML:{__html:N.highlighted}})]},S))})})})]})}const nD=/^```([^\n`]*)\r?\n([\s\S]*?)\r?\n?```$/;function yE(e){if(!e)return{code:""};const t=nD.exec(e.trim());if(!t)return{code:e};const r=t[1].trim();return{language:(r?r.split(/\s+/)[0]:void 0)||void 0,code:t[2]}}function rD({description:e,scriptCode:t,onCopy:r}){const[a,s]=ee.useState(!1);if(!e&&!t)return null;const{language:o,code:c}=yE(t),d=xE(c,o),h=()=>{c&&(Cp(c),s(!0),setTimeout(()=>s(!1),2e3),r==null||r())};return m.jsxs("section",{children:[m.jsx("h2",{className:"text-xl font-semibold text-white mb-3",children:"Proof of Concept"}),m.jsxs("div",{className:"space-y-4",children:[e&&m.jsx("div",{className:"prose-markdown",children:m.jsx(Gp,{remarkPlugins:[Kp],rehypePlugins:[Zp],components:Qp,children:e})}),c&&m.jsxs("div",{className:"group/poc relative rounded-md border border-[#2a2a2a] overflow-hidden",children:[m.jsxs("div",{className:"flex items-stretch",children:[m.jsxs("span",{className:"relative flex items-center text-[13px] text-[#999] font-mono px-4 py-2 border-r border-[#2a2a2a]",children:["PoC Script",m.jsx("span",{className:"absolute top-0 inset-x-0 h-0.5 bg-white/60 rounded-full"})]}),m.jsx("div",{className:"flex-1 border-b border-[#2a2a2a]"}),m.jsx("button",{onClick:h,className:"px-3 py-2 text-[#555] hover:text-white transition-colors flex-shrink-0 border-b border-[#2a2a2a]","aria-label":"Copy PoC code",children:a?m.jsx(Vs,{className:"w-3.5 h-3.5 text-emerald-400"}):m.jsx(go,{className:"w-3.5 h-3.5"})})]}),m.jsx("div",{className:"overflow-auto max-h-[400px] px-4 py-3",children:m.jsx("pre",{className:"font-mono text-[12px] leading-[22px] whitespace-pre [font-variant-ligatures:none]",children:m.jsx("code",{dangerouslySetInnerHTML:{__html:d}})})})]})]})]})}function vE(e){const t=e.match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^\s/]+\/[^\s/]+)/);if(t){const s=t[1].replace(/\.git$/,"");return{display:s,href:`https://github.com/${s}`,provider:"github"}}const r=e.match(/(?:https?:\/\/)?(?:www\.)?gitlab\.com\/([^\s/]+\/[^\s/]+)/);if(r){const s=r[1].replace(/\.git$/,"");return{display:s,href:`https://gitlab.com/${s}`,provider:"gitlab"}}const a=e.match(/(?:https?:\/\/)?(?:www\.)?bitbucket\.org\/([^\s/]+\/[^\s/]+)/);if(a){const s=a[1].replace(/\.git$/,"");return{display:s,href:`https://bitbucket.org/${s}`,provider:"bitbucket"}}return/^https?:\/\//i.test(e)?{display:e.replace(/^https?:\/\/(www\.)?/,""),href:e,provider:null}:/^[a-zA-Z0-9][\w.-]*\.[a-zA-Z]{2,}/.test(e)?{display:e,href:`https://${e}`,provider:null}:{display:e,href:null,provider:null}}function yo(e,t){return e?vE(e).display.replace(/\/$/,""):t||"Untitled pentest"}function iD({className:e}){return m.jsx("svg",{viewBox:"0 0 24 24",fill:"currentColor",className:e,"aria-hidden":"true",children:m.jsx("path",{d:"M2.65 3a.72.72 0 0 0-.72.83l2.86 17.39a.98.98 0 0 0 .96.82h13.72a.72.72 0 0 0 .72-.6l2.86-17.4A.72.72 0 0 0 22.3 3H2.65Zm12.1 12.53H9.3L8.06 8.9h7.8l-1.11 6.63Z"})})}function aD({provider:e,className:t}){const r=t??"w-4 h-4";return e==="gitlab"?m.jsx(UC,{className:`${r} text-orange-400`}):e==="bitbucket"?m.jsx(iD,{className:`${r} text-blue-400`}):m.jsx(IC,{className:`${r} text-white`})}const sD={attack_vector:{N:"Remotely exploitable",A:"Adjacent network",L:"Local access
Line truncated
|
||||
* @license React
|
||||
* use-sync-external-store-shim.production.js
|
||||
*
|
||||
@@ -471,7 +466,7 @@ https://github.com/highlightjs/highlight.js/issues/2277`),Ht=ye,ft=Le),Qe===void
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var N1;function KI(){if(N1)return Rm;N1=1;var e=Oo();function t(g,b){return g===b&&(g!==0||1/g===1/b)||g!==g&&b!==b}var r=typeof Object.is=="function"?Object.is:t,a=e.useState,s=e.useEffect,o=e.useLayoutEffect,c=e.useDebugValue;function d(g,b){var y=b(),_=a({inst:{value:y,getSnapshot:b}}),N=_[0].inst,S=_[1];return o(function(){N.value=y,N.getSnapshot=b,h(N)&&S({inst:N})},[g,y,b]),s(function(){return h(N)&&S({inst:N}),g(function(){h(N)&&S({inst:N})})},[g]),c(y),y}function h(g){var b=g.getSnapshot;g=g.value;try{var y=b();return!r(g,y)}catch{return!0}}function f(g,b){return b()}var p=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?f:d;return Rm.useSyncExternalStore=e.useSyncExternalStore!==void 0?e.useSyncExternalStore:p,Rm}var S1;function ZI(){return S1||(S1=1,Om.exports=KI()),Om.exports}/**
|
||||
*/var w1;function VI(){if(w1)return Om;w1=1;var e=Mo();function t(g,b){return g===b&&(g!==0||1/g===1/b)||g!==g&&b!==b}var r=typeof Object.is=="function"?Object.is:t,a=e.useState,s=e.useEffect,o=e.useLayoutEffect,c=e.useDebugValue;function d(g,b){var y=b(),_=a({inst:{value:y,getSnapshot:b}}),N=_[0].inst,S=_[1];return o(function(){N.value=y,N.getSnapshot=b,h(N)&&S({inst:N})},[g,y,b]),s(function(){return h(N)&&S({inst:N}),g(function(){h(N)&&S({inst:N})})},[g]),c(y),y}function h(g){var b=g.getSnapshot;g=g.value;try{var y=b();return!r(g,y)}catch{return!0}}function f(g,b){return b()}var p=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?f:d;return Om.useSyncExternalStore=e.useSyncExternalStore!==void 0?e.useSyncExternalStore:p,Om}var E1;function YI(){return E1||(E1=1,Mm.exports=VI()),Mm.exports}/**
|
||||
* @license React
|
||||
* use-sync-external-store-shim/with-selector.production.js
|
||||
*
|
||||
@@ -479,34 +474,34 @@ https://github.com/highlightjs/highlight.js/issues/2277`),Ht=ye,ft=Le),Qe===void
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/var k1;function QI(){if(k1)return Mm;k1=1;var e=Oo(),t=ZI();function r(f,p){return f===p&&(f!==0||1/f===1/p)||f!==f&&p!==p}var a=typeof Object.is=="function"?Object.is:r,s=t.useSyncExternalStore,o=e.useRef,c=e.useEffect,d=e.useMemo,h=e.useDebugValue;return Mm.useSyncExternalStoreWithSelector=function(f,p,g,b,y){var _=o(null);if(_.current===null){var N={hasValue:!1,value:null};_.current=N}else N=_.current;_=d(function(){function w(R){if(!C){if(C=!0,E=R,R=b(R),y!==void 0&&N.hasValue){var H=N.value;if(y(H,R))return A=H}return A=R}if(H=A,a(E,R))return H;var z=b(R);return y!==void 0&&y(H,z)?(E=R,H):(E=R,A=z)}var C=!1,E,A,B=g===void 0?null:g;return[function(){return w(p())},B===null?void 0:function(){return w(B())}]},[p,g,b,y]);var S=s(f,_[0],_[1]);return c(function(){N.hasValue=!0,N.value=S},[S]),h(S),S},Mm}var C1;function WI(){return C1||(C1=1,Am.exports=QI()),Am.exports}var JI=WI();const e8=Mo(JI),t8={},T1=e=>{let t;const r=new Set,a=(p,g)=>{const b=typeof p=="function"?p(t):p;if(!Object.is(b,t)){const y=t;t=g??(typeof b!="object"||b===null)?b:Object.assign({},t,b),r.forEach(_=>_(t,y))}},s=()=>t,h={setState:a,getState:s,getInitialState:()=>f,subscribe:p=>(r.add(p),()=>r.delete(p)),destroy:()=>{(t8?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),r.clear()}},f=t=e(a,s,h);return h},n8=e=>e?T1(e):T1,{useDebugValue:r8}=fa,{useSyncExternalStoreWithSelector:i8}=e8,a8=e=>e;function ON(e,t=a8,r){const a=i8(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,r);return r8(a),a}const A1=(e,t)=>{const r=n8(e),a=(s,o=t)=>ON(r,s,o);return Object.assign(a,r),a},s8=(e,t)=>e?A1(e,t):A1;function Pt(e,t){if(Object.is(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;if(e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(const[a,s]of e)if(!Object.is(s,t.get(a)))return!1;return!0}if(e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(const a of e)if(!t.has(a))return!1;return!0}const r=Object.keys(e);if(r.length!==Object.keys(t).length)return!1;for(const a of r)if(!Object.prototype.hasOwnProperty.call(t,a)||!Object.is(e[a],t[a]))return!1;return!0}z_();const ld=ee.createContext(null),l8=ld.Provider,RN=Lr.error001("react");function dt(e,t){const r=ee.useContext(ld);if(r===null)throw new Error(RN);return ON(r,e,t)}function Lt(){const e=ee.useContext(ld);if(e===null)throw new Error(RN);return ee.useMemo(()=>({getState:e.getState,setState:e.setState,subscribe:e.subscribe}),[e])}const M1={display:"none"},o8={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},jN="react-flow__node-desc",DN="react-flow__edge-desc",c8="react-flow__aria-live",u8=e=>e.ariaLiveMessage,d8=e=>e.ariaLabelConfig;function f8({rfId:e}){const t=dt(u8);return m.jsx("div",{id:`${c8}-${e}`,"aria-live":"assertive","aria-atomic":"true",style:o8,children:t})}function h8({rfId:e,disableKeyboardA11y:t}){const r=dt(d8);return m.jsxs(m.Fragment,{children:[m.jsx("div",{id:`${jN}-${e}`,style:M1,children:t?r["node.a11yDescription.default"]:r["node.a11yDescription.keyboardDisabled"]}),m.jsx("div",{id:`${DN}-${e}`,style:M1,children:r["edge.a11yDescription.default"]}),!t&&m.jsx(f8,{rfId:e})]})}const od=ee.forwardRef(({position:e="top-left",children:t,className:r,style:a,...s},o)=>{const c=`${e}`.split("-");return m.jsx("div",{className:on(["react-flow__panel",r,...c]),style:a,ref:o,...s,children:t})});od.displayName="Panel";const O1="https://reactflow.dev?utm_source=attribution";function m8({proOptions:e,position:t="bottom-right"}){return e!=null&&e.hideAttribution?null:m.jsx(od,{position:t,className:"react-flow__attribution","data-message":`Please only hide this attribution when you are subscribed to React Flow Pro: ${O1}`,children:m.jsx("a",{href:O1,target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}const p8=e=>{const t=[],r=[];for(const[,a]of e.nodeLookup)a.selected&&t.push(a.internals.userNode);for(const[,a]of e.edgeLookup)a.selected&&r.push(a);return{selectedNodes:t,selectedEdges:r}},hu=e=>e.id;function g8(e,t){return Pt(e.selectedNodes.map(hu),t.selectedNodes.map(hu))&&Pt(e.selectedEdges.map(hu),t.selectedEdges.map(hu))}function x8({onSelectionChange:e}){const t=Lt(),{selectedNodes:r,selectedEdges:a}=dt(p8,g8);return ee.useEffect(()=>{const s={nodes:r,edges:a};e==null||e(s),t.getState().onSelectionChangeHandlers.forEach(o=>o(s))},[r,a,e]),null}const b8=e=>!!e.onSelectionChangeHandlers;function y8({onSelectionChange:e}){const t=dt(b8);return e||t?m.jsx(x8,{onSelectionChange:e}):null}const LN=[0,0],v8={x:0,y:0,zoom:1},_8=["nodes","edges","defaultNodes","defaultEdges","onConnect","onConnectStart","onConnectEnd","onClickConnectStart","onClickConnectEnd","nodesDraLine truncated
|
||||
*/var N1;function XI(){if(N1)return Am;N1=1;var e=Mo(),t=YI();function r(f,p){return f===p&&(f!==0||1/f===1/p)||f!==f&&p!==p}var a=typeof Object.is=="function"?Object.is:r,s=t.useSyncExternalStore,o=e.useRef,c=e.useEffect,d=e.useMemo,h=e.useDebugValue;return Am.useSyncExternalStoreWithSelector=function(f,p,g,b,y){var _=o(null);if(_.current===null){var N={hasValue:!1,value:null};_.current=N}else N=_.current;_=d(function(){function w(R){if(!C){if(C=!0,E=R,R=b(R),y!==void 0&&N.hasValue){var H=N.value;if(y(H,R))return A=H}return A=R}if(H=A,a(E,R))return H;var z=b(R);return y!==void 0&&y(H,z)?(E=R,H):(E=R,A=z)}var C=!1,E,A,B=g===void 0?null:g;return[function(){return w(p())},B===null?void 0:function(){return w(B())}]},[p,g,b,y]);var S=s(f,_[0],_[1]);return c(function(){N.hasValue=!0,N.value=S},[S]),h(S),S},Am}var S1;function KI(){return S1||(S1=1,Tm.exports=XI()),Tm.exports}var ZI=KI();const QI=Ao(ZI),WI={},k1=e=>{let t;const r=new Set,a=(p,g)=>{const b=typeof p=="function"?p(t):p;if(!Object.is(b,t)){const y=t;t=g??(typeof b!="object"||b===null)?b:Object.assign({},t,b),r.forEach(_=>_(t,y))}},s=()=>t,h={setState:a,getState:s,getInitialState:()=>f,subscribe:p=>(r.add(p),()=>r.delete(p)),destroy:()=>{(WI?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),r.clear()}},f=t=e(a,s,h);return h},JI=e=>e?k1(e):k1,{useDebugValue:e8}=da,{useSyncExternalStoreWithSelector:t8}=QI,n8=e=>e;function CN(e,t=n8,r){const a=t8(e.subscribe,e.getState,e.getServerState||e.getInitialState,t,r);return e8(a),a}const C1=(e,t)=>{const r=JI(e),a=(s,o=t)=>CN(r,s,o);return Object.assign(a,r),a},r8=(e,t)=>e?C1(e,t):C1;function qt(e,t){if(Object.is(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;if(e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(const[a,s]of e)if(!Object.is(s,t.get(a)))return!1;return!0}if(e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(const a of e)if(!t.has(a))return!1;return!0}const r=Object.keys(e);if(r.length!==Object.keys(t).length)return!1;for(const a of r)if(!Object.prototype.hasOwnProperty.call(t,a)||!Object.is(e[a],t[a]))return!1;return!0}D_();const sd=ee.createContext(null),i8=sd.Provider,TN=Lr.error001("react");function dt(e,t){const r=ee.useContext(sd);if(r===null)throw new Error(TN);return CN(r,e,t)}function Lt(){const e=ee.useContext(sd);if(e===null)throw new Error(TN);return ee.useMemo(()=>({getState:e.getState,setState:e.setState,subscribe:e.subscribe}),[e])}const T1={display:"none"},a8={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},AN="react-flow__node-desc",MN="react-flow__edge-desc",s8="react-flow__aria-live",l8=e=>e.ariaLiveMessage,o8=e=>e.ariaLabelConfig;function c8({rfId:e}){const t=dt(l8);return m.jsx("div",{id:`${s8}-${e}`,"aria-live":"assertive","aria-atomic":"true",style:a8,children:t})}function u8({rfId:e,disableKeyboardA11y:t}){const r=dt(o8);return m.jsxs(m.Fragment,{children:[m.jsx("div",{id:`${AN}-${e}`,style:T1,children:t?r["node.a11yDescription.default"]:r["node.a11yDescription.keyboardDisabled"]}),m.jsx("div",{id:`${MN}-${e}`,style:T1,children:r["edge.a11yDescription.default"]}),!t&&m.jsx(c8,{rfId:e})]})}const ld=ee.forwardRef(({position:e="top-left",children:t,className:r,style:a,...s},o)=>{const c=`${e}`.split("-");return m.jsx("div",{className:on(["react-flow__panel",r,...c]),style:a,ref:o,...s,children:t})});ld.displayName="Panel";const A1="https://reactflow.dev?utm_source=attribution";function d8({proOptions:e,position:t="bottom-right"}){return e!=null&&e.hideAttribution?null:m.jsx(ld,{position:t,className:"react-flow__attribution","data-message":`Please only hide this attribution when you are subscribed to React Flow Pro: ${A1}`,children:m.jsx("a",{href:A1,target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}const f8=e=>{const t=[],r=[];for(const[,a]of e.nodeLookup)a.selected&&t.push(a.internals.userNode);for(const[,a]of e.edgeLookup)a.selected&&r.push(a);return{selectedNodes:t,selectedEdges:r}},fu=e=>e.id;function h8(e,t){return qt(e.selectedNodes.map(fu),t.selectedNodes.map(fu))&&qt(e.selectedEdges.map(fu),t.selectedEdges.map(fu))}function m8({onSelectionChange:e}){const t=Lt(),{selectedNodes:r,selectedEdges:a}=dt(f8,h8);return ee.useEffect(()=>{const s={nodes:r,edges:a};e==null||e(s),t.getState().onSelectionChangeHandlers.forEach(o=>o(s))},[r,a,e]),null}const p8=e=>!!e.onSelectionChangeHandlers;function g8({onSelectionChange:e}){const t=dt(p8);return e||t?m.jsx(m8,{onSelectionChange:e}):null}const ON=[0,0],x8={x:0,y:0,zoom:1},b8=["nodes","edges","defaultNodes","defaultEdges","onConnect","onConnectStart","onConnectEnd","onClickConnectStart","onClickConnectEnd","nodesDraLine truncated
|
||||
`).replace(`
|
||||
|
||||
`,`
|
||||
+`).split(`
|
||||
`)),p=f.reduce((g,b)=>g.concat(...b),[]);return[f,p]}return[[],[]]},[e]);return ee.useEffect(()=>{const h=(t==null?void 0:t.target)??L1,f=(t==null?void 0:t.actInsideInputWithModifier)??!0;if(e!==null){const p=y=>{var S,w;if(s.current=y.ctrlKey||y.metaKey||y.shiftKey||y.altKey,(!s.current||s.current&&!f)&&mN(y))return!1;const N=I1(y.code,d);if(o.current.add(y[N]),z1(c,o.current,!1)){const C=((w=(S=y.composedPath)==null?void 0:S.call(y))==null?void 0:w[0])||y.target,E=(C==null?void 0:C.nodeName)==="BUTTON"||(C==null?void 0:C.nodeName)==="A";t.preventDefault!==!1&&(s.current||!E)&&y.preventDefault(),a(!0)}},g=y=>{const _=I1(y.code,d);z1(c,o.current,!0)?(a(!1),o.current.clear()):o.current.delete(y[_]),y.key==="Meta"&&o.current.clear(),s.current=!1},b=()=>{o.current.clear(),a(!1)};return h==null||h.addEventListener("keydown",p),h==null||h.addEventListener("keyup",g),window.addEventListener("blur",b),window.addEventListener("contextmenu",b),()=>{h==null||h.removeEventListener("keydown",p),h==null||h.removeEventListener("keyup",g),window.removeEventListener("blur",b),window.removeEventListener("contextmenu",b)}}},[e,a]),r}function z1(e,t,r){return e.filter(a=>r||a.length===t.size).some(a=>a.every(s=>t.has(s)))}function I1(e,t){return t.includes(e)?"code":"key"}const S8=()=>{const e=Lt();return ee.useMemo(()=>({zoomIn:async t=>{const{panZoom:r}=e.getState();return r?r.scaleBy(1.2,t):!1},zoomOut:async t=>{const{panZoom:r}=e.getState();return r?r.scaleBy(1/1.2,t):!1},zoomTo:async(t,r)=>{const{panZoom:a}=e.getState();return a?a.scaleTo(t,r):!1},getZoom:()=>e.getState().transform[2],setViewport:async(t,r)=>{const{transform:[a,s,o],panZoom:c}=e.getState();return c?(await c.setViewport({x:t.x??a,y:t.y??s,zoom:t.zoom??o},r),!0):!1},getViewport:()=>{const[t,r,a]=e.getState().transform;return{x:t,y:r,zoom:a}},setCenter:async(t,r,a)=>e.getState().setCenter(t,r,a),fitBounds:async(t,r)=>{const{width:a,height:s,minZoom:o,maxZoom:c,panZoom:d}=e.getState(),h=ug(t,a,s,o,c,(r==null?void 0:r.padding)??.1);return d?(await d.setViewport(h,{duration:r==null?void 0:r.duration,ease:r==null?void 0:r.ease,interpolate:r==null?void 0:r.interpolate}),!0):!1},screenToFlowPosition:(t,r={})=>{const{transform:a,snapGrid:s,snapToGrid:o,domNode:c}=e.getState();if(!c)return t;const{x:d,y:h}=c.getBoundingClientRect(),f={x:t.x-d,y:t.y-h},p=r.snapGrid??s,g=r.snapToGrid??o;return $o(f,a,g,p)},flowToScreenPosition:t=>{const{transform:r,domNode:a}=e.getState();if(!a)return t;const{x:s,y:o}=a.getBoundingClientRect(),c=Js(t,r);return{x:c.x+s,y:c.y+o}}}),[])};function zN(e,t){const r=[],a=new Map,s=[];for(const o of e)if(o.type==="add"){s.push(o);continue}else if(o.type==="remove"||o.type==="replace")a.set(o.id,[o]);else{const c=a.get(o.id);c?c.push(o):a.set(o.id,[o])}for(const o of t){const c=a.get(o.id);if(!c){r.push(o);continue}if(c[0].type==="remove")continue;if(c[0].type==="replace"){r.push({...c[0].item});continue}const d={...o};for(const h of c)k8(h,d);r.push(d)}return s.length&&s.forEach(o=>{o.index!==void 0?r.splice(o.index,0,{...o.item}):r.push({...o.item})}),r}function k8(e,t){switch(e.type){case"select":{t.selected=e.selected;break}case"position":{typeof e.position<"u"&&(t.position=e.position),typeof e.dragging<"u"&&(t.dragging=e.dragging);break}case"dimensions":{typeof e.dimensions<"u"&&(t.measured={...e.dimensions},e.setAttributes&&((e.setAttributes===!0||e.setAttributes==="width")&&(t.width=e.dimensions.width),(e.setAttributes===!0||e.setAttributes==="height")&&(t.height=e.dimensions.height))),typeof e.resizing=="boolean"&&(t.resizing=e.resizing);break}}}function IN(e,t){return zN(e,t)}function BN(e,t){return zN(e,t)}function Ha(e,t){return{id:e,type:"select",selected:t}}function qs(e,t=new Set,r=!1){const a=[];for(const[s,o]of e){const c=t.has(s);!(o.selected===void 0&&!c)&&o.selected!==c&&(r&&(o.selected=c),a.push(Ha(o.id,c)))}return a}function B1({items:e=[],lookup:t}){var s;const r=[],a=new Map(e.map(o=>[o.id,o]));for(const[o,c]of e.entries()){const d=t.get(c.id),h=((s=d==null?void 0:d.internals)==null?void 0:s.userNode)??d;h!==void 0&&h!==c&&r.push({id:c.id,item:c,type:"replace"}),h===void 0&&r.push({item:c,type:"add",index:o})}for(const[o]of t)a.get(o)===void 0&&r.push({id:o,type:"remove"});return r}function U1(e){return{id:e.id,type:"remove"}}const C8=uN();function T8(e,t,r={}){return hI(e,t,{...r,onError:r.onError??C8})}const H1=e=>Wz(e),A8=e=>aN(e);function UN(e){return ee.forwardRef(e)}const HN=typeof window<"u"?ee.useLayoutEffect:ee.useEffect;function $1(e){const[t,r]=ee.useState(BigInt(0)),[a]=ee.useState(()=>M8(()=>r(s=>s+BigInt(1))));return HN(()=>{const s=a.get();s.length&&(e(s),a.reset())},[t]),a}function M8(e){let t=[];return{get:()=>t,reset:()=>{t=[]},push:r=>{t.push(r),e()}}}const $N=ee.createContext(null);function O8({children:e}){const t=Lt(),r=ee.useCallback(d=>{const{nodes:h=[],setNodes:f,hasDefaultNodes:p,onNodesChange:g,nodeLookup:b,fitViewQueued:y,onNodesChangeMiddlewareMap:_}=t.getState();let N=h;for(const w of d)N=
Line truncated
|
||||
M${z.x},${z.y}h${z.width}v${z.height}h${-z.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}h2.displayName="MiniMap";const SB=ee.memo(h2),kB=e=>t=>e?`${Math.max(1/t.transform[2],1)}`:void 0,CB={[el.Line]:"right",[el.Handle]:"bottom-right"};function TB({nodeId:e,position:t,variant:r=el.Handle,className:a,style:s=void 0,children:o,color:c,minWidth:d=10,minHeight:h=10,maxWidth:f=Number.MAX_VALUE,maxHeight:p=Number.MAX_VALUE,keepAspectRatio:g=!1,resizeDirection:b,autoScale:y=!0,shouldResize:_,onResizeStart:N,onResize:S,onResizeEnd:w}){const C=FN(),E=typeof e=="string"?e:C,A=Lt(),B=ee.useRef(null),R=r===el.Handle,H=dt(ee.useCallback(kB(R&&y),[R,y]),Pt),z=ee.useRef(null),Y=t??CB[r];ee.useEffect(()=>{if(!(!B.current||!E))return z.current||(z.current=XI({domNode:B.current,nodeId:E,getStoreItems:()=>{const{nodeLookup:I,transform:Z,snapGrid:P,snapToGrid:k,nodeOrigin:$,domNode:O}=A.getState();return{nodeLookup:I,transform:Z,snapGrid:P,snapToGrid:k,nodeOrigin:$,paneDomNode:O}},onChange:(I,Z)=>{const{triggerNodeChanges:P,nodeLookup:k,parentLookup:$,nodeOrigin:O}=A.getState(),U=[],K={x:I.x,y:I.y},X=k.get(E);if(X&&X.expandParent&&X.parentId){const M=X.origin??O,L=I.width??X.measured.width??0,F=I.height??X.measured.height??0,D={id:X.id,parentId:X.parentId,rect:{width:L,height:F,...fN({x:I.x??X.position.x,y:I.y??X.position.y},{width:L,height:F},X.parentId,k,M)}},V=gg([D],k,$,O);U.push(...V),K.x=I.x?Math.max(M[0]*L,I.x):void 0,K.y=I.y?Math.max(M[1]*F,I.y):void 0}if(K.x!==void 0&&K.y!==void 0){const M={id:E,type:"position",position:{...K}};U.push(M)}if(I.width!==void 0&&I.height!==void 0){const L={id:E,type:"dimensions",resizing:!0,setAttributes:b?b==="horizontal"?"width":"height":!0,dimensions:{width:I.width,height:I.height}};U.push(L)}for(const M of Z){const L={...M,type:"position"};U.push(L)}P(U)},onEnd:({width:I,height:Z})=>{const P={id:E,type:"dimensions",resizing:!1,dimensions:{width:I,height:Z}};A.getState().triggerNodeChanges([P])}})),z.current.update({controlPosition:Y,boundaries:{minWidth:d,minHeight:h,maxWidth:f,maxHeight:p},keepAspectRatio:g,resizeDirection:b,onResizeStart:N,onResize:S,onResizeEnd:w,shouldResize:_}),()=>{var I;(I=z.current)==null||I.destroy()}},[Y,d,h,f,p,g,N,S,w,_]);const j=Y.split("-");return m.jsx("div",{className:on(["react-flow__resize-control","nodrag",...j,r,a]),ref:B,style:{...s,scale:H,...c&&{[R?"backgroundColor":"borderColor"]:c}},children:o})}ee.memo(TB);var vt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),zr=vt((e,t)=>{var r=Object.defineProperty,a=(P,k,$)=>k in P?r(P,k,{enumerable:!0,configurable:!0,writable:!0,value:$}):P[k]=$,s=(P,k)=>()=>(k||P((k={exports:{}}).exports,k),k.exports),o=(P,k,$)=>a(P,typeof k!="symbol"?k+"":k,$),c=s((P,k)=>{var $="\0",O="\0",U="",K=class{constructor(V){o(this,"_isDirected",!0),o(this,"_isMultigraph",!1),o(this,"_isCompound",!1),o(this,"_label"),o(this,"_defaultNodeLabelFn",()=>{}),o(this,"_defaultEdgeLabelFn",()=>{}),o(this,"_nodes",{}),o(this,"_in",{}),o(this,"_preds",{}),o(this,"_out",{}),o(this,"_sucs",{}),o(this,"_edgeObjs",{}),o(this,"_edgeLabels",{}),o(this,"_nodeCount",0),o(this,"_edgeCount",0),o(this,"_parent"),o(this,"_children"),V&&(this._isDirected=Object.hasOwn(V,"directed")?V.directed:!0,this._isMultigraph=Object.hasOwn(V,"multigraph")?V.multigraph:!1,this._isCompound=Object.hasOwn(V,"compound")?V.compound:!1),this._isCompound&&(this._parent={},this._children={},this._children[O]={})}isDirected(){return this._isDirected}isMultigraph(){return this._isMultigraph}isCompound(){return this._isCompound}setGraph(V){return this._label=V,this}graph(){return this._label}setDefaultNodeLabel(V){return this._defaultNodeLabelFn=V,typeof V!="function"&&(this._defaultNodeLabelFn=()=>V),this}nodeCount(){return this._nodeCount}nodes(){return Object.keys(this._nodes)}sources(){var V=this;return this.nodes().filter(q=>Object.keys(V._in[q]).length===0)}sinks(){var V=this;return this.nodes().filter(q=>Object.keys(V._out[q]).length===0)}setNodes(V,q){var Q=arguments,J=this;return V.forEach(function(W){Q.length>1?J.setNode(W,q):J.setNode(W)}),this}setNode(V,q){return Object.hasOwn(this._nodes,V)?(arguments.length>1&&(this._nodes[V]=q),this):(this._nodes[V]=arguments.length>1?q:this._defaultNodeLabelFn(V),this._isCompound&&(this._parent[V]=O,this._children[V]={},this._children[O][V]=!0),this._in[V]={},this._preds[V]={},this._out[V]={},this._sucs[V]={},++this._nodeCount,this)}node(V){return this._nodes[V]}hasNode(V){return Object.hasOwn(this._nodes,V)}removeNode(V){var q=this;if(Object.hasOwn(this._nodes,V)){var Q=J=>q.removeEdge(q._edgeObjs[J]);delete this._nodes[V],this._isCompound&&(this._removeFromParentsChildList(V),delete this._parent[V],this.children(V).forEach(function(J){q.setParent(J)}),delete this._children[V]),Object.keys(this._in[V]).forEach(Q),delete this._in[V],delete this._preds[V],Object.keys(this._out[V]).forEach(Q),delete this._out[V],delete this._sucs[V],--this._nodeCount}return this}setParent(V,q){if(!this._isCompound
Line truncated
|
||||
`).length>t;return m.jsxs("div",{children:[m.jsx("div",{className:r&&o?"max-h-[1200px] overflow-auto":"",style:!r&&o?{display:"-webkit-box",WebkitLineClamp:t,WebkitBoxOrient:"vertical",overflow:"hidden"}:void 0,children:m.jsx(da,{text:e})}),o&&m.jsx("button",{onClick:()=>a(!r),className:"text-xs text-[#555] hover:text-[#888] mt-1",children:r?"Show less":"Show more"})]})}function Ei({children:e,className:t=""}){const[r,a]=ee.useState(!1),o=typeof e=="string"?e.trimEnd().split(`
|
||||
`):null,c=o!==null&&o.length>i_,d=c&&!r?o.slice(0,i_).join(`
|
||||
`):e;return m.jsxs("div",{children:[m.jsx("pre",{className:`font-mono text-[13px] leading-relaxed whitespace-pre-wrap break-words mt-1 ${r?"overflow-auto max-h-[1200px]":"overflow-hidden"} ${t}`,children:d}),c&&m.jsx("button",{onClick:()=>a(!r),className:"text-xs text-[#555] hover:text-[#888] mt-0.5",children:r?"Show less":"Show more"})]})}function bg({code:e,language:t,className:r="",collapsible:a=!1}){const[s,o]=ee.useState(!1),c=e.trimEnd().split(`
|
||||
`),d=a&&c.length>a_,h=d&&!s?c.slice(0,a_).join(`
|
||||
`):e;let f;try{f=t?zn.highlight(h,{language:t,ignoreIllegals:!0}).value:zn.highlightAuto(h).value}catch{f=zn.highlightAuto(h).value}return m.jsxs("div",{children:[m.jsx("pre",{className:`font-mono text-[12px] leading-relaxed px-0 py-1 mt-1 whitespace-pre-wrap break-all ${a?s?"overflow-auto max-h-[1200px]":"overflow-hidden":"overflow-auto max-h-[400px]"} ${r}`,children:m.jsx("code",{dangerouslySetInnerHTML:{__html:f}})}),d&&m.jsx("button",{onClick:()=>o(!s),className:"text-xs text-[#555] hover:text-[#888] mt-0.5",children:s?"Show less":"Show more"})]})}const o7=50,s_=200,l_=25,o_=24,c7=[/\n?\[Command still running after [\d.]+s - showing output so far\.?\s*(?:Use C-c to interrupt if needed\.)?\]/g,/^\[Below is the output of the previous command\.\]\n?/gm,/^No command is currently running\. Cannot send input\.$/gm,/^A command is already running\. Use is_input=true to send input to it, or interrupt it first \(e\.g\., with C-c\)\.$/gm],u7=/^Chunk ID: [0-9a-f]+\s*$/,d7=[/^Wall time: [\d.]+ seconds\s*$/,/^Process exited with code -?\d+\s*$/,/^Process running with session ID \d+\s*$/,/^Original token count: \d+\s*$/];function f7(e){const t=[];for(let r=0;r<e.length;r++){if(u7.test(e[r])){let a=r+1;for(;a<e.length&&d7.some(s=>s.test(e[a]));)a++;a<e.length&&e[a].trim()==="Output:"&&a++,r=a-1;continue}t.push(e[r])}return t}function h7(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function zm(e){return e.length>s_?e.slice(0,s_-3)+"...":e}function m7(e,t=""){let r=e.replace(/\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*\x07)/g,"").replace(/\r/g,"");for(const a of c7)r=r.replace(a,"");if(r.trim()){const a=f7(r.split(`
|
||||
`)),s=[];for(const o of a)s.length===0&&!o.trim()||/^\[STRIX_\d+\]\$\s*/.test(o)||t&&o.trim()===t.trim()||t&&new RegExp(`^[\\$#>]\\s*${h7(t.trim())}\\s*$`).test(o)||s.push(o);for(;s.length>0&&/^\[STRIX_\d+\]\$\s*/.test(s[s.length-1]);)s.pop();r=s.join(`
|
||||
`)}return r.trim()}function p7(e){const t=e.split(`
|
||||
`);if(t.length<=o7)return t.map(zm).join(`
|
||||
`);const r=t.length-l_-o_;return[...t.slice(0,l_).map(zm),`... ${r} lines truncated ...`,...t.slice(-o_).map(zm)].join(`
|
||||
`)}function g7({toolName:e,args:t,result:r}){const a=e==="write_stdin",s=a?t.chars??t.input??"":t.command??t.cmd??"",o=r;let c=null,d=null,h=null;if(o&&typeof o=="object"){c=typeof o.content=="string"?o.content:null,d=typeof o.error=="string"?o.error:null,h=typeof o.exit_code=="number"?o.exit_code:null;const p=typeof o.status=="string"?o.status:"";(p==="running"||p==="command still running")&&(c=null)}else typeof o=="string"&&(c=o);const f=c?p7(m7(c,s)):null;return m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:a?"Terminal input":"Terminal"}),s&&m.jsx(bg,{code:s,language:"bash",collapsible:!0}),d&&m.jsx(Ei,{className:"text-red-400/70",children:d}),f&&m.jsx(Ei,{className:"text-[#666]",children:f}),h!=null&&h!==0&&m.jsxs("div",{className:"font-mono text-[13px] text-red-400/70 mt-0.5",children:["exit code ",h]})]})}const c_={back:"going back in browser history",forward:"going forward in browser history",scroll_down:"scrolling down",scroll_up:"scrolling up",refresh:"refreshing",close_tab:"closing tab",switch_tab:"switching tab",list_tabs:"listing tabs",view_source:"viewing page source",get_console_logs:"getting console logs",screenshot:"taking screenshot",wait:"waiting...",close:"closing"},u_={click:"clicking",double_click:"double clicking",hover:"hovering"};function Im({prefix:e,url:t,suffix:r}){return m.jsxs("span",{className:"text-[#888] text-[13px]",children:[e,t&&m.jsx("a",{href:t,target:"_blank",rel:"noopener noreferrer",className:"text-cyan-400/80 hover:underline",children:t}),r]})}function x7(e){const t=e.action??"",r=e.url??void 0;if(t in c_)return c_[t];if(t==="launch")return r?m.jsx(Im,{prefix:"launching ",url:r}):"launching";if(t==="goto"||t==="navigate")return m.jsx(Im,{prefix:"navigating to ",url:r});if(t==="new_tab")return m.jsx(Im,{prefix:"opening tab ",url:r});if(t in u_)return u_[t];if(t==="type")return`typing "${(e.text??"").slice(0,40)}"`;if(t==="press_key"||t==="key_press")return`pressing key ${e.key??""}`;if(t==="save_pdf"||t==="save_as_pdf"){const a=e.file_path??"";return`saving PDF${a?` to ${a}`:""}`}return t==="execute_js"?"executing javascript":t||"browser action"}function b7({args:e}){const r=(e.action??"")==="execute_js"?e.js_code??e.code??"":"",a=x7(e);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[m.jsx("span",{className:"text-blue-400/80 font-semibold text-sm shrink-0",children:"Browser"}),m.jsx("span",{className:"min-w-0 truncate text-[#888] text-[13px]",children:a})]}),r&&m.jsx(bg,{code:r,language:"javascript",collapsible:!0})]})}function yg(e){return e.length>60?"..."+e.slice(-57):e}const pu=30;function y7({toolName:e,args:t}){const r=t.path??t.file_path??"",a=t.command??"",s=t.old_str??"",o=t.new_str??"",c=t.regex??"";let d;e==="list_files"?d="list":e==="search_files"?d="search":a==="view"?d="view":a==="create"?d="create":a==="str_replace"?d="edit":a==="undo_edit"?d="undo":a==="insert"?d="insert":d="file";const h=r?yg(r):"",f=c?` /${c}/`:"",p=s?s.split(`
|
||||
`)),p=f.reduce((g,b)=>g.concat(...b),[]);return[f,p]}return[[],[]]},[e]);return ee.useEffect(()=>{const h=(t==null?void 0:t.target)??j1,f=(t==null?void 0:t.actInsideInputWithModifier)??!0;if(e!==null){const p=y=>{var S,w;if(s.current=y.ctrlKey||y.metaKey||y.shiftKey||y.altKey,(!s.current||s.current&&!f)&&uN(y))return!1;const N=L1(y.code,d);if(o.current.add(y[N]),D1(c,o.current,!1)){const C=((w=(S=y.composedPath)==null?void 0:S.call(y))==null?void 0:w[0])||y.target,E=(C==null?void 0:C.nodeName)==="BUTTON"||(C==null?void 0:C.nodeName)==="A";t.preventDefault!==!1&&(s.current||!E)&&y.preventDefault(),a(!0)}},g=y=>{const _=L1(y.code,d);D1(c,o.current,!0)?(a(!1),o.current.clear()):o.current.delete(y[_]),y.key==="Meta"&&o.current.clear(),s.current=!1},b=()=>{o.current.clear(),a(!1)};return h==null||h.addEventListener("keydown",p),h==null||h.addEventListener("keyup",g),window.addEventListener("blur",b),window.addEventListener("contextmenu",b),()=>{h==null||h.removeEventListener("keydown",p),h==null||h.removeEventListener("keyup",g),window.removeEventListener("blur",b),window.removeEventListener("contextmenu",b)}}},[e,a]),r}function D1(e,t,r){return e.filter(a=>r||a.length===t.size).some(a=>a.every(s=>t.has(s)))}function L1(e,t){return t.includes(e)?"code":"key"}const w8=()=>{const e=Lt();return ee.useMemo(()=>({zoomIn:async t=>{const{panZoom:r}=e.getState();return r?r.scaleBy(1.2,t):!1},zoomOut:async t=>{const{panZoom:r}=e.getState();return r?r.scaleBy(1/1.2,t):!1},zoomTo:async(t,r)=>{const{panZoom:a}=e.getState();return a?a.scaleTo(t,r):!1},getZoom:()=>e.getState().transform[2],setViewport:async(t,r)=>{const{transform:[a,s,o],panZoom:c}=e.getState();return c?(await c.setViewport({x:t.x??a,y:t.y??s,zoom:t.zoom??o},r),!0):!1},getViewport:()=>{const[t,r,a]=e.getState().transform;return{x:t,y:r,zoom:a}},setCenter:async(t,r,a)=>e.getState().setCenter(t,r,a),fitBounds:async(t,r)=>{const{width:a,height:s,minZoom:o,maxZoom:c,panZoom:d}=e.getState(),h=og(t,a,s,o,c,(r==null?void 0:r.padding)??.1);return d?(await d.setViewport(h,{duration:r==null?void 0:r.duration,ease:r==null?void 0:r.ease,interpolate:r==null?void 0:r.interpolate}),!0):!1},screenToFlowPosition:(t,r={})=>{const{transform:a,snapGrid:s,snapToGrid:o,domNode:c}=e.getState();if(!c)return t;const{x:d,y:h}=c.getBoundingClientRect(),f={x:t.x-d,y:t.y-h},p=r.snapGrid??s,g=r.snapToGrid??o;return Ho(f,a,g,p)},flowToScreenPosition:t=>{const{transform:r,domNode:a}=e.getState();if(!a)return t;const{x:s,y:o}=a.getBoundingClientRect(),c=Js(t,r);return{x:c.x+s,y:c.y+o}}}),[])};function RN(e,t){const r=[],a=new Map,s=[];for(const o of e)if(o.type==="add"){s.push(o);continue}else if(o.type==="remove"||o.type==="replace")a.set(o.id,[o]);else{const c=a.get(o.id);c?c.push(o):a.set(o.id,[o])}for(const o of t){const c=a.get(o.id);if(!c){r.push(o);continue}if(c[0].type==="remove")continue;if(c[0].type==="replace"){r.push({...c[0].item});continue}const d={...o};for(const h of c)E8(h,d);r.push(d)}return s.length&&s.forEach(o=>{o.index!==void 0?r.splice(o.index,0,{...o.item}):r.push({...o.item})}),r}function E8(e,t){switch(e.type){case"select":{t.selected=e.selected;break}case"position":{typeof e.position<"u"&&(t.position=e.position),typeof e.dragging<"u"&&(t.dragging=e.dragging);break}case"dimensions":{typeof e.dimensions<"u"&&(t.measured={...e.dimensions},e.setAttributes&&((e.setAttributes===!0||e.setAttributes==="width")&&(t.width=e.dimensions.width),(e.setAttributes===!0||e.setAttributes==="height")&&(t.height=e.dimensions.height))),typeof e.resizing=="boolean"&&(t.resizing=e.resizing);break}}}function jN(e,t){return RN(e,t)}function DN(e,t){return RN(e,t)}function Ha(e,t){return{id:e,type:"select",selected:t}}function qs(e,t=new Set,r=!1){const a=[];for(const[s,o]of e){const c=t.has(s);!(o.selected===void 0&&!c)&&o.selected!==c&&(r&&(o.selected=c),a.push(Ha(o.id,c)))}return a}function z1({items:e=[],lookup:t}){var s;const r=[],a=new Map(e.map(o=>[o.id,o]));for(const[o,c]of e.entries()){const d=t.get(c.id),h=((s=d==null?void 0:d.internals)==null?void 0:s.userNode)??d;h!==void 0&&h!==c&&r.push({id:c.id,item:c,type:"replace"}),h===void 0&&r.push({item:c,type:"add",index:o})}for(const[o]of t)a.get(o)===void 0&&r.push({id:o,type:"remove"});return r}function I1(e){return{id:e.id,type:"remove"}}const N8=sN();function S8(e,t,r={}){return uI(e,t,{...r,onError:r.onError??N8})}const B1=e=>Kz(e),k8=e=>tN(e);function LN(e){return ee.forwardRef(e)}const zN=typeof window<"u"?ee.useLayoutEffect:ee.useEffect;function U1(e){const[t,r]=ee.useState(BigInt(0)),[a]=ee.useState(()=>C8(()=>r(s=>s+BigInt(1))));return zN(()=>{const s=a.get();s.length&&(e(s),a.reset())},[t]),a}function C8(e){let t=[];return{get:()=>t,reset:()=>{t=[]},push:r=>{t.push(r),e()}}}const IN=ee.createContext(null);function T8({children:e}){const t=Lt(),r=ee.useCallback(d=>{const{nodes:h=[],setNodes:f,hasDefaultNodes:p,onNodesChange:g,nodeLookup:b,fitViewQueued:y,onNodesChangeMiddlewareMap:_}=t.getState();let N=h;for(const w of d)N=
Line truncated
|
||||
M${z.x},${z.y}h${z.width}v${z.height}h${-z.width}z`,fillRule:"evenodd",pointerEvents:"none"})]})})}c2.displayName="MiniMap";const wB=ee.memo(c2),EB=e=>t=>e?`${Math.max(1/t.transform[2],1)}`:void 0,NB={[el.Line]:"right",[el.Handle]:"bottom-right"};function SB({nodeId:e,position:t,variant:r=el.Handle,className:a,style:s=void 0,children:o,color:c,minWidth:d=10,minHeight:h=10,maxWidth:f=Number.MAX_VALUE,maxHeight:p=Number.MAX_VALUE,keepAspectRatio:g=!1,resizeDirection:b,autoScale:y=!0,shouldResize:_,onResizeStart:N,onResize:S,onResizeEnd:w}){const C=HN(),E=typeof e=="string"?e:C,A=Lt(),B=ee.useRef(null),R=r===el.Handle,H=dt(ee.useCallback(EB(R&&y),[R,y]),qt),z=ee.useRef(null),Y=t??NB[r];ee.useEffect(()=>{if(!(!B.current||!E))return z.current||(z.current=GI({domNode:B.current,nodeId:E,getStoreItems:()=>{const{nodeLookup:I,transform:Z,snapGrid:P,snapToGrid:k,nodeOrigin:$,domNode:O}=A.getState();return{nodeLookup:I,transform:Z,snapGrid:P,snapToGrid:k,nodeOrigin:$,paneDomNode:O}},onChange:(I,Z)=>{const{triggerNodeChanges:P,nodeLookup:k,parentLookup:$,nodeOrigin:O}=A.getState(),U=[],K={x:I.x,y:I.y},X=k.get(E);if(X&&X.expandParent&&X.parentId){const M=X.origin??O,L=I.width??X.measured.width??0,F=I.height??X.measured.height??0,D={id:X.id,parentId:X.parentId,rect:{width:L,height:F,...oN({x:I.x??X.position.x,y:I.y??X.position.y},{width:L,height:F},X.parentId,k,M)}},V=mg([D],k,$,O);U.push(...V),K.x=I.x?Math.max(M[0]*L,I.x):void 0,K.y=I.y?Math.max(M[1]*F,I.y):void 0}if(K.x!==void 0&&K.y!==void 0){const M={id:E,type:"position",position:{...K}};U.push(M)}if(I.width!==void 0&&I.height!==void 0){const L={id:E,type:"dimensions",resizing:!0,setAttributes:b?b==="horizontal"?"width":"height":!0,dimensions:{width:I.width,height:I.height}};U.push(L)}for(const M of Z){const L={...M,type:"position"};U.push(L)}P(U)},onEnd:({width:I,height:Z})=>{const P={id:E,type:"dimensions",resizing:!1,dimensions:{width:I,height:Z}};A.getState().triggerNodeChanges([P])}})),z.current.update({controlPosition:Y,boundaries:{minWidth:d,minHeight:h,maxWidth:f,maxHeight:p},keepAspectRatio:g,resizeDirection:b,onResizeStart:N,onResize:S,onResizeEnd:w,shouldResize:_}),()=>{var I;(I=z.current)==null||I.destroy()}},[Y,d,h,f,p,g,N,S,w,_]);const j=Y.split("-");return m.jsx("div",{className:on(["react-flow__resize-control","nodrag",...j,r,a]),ref:B,style:{...s,scale:H,...c&&{[R?"backgroundColor":"borderColor"]:c}},children:o})}ee.memo(SB);var vt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),zr=vt((e,t)=>{var r=Object.defineProperty,a=(P,k,$)=>k in P?r(P,k,{enumerable:!0,configurable:!0,writable:!0,value:$}):P[k]=$,s=(P,k)=>()=>(k||P((k={exports:{}}).exports,k),k.exports),o=(P,k,$)=>a(P,typeof k!="symbol"?k+"":k,$),c=s((P,k)=>{var $="\0",O="\0",U="",K=class{constructor(V){o(this,"_isDirected",!0),o(this,"_isMultigraph",!1),o(this,"_isCompound",!1),o(this,"_label"),o(this,"_defaultNodeLabelFn",()=>{}),o(this,"_defaultEdgeLabelFn",()=>{}),o(this,"_nodes",{}),o(this,"_in",{}),o(this,"_preds",{}),o(this,"_out",{}),o(this,"_sucs",{}),o(this,"_edgeObjs",{}),o(this,"_edgeLabels",{}),o(this,"_nodeCount",0),o(this,"_edgeCount",0),o(this,"_parent"),o(this,"_children"),V&&(this._isDirected=Object.hasOwn(V,"directed")?V.directed:!0,this._isMultigraph=Object.hasOwn(V,"multigraph")?V.multigraph:!1,this._isCompound=Object.hasOwn(V,"compound")?V.compound:!1),this._isCompound&&(this._parent={},this._children={},this._children[O]={})}isDirected(){return this._isDirected}isMultigraph(){return this._isMultigraph}isCompound(){return this._isCompound}setGraph(V){return this._label=V,this}graph(){return this._label}setDefaultNodeLabel(V){return this._defaultNodeLabelFn=V,typeof V!="function"&&(this._defaultNodeLabelFn=()=>V),this}nodeCount(){return this._nodeCount}nodes(){return Object.keys(this._nodes)}sources(){var V=this;return this.nodes().filter(q=>Object.keys(V._in[q]).length===0)}sinks(){var V=this;return this.nodes().filter(q=>Object.keys(V._out[q]).length===0)}setNodes(V,q){var Q=arguments,J=this;return V.forEach(function(W){Q.length>1?J.setNode(W,q):J.setNode(W)}),this}setNode(V,q){return Object.hasOwn(this._nodes,V)?(arguments.length>1&&(this._nodes[V]=q),this):(this._nodes[V]=arguments.length>1?q:this._defaultNodeLabelFn(V),this._isCompound&&(this._parent[V]=O,this._children[V]={},this._children[O][V]=!0),this._in[V]={},this._preds[V]={},this._out[V]={},this._sucs[V]={},++this._nodeCount,this)}node(V){return this._nodes[V]}hasNode(V){return Object.hasOwn(this._nodes,V)}removeNode(V){var q=this;if(Object.hasOwn(this._nodes,V)){var Q=J=>q.removeEdge(q._edgeObjs[J]);delete this._nodes[V],this._isCompound&&(this._removeFromParentsChildList(V),delete this._parent[V],this.children(V).forEach(function(J){q.setParent(J)}),delete this._children[V]),Object.keys(this._in[V]).forEach(Q),delete this._in[V],delete this._preds[V],Object.keys(this._out[V]).forEach(Q),delete this._out[V],delete this._sucs[V],--this._nodeCount}return this}setParent(V,q){if(!this._isCompound
Line truncated
|
||||
`).length>t;return m.jsxs("div",{children:[m.jsx("div",{className:r&&o?"max-h-[1200px] overflow-auto":"",style:!r&&o?{display:"-webkit-box",WebkitLineClamp:t,WebkitBoxOrient:"vertical",overflow:"hidden"}:void 0,children:m.jsx(ua,{text:e})}),o&&m.jsx("button",{onClick:()=>a(!r),className:"text-xs text-[#555] hover:text-[#888] mt-1",children:r?"Show less":"Show more"})]})}function wi({children:e,className:t=""}){const[r,a]=ee.useState(!1),o=typeof e=="string"?e.trimEnd().split(`
|
||||
`):null,c=o!==null&&o.length>n_,d=c&&!r?o.slice(0,n_).join(`
|
||||
`):e;return m.jsxs("div",{children:[m.jsx("pre",{className:`font-mono text-[13px] leading-relaxed whitespace-pre-wrap break-words mt-1 ${r?"overflow-auto max-h-[1200px]":"overflow-hidden"} ${t}`,children:d}),c&&m.jsx("button",{onClick:()=>a(!r),className:"text-xs text-[#555] hover:text-[#888] mt-0.5",children:r?"Show less":"Show more"})]})}function gg({code:e,language:t,className:r="",collapsible:a=!1}){const[s,o]=ee.useState(!1),c=e.trimEnd().split(`
|
||||
`),d=a&&c.length>r_,h=d&&!s?c.slice(0,r_).join(`
|
||||
`):e;let f;try{f=t?zn.highlight(h,{language:t,ignoreIllegals:!0}).value:zn.highlightAuto(h).value}catch{f=zn.highlightAuto(h).value}return m.jsxs("div",{children:[m.jsx("pre",{className:`font-mono text-[12px] leading-relaxed px-0 py-1 mt-1 whitespace-pre-wrap break-all ${a?s?"overflow-auto max-h-[1200px]":"overflow-hidden":"overflow-auto max-h-[400px]"} ${r}`,children:m.jsx("code",{dangerouslySetInnerHTML:{__html:f}})}),d&&m.jsx("button",{onClick:()=>o(!s),className:"text-xs text-[#555] hover:text-[#888] mt-0.5",children:s?"Show less":"Show more"})]})}const a7=50,i_=200,a_=25,s_=24,s7=[/\n?\[Command still running after [\d.]+s - showing output so far\.?\s*(?:Use C-c to interrupt if needed\.)?\]/g,/^\[Below is the output of the previous command\.\]\n?/gm,/^No command is currently running\. Cannot send input\.$/gm,/^A command is already running\. Use is_input=true to send input to it, or interrupt it first \(e\.g\., with C-c\)\.$/gm],l7=/^Chunk ID: [0-9a-f]+\s*$/,o7=[/^Wall time: [\d.]+ seconds\s*$/,/^Process exited with code -?\d+\s*$/,/^Process running with session ID \d+\s*$/,/^Original token count: \d+\s*$/];function c7(e){const t=[];for(let r=0;r<e.length;r++){if(l7.test(e[r])){let a=r+1;for(;a<e.length&&o7.some(s=>s.test(e[a]));)a++;a<e.length&&e[a].trim()==="Output:"&&a++,r=a-1;continue}t.push(e[r])}return t}function u7(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Lm(e){return e.length>i_?e.slice(0,i_-3)+"...":e}function d7(e,t=""){let r=e.replace(/\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*\x07)/g,"").replace(/\r/g,"");for(const a of s7)r=r.replace(a,"");if(r.trim()){const a=c7(r.split(`
|
||||
`)),s=[];for(const o of a)s.length===0&&!o.trim()||/^\[STRIX_\d+\]\$\s*/.test(o)||t&&o.trim()===t.trim()||t&&new RegExp(`^[\\$#>]\\s*${u7(t.trim())}\\s*$`).test(o)||s.push(o);for(;s.length>0&&/^\[STRIX_\d+\]\$\s*/.test(s[s.length-1]);)s.pop();r=s.join(`
|
||||
`)}return r.trim()}function f7(e){const t=e.split(`
|
||||
`);if(t.length<=a7)return t.map(Lm).join(`
|
||||
`);const r=t.length-a_-s_;return[...t.slice(0,a_).map(Lm),`... ${r} lines truncated ...`,...t.slice(-s_).map(Lm)].join(`
|
||||
`)}function h7({toolName:e,args:t,result:r}){const a=e==="write_stdin",s=a?t.chars??t.input??"":t.command??t.cmd??"",o=r;let c=null,d=null,h=null;if(o&&typeof o=="object"){c=typeof o.content=="string"?o.content:null,d=typeof o.error=="string"?o.error:null,h=typeof o.exit_code=="number"?o.exit_code:null;const p=typeof o.status=="string"?o.status:"";(p==="running"||p==="command still running")&&(c=null)}else typeof o=="string"&&(c=o);const f=c?f7(d7(c,s)):null;return m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:a?"Terminal input":"Terminal"}),s&&m.jsx(gg,{code:s,language:"bash",collapsible:!0}),d&&m.jsx(wi,{className:"text-red-400/70",children:d}),f&&m.jsx(wi,{className:"text-[#666]",children:f}),h!=null&&h!==0&&m.jsxs("div",{className:"font-mono text-[13px] text-red-400/70 mt-0.5",children:["exit code ",h]})]})}const l_={back:"going back in browser history",forward:"going forward in browser history",scroll_down:"scrolling down",scroll_up:"scrolling up",refresh:"refreshing",close_tab:"closing tab",switch_tab:"switching tab",list_tabs:"listing tabs",view_source:"viewing page source",get_console_logs:"getting console logs",screenshot:"taking screenshot",wait:"waiting...",close:"closing"},o_={click:"clicking",double_click:"double clicking",hover:"hovering"};function zm({prefix:e,url:t,suffix:r}){return m.jsxs("span",{className:"text-[#888] text-[13px]",children:[e,t&&m.jsx("a",{href:t,target:"_blank",rel:"noopener noreferrer",className:"text-cyan-400/80 hover:underline",children:t}),r]})}function m7(e){const t=e.action??"",r=e.url??void 0;if(t in l_)return l_[t];if(t==="launch")return r?m.jsx(zm,{prefix:"launching ",url:r}):"launching";if(t==="goto"||t==="navigate")return m.jsx(zm,{prefix:"navigating to ",url:r});if(t==="new_tab")return m.jsx(zm,{prefix:"opening tab ",url:r});if(t in o_)return o_[t];if(t==="type")return`typing "${(e.text??"").slice(0,40)}"`;if(t==="press_key"||t==="key_press")return`pressing key ${e.key??""}`;if(t==="save_pdf"||t==="save_as_pdf"){const a=e.file_path??"";return`saving PDF${a?` to ${a}`:""}`}return t==="execute_js"?"executing javascript":t||"browser action"}function p7({args:e}){const r=(e.action??"")==="execute_js"?e.js_code??e.code??"":"",a=m7(e);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[m.jsx("span",{className:"text-blue-400/80 font-semibold text-sm shrink-0",children:"Browser"}),m.jsx("span",{className:"min-w-0 truncate text-[#888] text-[13px]",children:a})]}),r&&m.jsx(gg,{code:r,language:"javascript",collapsible:!0})]})}function xg(e){return e.length>60?"..."+e.slice(-57):e}const mu=30;function g7({toolName:e,args:t}){const r=t.path??t.file_path??"",a=t.command??"",s=t.old_str??"",o=t.new_str??"",c=t.regex??"";let d;e==="list_files"?d="list":e==="search_files"?d="search":a==="view"?d="view":a==="create"?d="create":a==="str_replace"?d="edit":a==="undo_edit"?d="undo":a==="insert"?d="insert":d="file";const h=r?xg(r):"",f=c?` /${c}/`:"",p=s?s.split(`
|
||||
`):[],g=o?o.split(`
|
||||
`):[],b=p.length+g.length,y=b>pu,_=y?Math.round(pu*(p.length/b)):p.length,N=y?pu-_:g.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:d}),h&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:h})]}),f&&m.jsx("div",{className:"text-purple-400/60 font-mono text-[13px] break-all mt-0.5",children:f}),(s||o)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[p.slice(0,_).map((S,w)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),S]},`o${w}`)),g.slice(0,N).map((S,w)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),S]},`n${w}`)),y&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",b-pu," more lines"]})]})]})}const gu=30,v7="*** Begin Patch",_7="*** End Patch",d_="*** Add File: ",f_="*** Update File: ",h_="*** Delete File: ",w7={add:"create",update:"edit",delete:"delete"};function E7(e){const t=e.patch;return typeof t=="string"?t:t&&typeof t=="object"&&typeof t.patch=="string"?t.patch:typeof e.input=="string"?e.input:""}function N7(e){const t=[];let r=null;const a=()=>{r&&t.push(r),r=null};for(const s of e.split(`
|
||||
`))if(!(s===v7||s===_7))if(s.startsWith(d_))a(),r={kind:"add",path:s.slice(d_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(f_))a(),r={kind:"update",path:s.slice(f_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(h_))a(),r={kind:"delete",path:s.slice(h_.length).trim(),oldLines:[],newLines:[]};else if((r==null?void 0:r.kind)==="update"){if(s.startsWith("@@"))continue;s.startsWith("-")&&!s.startsWith("---")?r.oldLines.push(s.slice(1)):s.startsWith("+")&&!s.startsWith("+++")&&r.newLines.push(s.slice(1))}else(r==null?void 0:r.kind)==="add"&&(s.startsWith("+")?r.newLines.push(s.slice(1)):s.trim()&&r.newLines.push(s));return a(),t}function S7({op:e}){const t=w7[e.kind]??"file",r=e.oldLines.length+e.newLines.length,a=r>gu,s=a&&r>0?Math.round(gu*(e.oldLines.length/r)):e.oldLines.length,o=a?gu-s:e.newLines.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:t}),e.path&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:yg(e.path)})]}),(e.oldLines.length>0||e.newLines.length>0)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[e.oldLines.slice(0,s).map((c,d)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),c]},`o${d}`)),e.newLines.slice(0,o).map((c,d)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),c]},`n${d}`)),a&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",r-gu," more lines"]})]})]})}function k7({args:e,result:t,status:r}){const a=N7(E7(e));return a.length===0?m.jsxs("div",{children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm",children:"patch"}),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:t.trim()})]}):m.jsxs("div",{className:"space-y-2",children:[a.map((s,o)=>m.jsx(S7,{op:s},o)),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px]",children:t.trim()})]})}const C7=/data:image\/(png|jpe?g|gif|webp);base64,([A-Za-z0-9+/]+={0,2})/;function T7(e){let t=null;if(typeof e=="string")t=e;else if(e&&typeof e=="object"){const a=e;typeof a.image_url=="string"?t=a.image_url:typeof a.url=="string"&&(t=a.url)}if(!t)return null;const r=C7.exec(t);return!r||r[2].length<100||r[2].length%4!==0?null:`data:image/${r[1]};base64,${r[2]}`}function A7({args:e,result:t}){const r=(e.path??"").trim(),a=T7(t);let s=null;if(!a&&typeof t=="string"){const o=t.trim();o&&!o.toLowerCase().startsWith("data:image/")&&!o.startsWith("{")&&(s=o)}return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:"view image"}),r&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:yg(r)})]}),a&&m.jsx("img",{src:a,alt:r||"Tool image output",className:"mt-1.5 max-w-full max-h-96 rounded-lg border border-white/[0.06] object-contain"}),s&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:s})]})}const M7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400"},O7={high:"text-emerald-400",medium:"text-yellow-400",low:"text-orange-400"};function R7({args:e,result:t}){const r=e.title??"",a=e.description??"",s=e.impact??"",o=e.target??"",c=e.endpoint??"",d=e.method??"",h=e.technical_analysis??"",f=e.poc_description??"",{language:p,code:g}=EE(e.poc_script_code??""),b=e.remediation_steps??"",y=e.cve??"",_=e.cwe??"",N=e.counterevidence??"",S=(e.confidence??"").toLowerCase(),w=e.confidence_rationale??"",C=e.severity_change_conditions??"",E=e.fix_verification??"",A=t,B=(A&&typeof A=="object"?A.severity:null)??e.severity??"medium",R=String(B).toLowerCase(),H=(A&&typeof A=="object"?A.cvss_score:null)??e.cvss??null,z=M7[R]??"text-yellow-400";return m.jsxs("div",{className:"space-y-3",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:`font-semibold text-sm ${z}`,children:R.toUpperCase()}),H!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["CVSS ",H]}),y&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:y}),_&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:_}),S&&m.jsxs("span",{className:`text-[13px] ${O7[S]??"text-[#888]"}`,children:[S," confidence"]})]}),r&&m.jsx("div",{className:"text-[15px] text-white/80 font-semibold",children:r}),(o||c)&&m.jsxs("div",{className:"text-[13px] text-[#888] font-mono",children:[o,c?` ${d} ${c}`:""]}),a&&m.jsx(qt,{text:a,maxLines:20}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Impact"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text
Line truncated
|
||||
`),a=r.slice(0,t).map(s=>Xr(s,p2-5)).join(`
|
||||
`):[],b=p.length+g.length,y=b>mu,_=y?Math.round(mu*(p.length/b)):p.length,N=y?mu-_:g.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:d}),h&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:h})]}),f&&m.jsx("div",{className:"text-purple-400/60 font-mono text-[13px] break-all mt-0.5",children:f}),(s||o)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[p.slice(0,_).map((S,w)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),S]},`o${w}`)),g.slice(0,N).map((S,w)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),S]},`n${w}`)),y&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",b-mu," more lines"]})]})]})}const pu=30,x7="*** Begin Patch",b7="*** End Patch",c_="*** Add File: ",u_="*** Update File: ",d_="*** Delete File: ",y7={add:"create",update:"edit",delete:"delete"};function v7(e){const t=e.patch;return typeof t=="string"?t:t&&typeof t=="object"&&typeof t.patch=="string"?t.patch:typeof e.input=="string"?e.input:""}function _7(e){const t=[];let r=null;const a=()=>{r&&t.push(r),r=null};for(const s of e.split(`
|
||||
`))if(!(s===x7||s===b7))if(s.startsWith(c_))a(),r={kind:"add",path:s.slice(c_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(u_))a(),r={kind:"update",path:s.slice(u_.length).trim(),oldLines:[],newLines:[]};else if(s.startsWith(d_))a(),r={kind:"delete",path:s.slice(d_.length).trim(),oldLines:[],newLines:[]};else if((r==null?void 0:r.kind)==="update"){if(s.startsWith("@@"))continue;s.startsWith("-")&&!s.startsWith("---")?r.oldLines.push(s.slice(1)):s.startsWith("+")&&!s.startsWith("+++")&&r.newLines.push(s.slice(1))}else(r==null?void 0:r.kind)==="add"&&(s.startsWith("+")?r.newLines.push(s.slice(1)):s.trim()&&r.newLines.push(s));return a(),t}function w7({op:e}){const t=y7[e.kind]??"file",r=e.oldLines.length+e.newLines.length,a=r>pu,s=a&&r>0?Math.round(pu*(e.oldLines.length/r)):e.oldLines.length,o=a?pu-s:e.newLines.length;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:t}),e.path&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:xg(e.path)})]}),(e.oldLines.length>0||e.newLines.length>0)&&m.jsxs("div",{className:"font-mono text-[13px] leading-relaxed mt-1.5",children:[e.oldLines.slice(0,s).map((c,d)=>m.jsxs("div",{className:"text-red-400/60",children:[m.jsx("span",{className:"select-none text-red-400/30 mr-1",children:"-"}),c]},`o${d}`)),e.newLines.slice(0,o).map((c,d)=>m.jsxs("div",{className:"text-emerald-400/60",children:[m.jsx("span",{className:"select-none text-emerald-400/30 mr-1",children:"+"}),c]},`n${d}`)),a&&m.jsxs("div",{className:"text-[#444] mt-0.5",children:["... ",r-pu," more lines"]})]})]})}function E7({args:e,result:t,status:r}){const a=_7(v7(e));return a.length===0?m.jsxs("div",{children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm",children:"patch"}),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:t.trim()})]}):m.jsxs("div",{className:"space-y-2",children:[a.map((s,o)=>m.jsx(w7,{op:s},o)),r==="failed"&&typeof t=="string"&&t.trim()&&m.jsx("div",{className:"text-red-400/70 text-[13px]",children:t.trim()})]})}const N7=/data:image\/(png|jpe?g|gif|webp);base64,([A-Za-z0-9+/]+={0,2})/;function S7(e){let t=null;if(typeof e=="string")t=e;else if(e&&typeof e=="object"){const a=e;typeof a.image_url=="string"?t=a.image_url:typeof a.url=="string"&&(t=a.url)}if(!t)return null;const r=N7.exec(t);return!r||r[2].length<100||r[2].length%4!==0?null:`data:image/${r[1]};base64,${r[2]}`}function k7({args:e,result:t}){const r=(e.path??"").trim(),a=S7(t);let s=null;if(!a&&typeof t=="string"){const o=t.trim();o&&!o.toLowerCase().startsWith("data:image/")&&!o.startsWith("{")&&(s=o)}return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-baseline gap-2",children:[m.jsx("span",{className:"text-sky-400/80 font-semibold text-sm shrink-0",children:"view image"}),r&&m.jsx("span",{className:"text-[#888] font-mono text-[13px] break-all",children:xg(r)})]}),a&&m.jsx("img",{src:a,alt:r||"Tool image output",className:"mt-1.5 max-w-full max-h-96 rounded-lg border border-white/[0.06] object-contain"}),s&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1",children:s})]})}const C7={critical:"text-red-400",high:"text-orange-400",medium:"text-yellow-400",low:"text-blue-400",info:"text-cyan-400"},T7={high:"text-emerald-400",medium:"text-yellow-400",low:"text-orange-400"};function A7({args:e,result:t}){const r=e.title??"",a=e.description??"",s=e.impact??"",o=e.target??"",c=e.endpoint??"",d=e.method??"",h=e.technical_analysis??"",f=e.poc_description??"",{language:p,code:g}=yE(e.poc_script_code??""),b=e.remediation_steps??"",y=e.cve??"",_=e.cwe??"",N=e.counterevidence??"",S=(e.confidence??"").toLowerCase(),w=e.confidence_rationale??"",C=e.severity_change_conditions??"",E=e.fix_verification??"",A=t,B=(A&&typeof A=="object"?A.severity:null)??e.severity??"medium",R=String(B).toLowerCase(),H=(A&&typeof A=="object"?A.cvss_score:null)??e.cvss??null,z=C7[R]??"text-yellow-400";return m.jsxs("div",{className:"space-y-3",children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:`font-semibold text-sm ${z}`,children:R.toUpperCase()}),H!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["CVSS ",H]}),y&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:y}),_&&m.jsx("span",{className:"text-[#888] font-mono text-[13px]",children:_}),S&&m.jsxs("span",{className:`text-[13px] ${T7[S]??"text-[#888]"}`,children:[S," confidence"]})]}),r&&m.jsx("div",{className:"text-[15px] text-white/80 font-semibold",children:r}),(o||c)&&m.jsxs("div",{className:"text-[13px] text-[#888] font-mono",children:[o,c?` ${d} ${c}`:""]}),a&&m.jsx(Yt,{text:a,maxLines:20}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Impact"}),m.jsx("div",{className:"mt-1",children:m.jsx(Yt,{text
Line truncated
|
||||
`),a=r.slice(0,t).map(s=>Xr(s,d2-5)).join(`
|
||||
`);return r.length>t?a+`
|
||||
...`:a}function I7({args:e,result:t}){const r=e.httpql_filter??"",a=t,s=a?a.requests:null,o=Array.isArray(s)?s:[];return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"listing requests"}),r&&m.jsx("span",{className:"text-[#888] text-[13px]",children:Xr(r,150)})]}),o.length>0&&m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:[o.slice(0,20).map((c,d)=>{const h=(c.method??"GET").toUpperCase(),f=c.host??"",p=c.path??"",g=c.response,b=(g==null?void 0:g.statusCode)??null;return m.jsxs("div",{className:"flex gap-2",children:[m.jsx("span",{className:`w-10 shrink-0 font-bold ${g2[h]??"text-[#888]"}`,children:h}),m.jsx("span",{className:"text-[#777] truncate",children:Xr(f+p,180)}),b!=null&&m.jsx("span",{className:`ml-auto shrink-0 ${vg(b)}`,children:b})]},d)}),o.length>20&&m.jsxs("div",{className:"text-[#555]",children:["... +",o.length-20," more"]})]})]})}function B7({args:e,result:t}){const r=e.request_id,a=e.part??"request",s=e.search_pattern??"",o=t,c=o?o.matches:null,d=Array.isArray(c)?c:[],h=o?o.content??null:null,f=o?!!o.has_more:!1;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsxs("span",{className:"text-purple-400/80 font-semibold text-sm",children:[s?"searching":"viewing"," ",a]}),r!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",r]}),s&&m.jsxs("span",{className:"text-[#666] font-mono text-[13px]",children:["/",Xr(s,100),"/"]})]}),d.length>0&&m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-1",children:[d.slice(0,5).map((p,g)=>{const b=(p.before??"").replace(/\n/g," ").replace(/\r/g,"").slice(-100),y=(p.after??"").replace(/\n/g," ").replace(/\r/g,"").slice(0,100);return m.jsxs("div",{children:[b&&m.jsxs("span",{className:"text-[#555]",children:["...",b]}),m.jsx("span",{className:"text-amber-400/80 font-bold",children:p.match}),y&&m.jsxs("span",{className:"text-[#555]",children:[y,"..."]})]},g)}),d.length>5&&m.jsxs("div",{className:"text-[#555]",children:["... +",d.length-5," more matches"]})]}),h&&!d.length&&(()=>{const p=h.split(`
|
||||
`),g=p.slice(0,15).map(y=>Xr(y,p2)).join(`
|
||||
`),b=f||p.length>15;return m.jsx(Ei,{className:"text-[#666]",children:g+(b?`
|
||||
... more content available`:"")})})()]})}function U7({args:e,result:t}){const r=(e.method??"GET").toUpperCase(),a=e.url??"",s=e.headers,o=e.body,c=typeof o=="string"?o:"",d=t,h=d?d.error??null:null,f=d?d.status_code??null:null,p=d?d.response_time_ms??null:null,g=d?d.body:null,b=typeof g=="string"?g:null;return m.jsxs("div",{children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"request"}),m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:[m.jsxs("div",{children:[m.jsx("span",{className:"text-[#555] select-none mr-1",children:">>"}),m.jsx("span",{className:`font-bold ${g2[r]??"text-[#888]"}`,children:r}),m.jsx("span",{className:"text-[#888] ml-1 break-all",children:Xr(a,180)})]}),s&&typeof s=="object"&&Object.entries(s).slice(0,5).map(([y,_])=>m.jsxs("div",{className:"text-[#555] pl-5",children:[y,": ",_p(String(_),150)]},y))]}),c&&m.jsx(Ei,{className:"text-[#888]",children:wp(c,4)}),h&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1.5",children:_p(h,150)}),f!=null&&m.jsxs("div",{className:"font-mono text-[13px] mt-1.5",children:[m.jsx("span",{className:"text-[#555] select-none mr-1",children:"<<"}),m.jsx("span",{className:`font-bold ${vg(f)}`,children:f}),p!=null&&m.jsxs("span",{className:"text-[#555] ml-2",children:[p,"ms"]})]}),b&&m.jsx(Ei,{className:"text-[#666]",children:wp(b,6)})]})}function H7({args:e,result:t}){const r=e.request_id,a=e.modifications,s=t,o=s?s.status_code??null:null,c=s?s.response_time_ms??null:null,d=s?s.body:null,h=typeof d=="string"?d:null;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"repeating request"}),r!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",r]})]}),a&&typeof a=="object"&&Object.keys(a).length>0&&m.jsx("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:Object.entries(a).slice(0,5).map(([f,p])=>m.jsxs("div",{children:[m.jsxs("span",{className:"text-orange-400/60",children:[f,":"]})," ",m.jsx("span",{className:"text-[#777]",children:_p(typeof p=="string"?p:JSON.stringify(p),150)})]},f))}),o!=null&&m.jsxs("div",{className:"font-mono text-[13px] mt-1.5",children:[m.jsx("span",{className:"text-[#555] select-none mr-1",children:"<<"}),m.jsx("span",{className:`font-bold ${vg(o)}`,children:o}),c!=null&&m.jsxs("span",{className:"text-[#555] ml-2",children:[c,"ms"]})]}),h&&m.jsx(Ei,{className:"text-[#666]",children:wp(h,5)})]})}const $7={get:"getting",list:"listing",create:"creating",update:"updating",delete:"deleting"};function q7({args:e}){const t=e.action??"",r=e.scope_name??"",a=$7[t]??(t||"managing");return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsxs("span",{className:"text-purple-400/80 font-semibold text-sm",children:[a," proxy scope"]}),r&&m.jsx("span",{className:"text-[#888] text-[13px]",children:Xr(r,50)})]})}function P7({args:e}){const t=e.parent_id;return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"listing sitemap"}),t&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["under #",Xr(String(t),20)]})]})}function F7({args:e}){const t=e.entry_id;return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"viewing sitemap entry"}),t&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",Xr(String(t),20)]})]})}function G7(e){switch(e.toolName){case"list_requests":return m.jsx(I7,{...e});case"view_request":return m.jsx(B7,{...e});case"send_request":return m.jsx(U7,{...e});case"repeat_request":return m.jsx(H7,{...e});case"scope_rules":return m.jsx(q7,{...e});case"list_sitemap":return m.jsx(P7,{...e});case"view_sitemap_entry":return m.jsx(F7,{...e});default:return m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:e.toolName.replace(/_/g," ")})}}function V7({args:e}){const t=e.thought??e.content??"";return t?m.jsxs("div",{children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"Agent is thinking"}),m.jsx("div",{className:"mt-1.5 italic text-[#888]",children:m.jsx(qt,{text:t,maxLines:20})})]}):null}function Y7({toolName:e,args:t}){if(e==="create_agent"){const r=t.name??t.agent_name??"",a=t.task??"";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:"spawning"}),r&&m.jsx("span",{className:"text-cyan-400 font-semibold text-sm",children:r})]}),a&&m.jsx("div",{className:"mt-1.5",children:m.jsx(qt,{text:a,maxLines:15})})]})}if(e==="agent_finish"){const r=t.result_summary??"",a=t.success,s=t.findings,o=Array.isArray(s)?s:void 0;return m.jsxs("div",{children:[m.jsx("span",{className:`font-semibold text-sm ${a===!1?"text-red-400/80":"text-emerald-400/80"}`,children:a===!1?"A
Line truncated
|
||||
`);if(t.length<=K7)return t.map(Um).join(`
|
||||
`);const r=t.length-g_-x_;return[...t.slice(0,g_).map(Um),`... ${r} lines truncated ...`,...t.slice(-x_).map(Um)].join(`
|
||||
`)}function tU({args:e,result:t}){const r=e.action??"",a=e.code??e.script??"",s=t;let o=null;s&&typeof s=="object"?o=typeof s.stdout=="string"?s.stdout:null:typeof s=="string"&&(o=s);const c=r==="new_session"?"new session":r==="close"?"close session":r==="list_sessions"?"list sessions":null,d=o?eU(J7(o)):null;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-yellow-400/80 font-semibold text-sm",children:"Python"}),c&&m.jsx("span",{className:"text-[#888] text-[13px]",children:c})]}),a&&m.jsx(bg,{code:a,language:"python",collapsible:!0}),d&&m.jsx(Ei,{className:"text-[#666]",children:d})]})}function nU({args:e}){const t=e.targets,a=(Array.isArray(t)?t:[]).map(s=>typeof s=="object"&&s?s.original??null:null).filter(Boolean);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:"Starting penetration test"}),a.length===1&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["on ",a[0]]})]}),a.length>1&&m.jsx("div",{className:"mt-1.5 space-y-0.5",children:a.map((s,o)=>m.jsxs("div",{className:"text-[13px] text-[#888]",children:[m.jsx("span",{className:"text-[#555] mr-1",children:"•"}),s]},o))})]})}function rU({args:e}){const t=e.name??"Unknown Agent",r=e.task??"";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-[#888] text-[13px]",children:"subagent"}),m.jsx("span",{className:"text-purple-400 font-semibold text-sm",children:t})]}),r&&m.jsx("div",{className:"mt-1.5",children:m.jsx(qt,{text:r,maxLines:15})})]})}function iU(e){return e.toolName==="subagent_start_info"?m.jsx(rU,{...e}):m.jsx(nU,{...e})}function aU({args:e}){const t=e.executive_summary??"",r=e.methodology??"",a=e.technical_analysis??"",s=e.recommendations??"";return m.jsxs("div",{className:"space-y-3",children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:"Penetration test completed"}),t&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Executive Summary"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:t,maxLines:25})})]}),r&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Methodology"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:r,maxLines:25})})]}),a&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Technical Analysis"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:a,maxLines:25})})]}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Recommendations"}),m.jsx("div",{className:"mt-1",children:m.jsx(qt,{text:s,maxLines:25})})]}),!t&&!r&&!a&&!s&&m.jsx("div",{className:"text-[#555] text-xs",children:"Generating final report..."})]})}function sU({toolName:e,args:t,result:r}){if(e==="create_note"){const a=t.title??"",s=t.content??"",o=t.category??"general";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note"}),m.jsxs("span",{className:"text-[#555] text-[13px]",children:["(",o,")"]})]}),a&&m.jsx("div",{className:"mt-1.5 text-[#999] text-[13px]",children:a}),s&&m.jsx("div",{className:"mt-1",children:m.jsx(da,{text:s})})]})}if(e==="delete_note")return m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note removed"});if(e==="update_note"){const a=t.title??"",s=t.content??"";return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note updated"}),a&&m.jsx("div",{className:"mt-1.5 text-[#999] text-[13px]",children:a}),s&&m.jsx("div",{className:"mt-1",children:m.jsx(da,{text:s})})]})}if(e==="get_note"){const a=r,s=a&&typeof a=="object"&&a.success?a.note:void 0;return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note read"}),s&&m.jsxs(m.Fragment,{children:[m.jsxs("div",{className:"mt-1.5 text-[#999] text-[13px]",children:[s.title??"(untitled)",m.jsxs("span",{className:"text-[#555] ml-1",children:["(",s.category??"general",")"]}),(s.by_you||s.agent_name)&&m.jsxs("span",{className:"text-[#666] ml-1 text-xs",children:["by ",s.by_you?"you":s.agent_name]})]}),s.content&&m.jsx("div",{className:"mt-1",children:m.jsx(da,{text:s.content})})]})]})}if(e==="list_notes"){const a=r;let s=[];if(a&&typeof a=="object"&&a.success){const o=a.notes;s=Array.isArray(o)?o:[]}return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"notes"}),s.length>0?m.jsx("div",{className:"mt-1.5 space-y-0.5",children:s.map((o,c)=>m.jsxs("div",{className:"text-[13px]",children:[m.jsx("span",{className:"text-[#555] mr-1",children:"-"}),m.jsx("span",{className:"text-[#999]",chil
Line truncated
|
||||
...`:a}function R7({args:e,result:t}){const r=e.httpql_filter??"",a=t,s=a?a.requests:null,o=Array.isArray(s)?s:[];return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"listing requests"}),r&&m.jsx("span",{className:"text-[#888] text-[13px]",children:Xr(r,150)})]}),o.length>0&&m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:[o.slice(0,20).map((c,d)=>{const h=(c.method??"GET").toUpperCase(),f=c.host??"",p=c.path??"",g=c.response,b=(g==null?void 0:g.statusCode)??null;return m.jsxs("div",{className:"flex gap-2",children:[m.jsx("span",{className:`w-10 shrink-0 font-bold ${f2[h]??"text-[#888]"}`,children:h}),m.jsx("span",{className:"text-[#777] truncate",children:Xr(f+p,180)}),b!=null&&m.jsx("span",{className:`ml-auto shrink-0 ${bg(b)}`,children:b})]},d)}),o.length>20&&m.jsxs("div",{className:"text-[#555]",children:["... +",o.length-20," more"]})]})]})}function j7({args:e,result:t}){const r=e.request_id,a=e.part??"request",s=e.search_pattern??"",o=t,c=o?o.matches:null,d=Array.isArray(c)?c:[],h=o?o.content??null:null,f=o?!!o.has_more:!1;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsxs("span",{className:"text-purple-400/80 font-semibold text-sm",children:[s?"searching":"viewing"," ",a]}),r!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",r]}),s&&m.jsxs("span",{className:"text-[#666] font-mono text-[13px]",children:["/",Xr(s,100),"/"]})]}),d.length>0&&m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-1",children:[d.slice(0,5).map((p,g)=>{const b=(p.before??"").replace(/\n/g," ").replace(/\r/g,"").slice(-100),y=(p.after??"").replace(/\n/g," ").replace(/\r/g,"").slice(0,100);return m.jsxs("div",{children:[b&&m.jsxs("span",{className:"text-[#555]",children:["...",b]}),m.jsx("span",{className:"text-amber-400/80 font-bold",children:p.match}),y&&m.jsxs("span",{className:"text-[#555]",children:[y,"..."]})]},g)}),d.length>5&&m.jsxs("div",{className:"text-[#555]",children:["... +",d.length-5," more matches"]})]}),h&&!d.length&&(()=>{const p=h.split(`
|
||||
`),g=p.slice(0,15).map(y=>Xr(y,d2)).join(`
|
||||
`),b=f||p.length>15;return m.jsx(wi,{className:"text-[#666]",children:g+(b?`
|
||||
... more content available`:"")})})()]})}function D7({args:e,result:t}){const r=(e.method??"GET").toUpperCase(),a=e.url??"",s=e.headers,o=e.body,c=typeof o=="string"?o:"",d=t,h=d?d.error??null:null,f=d?d.status_code??null:null,p=d?d.response_time_ms??null:null,g=d?d.body:null,b=typeof g=="string"?g:null;return m.jsxs("div",{children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"request"}),m.jsxs("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:[m.jsxs("div",{children:[m.jsx("span",{className:"text-[#555] select-none mr-1",children:">>"}),m.jsx("span",{className:`font-bold ${f2[r]??"text-[#888]"}`,children:r}),m.jsx("span",{className:"text-[#888] ml-1 break-all",children:Xr(a,180)})]}),s&&typeof s=="object"&&Object.entries(s).slice(0,5).map(([y,_])=>m.jsxs("div",{className:"text-[#555] pl-5",children:[y,": ",vp(String(_),150)]},y))]}),c&&m.jsx(wi,{className:"text-[#888]",children:_p(c,4)}),h&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1.5",children:vp(h,150)}),f!=null&&m.jsxs("div",{className:"font-mono text-[13px] mt-1.5",children:[m.jsx("span",{className:"text-[#555] select-none mr-1",children:"<<"}),m.jsx("span",{className:`font-bold ${bg(f)}`,children:f}),p!=null&&m.jsxs("span",{className:"text-[#555] ml-2",children:[p,"ms"]})]}),b&&m.jsx(wi,{className:"text-[#666]",children:_p(b,6)})]})}function L7({args:e,result:t}){const r=e.request_id,a=e.modifications,s=t,o=s?s.status_code??null:null,c=s?s.response_time_ms??null:null,d=s?s.body:null,h=typeof d=="string"?d:null;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"repeating request"}),r!=null&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",r]})]}),a&&typeof a=="object"&&Object.keys(a).length>0&&m.jsx("div",{className:"mt-1.5 font-mono text-[13px] space-y-0.5",children:Object.entries(a).slice(0,5).map(([f,p])=>m.jsxs("div",{children:[m.jsxs("span",{className:"text-orange-400/60",children:[f,":"]})," ",m.jsx("span",{className:"text-[#777]",children:vp(typeof p=="string"?p:JSON.stringify(p),150)})]},f))}),o!=null&&m.jsxs("div",{className:"font-mono text-[13px] mt-1.5",children:[m.jsx("span",{className:"text-[#555] select-none mr-1",children:"<<"}),m.jsx("span",{className:`font-bold ${bg(o)}`,children:o}),c!=null&&m.jsxs("span",{className:"text-[#555] ml-2",children:[c,"ms"]})]}),h&&m.jsx(wi,{className:"text-[#666]",children:_p(h,5)})]})}const z7={get:"getting",list:"listing",create:"creating",update:"updating",delete:"deleting"};function I7({args:e}){const t=e.action??"",r=e.scope_name??"",a=z7[t]??(t||"managing");return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsxs("span",{className:"text-purple-400/80 font-semibold text-sm",children:[a," proxy scope"]}),r&&m.jsx("span",{className:"text-[#888] text-[13px]",children:Xr(r,50)})]})}function B7({args:e}){const t=e.parent_id;return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"listing sitemap"}),t&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["under #",Xr(String(t),20)]})]})}function U7({args:e}){const t=e.entry_id;return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"viewing sitemap entry"}),t&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["#",Xr(String(t),20)]})]})}function H7(e){switch(e.toolName){case"list_requests":return m.jsx(R7,{...e});case"view_request":return m.jsx(j7,{...e});case"send_request":return m.jsx(D7,{...e});case"repeat_request":return m.jsx(L7,{...e});case"scope_rules":return m.jsx(I7,{...e});case"list_sitemap":return m.jsx(B7,{...e});case"view_sitemap_entry":return m.jsx(U7,{...e});default:return m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:e.toolName.replace(/_/g," ")})}}function $7({args:e}){const t=e.thought??e.content??"";return t?m.jsxs("div",{children:[m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:"Agent is thinking"}),m.jsx("div",{className:"mt-1.5 italic text-[#888]",children:m.jsx(Yt,{text:t,maxLines:20})})]}):null}function q7({toolName:e,args:t}){if(e==="create_agent"){const r=t.name??t.agent_name??"",a=t.task??"";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:"spawning"}),r&&m.jsx("span",{className:"text-cyan-400 font-semibold text-sm",children:r})]}),a&&m.jsx("div",{className:"mt-1.5",children:m.jsx(Yt,{text:a,maxLines:15})})]})}if(e==="agent_finish"){const r=t.result_summary??"",a=t.success,s=t.findings,o=Array.isArray(s)?s:void 0;return m.jsxs("div",{children:[m.jsx("span",{className:`font-semibold text-sm ${a===!1?"text-red-400/80":"text-emerald-400/80"}`,children:a===!1?"A
Line truncated
|
||||
`);if(t.length<=F7)return t.map(Bm).join(`
|
||||
`);const r=t.length-m_-p_;return[...t.slice(0,m_).map(Bm),`... ${r} lines truncated ...`,...t.slice(-p_).map(Bm)].join(`
|
||||
`)}function Z7({args:e,result:t}){const r=e.action??"",a=e.code??e.script??"",s=t;let o=null;s&&typeof s=="object"?o=typeof s.stdout=="string"?s.stdout:null:typeof s=="string"&&(o=s);const c=r==="new_session"?"new session":r==="close"?"close session":r==="list_sessions"?"list sessions":null,d=o?K7(X7(o)):null;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-yellow-400/80 font-semibold text-sm",children:"Python"}),c&&m.jsx("span",{className:"text-[#888] text-[13px]",children:c})]}),a&&m.jsx(gg,{code:a,language:"python",collapsible:!0}),d&&m.jsx(wi,{className:"text-[#666]",children:d})]})}function Q7({args:e}){const t=e.targets,a=(Array.isArray(t)?t:[]).map(s=>typeof s=="object"&&s?s.original??null:null).filter(Boolean);return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:"Starting penetration test"}),a.length===1&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["on ",a[0]]})]}),a.length>1&&m.jsx("div",{className:"mt-1.5 space-y-0.5",children:a.map((s,o)=>m.jsxs("div",{className:"text-[13px] text-[#888]",children:[m.jsx("span",{className:"text-[#555] mr-1",children:"•"}),s]},o))})]})}function W7({args:e}){const t=e.name??"Unknown Agent",r=e.task??"";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-[#888] text-[13px]",children:"subagent"}),m.jsx("span",{className:"text-purple-400 font-semibold text-sm",children:t})]}),r&&m.jsx("div",{className:"mt-1.5",children:m.jsx(Yt,{text:r,maxLines:15})})]})}function J7(e){return e.toolName==="subagent_start_info"?m.jsx(W7,{...e}):m.jsx(Q7,{...e})}function eU({args:e}){const t=e.executive_summary??"",r=e.methodology??"",a=e.technical_analysis??"",s=e.recommendations??"";return m.jsxs("div",{className:"space-y-3",children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:"Penetration test completed"}),t&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Executive Summary"}),m.jsx("div",{className:"mt-1",children:m.jsx(Yt,{text:t,maxLines:25})})]}),r&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Methodology"}),m.jsx("div",{className:"mt-1",children:m.jsx(Yt,{text:r,maxLines:25})})]}),a&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Technical Analysis"}),m.jsx("div",{className:"mt-1",children:m.jsx(Yt,{text:a,maxLines:25})})]}),s&&m.jsxs("div",{children:[m.jsx("span",{className:"text-emerald-400/60 text-sm font-semibold",children:"Recommendations"}),m.jsx("div",{className:"mt-1",children:m.jsx(Yt,{text:s,maxLines:25})})]}),!t&&!r&&!a&&!s&&m.jsx("div",{className:"text-[#555] text-xs",children:"Generating final report..."})]})}function tU({toolName:e,args:t,result:r}){if(e==="create_note"){const a=t.title??"",s=t.content??"",o=t.category??"general";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note"}),m.jsxs("span",{className:"text-[#555] text-[13px]",children:["(",o,")"]})]}),a&&m.jsx("div",{className:"mt-1.5 text-[#999] text-[13px]",children:a}),s&&m.jsx("div",{className:"mt-1",children:m.jsx(ua,{text:s})})]})}if(e==="delete_note")return m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note removed"});if(e==="update_note"){const a=t.title??"",s=t.content??"";return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note updated"}),a&&m.jsx("div",{className:"mt-1.5 text-[#999] text-[13px]",children:a}),s&&m.jsx("div",{className:"mt-1",children:m.jsx(ua,{text:s})})]})}if(e==="get_note"){const a=r,s=a&&typeof a=="object"&&a.success?a.note:void 0;return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note read"}),s&&m.jsxs(m.Fragment,{children:[m.jsxs("div",{className:"mt-1.5 text-[#999] text-[13px]",children:[s.title??"(untitled)",m.jsxs("span",{className:"text-[#555] ml-1",children:["(",s.category??"general",")"]}),(s.by_you||s.agent_name)&&m.jsxs("span",{className:"text-[#666] ml-1 text-xs",children:["by ",s.by_you?"you":s.agent_name]})]}),s.content&&m.jsx("div",{className:"mt-1",children:m.jsx(ua,{text:s.content})})]})]})}if(e==="list_notes"){const a=r;let s=[];if(a&&typeof a=="object"&&a.success){const o=a.notes;s=Array.isArray(o)?o:[]}return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"notes"}),s.length>0?m.jsx("div",{className:"mt-1.5 space-y-0.5",children:s.map((o,c)=>m.jsxs("div",{className:"text-[13px]",children:[m.jsx("span",{className:"text-[#555] mr-1",children:"-"}),m.jsx("span",{className:"text-[#999]",chil
Line truncated
|
||||
`)){const s=a.match(/^#{1,6}\s+(.*)$/);if(s){const o=s[1].trim().toLowerCase();if(o===r)continue;r=o}else a.trim()!==""&&(r=null);t.push(a)}return t.join(`
|
||||
`)}function QH({onOpenEmail:e}){return m.jsx("button",{onClick:e,className:"group w-full cursor-pointer rounded-xl border border-emerald-500/25 bg-emerald-500/[0.06] p-4 text-left transition-colors hover:border-emerald-500/40",children:m.jsxs("div",{className:"flex items-center gap-3",children:[m.jsx("div",{className:"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg",style:{border:"1px solid rgba(16,185,129,0.3)",background:"rgba(16,185,129,0.08)"},children:m.jsx(Np,{className:"h-4 w-4 text-emerald-400","aria-hidden":"true"})}),m.jsxs("div",{className:"min-w-0 flex-1",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Email an encrypted PDF report of this run"}),m.jsx("p",{className:"mt-0.5 text-xs text-[#888]",children:"Encrypted with a key only you can see, email verified with a one-time code before sending."})]}),m.jsx("span",{className:"flex-shrink-0 rounded-lg bg-white px-3 py-1.5 text-xs font-semibold text-black transition-opacity group-hover:opacity-90",children:"Export report to PDF"})]})})}function WH({summary:e,counts:t,total:r,reportMarkdown:a,raw:s,finished:o,onOpenEmail:c}){const d=[["Executive Summary",e.executiveSummary],["Technical Analysis",e.technicalAnalysis],["Methodology",e.methodology],["Recommendations",e.recommendations]].filter(([,h])=>!!h).map(([h,f])=>({title:h,content:KH(f)}));return m.jsxs("div",{className:"space-y-6",children:[m.jsx("div",{className:"animate-card-in",children:m.jsx(zH,{raw:s,durationSeconds:e.durationSeconds})}),r>0&&m.jsx("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:m.jsx(bD,{findings:{total:r,...t}})}),o&&m.jsx("div",{className:"animate-card-in",children:m.jsx(QH,{onOpenEmail:c})}),o&&m.jsxs("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Strix Cloud"}),m.jsx("p",{className:"mt-0.5 text-xs text-[#666]",children:"Run your next pentest in Strix Cloud."}),m.jsxs("div",{className:"mt-3 flex flex-wrap gap-2.5",children:[m.jsx(Ao,{label:"Run a pentest in Strix Cloud",desc:"Validated findings, autofix, and PR reviews.",slug:"overview_cloud",surface:"overview",icon:Q_,primary:!0}),m.jsx(Ao,{label:"Try Strix Enterprise",desc:"SSO, compliance-ready reports, VPC or self-hosted deployment.",slug:"book_demo",surface:"overview",icon:$_,href:Ap})]})]}),d.length>0?m.jsx("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5 space-y-8",children:d.map(h=>m.jsx(ca,{title:h.title,content:h.content},h.title))}):a?m.jsx("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:m.jsx(ca,{content:ZH(a)})}):r===0&&m.jsx("p",{className:"text-sm text-[#888]",children:"No summary available for this run yet."})]})}function Pm({active:e,onClick:t,children:r}){return m.jsxs("button",{onClick:t,className:`cursor-pointer relative pb-2.5 text-sm font-semibold transition-colors ${e?"text-white":"text-[#666] hover:text-white"}`,children:[r,e&&m.jsx("span",{className:"absolute bottom-0 inset-x-0 h-0.5 bg-white rounded-full"})]})}function JH({run:e,canSteer:t}){const{agents:r,events:a}=e.transcript,s=ee.useMemo(()=>UU(r,a),[r,a]),[o,c]=ee.useState(null),d=o?r.find(f=>f.id===o)??null:null,h=t&&!e.finished;return m.jsxs("div",{className:"space-y-5",children:[m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Ro,{className:"w-4 h-4 text-[#888]","aria-hidden":"true"}),m.jsx("h2",{className:"text-sm font-semibold text-white",children:"Agent graph"}),m.jsxs("span",{className:"text-xs text-[#666]",children:[r.length," agent",r.length===1?"":"s"]})]}),m.jsx("p",{className:"mt-1 mb-4 text-xs text-[#666]",children:"Click an agent to open its full transcript."}),m.jsx("div",{className:"h-[480px] rounded-lg border border-[#1a1a1a] overflow-hidden",children:m.jsx(l7,{agents:s,selectedAgentId:o,onSelectAgent:f=>c(f),eventsLoaded:!0,eventsEmpty:s.size===0,scanCompleted:e.finished})})]}),h&&m.jsx(C2,{agents:r}),m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Run this pentest with more depth"}),m.jsx("p",{className:"mt-0.5 text-xs text-[#666]",children:"Run this pentest again in Strix Cloud."}),m.jsxs("div",{className:"mt-3 flex flex-wrap gap-2.5",children:[m.jsx(Ao,{label:"Re-run in Strix Pro with more depth",desc:"More depth, validated findings, and autofix.",slug:"live_scan",surface:"agents",icon:Q_,primary:!0}),m.jsx(Ao,{label:"Try Strix Enterprise",desc:"SSO, compliance-ready reports, VPC or self-hosted deployment.",slug:"book_demo",surface:"agents",icon:$_,href:Ap})]})]}),m.jsx(lH,{open:d!==null,agent:d,events:a,steerable:h,onClose:()=>c(null)})]})}$k.createR
Line truncated
|
||||
`)}function VH({onOpenEmail:e}){return m.jsx("button",{onClick:e,className:"group w-full cursor-pointer rounded-xl border border-emerald-500/25 bg-emerald-500/[0.06] p-4 text-left transition-colors hover:border-emerald-500/40",children:m.jsxs("div",{className:"flex items-center gap-3",children:[m.jsx("div",{className:"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg",style:{border:"1px solid rgba(16,185,129,0.3)",background:"rgba(16,185,129,0.08)"},children:m.jsx(Ep,{className:"h-4 w-4 text-emerald-400","aria-hidden":"true"})}),m.jsxs("div",{className:"min-w-0 flex-1",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Email an encrypted PDF report of this run"}),m.jsx("p",{className:"mt-0.5 text-xs text-[#888]",children:"Encrypted with a key only you can see, email verified with a one-time code before sending."})]}),m.jsx("span",{className:"flex-shrink-0 rounded-lg bg-white px-3 py-1.5 text-xs font-semibold text-black transition-opacity group-hover:opacity-90",children:"Export report to PDF"})]})})}function YH({summary:e,counts:t,total:r,reportMarkdown:a,raw:s,finished:o,onOpenEmail:c}){const d=[["Executive Summary",e.executiveSummary],["Technical Analysis",e.technicalAnalysis],["Methodology",e.methodology],["Recommendations",e.recommendations]].filter(([,h])=>!!h).map(([h,f])=>({title:h,content:FH(f)}));return m.jsxs("div",{className:"space-y-6",children:[m.jsx("div",{className:"animate-card-in",children:m.jsx(OH,{raw:s,durationSeconds:e.durationSeconds})}),r>0&&m.jsx("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:m.jsx(pD,{findings:{total:r,...t}})}),o&&m.jsx("div",{className:"animate-card-in",children:m.jsx(VH,{onOpenEmail:c})}),d.length>0?m.jsx("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5 space-y-8",children:d.map(h=>m.jsx(oa,{title:h.title,content:h.content},h.title))}):a?m.jsx("div",{className:"animate-card-in rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:m.jsx(oa,{content:GH(a)})}):r===0&&m.jsx("p",{className:"text-sm text-[#888]",children:"No summary available for this run yet."})]})}function qm({active:e,onClick:t,children:r}){return m.jsxs("button",{onClick:t,className:`cursor-pointer relative pb-2.5 text-sm font-semibold transition-colors ${e?"text-white":"text-[#666] hover:text-white"}`,children:[r,e&&m.jsx("span",{className:"absolute bottom-0 inset-x-0 h-0.5 bg-white rounded-full"})]})}function XH({run:e,canSteer:t}){const{agents:r,events:a}=e.transcript,s=ee.useMemo(()=>DU(r,a),[r,a]),[o,c]=ee.useState(null),d=o?r.find(f=>f.id===o)??null:null,h=t&&!e.finished;return m.jsxs("div",{className:"space-y-5",children:[m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Oo,{className:"w-4 h-4 text-[#888]","aria-hidden":"true"}),m.jsx("h2",{className:"text-sm font-semibold text-white",children:"Agent graph"}),m.jsxs("span",{className:"text-xs text-[#666]",children:[r.length," agent",r.length===1?"":"s"]})]}),m.jsx("p",{className:"mt-1 mb-4 text-xs text-[#666]",children:"Click an agent to open its full transcript."}),m.jsx("div",{className:"h-[480px] rounded-lg border border-[#1a1a1a] overflow-hidden",children:m.jsx(i7,{agents:s,selectedAgentId:o,onSelectAgent:f=>c(f),eventsLoaded:!0,eventsEmpty:s.size===0,scanCompleted:e.finished})})]}),h&&m.jsx(E2,{agents:r}),m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Run this pentest with more depth"}),m.jsx("p",{className:"mt-0.5 text-xs text-[#666]",children:"Re-run this pentest on managed infra in the cloud."}),m.jsx("div",{className:"mt-3 flex flex-wrap gap-2.5",children:m.jsx(C2,{label:"Re-run in Strix Pro with more depth",desc:"Run this pentest on managed infra with more depth.",slug:"live_scan",surface:"agents",icon:uT})})]}),m.jsx(nH,{open:d!==null,agent:d,events:a,steerable:h,onClose:()=>c(null)})]})}Bk.createRoot(document.getElementById("root")).render(m.jsx(ee.StrictMode,{children:m.jsx(BH,{})}));
|
||||
+1
-1
@@ -7,4 +7,4 @@ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5p
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #555;--xy-background-pattern-lines-color-default: #333;--xy-background-pattern-cross-color-default: #333;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__panLine truncated
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #555;--xy-background-pattern-lines-color-default: #333;--xy-background-pattern-cross-color-default: #333;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__panLine truncated
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Strix Results</title>
|
||||
<script type="module" crossorigin src="./assets/index-DbPDzbcJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CrLE9rrV.css">
|
||||
<script type="module" crossorigin src="./assets/index-Bpn8GiSb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-qwPOPAGC.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+40
-18
@@ -1,22 +1,20 @@
|
||||
"""Background pre-import of the heavy scan dependencies.
|
||||
|
||||
The scan engine's import graph (the agents SDK, OpenAI client, LiteLLM, the
|
||||
Caido SDK) costs seconds to import cold, but none of it is needed until a scan
|
||||
actually starts. Importing it on a daemon thread at CLI entry overlaps that
|
||||
cost with the I/O-bound startup work that always precedes a scan (argument
|
||||
parsing, Docker checks, image pull, TUI setup). The Docker SDK is not on the
|
||||
list: the Docker checks import it on the main thread during that same window.
|
||||
|
||||
The main thread must call :func:`wait_for_import_warmup` before its first
|
||||
import from that graph. Two threads that enter the same package graph from
|
||||
different modules hold each other's import locks, and CPython breaks the cycle
|
||||
by failing one of the imports.
|
||||
Caido SDK, the Docker SDK) costs seconds to import cold, but none of it is
|
||||
needed until a scan actually starts. Importing it on a daemon thread at CLI
|
||||
entry overlaps that cost with the I/O-bound startup work that always precedes
|
||||
a scan (argument parsing, Docker checks, image pull, TUI setup), so by the
|
||||
time the scan begins the modules are already in ``sys.modules``. Any thread
|
||||
that needs one of them before the warm-up finishes just blocks on the normal
|
||||
import lock, so behaviour is unchanged either way.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
|
||||
|
||||
@@ -26,17 +24,45 @@ WARMUP_MODULES = (
|
||||
"strix.core.runner",
|
||||
"litellm",
|
||||
"caido_sdk_client",
|
||||
"docker",
|
||||
)
|
||||
|
||||
_lock = threading.Lock()
|
||||
_thread: threading.Thread | None = None
|
||||
|
||||
|
||||
def _purge_orphaned_modules(before: frozenset[str]) -> None:
|
||||
"""Remove submodules stranded by an import attempt that just failed.
|
||||
|
||||
When a package import fails partway (for example CPython's import-lock
|
||||
deadlock avoidance breaking a cross-thread cycle), the failed package is
|
||||
removed from ``sys.modules`` but submodules it already finished stay
|
||||
behind. A later import of one of those submodules then short-circuits on
|
||||
the cached entry without re-importing its parent, and re-entering the
|
||||
parent from inside a submodule crashes with "partially initialized
|
||||
module". Dropping the orphans (cached submodules whose ancestor package is
|
||||
gone) restores a clean slate, and touches nothing another thread imported
|
||||
successfully.
|
||||
"""
|
||||
added = set(sys.modules) - before
|
||||
for name in added:
|
||||
parent = name.rpartition(".")[0]
|
||||
while parent:
|
||||
if parent not in sys.modules:
|
||||
sys.modules.pop(name, None)
|
||||
logger.debug("Import warm-up purged orphaned module %r", name)
|
||||
break
|
||||
parent = parent.rpartition(".")[0]
|
||||
|
||||
|
||||
def _warm(modules: tuple[str, ...]) -> None:
|
||||
for name in modules:
|
||||
before = frozenset(sys.modules)
|
||||
try:
|
||||
importlib.import_module(name)
|
||||
except Exception: # noqa: BLE001 - a failed warm-up must never fail the run.
|
||||
logger.debug("Import warm-up for %r failed", name, exc_info=True)
|
||||
_purge_orphaned_modules(before)
|
||||
|
||||
|
||||
def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.Thread:
|
||||
@@ -46,15 +72,11 @@ def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.
|
||||
runtime that has no local Docker) warm a narrower set.
|
||||
"""
|
||||
global _thread # noqa: PLW0603
|
||||
if _thread is None:
|
||||
with _lock:
|
||||
if _thread is not None:
|
||||
return _thread
|
||||
_thread = threading.Thread(
|
||||
target=_warm, args=(modules,), name="strix-import-warmup", daemon=True
|
||||
)
|
||||
_thread.start()
|
||||
return _thread
|
||||
|
||||
|
||||
def wait_for_import_warmup() -> None:
|
||||
"""Block until the warm-up thread has finished, if one was started."""
|
||||
if _thread is not None:
|
||||
_thread.join()
|
||||
return _thread
|
||||
+30
-33
@@ -28,10 +28,9 @@ Design notes:
|
||||
* File locations must be repo-relative POSIX paths. Paths that look
|
||||
like URIs, absolute paths, or traversal patterns are rejected rather
|
||||
than emitted as invalid code-scanning alerts.
|
||||
* Findings whose fix candidate completed verified preparation
|
||||
(``fix_preparation.state == "ready"`` with a ``candidate_digest``
|
||||
matching the candidate the result carries) are emitted as SARIF
|
||||
``fixes`` so code-scanning can render a one-click suggested change.
|
||||
* Findings with a fix suggestion (``code_locations[].fix_before`` +
|
||||
``fix_after``) are emitted as SARIF ``fixes`` so code-scanning can
|
||||
render a one-click suggested change.
|
||||
* Endpoint / target-only findings (typical of DAST) carry a SARIF
|
||||
``logicalLocations`` entry so the finding keeps a meaningful anchor
|
||||
even without a source file + line.
|
||||
@@ -57,10 +56,6 @@ import re
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from strix.fix.contracts import FixCandidateV1
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -588,44 +583,46 @@ def _result_properties(
|
||||
|
||||
|
||||
def _build_fixes(report: dict[str, Any]) -> list[dict[str, Any]] | None:
|
||||
"""Build SARIF ``fixes`` from a verified prepared finding.
|
||||
"""Build SARIF ``fixes`` from a finding's code-location fix pairs.
|
||||
|
||||
SARIF consumers can apply ``fixes`` automatically. Strix emits them only
|
||||
when preparation recorded a ``ready`` result. The result carries the
|
||||
candidate preparation verified — after anchoring corrected its reported
|
||||
lines — and its ``candidate_digest`` must match that candidate, so the
|
||||
emitted replacements are exactly what preparation verified, never a
|
||||
stale or diverged draft.
|
||||
Strix findings carry the suggested change inline on each code
|
||||
location as ``fix_before`` + ``fix_after``. We map every location
|
||||
that has both (and a safe repo-relative URI + start line) into a
|
||||
SARIF ``artifactChange``, replacing the finding's region with the
|
||||
fixed text. Returns None when no location carries a usable fix pair.
|
||||
"""
|
||||
preparation = report.get("fix_preparation")
|
||||
if not isinstance(preparation, dict) or preparation.get("state") != "ready":
|
||||
return None
|
||||
raw_candidate = preparation.get("candidate") or report.get("fix_candidate")
|
||||
if not isinstance(raw_candidate, dict):
|
||||
return None
|
||||
try:
|
||||
candidate = FixCandidateV1.model_validate(raw_candidate)
|
||||
except ValidationError:
|
||||
return None
|
||||
if preparation.get("candidate_digest") != candidate.digest():
|
||||
raw_locations = report.get("code_locations")
|
||||
if not isinstance(raw_locations, list):
|
||||
return None
|
||||
|
||||
artifact_changes: list[dict[str, Any]] = []
|
||||
for edit in candidate.draft_edits:
|
||||
uri = _sarif_uri(edit.file)
|
||||
for location in raw_locations:
|
||||
if not isinstance(location, dict):
|
||||
continue
|
||||
file_path = _string_value(location.get("file"))
|
||||
fix_before = _string_value(location.get("fix_before"))
|
||||
fix_after = _string_value(location.get("fix_after"))
|
||||
start_line = location.get("start_line")
|
||||
if not (file_path and fix_before and fix_after):
|
||||
continue
|
||||
if type(start_line) is not int or start_line < 1:
|
||||
continue
|
||||
uri = _sarif_uri(file_path)
|
||||
if uri is None:
|
||||
continue
|
||||
deleted_region: dict[str, Any] = {
|
||||
"startLine": edit.start_line,
|
||||
"endLine": edit.end_line,
|
||||
}
|
||||
|
||||
deleted_region: dict[str, Any] = {"startLine": start_line}
|
||||
end_line = location.get("end_line")
|
||||
if type(end_line) is int and end_line >= start_line:
|
||||
deleted_region["endLine"] = end_line
|
||||
|
||||
artifact_changes.append(
|
||||
{
|
||||
"artifactLocation": {"uri": uri},
|
||||
"replacements": [
|
||||
{
|
||||
"deletedRegion": deleted_region,
|
||||
"insertedContent": {"text": edit.after},
|
||||
"insertedContent": {"text": fix_after},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
+49
-194
@@ -36,23 +36,6 @@ _global_report_state: Optional["ReportState"] = None
|
||||
_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f]+")
|
||||
|
||||
|
||||
class ReportRollbackError(RuntimeError):
|
||||
"""Raised when a failed deletion could not rewrite the on-disk indexes.
|
||||
|
||||
The report is still on file in memory; the indexes are rewritten from
|
||||
memory on the next save.
|
||||
"""
|
||||
|
||||
def __init__(self, report_id: str, *, cause: BaseException) -> None:
|
||||
self.report_id = report_id
|
||||
self.cause = cause
|
||||
super().__init__(
|
||||
f"Deletion of report '{report_id}' failed ({cause}) and the on-disk indexes "
|
||||
"could not be restored; the report is still on file and the indexes are "
|
||||
"rewritten on the next save"
|
||||
)
|
||||
|
||||
|
||||
def _strix_version() -> str | None:
|
||||
"""Best-effort package version for the SARIF tool.driver.version field."""
|
||||
try:
|
||||
@@ -90,11 +73,8 @@ UPDATABLE_REPORT_FIELDS = frozenset(
|
||||
"cve",
|
||||
"cwe",
|
||||
"code_locations",
|
||||
"http_exchange_ids",
|
||||
"fix_verification",
|
||||
"fix_pr_body",
|
||||
"fix_candidate",
|
||||
"fix_preparation",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -187,7 +167,7 @@ def get_global_report_state() -> Optional["ReportState"]:
|
||||
return _global_report_state
|
||||
|
||||
|
||||
def set_global_report_state(report_state: Optional["ReportState"]) -> None:
|
||||
def set_global_report_state(report_state: "ReportState") -> None:
|
||||
global _global_report_state # noqa: PLW0603
|
||||
_global_report_state = report_state
|
||||
# New run: drop any streamed-cost entries a prior run left unconsumed.
|
||||
@@ -240,7 +220,6 @@ class ReportState:
|
||||
self.caido_url: str | None = None
|
||||
self.vulnerability_found_callback: Callable[[dict[str, Any]], None] | None = None
|
||||
self.vulnerability_updated_callback: Callable[[dict[str, Any]], None] | None = None
|
||||
self.vulnerability_deleted_callback: Callable[[dict[str, Any]], None] | None = None
|
||||
|
||||
self._sarif_repo_ctx: dict[str, Any] | None = None
|
||||
self._sarif_repo_ctx_ready: bool = False
|
||||
@@ -354,17 +333,14 @@ class ReportState:
|
||||
cve: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
finding_class: str | None = None,
|
||||
dependency_metadata: dict[str, str] | None = None,
|
||||
fix_candidate: dict[str, Any] | None = None,
|
||||
fix_preparation: dict[str, Any] | None = None,
|
||||
agent_id: str | None = None,
|
||||
agent_name: str | None = None,
|
||||
) -> str:
|
||||
report_id = self._next_report_id()
|
||||
report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
|
||||
|
||||
report: dict[str, Any] = {
|
||||
"id": report_id,
|
||||
@@ -415,8 +391,6 @@ class ReportState:
|
||||
report["cwe"] = cwe.strip()
|
||||
if code_locations:
|
||||
report["code_locations"] = code_locations
|
||||
if http_exchange_ids:
|
||||
report["http_exchange_ids"] = http_exchange_ids
|
||||
if fix_verification:
|
||||
report["fix_verification"] = fix_verification.strip()
|
||||
if fix_pr_body:
|
||||
@@ -424,44 +398,22 @@ class ReportState:
|
||||
report["finding_class"] = (finding_class or "dynamic").strip().lower()
|
||||
if dependency_metadata:
|
||||
report["dependency_metadata"] = dependency_metadata
|
||||
if fix_candidate:
|
||||
report["fix_candidate"] = fix_candidate
|
||||
if fix_preparation:
|
||||
report["fix_preparation"] = fix_preparation
|
||||
if agent_id:
|
||||
report["agent_id"] = agent_id
|
||||
if agent_name:
|
||||
report["agent_name"] = agent_name
|
||||
|
||||
if self.vulnerability_found_callback:
|
||||
self.vulnerability_found_callback(report)
|
||||
|
||||
self.vulnerability_reports.append(report)
|
||||
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
||||
posthog.finding(severity, cwe=cwe, is_cve=bool(cve))
|
||||
scarf.finding(severity, cwe=cwe, is_cve=bool(cve))
|
||||
|
||||
if self.vulnerability_found_callback:
|
||||
self.vulnerability_found_callback(report)
|
||||
|
||||
self.save_run_data()
|
||||
return report_id
|
||||
|
||||
def _deleted_vulnerability_reports(self) -> list[dict[str, Any]]:
|
||||
raw = self.run_record.get("deleted_vulnerability_reports")
|
||||
return [e for e in raw if isinstance(e, dict)] if isinstance(raw, list) else []
|
||||
|
||||
def _next_report_id(self) -> str:
|
||||
"""Allocate the id after every id this run has ever handed out.
|
||||
|
||||
A deleted report leaves the list, so counting entries would hand its id
|
||||
to the next finding and let that finding overwrite the deleted MD on disk
|
||||
and inherit its history in every consumer that keys on the id.
|
||||
"""
|
||||
used = 0
|
||||
for entry in [*self.vulnerability_reports, *self._deleted_vulnerability_reports()]:
|
||||
match = re.fullmatch(r"vuln-(\d+)", str(entry.get("id", "")))
|
||||
if match:
|
||||
used = max(used, int(match.group(1)))
|
||||
return f"vuln-{used + 1:04d}"
|
||||
|
||||
def update_vulnerability_report(
|
||||
self,
|
||||
report_id: str,
|
||||
@@ -534,18 +486,11 @@ class ReportState:
|
||||
)
|
||||
history.append(entry)
|
||||
|
||||
revised = {**report, **changed}
|
||||
report.update(changed)
|
||||
for dependent in superseded:
|
||||
revised.pop(dependent, None)
|
||||
revised["update_history"] = history
|
||||
revised["updated_at"] = entry["timestamp"]
|
||||
|
||||
# Persistence must accept the revision before local state changes. A
|
||||
# failed callback leaves the old evidence intact and the update retryable.
|
||||
if self.vulnerability_updated_callback:
|
||||
self.vulnerability_updated_callback(revised)
|
||||
report.clear()
|
||||
report.update(revised)
|
||||
report.pop(dependent, None)
|
||||
report["update_history"] = history
|
||||
report["updated_at"] = entry["timestamp"]
|
||||
|
||||
# The markdown on disk still shows the superseded evidence, so let the
|
||||
# writer re-render it.
|
||||
@@ -557,91 +502,12 @@ class ReportState:
|
||||
", ".join(entry["fields"]) or "no field replaced",
|
||||
)
|
||||
|
||||
if self.vulnerability_updated_callback:
|
||||
self.vulnerability_updated_callback(report)
|
||||
|
||||
self.save_run_data()
|
||||
return report
|
||||
|
||||
def delete_vulnerability_report(
|
||||
self,
|
||||
report_id: str,
|
||||
*,
|
||||
delete_reason: str,
|
||||
deleted_by_agent_id: str | None = None,
|
||||
deleted_by_agent_name: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Withdraw a report from the run, keeping a record of the withdrawal.
|
||||
|
||||
Returns the removed report, or ``None`` when the id is unknown. Every
|
||||
agent in a run shares one trust boundary, so any of them may withdraw
|
||||
any report (just as any of them may revise one); the run record keeps
|
||||
who filed it, who withdrew it and why. The report leaves
|
||||
``vulnerability_reports`` and its rendered artifacts, and its id is
|
||||
never reissued.
|
||||
|
||||
The rewritten artifacts and the ``vulnerability_deleted_callback`` must
|
||||
both accept the deletion first: if either fails the report is put back
|
||||
and the error propagates, so the deletion can be retried
|
||||
(:class:`ReportRollbackError` when the indexes could not be put back).
|
||||
"""
|
||||
report = next((r for r in self.vulnerability_reports if r.get("id") == report_id), None)
|
||||
if report is None:
|
||||
logger.warning("cannot delete unknown vulnerability report %s", report_id)
|
||||
return None
|
||||
|
||||
entry: dict[str, Any] = {
|
||||
"id": report_id,
|
||||
"title": report.get("title"),
|
||||
"severity": report.get("severity"),
|
||||
"filed_at": report.get("timestamp"),
|
||||
"deleted_at": datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"),
|
||||
"reason": delete_reason.strip()[:500],
|
||||
}
|
||||
if report.get("agent_id"):
|
||||
entry["filed_by_agent_id"] = report["agent_id"]
|
||||
if deleted_by_agent_id:
|
||||
entry["agent_id"] = deleted_by_agent_id
|
||||
if deleted_by_agent_name:
|
||||
entry["agent_name"] = deleted_by_agent_name
|
||||
|
||||
position = self.vulnerability_reports.index(report)
|
||||
was_saved = report_id in self._saved_vuln_ids
|
||||
history = self._deleted_vulnerability_reports()
|
||||
|
||||
self.vulnerability_reports.remove(report)
|
||||
self._saved_vuln_ids.discard(report_id)
|
||||
self.run_record["deleted_vulnerability_reports"] = [*history, entry]
|
||||
try:
|
||||
# Local artifacts first: they can be put back if persistence then
|
||||
# refuses, whereas a row deleted elsewhere cannot.
|
||||
self._sync_llm_usage_record()
|
||||
self._write_artifacts()
|
||||
if self.vulnerability_deleted_callback:
|
||||
self.vulnerability_deleted_callback({**report, "deletion": entry})
|
||||
except Exception as exc:
|
||||
self.vulnerability_reports.insert(position, report)
|
||||
if was_saved:
|
||||
self._saved_vuln_ids.add(report_id)
|
||||
if history:
|
||||
self.run_record["deleted_vulnerability_reports"] = history
|
||||
else:
|
||||
self.run_record.pop("deleted_vulnerability_reports", None)
|
||||
try:
|
||||
self._write_artifacts()
|
||||
except Exception as rollback_exc:
|
||||
logger.exception(
|
||||
"could not restore artifacts after failed deletion of %s", report_id
|
||||
)
|
||||
raise ReportRollbackError(report_id, cause=exc) from rollback_exc
|
||||
raise
|
||||
|
||||
md_path = self.get_run_dir() / "vulnerabilities" / f"{report_id}.md"
|
||||
try:
|
||||
md_path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
logger.exception("could not remove %s", md_path)
|
||||
|
||||
logger.info("Deleted vulnerability report %s - %s", report_id, report.get("title"))
|
||||
return report
|
||||
|
||||
def get_existing_vulnerabilities(self) -> list[dict[str, Any]]:
|
||||
return list(self.vulnerability_reports)
|
||||
|
||||
@@ -825,54 +691,47 @@ class ReportState:
|
||||
return None
|
||||
|
||||
def _save_artifacts(self) -> None:
|
||||
"""Write scan artifacts under ``run_dir``; a write failure is logged."""
|
||||
"""Write scan artifacts under ``run_dir``."""
|
||||
run_dir = self.get_run_dir()
|
||||
try:
|
||||
self._write_artifacts()
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
coverage = self._coverage_document()
|
||||
if coverage is not None:
|
||||
try:
|
||||
write_coverage(run_dir, coverage)
|
||||
except OSError:
|
||||
logger.exception("coverage.json write failed (non-fatal)")
|
||||
|
||||
if self.final_scan_result:
|
||||
write_executive_report(run_dir, self.final_scan_result)
|
||||
|
||||
if self.vulnerability_reports:
|
||||
write_vulnerabilities(run_dir, self.vulnerability_reports, self._saved_vuln_ids)
|
||||
|
||||
# SARIF 2.1.0 emitter for CI / ASPM integration. Always emit (even
|
||||
# empty) so a clean run overwrites a prior findings.sarif rather than
|
||||
# leaving a stale one — codeql-action's "absent from new submission →
|
||||
# fixed" needs the fresh empty doc to auto-resolve alerts. Isolated
|
||||
# in its own try: a SARIF-build error must NEVER break the CSV/MD/
|
||||
# run-record path (the emitter's own contract).
|
||||
try:
|
||||
write_sarif(
|
||||
run_dir,
|
||||
self.vulnerability_reports,
|
||||
tool_version=_strix_version(),
|
||||
repository_context=self._sarif_repository_context(),
|
||||
coverage=coverage,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("SARIF emit failed (non-fatal; CSV/MD unaffected)")
|
||||
|
||||
write_run_record(run_dir, self.run_record)
|
||||
|
||||
logger.info("Essential scan data saved to: %s", run_dir)
|
||||
except (OSError, RuntimeError):
|
||||
logger.exception("Failed to save scan data")
|
||||
|
||||
def _write_artifacts(self) -> None:
|
||||
"""Write scan artifacts under ``run_dir``, raising when the index or
|
||||
run record cannot be written."""
|
||||
run_dir = self.get_run_dir()
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
coverage = self._coverage_document()
|
||||
if coverage is not None:
|
||||
try:
|
||||
write_coverage(run_dir, coverage)
|
||||
except OSError:
|
||||
logger.exception("coverage.json write failed (non-fatal)")
|
||||
|
||||
if self.final_scan_result:
|
||||
write_executive_report(run_dir, self.final_scan_result)
|
||||
|
||||
# An index is written for an empty list too once a report was
|
||||
# deleted, or the CSV/JSON on disk would still list it.
|
||||
if self.vulnerability_reports or self._deleted_vulnerability_reports():
|
||||
write_vulnerabilities(run_dir, self.vulnerability_reports, self._saved_vuln_ids)
|
||||
|
||||
# SARIF 2.1.0 emitter for CI / ASPM integration. Always emit (even
|
||||
# empty) so a clean run overwrites a prior findings.sarif rather than
|
||||
# leaving a stale one — codeql-action's "absent from new submission →
|
||||
# fixed" needs the fresh empty doc to auto-resolve alerts. Isolated
|
||||
# in its own try: a SARIF-build error must NEVER break the CSV/MD/
|
||||
# run-record path (the emitter's own contract).
|
||||
try:
|
||||
write_sarif(
|
||||
run_dir,
|
||||
self.vulnerability_reports,
|
||||
tool_version=_strix_version(),
|
||||
repository_context=self._sarif_repository_context(),
|
||||
coverage=coverage,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("SARIF emit failed (non-fatal; CSV/MD unaffected)")
|
||||
|
||||
write_run_record(run_dir, self.run_record)
|
||||
|
||||
logger.info("Essential scan data saved to: %s", run_dir)
|
||||
|
||||
def _sarif_repository_context(self) -> dict[str, Any] | None:
|
||||
"""Repo/commit/branch context for SARIF provenance (repo scans only).
|
||||
|
||||
@@ -921,9 +780,6 @@ class ReportState:
|
||||
context["ref"] = f"refs/heads/{branch}"
|
||||
return context
|
||||
|
||||
def get_repository_context(self) -> dict[str, Any] | None:
|
||||
return self._derive_repository_context()
|
||||
|
||||
def _sync_llm_usage_record(self) -> None:
|
||||
self.run_record["llm_usage"] = self._build_llm_usage_record()
|
||||
|
||||
@@ -944,7 +800,6 @@ def openrouter_stream_cost(usage: Any) -> float | None:
|
||||
"""
|
||||
if not isinstance(usage, dict):
|
||||
return None
|
||||
|
||||
total = 0.0
|
||||
cost = usage.get("cost")
|
||||
if isinstance(cost, int | float) and cost > 0:
|
||||
|
||||
+2
-14
@@ -332,10 +332,7 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.extend(f" {ln}" for ln in snippet.splitlines())
|
||||
lines.append(f" {fence}")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
preparation = report.get("fix_preparation")
|
||||
prepared = isinstance(preparation, dict) and preparation.get("state") == "ready"
|
||||
label = "Prepared Fix" if prepared else "Draft Fix Candidate"
|
||||
lines.append(f"\n **{label}:**")
|
||||
lines.append("\n **Suggested Fix:**")
|
||||
lines.append("```diff")
|
||||
if loc.get("fix_before"):
|
||||
lines.extend(f"- {ln}" for ln in str(loc["fix_before"]).splitlines())
|
||||
@@ -350,19 +347,10 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.append("")
|
||||
|
||||
if report.get("fix_verification"):
|
||||
lines.append("## Reported Candidate Checks\n")
|
||||
lines.append("## Fix Verification\n")
|
||||
lines.append(str(report["fix_verification"]))
|
||||
lines.append("")
|
||||
|
||||
if isinstance(report.get("fix_preparation"), dict):
|
||||
preparation = report["fix_preparation"]
|
||||
lines.append("## Fix Preparation\n")
|
||||
lines.append(f"State: `{preparation.get('state', 'unknown')}`")
|
||||
if preparation.get("stop_reason"):
|
||||
lines.append("")
|
||||
lines.append(str(preparation["stop_reason"]))
|
||||
lines.append("")
|
||||
|
||||
if report.get("assumptions"):
|
||||
lines.append("## Assumptions\n")
|
||||
lines.append(str(report["assumptions"]))
|
||||
|
||||
+143
-134
@@ -3,16 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents.sandbox.entries import BaseEntry, LocalDir
|
||||
from agents.sandbox.entries import BaseEntry, File, LocalDir
|
||||
from agents.sandbox.manifest import Environment, Manifest
|
||||
|
||||
from strix.config import load_settings
|
||||
@@ -22,8 +21,6 @@ from strix.runtime.caido_handle import CaidoBootstrapHandle
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents.sandbox.session import BaseSandboxSession
|
||||
|
||||
from strix.runtime.status import StatusSink
|
||||
|
||||
|
||||
@@ -41,13 +38,6 @@ _WORKSPACE_ROOT = "/workspace"
|
||||
|
||||
_PROTECTED_METADATA_NAMES = (".git", ".agents", ".codex")
|
||||
|
||||
# Extra files travel as one tar archive: a single upload plus one extraction
|
||||
# inside the sandbox, instead of several round trips per file.
|
||||
_EXTRA_FILE_ARCHIVE_REL = ".strix-extra-files.tar"
|
||||
_EXTRA_FILE_ARCHIVE = f"{_WORKSPACE_ROOT}/{_EXTRA_FILE_ARCHIVE_REL}"
|
||||
_EXTRA_FILE_EXTRACT_TIMEOUT_S = 120
|
||||
_EXTRA_FILE_MODE = 0o644
|
||||
|
||||
|
||||
def _host_identity_env() -> dict[str, str]:
|
||||
# Read the platform through a local so it is not narrowed to whichever OS is
|
||||
@@ -70,9 +60,8 @@ def build_bind_mounts(local_sources: list[dict[str, Any]]) -> list[dict[str, Any
|
||||
continue
|
||||
resolved = Path(host_path).expanduser().resolve()
|
||||
target = f"{_WORKSPACE_ROOT}/{ws_subdir}"
|
||||
read_only = bool(src.get("read_only"))
|
||||
bind_mounts.append({"source": str(resolved), "target": target, "read_only": read_only})
|
||||
if src.get("protect_metadata") and not read_only:
|
||||
bind_mounts.append({"source": str(resolved), "target": target, "read_only": False})
|
||||
if src.get("protect_metadata"):
|
||||
bind_mounts.extend(_metadata_mounts(resolved, target))
|
||||
return bind_mounts
|
||||
|
||||
@@ -144,82 +133,98 @@ def _extra_file_content(extra_file: dict[str, Any]) -> bytes | None:
|
||||
return None
|
||||
|
||||
|
||||
def build_extra_file_archive(
|
||||
def build_extra_file_entries(
|
||||
extra_files: list[dict[str, Any]],
|
||||
local_sources: list[dict[str, Any]] | None = None,
|
||||
) -> bytes | None:
|
||||
"""Pack extra files into one tar archive rooted at ``/workspace``.
|
||||
) -> dict[str | Path, BaseEntry]:
|
||||
"""Map extra files to in-memory ``File`` manifest entries.
|
||||
|
||||
Each item is ``{"workspace_path": "/workspace/<rel>", "content": bytes|str}``
|
||||
and becomes a regular-file member at ``<rel>``. Extracting the archive as
|
||||
the sandbox user (see ``stage_extra_files``) leaves every file owned by
|
||||
that user, so the agent can edit it and create siblings. Invalid items —
|
||||
including paths that collide with a ``local_sources`` tree or with an
|
||||
earlier extra file — are skipped with a warning. Returns ``None`` when no
|
||||
valid file remains.
|
||||
Each item is ``{"workspace_path": "/workspace/<rel>", "content": bytes|str}``;
|
||||
manifest backends materialize the entry at the requested path alongside the
|
||||
``LocalDir`` source uploads. Invalid items — including paths that collide
|
||||
with a ``local_sources`` tree or with an earlier extra file, which would
|
||||
otherwise replace its manifest entry — are skipped with a warning.
|
||||
"""
|
||||
source_roots = _source_root_rels(local_sources)
|
||||
placed: list[str] = [_EXTRA_FILE_ARCHIVE_REL]
|
||||
buffer = io.BytesIO()
|
||||
mtime = int(time.time())
|
||||
with tarfile.open(fileobj=buffer, mode="w", format=tarfile.PAX_FORMAT) as archive:
|
||||
for extra_file in extra_files:
|
||||
rel, content = _validated_extra_file(extra_file, source_roots + placed)
|
||||
if rel is None or content is None:
|
||||
continue
|
||||
placed.append(rel)
|
||||
info = tarfile.TarInfo(name=rel)
|
||||
info.size = len(content)
|
||||
info.mode = _EXTRA_FILE_MODE
|
||||
info.mtime = mtime
|
||||
archive.addfile(info, io.BytesIO(content))
|
||||
if len(placed) == 1:
|
||||
return None
|
||||
return buffer.getvalue()
|
||||
placed: list[str] = []
|
||||
entries: dict[str | Path, BaseEntry] = {}
|
||||
for extra_file in extra_files:
|
||||
rel = _extra_file_rel_path(str(extra_file.get("workspace_path") or ""))
|
||||
content = _extra_file_content(extra_file)
|
||||
if rel is None or content is None:
|
||||
logger.warning(
|
||||
"Skipping invalid extra file entry (workspace_path=%r)",
|
||||
extra_file.get("workspace_path"),
|
||||
)
|
||||
continue
|
||||
if _collides_with_source_root(rel, source_roots + placed):
|
||||
logger.warning(
|
||||
"Skipping extra file colliding with a local source tree or an "
|
||||
"earlier extra file (workspace_path=%r)",
|
||||
extra_file.get("workspace_path"),
|
||||
)
|
||||
continue
|
||||
placed.append(rel)
|
||||
entries[rel] = File(content=content)
|
||||
return entries
|
||||
|
||||
|
||||
def _validated_extra_file(
|
||||
extra_file: dict[str, Any], taken: list[str]
|
||||
) -> tuple[str | None, bytes | None]:
|
||||
rel = _extra_file_rel_path(str(extra_file.get("workspace_path") or ""))
|
||||
content = _extra_file_content(extra_file)
|
||||
if rel is None or content is None:
|
||||
logger.warning(
|
||||
"Skipping invalid extra file entry (workspace_path=%r)",
|
||||
extra_file.get("workspace_path"),
|
||||
)
|
||||
return None, None
|
||||
if _collides_with_source_root(rel, taken):
|
||||
logger.warning(
|
||||
"Skipping extra file colliding with a local source tree or an "
|
||||
"earlier extra file (workspace_path=%r)",
|
||||
extra_file.get("workspace_path"),
|
||||
)
|
||||
return None, None
|
||||
return rel, content
|
||||
def extra_file_staging_dir(scan_id: str) -> Path:
|
||||
"""A fresh host staging directory for a scan's extra-file bind mounts.
|
||||
|
||||
|
||||
async def stage_extra_files(session: BaseSandboxSession, archive: bytes) -> None:
|
||||
"""Upload ``archive`` and unpack it under ``/workspace`` as the sandbox user.
|
||||
|
||||
``--no-same-owner`` keeps ownership with the extracting user even where
|
||||
the session runs as root, so the agent user can always write the files.
|
||||
The docker daemon resolves bind sources in its own filesystem. With a
|
||||
remote daemon (e.g. a dind sidecar) the run directory is not shared, so
|
||||
staging lives under the temp dir like every other bind-mount source.
|
||||
"""
|
||||
await session.write(Path(_EXTRA_FILE_ARCHIVE), io.BytesIO(archive))
|
||||
result = await session.exec(
|
||||
"sh",
|
||||
"-c",
|
||||
'tar --no-same-owner -xf "$1" -C "$2" && rm -f -- "$1"',
|
||||
"sh",
|
||||
_EXTRA_FILE_ARCHIVE,
|
||||
_WORKSPACE_ROOT,
|
||||
timeout=_EXTRA_FILE_EXTRACT_TIMEOUT_S,
|
||||
)
|
||||
if not result.ok():
|
||||
stderr = result.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise RuntimeError(
|
||||
f"unpacking extra files in the sandbox failed (exit {result.exit_code}): {stderr}"
|
||||
safe = "".join(c if c.isalnum() or c in "-_." else "-" for c in scan_id)
|
||||
return Path(tempfile.mkdtemp(prefix=f"strix-extra-files-{safe}-"))
|
||||
|
||||
|
||||
def build_extra_file_bind_mounts(
|
||||
extra_files: list[dict[str, Any]],
|
||||
staging_dir: Path,
|
||||
local_sources: list[dict[str, Any]] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Stage extra files on the host and map them to read-only bind mounts.
|
||||
|
||||
Bind-mount backends bypass the manifest, so the content is written under
|
||||
``staging_dir`` (one numbered subdirectory per file to avoid basename
|
||||
collisions) and mounted read-only at the same ``/workspace/<rel>`` path the
|
||||
manifest path would use. Invalid items — including paths that collide with
|
||||
a ``local_sources`` tree or with an earlier extra file, which would
|
||||
duplicate or shadow its mount target — are skipped with a warning.
|
||||
"""
|
||||
source_roots = _source_root_rels(local_sources)
|
||||
placed: list[str] = []
|
||||
mounts: list[dict[str, Any]] = []
|
||||
for index, extra_file in enumerate(extra_files):
|
||||
rel = _extra_file_rel_path(str(extra_file.get("workspace_path") or ""))
|
||||
content = _extra_file_content(extra_file)
|
||||
if rel is None or content is None:
|
||||
logger.warning(
|
||||
"Skipping invalid extra file entry (workspace_path=%r)",
|
||||
extra_file.get("workspace_path"),
|
||||
)
|
||||
continue
|
||||
if _collides_with_source_root(rel, source_roots + placed):
|
||||
logger.warning(
|
||||
"Skipping extra file colliding with a local source tree or an "
|
||||
"earlier extra file (workspace_path=%r)",
|
||||
extra_file.get("workspace_path"),
|
||||
)
|
||||
continue
|
||||
placed.append(rel)
|
||||
host_file = staging_dir / str(index) / Path(rel).name
|
||||
host_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
host_file.write_bytes(content)
|
||||
mounts.append(
|
||||
{
|
||||
"source": str(host_file),
|
||||
"target": f"{_WORKSPACE_ROOT}/{rel}",
|
||||
"read_only": True,
|
||||
}
|
||||
)
|
||||
return mounts
|
||||
|
||||
|
||||
def _metadata_mounts(tree: Path, target: str) -> list[dict[str, Any]]:
|
||||
@@ -269,9 +274,10 @@ async def create_or_reuse(
|
||||
``/workspace/<workspace_subdir>`` inside the container.
|
||||
|
||||
Each ``extra_files`` entry (``{"workspace_path": "/workspace/<rel>",
|
||||
"content": bytes | str}``) lands as a regular, agent-writable file at its
|
||||
``workspace_path`` on every backend: the files are uploaded as one archive
|
||||
and unpacked inside the sandbox right after bring-up.
|
||||
"content": bytes | str}``) lands as a single file at its ``workspace_path``
|
||||
regardless of backend: an in-memory ``File`` manifest entry on manifest
|
||||
backends, a read-only bind mount of a host-staged copy on bind-mount
|
||||
backends.
|
||||
"""
|
||||
|
||||
def report(phase: str) -> None:
|
||||
@@ -286,15 +292,20 @@ async def create_or_reuse(
|
||||
backend_name = load_settings().runtime.backend
|
||||
backend = get_backend(backend_name)
|
||||
|
||||
staging_dir: Path | None = None
|
||||
if backend_supports_bind_mounts(backend_name):
|
||||
bind_mounts = build_bind_mounts(local_sources)
|
||||
entries: dict[str | Path, BaseEntry] = {}
|
||||
if extra_files:
|
||||
staging_dir = extra_file_staging_dir(scan_id)
|
||||
bind_mounts.extend(
|
||||
build_extra_file_bind_mounts(extra_files, staging_dir, local_sources)
|
||||
)
|
||||
else:
|
||||
bind_mounts = []
|
||||
entries = build_manifest_entries(local_sources)
|
||||
extra_file_archive = (
|
||||
build_extra_file_archive(extra_files, local_sources) if extra_files else None
|
||||
)
|
||||
if extra_files:
|
||||
entries.update(build_extra_file_entries(extra_files, local_sources))
|
||||
|
||||
# Caido runs as an in-container sidecar; HTTP(S) traffic from any
|
||||
# process started via ``session.exec`` (the SDK's Shell tool, etc.)
|
||||
@@ -324,58 +335,54 @@ async def create_or_reuse(
|
||||
image,
|
||||
)
|
||||
report("Starting sandbox container")
|
||||
client, session = await backend(
|
||||
image=image,
|
||||
manifest=manifest,
|
||||
exposed_ports=(_CONTAINER_CAIDO_PORT,),
|
||||
bind_mounts=bind_mounts,
|
||||
)
|
||||
|
||||
if extra_file_archive is not None:
|
||||
report("Placing workspace files")
|
||||
try:
|
||||
await stage_extra_files(session, extra_file_archive)
|
||||
except BaseException:
|
||||
await _discard_session(client, session)
|
||||
raise
|
||||
|
||||
report("Setting up the proxy")
|
||||
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
|
||||
scheme = "https" if caido_endpoint.tls else "http"
|
||||
host_caido_url = f"{scheme}://{caido_endpoint.host}:{caido_endpoint.port}"
|
||||
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
|
||||
|
||||
# The Caido login + project setup polls the guest for a couple of seconds
|
||||
# and nothing needs the client before the first proxy tool call, so it
|
||||
# runs concurrently with the rest of scan start; consumers resolve the
|
||||
# handle at first use (see CaidoBootstrapHandle).
|
||||
caido_client = CaidoBootstrapHandle(
|
||||
asyncio.create_task(
|
||||
bootstrap_caido(
|
||||
session,
|
||||
host_url=host_caido_url,
|
||||
container_url=container_caido_url,
|
||||
),
|
||||
name=f"caido-bootstrap-{scan_id}",
|
||||
try:
|
||||
client, session = await backend(
|
||||
image=image,
|
||||
manifest=manifest,
|
||||
exposed_ports=(_CONTAINER_CAIDO_PORT,),
|
||||
bind_mounts=bind_mounts,
|
||||
)
|
||||
)
|
||||
|
||||
bundle = {
|
||||
"client": client,
|
||||
"session": session,
|
||||
"caido_client": caido_client,
|
||||
}
|
||||
_SESSION_CACHE[scan_id] = bundle
|
||||
report("Setting up the proxy")
|
||||
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
|
||||
scheme = "https" if caido_endpoint.tls else "http"
|
||||
host_caido_url = f"{scheme}://{caido_endpoint.host}:{caido_endpoint.port}"
|
||||
logger.debug("Caido host endpoint resolved: %s", host_caido_url)
|
||||
|
||||
# The Caido login + project setup polls the guest for a couple of seconds
|
||||
# and nothing needs the client before the first proxy tool call, so it
|
||||
# runs concurrently with the rest of scan start; consumers resolve the
|
||||
# handle at first use (see CaidoBootstrapHandle).
|
||||
caido_client = CaidoBootstrapHandle(
|
||||
asyncio.create_task(
|
||||
bootstrap_caido(
|
||||
session,
|
||||
host_url=host_caido_url,
|
||||
container_url=container_caido_url,
|
||||
),
|
||||
name=f"caido-bootstrap-{scan_id}",
|
||||
)
|
||||
)
|
||||
|
||||
bundle = {
|
||||
"client": client,
|
||||
"session": session,
|
||||
"caido_client": caido_client,
|
||||
"extra_file_staging_dir": staging_dir,
|
||||
}
|
||||
_SESSION_CACHE[scan_id] = bundle
|
||||
except BaseException:
|
||||
# Until the bundle is cached, cleanup(scan_id) cannot find the
|
||||
# staging dir, so it is removed here.
|
||||
_remove_staging_dir(staging_dir)
|
||||
raise
|
||||
logger.info("Sandbox session for scan %s ready and cached", scan_id)
|
||||
return bundle
|
||||
|
||||
|
||||
async def _discard_session(client: Any, session: Any) -> None:
|
||||
"""Best-effort teardown of a session that never made it into the cache."""
|
||||
try:
|
||||
await client.delete(session)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning("Discarding a half-started sandbox session failed", exc_info=True)
|
||||
def _remove_staging_dir(staging_dir: Path | None) -> None:
|
||||
if staging_dir is not None:
|
||||
shutil.rmtree(staging_dir, ignore_errors=True)
|
||||
|
||||
|
||||
async def cleanup(scan_id: str) -> None:
|
||||
@@ -391,6 +398,8 @@ async def cleanup(scan_id: str) -> None:
|
||||
logger.debug("cleanup(%s): no cached session", scan_id)
|
||||
return
|
||||
|
||||
_remove_staging_dir(bundle.get("extra_file_staging_dir"))
|
||||
|
||||
caido_client = bundle.get("caido_client")
|
||||
if caido_client is not None:
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from collections import Counter
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
@@ -7,6 +8,7 @@ from typing import TypeGuard
|
||||
|
||||
import yaml
|
||||
|
||||
from strix.telemetry import posthog, scarf
|
||||
from strix.utils.resource_paths import get_strix_resource_path
|
||||
|
||||
|
||||
@@ -239,19 +241,16 @@ def validate_requested_skills(skill_list: list[str], max_skills: int = 5) -> str
|
||||
return None
|
||||
|
||||
|
||||
_LOADED_SKILLS: set[str] = set()
|
||||
|
||||
|
||||
def _track_skill_loaded(skill_name: str, file_path: Path) -> None:
|
||||
builtin = get_strix_resource_path("skills")
|
||||
if not file_path.is_relative_to(builtin):
|
||||
skill_name = "custom"
|
||||
_LOADED_SKILLS.add(skill_name)
|
||||
|
||||
def _send() -> None:
|
||||
posthog.skill_loaded(skill_name)
|
||||
scarf.skill_loaded(skill_name)
|
||||
|
||||
def get_loaded_skill_names() -> list[str]:
|
||||
"""Distinct skills loaded so far in this process (custom skills collapse to ``"custom"``)."""
|
||||
return sorted(_LOADED_SKILLS)
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
|
||||
|
||||
def _candidate_skill_files(skill_name: str) -> list[Path]:
|
||||
|
||||
@@ -12,11 +12,11 @@ Privacy is our priority. All collected data is anonymized by default. Each sessi
|
||||
|
||||
We collect only very **basic** usage data including:
|
||||
|
||||
**Session Errors:** Duration, the failure category, the scan phase, and the exception class name (not messages or stack traces)\
|
||||
**Session Errors:** Duration and error types (not messages or stack traces)\
|
||||
**System Context:** OS type, architecture, Strix version\
|
||||
**Scan Context:** Scan mode (quick/standard/deep), scan type (whitebox/blackbox)\
|
||||
**Model Usage:** Which LLM model is being used and whether it runs via an API key or a model subscription (not prompts or responses)\
|
||||
**Feature Usage:** Which built-in skills were used during a scan (reported once, at scan end)\
|
||||
**Feature Usage:** Which built-in skills are loaded\
|
||||
**Aggregate Metrics:** Vulnerability counts by severity and weakness category (CWE)
|
||||
|
||||
### What We **Never** Collect
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
from . import posthog, scarf
|
||||
from ._common import set_scan_phase
|
||||
|
||||
|
||||
def report_error(error_type: str, exc: BaseException | None = None) -> None:
|
||||
"""Beacon a failure category, plus the exception class when one is given.
|
||||
|
||||
Only class names travel: never the message, arguments, or traceback.
|
||||
"""
|
||||
posthog.error(error_type, exc)
|
||||
scarf.error(error_type, exc)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"posthog",
|
||||
"report_error",
|
||||
"scarf",
|
||||
"set_scan_phase",
|
||||
]
|
||||
@@ -5,7 +5,7 @@ import platform
|
||||
import sys
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@@ -54,43 +54,3 @@ def base_props() -> dict[str, Any]:
|
||||
"python": f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"strix_version": get_version(),
|
||||
}
|
||||
|
||||
|
||||
# Coarse stage of the current run, attached to ``error`` beacons so a failure
|
||||
# can be placed without a message or trace. Process-local, like the rest of the
|
||||
# CLI telemetry: one process runs one scan.
|
||||
_scan_phase = "startup"
|
||||
|
||||
|
||||
def set_scan_phase(phase: str) -> None:
|
||||
global _scan_phase # noqa: PLW0603
|
||||
_scan_phase = phase
|
||||
|
||||
|
||||
def get_scan_phase() -> str:
|
||||
return _scan_phase
|
||||
|
||||
|
||||
def _exception_name(exc: BaseException) -> str:
|
||||
cls = type(exc)
|
||||
package = cls.__module__.split(".")[0]
|
||||
return cls.__name__ if package == "builtins" else f"{package}.{cls.__name__}"
|
||||
|
||||
|
||||
def _unwrap_group(exc: BaseException) -> BaseException:
|
||||
if not isinstance(exc, BaseExceptionGroup):
|
||||
return exc
|
||||
group = cast("BaseExceptionGroup[BaseException]", exc)
|
||||
return group.exceptions[0] if group.exceptions else group
|
||||
|
||||
|
||||
def exception_props(exc: BaseException) -> dict[str, str]:
|
||||
"""Class names only. Messages, arguments, and tracebacks never leave the machine."""
|
||||
exc = _unwrap_group(exc)
|
||||
props = {"exception_type": _exception_name(exc)}
|
||||
cause = exc.__cause__
|
||||
if cause is None and not exc.__suppress_context__:
|
||||
cause = exc.__context__
|
||||
if cause is not None:
|
||||
props["exception_cause"] = _exception_name(cause)
|
||||
return props
|
||||
+13
-19
@@ -4,14 +4,10 @@ from typing import TYPE_CHECKING, Any
|
||||
import requests
|
||||
|
||||
from strix.config import load_settings
|
||||
from strix.skills import get_loaded_skill_names
|
||||
from strix.telemetry._common import (
|
||||
SEND_TIMEOUT,
|
||||
SESSION_ID,
|
||||
base_props,
|
||||
exception_props,
|
||||
get_scan_phase,
|
||||
get_version,
|
||||
is_first_run,
|
||||
)
|
||||
|
||||
@@ -39,12 +35,7 @@ def _send(event: str, properties: dict[str, Any]) -> bool:
|
||||
"api_key": _POSTHOG_PUBLIC_API_KEY,
|
||||
"event": event,
|
||||
"distinct_id": SESSION_ID,
|
||||
"properties": {
|
||||
**properties,
|
||||
"$lib": "strix-cli",
|
||||
"$lib_version": get_version(),
|
||||
"$process_person_profile": False,
|
||||
},
|
||||
"properties": properties,
|
||||
}
|
||||
with requests.post(f"{_POSTHOG_HOST}/capture/", json=payload, timeout=SEND_TIMEOUT):
|
||||
pass
|
||||
@@ -91,6 +82,16 @@ def finding(severity: str, cwe: str | None = None, is_cve: bool = False) -> None
|
||||
)
|
||||
|
||||
|
||||
def skill_loaded(skill_name: str) -> None:
|
||||
_send(
|
||||
"skill_loaded",
|
||||
{
|
||||
**base_props(),
|
||||
"skill": skill_name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
|
||||
if report_state.posthog_scan_ended_sent:
|
||||
return
|
||||
@@ -129,7 +130,6 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
|
||||
"vulnerabilities_total": len(report_state.vulnerability_reports),
|
||||
**{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
|
||||
**llm_props,
|
||||
"skills": get_loaded_skill_names(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -180,12 +180,6 @@ def viewer_agent_steered() -> None:
|
||||
_send("viewer_agent_steered", {**base_props()})
|
||||
|
||||
|
||||
def error(error_type: str, exc: BaseException | None = None) -> None:
|
||||
props: dict[str, Any] = {
|
||||
**base_props(),
|
||||
"error_type": error_type,
|
||||
"phase": get_scan_phase(),
|
||||
}
|
||||
if exc is not None:
|
||||
props.update(exception_props(exc))
|
||||
def error(error_type: str) -> None:
|
||||
props = {**base_props(), "error_type": error_type}
|
||||
_send("error", props)
|
||||
@@ -7,13 +7,10 @@ from typing import TYPE_CHECKING, Any
|
||||
import requests
|
||||
|
||||
from strix.config import load_settings
|
||||
from strix.skills import get_loaded_skill_names
|
||||
from strix.telemetry._common import (
|
||||
SEND_TIMEOUT,
|
||||
SESSION_ID,
|
||||
base_props,
|
||||
exception_props,
|
||||
get_scan_phase,
|
||||
get_version,
|
||||
is_first_run,
|
||||
)
|
||||
@@ -93,6 +90,17 @@ def finding(severity: str, cwe: str | None = None, is_cve: bool = False) -> None
|
||||
)
|
||||
|
||||
|
||||
def skill_loaded(skill_name: str) -> None:
|
||||
_send(
|
||||
"skill_loaded",
|
||||
{
|
||||
**base_props(),
|
||||
"session": SESSION_ID,
|
||||
"skill": skill_name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def end(report_state: ReportState, exit_reason: str = "completed") -> None:
|
||||
if report_state.scarf_scan_ended_sent:
|
||||
return
|
||||
@@ -132,18 +140,14 @@ def end(report_state: ReportState, exit_reason: str = "completed") -> None:
|
||||
"vulnerabilities_total": len(report_state.vulnerability_reports),
|
||||
**{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
|
||||
**llm_props,
|
||||
"skills": ",".join(get_loaded_skill_names()),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def error(error_type: str, exc: BaseException | None = None) -> None:
|
||||
def error(error_type: str) -> None:
|
||||
props: dict[str, Any] = {
|
||||
**base_props(),
|
||||
"session": SESSION_ID,
|
||||
"error_type": error_type,
|
||||
"phase": get_scan_phase(),
|
||||
}
|
||||
if exc is not None:
|
||||
props.update(exception_props(exc))
|
||||
_send("error", props)
|
||||
@@ -15,7 +15,6 @@ from agents import RunContextWrapper, function_tool
|
||||
from strix.core.agents import Status, coordinator_from_context
|
||||
from strix.core.execution import notify_parent_on_terminal
|
||||
from strix.core.hooks import LLM_TURN_KEY
|
||||
from strix.report.state import get_global_report_state
|
||||
from strix.skills import validate_requested_skills
|
||||
|
||||
|
||||
@@ -29,40 +28,6 @@ def _ctx(ctx: RunContextWrapper) -> dict[str, Any]:
|
||||
return ctx.context if isinstance(ctx.context, dict) else {}
|
||||
|
||||
|
||||
def _filed_reports_by(agent_id: str) -> list[dict[str, Any]]:
|
||||
"""Vulnerability reports the agent actually filed, from report state.
|
||||
|
||||
The narrative ``findings`` an agent hands to ``agent_finish`` is prose; a
|
||||
parent that wants to act on a child's work needs the report ids. Read them
|
||||
from the report state rather than trusting the child's description.
|
||||
"""
|
||||
state = get_global_report_state()
|
||||
if state is None:
|
||||
return []
|
||||
filed: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for report in state.get_existing_vulnerabilities():
|
||||
if report.get("agent_id") != agent_id:
|
||||
continue
|
||||
report_id = str(report.get("id") or "")
|
||||
if not report_id or report_id in seen:
|
||||
continue
|
||||
seen.add(report_id)
|
||||
filed.append(report)
|
||||
return filed
|
||||
|
||||
|
||||
def _render_filed_report(report: dict[str, Any]) -> str:
|
||||
line = f"- {report.get('id')}"
|
||||
severity = report.get("severity")
|
||||
if severity:
|
||||
line += f" [{str(severity).upper()}]"
|
||||
title = report.get("title")
|
||||
if title:
|
||||
line += f" {title}"
|
||||
return line
|
||||
|
||||
|
||||
def _render_completion_report(
|
||||
*,
|
||||
agent_name: str,
|
||||
@@ -73,7 +38,6 @@ def _render_completion_report(
|
||||
findings: list[str],
|
||||
recommendations: list[str],
|
||||
open_items: list[str],
|
||||
filed_reports: list[dict[str, Any]] | None = None,
|
||||
) -> str:
|
||||
"""Render a child's completion report as plain structured text.
|
||||
|
||||
@@ -99,12 +63,6 @@ def _render_completion_report(
|
||||
lines.append("Findings:")
|
||||
lines.extend(f"- {f}" for f in findings)
|
||||
lines.append("")
|
||||
lines.append("Vulnerability reports filed by this agent (authoritative; use these ids):")
|
||||
if filed_reports:
|
||||
lines.extend(_render_filed_report(r) for r in filed_reports)
|
||||
else:
|
||||
lines.append("- (none)")
|
||||
lines.append("")
|
||||
lines.append("Open items (unresolved, need follow-up):")
|
||||
if open_items:
|
||||
lines.extend(f"- {o}" for o in open_items)
|
||||
@@ -191,11 +149,8 @@ async def send_message_to_agent(
|
||||
**Don't** use for routine "hello/status" pings, for context the
|
||||
target already has (children inherit parent history), or when
|
||||
parent/child completion via ``agent_finish`` already covers the
|
||||
flow. In interactive runs a message wakes the target regardless of
|
||||
flow. Messages to any registered agent wake it, regardless of
|
||||
status, so a follow-up can restart a completed/stopped/failed agent.
|
||||
In non-interactive runs a finished agent is gone for good: the call
|
||||
fails with the target's status, and you should read its filed
|
||||
reports (``list_reports``) or spawn a new agent instead of waiting.
|
||||
|
||||
Args:
|
||||
target_agent_id: Recipient's 8-char id.
|
||||
@@ -240,23 +195,10 @@ async def send_message_to_agent(
|
||||
},
|
||||
)
|
||||
if not delivered:
|
||||
_, status = await coordinator.reachability(target_agent_id)
|
||||
if status is None:
|
||||
error = f"Target agent '{target_agent_id}' not found"
|
||||
else:
|
||||
error = (
|
||||
f"Target agent '{target_agent_id}' is '{status}' and cannot be woken in "
|
||||
"this run; it will never read this message. Its filed reports are in "
|
||||
"list_reports / get_report. Do not wait_for_agents on it - spawn a new "
|
||||
"agent if more work is needed."
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": error,
|
||||
"target_agent_id": target_agent_id,
|
||||
"target_status": status,
|
||||
"delivery_status": "not_delivered",
|
||||
"error": f"Target agent '{target_agent_id}' not found or message delivery failed",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
@@ -422,31 +364,6 @@ async def wait_for_agents( # noqa: PLR0911
|
||||
default=str,
|
||||
)
|
||||
|
||||
# Non-interactive agents cannot be woken once terminal, so with nobody
|
||||
# running or waiting there is no message left to wait for.
|
||||
if not await coordinator.active_agents_except(me):
|
||||
_, statuses, names, _ = await coordinator.graph_snapshot()
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"wait_outcome": "no_active_agents",
|
||||
"reason": reason,
|
||||
"agents": [
|
||||
{"agent_id": aid, "name": names.get(aid, aid), "status": status}
|
||||
for aid, status in statuses.items()
|
||||
if aid != me
|
||||
],
|
||||
"note": (
|
||||
"No other agent is running or waiting, so no message can arrive. "
|
||||
"Finished agents' results are in list_reports / get_report and their "
|
||||
"completion reports are already in your history. Continue your own "
|
||||
"work, spawn a new agent, or finish."
|
||||
),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
await coordinator.park_waiting(me, wait_kind="agents")
|
||||
try:
|
||||
await asyncio.wait_for(coordinator.wait_for_message(me), timeout_seconds)
|
||||
@@ -693,9 +610,6 @@ async def agent_finish(
|
||||
default=str,
|
||||
)
|
||||
|
||||
filed_reports = _filed_reports_by(me)
|
||||
filed_report_ids = [str(r.get("id")) for r in filed_reports]
|
||||
|
||||
parent_notified = False
|
||||
if report_to_parent and await coordinator.claim_parent_notice(me):
|
||||
async with coordinator._lock:
|
||||
@@ -709,7 +623,6 @@ async def agent_finish(
|
||||
findings=list(findings or []),
|
||||
recommendations=list(final_recommendations or []),
|
||||
open_items=list(open_items or []),
|
||||
filed_reports=filed_reports,
|
||||
)
|
||||
await coordinator.send(
|
||||
parent_id,
|
||||
@@ -719,7 +632,6 @@ async def agent_finish(
|
||||
"content": report,
|
||||
"type": "completion",
|
||||
"priority": "high",
|
||||
"filed_report_ids": filed_report_ids,
|
||||
},
|
||||
)
|
||||
parent_notified = True
|
||||
@@ -730,11 +642,10 @@ async def agent_finish(
|
||||
await notify_parent_on_terminal(coordinator, me, "completed")
|
||||
|
||||
logger.info(
|
||||
"agent_finish: %s success=%s findings=%d filed_reports=%d parent_notified=%s",
|
||||
"agent_finish: %s success=%s findings=%d parent_notified=%s",
|
||||
me,
|
||||
success,
|
||||
len(findings or []),
|
||||
len(filed_report_ids),
|
||||
parent_notified,
|
||||
)
|
||||
|
||||
@@ -745,7 +656,6 @@ async def agent_finish(
|
||||
"parent_notified": parent_notified,
|
||||
"agent_id": me,
|
||||
"summary": result_summary,
|
||||
"filed_report_ids": filed_report_ids,
|
||||
"findings_count": len(findings or []),
|
||||
"open_items_count": len(open_items or []),
|
||||
"has_recommendations": bool(final_recommendations),
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from strix.tools.mcp.agent_tools import (
|
||||
call_mcp,
|
||||
describe_mcp,
|
||||
get_mcp_tool_schema,
|
||||
list_mcps,
|
||||
search_mcp_tools,
|
||||
)
|
||||
from strix.tools.mcp.agent_tools import call_mcp, describe_mcp, list_mcps
|
||||
from strix.tools.mcp.client import (
|
||||
ConnectedMcpServer,
|
||||
attach_mcp_requests,
|
||||
@@ -25,10 +19,8 @@ from strix.tools.mcp.naming import namespaced_tool_name
|
||||
from strix.tools.mcp.registry import (
|
||||
CALL_MCP_TOOL,
|
||||
DESCRIBE_MCP_TOOL,
|
||||
GET_MCP_TOOL_SCHEMA_TOOL,
|
||||
MCP_DISPATCH_TOOLS,
|
||||
MCP_REGISTRY_CONTEXT_KEY,
|
||||
SEARCH_MCP_TOOLS_TOOL,
|
||||
McpCallInfo,
|
||||
McpConnectionEntry,
|
||||
McpConnectionRequest,
|
||||
@@ -43,10 +35,8 @@ from strix.tools.mcp.session import McpConnectionUnavailableError, SupervisedMcp
|
||||
__all__ = [
|
||||
"CALL_MCP_TOOL",
|
||||
"DESCRIBE_MCP_TOOL",
|
||||
"GET_MCP_TOOL_SCHEMA_TOOL",
|
||||
"MCP_DISPATCH_TOOLS",
|
||||
"MCP_REGISTRY_CONTEXT_KEY",
|
||||
"SEARCH_MCP_TOOLS_TOOL",
|
||||
"BearerAuth",
|
||||
"ConnectedMcpServer",
|
||||
"FailureInfo",
|
||||
@@ -66,10 +56,8 @@ __all__ = [
|
||||
"classify",
|
||||
"connect_mcp_servers",
|
||||
"describe_mcp",
|
||||
"get_mcp_tool_schema",
|
||||
"list_mcps",
|
||||
"load_user_mcp_configs",
|
||||
"namespaced_tool_name",
|
||||
"resolve_mcp_call",
|
||||
"search_mcp_tools",
|
||||
]
|
||||
+33
-148
@@ -1,21 +1,18 @@
|
||||
"""The generic MCP discovery and dispatch tools every agent carries.
|
||||
"""The three generic MCP dispatch tools every agent carries.
|
||||
|
||||
Under the generic-dispatch model an agent does not get one tool per MCP tool.
|
||||
It gets four primary tools and discovers connections on demand:
|
||||
It gets exactly these three and discovers connections on demand:
|
||||
|
||||
- ``list_mcps()`` returns the connections available this run — each connection's
|
||||
id, name, description, and tool count, with no tool schemas — so the model can
|
||||
discover what it can reach without any inventory in the system prompt.
|
||||
- ``search_mcp_tools(connection, query)`` returns a small ranked candidate set.
|
||||
- ``get_mcp_tool_schema(connection, tool)`` returns one exact input schema.
|
||||
- ``describe_mcp(connection)`` returns, as text, one connection's tools with
|
||||
their names, descriptions, and JSON input schemas — the schemas the model
|
||||
needs, fetched on demand instead of loaded onto every request up front.
|
||||
- ``call_mcp(connection, tool, arguments)`` dispatches one call to a
|
||||
connection's tool and returns its result.
|
||||
|
||||
``describe_mcp`` remains as a compatibility path for older prompts, but current
|
||||
agents use targeted search and one-schema lookup instead of receiving a whole
|
||||
provider catalog in one model turn.
|
||||
|
||||
All four read the per-run :class:`~strix.tools.mcp.registry.McpRegistry` from the
|
||||
All three read the per-run :class:`~strix.tools.mcp.registry.McpRegistry` from the
|
||||
run context under :data:`~strix.tools.mcp.registry.MCP_REGISTRY_CONTEXT_KEY`. They
|
||||
are ordinary ``FunctionTool`` objects placed in the agent factory's base tool set,
|
||||
so the factory's output-bounding and disk-spill wrapping apply to their results
|
||||
@@ -64,11 +61,12 @@ async def list_mcps(ctx: RunContextWrapper) -> dict[str, Any]:
|
||||
"""List the MCP connections available this run, so you can discover them.
|
||||
|
||||
Read-only. Returns one entry per connection with its ``id`` (the exact name
|
||||
you pass to the other MCP tools), ``name``, ``description``, and
|
||||
``tool_count``. The response does not include tool schemas. Call
|
||||
``search_mcp_tools`` next. Then call ``get_mcp_tool_schema`` for one selected
|
||||
tool before you call ``call_mcp``. Returns an empty ``connections`` list when
|
||||
the run has no MCP connections.
|
||||
you pass to ``describe_mcp`` and ``call_mcp``), ``name``, ``description``, and
|
||||
``tool_count`` — no tool schemas. The three MCP tools work in order: call
|
||||
``list_mcps`` to discover the available connections, then ``describe_mcp`` on
|
||||
one connection to inspect its tools and their input schemas, then ``call_mcp``
|
||||
to run one of its tools. Returns an empty ``connections`` list when the run has
|
||||
no MCP connections.
|
||||
"""
|
||||
registry = _registry_from_ctx(ctx)
|
||||
if registry is None or not registry:
|
||||
@@ -82,7 +80,6 @@ async def list_mcps(ctx: RunContextWrapper) -> dict[str, Any]:
|
||||
"description": summary.purpose,
|
||||
"tool_count": summary.tool_count,
|
||||
"dead": dead_by_name.get(summary.name, False),
|
||||
"state": summary.state,
|
||||
}
|
||||
for summary in registry.summaries()
|
||||
]
|
||||
@@ -91,12 +88,13 @@ async def list_mcps(ctx: RunContextWrapper) -> dict[str, Any]:
|
||||
|
||||
@function_tool(timeout=60)
|
||||
async def describe_mcp(ctx: RunContextWrapper, connection: str) -> str:
|
||||
"""Return one connection's full tool catalog as a compatibility fallback.
|
||||
"""List the tools one MCP connection offers, with their input schemas.
|
||||
|
||||
Read-only. This result can be large because it includes every tool schema.
|
||||
First use ``search_mcp_tools`` and ``get_mcp_tool_schema``. Use this fallback
|
||||
only when targeted search cannot identify an expected tool. Nothing is read
|
||||
from the connected account.
|
||||
Read-only. Look up a connection by the id ``list_mcps`` reported for it; this
|
||||
returns each of its tools with the tool's name, description, and JSON input
|
||||
schema — the argument shape you pass to ``call_mcp``. Call this before
|
||||
``call_mcp`` on any connection you have not used yet. Nothing is fetched from
|
||||
or run against the connection's data.
|
||||
|
||||
Args:
|
||||
connection: The connection name exactly as reported by ``list_mcps``.
|
||||
@@ -108,7 +106,7 @@ async def describe_mcp(ctx: RunContextWrapper, connection: str) -> str:
|
||||
if entry is None:
|
||||
return _unknown_connection(connection, registry)
|
||||
try:
|
||||
tools = await entry.ensure_catalog()
|
||||
tools = await entry.session.list_tools()
|
||||
except McpConnectionUnavailableError as exc:
|
||||
return str(exc)
|
||||
if not tools:
|
||||
@@ -118,121 +116,6 @@ async def describe_mcp(ctx: RunContextWrapper, connection: str) -> str:
|
||||
return f"{header}\n{body}"
|
||||
|
||||
|
||||
def _search_score(tool: MCPTool, query_terms: list[str], active: bool) -> tuple[int, str]:
|
||||
name = tool.name.lower()
|
||||
description = (tool.description or "").lower()
|
||||
if not query_terms:
|
||||
return (100 if active else 0, name)
|
||||
score = 100 if active else 0
|
||||
matched_terms = 0
|
||||
for term in query_terms:
|
||||
if name == term:
|
||||
score += 60
|
||||
matched_terms += 1
|
||||
elif name.startswith(term):
|
||||
score += 35
|
||||
matched_terms += 1
|
||||
elif term in name:
|
||||
score += 25
|
||||
matched_terms += 1
|
||||
elif term in description:
|
||||
score += 10
|
||||
matched_terms += 1
|
||||
if matched_terms == 0:
|
||||
return (-1, name)
|
||||
return (score + (matched_terms * 5), name)
|
||||
|
||||
|
||||
@function_tool(timeout=60)
|
||||
async def search_mcp_tools(
|
||||
ctx: RunContextWrapper,
|
||||
connection: str,
|
||||
query: str,
|
||||
limit: int = 8,
|
||||
) -> dict[str, Any] | str:
|
||||
"""Search one connection's tools without returning their full schemas.
|
||||
|
||||
The first search lazily connects the provider and caches its catalog. Results
|
||||
contain only names, short descriptions, and whether each tool is in the
|
||||
scan's active set. Call ``get_mcp_tool_schema`` for the selected tool.
|
||||
|
||||
Args:
|
||||
connection: The connection name exactly as reported by ``list_mcps``.
|
||||
query: Capability words such as ``page content`` or ``workspace identity``.
|
||||
limit: Maximum candidates to return, from 1 through 20.
|
||||
"""
|
||||
registry = _registry_from_ctx(ctx)
|
||||
if registry is None or not registry:
|
||||
return _NO_CONNECTIONS
|
||||
entry = registry.get(connection)
|
||||
if entry is None:
|
||||
return _unknown_connection(connection, registry)
|
||||
try:
|
||||
tools = await entry.ensure_catalog()
|
||||
except McpConnectionUnavailableError as exc:
|
||||
return str(exc)
|
||||
terms = [term for term in query.lower().split() if term]
|
||||
bounded_limit = min(max(limit, 1), 20)
|
||||
ranked = [
|
||||
(score, tool)
|
||||
for tool in tools
|
||||
if (score := _search_score(tool, terms, tool.name in entry.active_tools))[0] >= 0
|
||||
]
|
||||
if not ranked:
|
||||
ranked = [
|
||||
((100, tool.name.lower()), tool) for tool in tools if tool.name in entry.active_tools
|
||||
]
|
||||
ranked.sort(key=lambda item: (-item[0][0], item[0][1]))
|
||||
return {
|
||||
"connection": connection,
|
||||
"query": query,
|
||||
"matches": [
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": (tool.description or "").strip() or None,
|
||||
"active": tool.name in entry.active_tools,
|
||||
}
|
||||
for _, tool in ranked[:bounded_limit]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@function_tool(timeout=60)
|
||||
async def get_mcp_tool_schema(
|
||||
ctx: RunContextWrapper,
|
||||
connection: str,
|
||||
tool: str,
|
||||
) -> dict[str, Any] | str:
|
||||
"""Return one MCP tool's exact input schema.
|
||||
|
||||
Args:
|
||||
connection: The connection name exactly as reported by ``list_mcps``.
|
||||
tool: One exact tool name returned by ``search_mcp_tools``.
|
||||
"""
|
||||
registry = _registry_from_ctx(ctx)
|
||||
if registry is None or not registry:
|
||||
return _NO_CONNECTIONS
|
||||
entry = registry.get(connection)
|
||||
if entry is None:
|
||||
return _unknown_connection(connection, registry)
|
||||
try:
|
||||
tools = await entry.ensure_catalog()
|
||||
except McpConnectionUnavailableError as exc:
|
||||
return str(exc)
|
||||
match = next((candidate for candidate in tools if candidate.name == tool), None)
|
||||
if match is None:
|
||||
return (
|
||||
f"Unknown tool {tool!r} on MCP connection {connection!r}. "
|
||||
"Call search_mcp_tools to find an available tool."
|
||||
)
|
||||
return {
|
||||
"connection": connection,
|
||||
"name": match.name,
|
||||
"description": (match.description or "").strip() or None,
|
||||
"input_schema": match.inputSchema or {"type": "object"},
|
||||
}
|
||||
|
||||
|
||||
@function_tool(timeout=120, strict_mode=False)
|
||||
async def call_mcp(
|
||||
ctx: RunContextWrapper,
|
||||
@@ -242,18 +125,19 @@ async def call_mcp(
|
||||
) -> Any:
|
||||
"""Call one tool on one MCP connection and return its result.
|
||||
|
||||
Use the connection id from ``list_mcps``. Use the tool name from
|
||||
``search_mcp_tools``. Pass an object that matches the schema from
|
||||
``get_mcp_tool_schema``. Omit the arguments for a tool that takes no
|
||||
arguments.
|
||||
Address the tool by the connection id from ``list_mcps`` and the tool name
|
||||
from ``describe_mcp`` on that connection. Pass the tool's arguments as an
|
||||
object matching the input schema ``describe_mcp`` showed for it (omit it, or
|
||||
pass an empty object, for a tool that takes no arguments).
|
||||
|
||||
Args:
|
||||
connection: The connection name exactly as reported by ``list_mcps``.
|
||||
tool: The tool name exactly as reported by ``search_mcp_tools``.
|
||||
tool: The tool name, exactly as reported by ``describe_mcp``.
|
||||
arguments: The tool's arguments as a JSON object of names to values (for
|
||||
example ``{"path": "app.py"}``), or omitted/empty for a tool that
|
||||
takes none. Pass an object, not a stringified object. Match the shape
|
||||
that ``get_mcp_tool_schema`` returned.
|
||||
takes none. Pass an object, not a stringified one. Its shape is
|
||||
whatever ``describe_mcp`` showed for the tool rather than a shape this
|
||||
tool fixes in advance.
|
||||
"""
|
||||
registry = _registry_from_ctx(ctx)
|
||||
if registry is None or not registry:
|
||||
@@ -263,7 +147,7 @@ async def call_mcp(
|
||||
return _unknown_connection(connection, registry)
|
||||
invalid_arguments = (
|
||||
f"Invalid arguments for {connection!r}.{tool}: expected a JSON object of "
|
||||
"argument names to values, or none. Call get_mcp_tool_schema for the schema."
|
||||
"argument names to values, or none. Call describe_mcp for the input schema."
|
||||
)
|
||||
if isinstance(arguments, str):
|
||||
# The ``arguments`` parameter is schema-less (an open object is not
|
||||
@@ -278,17 +162,18 @@ async def call_mcp(
|
||||
if arguments is not None and not isinstance(arguments, dict):
|
||||
return invalid_arguments
|
||||
try:
|
||||
available = await entry.ensure_catalog()
|
||||
available = await entry.session.list_tools()
|
||||
except McpConnectionUnavailableError as exc:
|
||||
return _errored_tool_output(str(exc))
|
||||
valid_names = {mcp_tool.name for mcp_tool in available}
|
||||
if tool not in valid_names:
|
||||
offered = ", ".join(sorted(valid_names)) or "(none)"
|
||||
return (
|
||||
f"Unknown tool {tool!r} on MCP connection {connection!r}. "
|
||||
"Call search_mcp_tools, then get_mcp_tool_schema."
|
||||
f"Tools this connection offers: {offered}. "
|
||||
"Call describe_mcp for their input schemas."
|
||||
)
|
||||
session = await entry.ensure_connected()
|
||||
return await session.dispatch(
|
||||
return await entry.session.dispatch(
|
||||
tool,
|
||||
arguments or {},
|
||||
label=namespaced_tool_name(connection, tool),
|
||||
|
||||
+24
-29
@@ -14,7 +14,6 @@ fails the run.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
@@ -282,10 +281,8 @@ async def _count_session_tools(config: McpConnectionConfig, session: SupervisedM
|
||||
|
||||
async def connect_mcp_servers(
|
||||
configs: list[McpConnectionConfig],
|
||||
*,
|
||||
max_concurrency: int = 6,
|
||||
) -> list[ConnectedMcpServer]:
|
||||
"""Connect MCP configs concurrently under a fixed bound.
|
||||
"""Connect each MCP config on its own supervising task and return the sessions.
|
||||
|
||||
Each connection becomes a :class:`~strix.tools.mcp.session.SupervisedMcpSession`
|
||||
that owns ``connect()``, the held-open session, and ``cleanup()`` on one
|
||||
@@ -304,43 +301,41 @@ async def connect_mcp_servers(
|
||||
:class:`~strix.tools.mcp.registry.McpRegistry` from these sessions, and the
|
||||
agent reaches each tool on demand through ``describe_mcp`` / ``call_mcp``.
|
||||
"""
|
||||
semaphore = asyncio.Semaphore(max(1, max_concurrency))
|
||||
connected: list[ConnectedMcpServer] = []
|
||||
sessions: list[SupervisedMcpSession] = []
|
||||
|
||||
async def connect_one(config: McpConnectionConfig) -> ConnectedMcpServer | None:
|
||||
async with semaphore:
|
||||
try:
|
||||
for config in configs:
|
||||
session = SupervisedMcpSession(config)
|
||||
sessions.append(session)
|
||||
if not await session.start():
|
||||
# Initial connect failed; already logged inside the session. Drop it.
|
||||
await session.aclose()
|
||||
sessions.remove(session)
|
||||
continue
|
||||
try:
|
||||
if not await session.start():
|
||||
await session.aclose()
|
||||
return None
|
||||
tool_count = await _count_session_tools(config, session)
|
||||
except McpConnectionUnavailableError:
|
||||
# The session died between connecting and its first listing; skip it.
|
||||
logger.warning("MCP connection %r died before its first listing", config.name)
|
||||
await session.aclose()
|
||||
return None
|
||||
except BaseException:
|
||||
with contextlib.suppress(BaseException):
|
||||
await session.aclose()
|
||||
raise
|
||||
sessions.remove(session)
|
||||
continue
|
||||
logger.info("Connected MCP server %r (%d tools)", config.name, tool_count)
|
||||
return ConnectedMcpServer(
|
||||
session=session,
|
||||
name=config.name,
|
||||
tool_count=tool_count,
|
||||
notes=config.notes,
|
||||
connected.append(
|
||||
ConnectedMcpServer(
|
||||
session=session, name=config.name, tool_count=tool_count, notes=config.notes
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
results = await asyncio.gather(*(connect_one(config) for config in configs))
|
||||
except BaseException:
|
||||
await asyncio.gather(
|
||||
*(session.aclose() for session in sessions),
|
||||
return_exceptions=True,
|
||||
)
|
||||
# Cancelled or errored mid-attach: close every session started so far,
|
||||
# each on its own task, then re-raise. The runner only receives the list
|
||||
# on a clean return, so on an abnormal exit this function owns the cleanup.
|
||||
for session in sessions:
|
||||
with contextlib.suppress(BaseException):
|
||||
await session.aclose()
|
||||
raise
|
||||
return [result for result in results if result is not None]
|
||||
|
||||
return connected
|
||||
|
||||
|
||||
async def attach_mcp_requests(
|
||||
|
||||
@@ -62,9 +62,6 @@ class McpConnectionConfig(BaseModel):
|
||||
"""Tool allowlist, applied after the server lists its tools. ``None`` (the
|
||||
default) exposes every tool the server lists; a list restricts to it."""
|
||||
|
||||
active_tools: list[str] = Field(default_factory=list)
|
||||
"""Small scan-relevant subset ranked ahead of the broader allowed catalog."""
|
||||
|
||||
notes: str | None = None
|
||||
"""Free-text notes for the agent describing what this connection is and how
|
||||
to use it. When set, the note becomes the connection's purpose line in the
|
||||
|
||||
+28
-246
@@ -22,18 +22,13 @@ single dispatch point.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Literal, NamedTuple
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from strix.tools.mcp.session import McpConnectionUnavailableError, SupervisedMcpSession
|
||||
from strix.tools.mcp.session import SupervisedMcpSession
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from agents.mcp import MCPServer
|
||||
|
||||
from strix.tools.mcp.client import ResultTransform
|
||||
@@ -54,60 +49,37 @@ MCP_REGISTRY_CONTEXT_KEY = "mcp_registry"
|
||||
# tracer all recognise a connection-scoped dispatch call by the same names.
|
||||
CALL_MCP_TOOL = "call_mcp"
|
||||
DESCRIBE_MCP_TOOL = "describe_mcp"
|
||||
SEARCH_MCP_TOOLS_TOOL = "search_mcp_tools"
|
||||
GET_MCP_TOOL_SCHEMA_TOOL = "get_mcp_tool_schema"
|
||||
MCP_DISPATCH_TOOLS = frozenset(
|
||||
{
|
||||
CALL_MCP_TOOL,
|
||||
DESCRIBE_MCP_TOOL,
|
||||
SEARCH_MCP_TOOLS_TOOL,
|
||||
GET_MCP_TOOL_SCHEMA_TOOL,
|
||||
}
|
||||
)
|
||||
|
||||
McpConnectionState = Literal[
|
||||
"configured",
|
||||
"connecting",
|
||||
"connected",
|
||||
"catalog_loading",
|
||||
"catalog_ready",
|
||||
"unavailable",
|
||||
]
|
||||
_RETRY_DELAY_SECONDS = 5.0
|
||||
MCP_DISPATCH_TOOLS = frozenset({CALL_MCP_TOOL, DESCRIBE_MCP_TOOL})
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class McpConnectionEntry:
|
||||
"""One configured MCP connection a scan may reach, keyed by ``name``.
|
||||
"""One live MCP connection a scan may reach, keyed by ``name``.
|
||||
|
||||
Registration is inert. The first warm-up, search, schema lookup, or call
|
||||
creates one shared connection task; the first catalog operation creates one
|
||||
shared listing task. Root and child agents therefore reuse the same session
|
||||
and catalog even when they request a cold connection concurrently.
|
||||
``session`` is the :class:`~strix.tools.mcp.session.SupervisedMcpSession` that
|
||||
owns the connection on its own task; the dispatch tools list tools and call
|
||||
tools through it (``session.list_tools`` / ``session.dispatch``) so a session
|
||||
failure is contained and can reconnect. ``purpose`` is the human label
|
||||
``list_mcps`` reports as the connection's description (the user's connection
|
||||
notes, or whatever the caller supplies). ``tool_count`` is how many tools the
|
||||
connection offers, also reported by ``list_mcps``. ``result_transform``, when
|
||||
set, runs on each call's structured result at the single dispatch point
|
||||
(strix-pro's sanitizer uses it). ``provider`` is an optional source label
|
||||
(e.g. ``"supabase"``) the caller tags the connection with; the command-line
|
||||
path leaves it ``None``, and event tagging surfaces it when set.
|
||||
|
||||
The connection config the session reconnects with (and its bearer token) lives
|
||||
on ``session`` in memory only. It is reached via :attr:`config` for the
|
||||
reconnect path and is never logged, serialized into the event stream, or
|
||||
written to disk.
|
||||
"""
|
||||
|
||||
session: SupervisedMcpSession
|
||||
name: str
|
||||
connection_config: McpConnectionConfig | None = dataclasses.field(
|
||||
default=None,
|
||||
repr=False,
|
||||
)
|
||||
session: SupervisedMcpSession | None = dataclasses.field(default=None, repr=False)
|
||||
purpose: str | None = None
|
||||
tool_count: int = 0
|
||||
result_transform: ResultTransform | None = None
|
||||
provider: str | None = None
|
||||
state: McpConnectionState = "configured"
|
||||
_catalog: list[Any] | None = dataclasses.field(default=None, repr=False)
|
||||
_connect_task: asyncio.Task[SupervisedMcpSession] | None = dataclasses.field(
|
||||
default=None,
|
||||
repr=False,
|
||||
)
|
||||
_catalog_task: asyncio.Task[list[Any]] | None = dataclasses.field(
|
||||
default=None,
|
||||
repr=False,
|
||||
)
|
||||
_retry_after: float = dataclasses.field(default=0.0, repr=False)
|
||||
_status_sink: Callable[[], None] | None = dataclasses.field(default=None, repr=False)
|
||||
|
||||
@property
|
||||
def server(self) -> MCPServer | None:
|
||||
@@ -116,143 +88,12 @@ class McpConnectionEntry:
|
||||
Kept so existing callers that read ``entry.server`` keep working; new code
|
||||
should call through ``entry.session`` so reconnect and containment apply.
|
||||
"""
|
||||
return self.session.server if self.session is not None else None
|
||||
return self.session.server
|
||||
|
||||
@property
|
||||
def config(self) -> McpConnectionConfig | None:
|
||||
"""The reconnect config. Carries the bearer token; never log it."""
|
||||
if self.connection_config is not None:
|
||||
return self.connection_config
|
||||
return self.session.config if self.session is not None else None
|
||||
|
||||
@property
|
||||
def active_tools(self) -> frozenset[str]:
|
||||
config = self.config
|
||||
return frozenset(config.active_tools if config is not None else ())
|
||||
|
||||
def set_status_sink(self, sink: Callable[[], None] | None) -> None:
|
||||
self._status_sink = sink
|
||||
if self.session is not None:
|
||||
self.session.set_on_dead(self._on_dead)
|
||||
|
||||
def _set_state(self, state: McpConnectionState) -> None:
|
||||
if self.state == state:
|
||||
return
|
||||
self.state = state
|
||||
if self._status_sink is not None:
|
||||
self._status_sink()
|
||||
|
||||
def _on_dead(self) -> None:
|
||||
self.session = None
|
||||
self._catalog = None
|
||||
self._catalog_task = None
|
||||
self._retry_after = time.monotonic() + _RETRY_DELAY_SECONDS
|
||||
self._set_state("unavailable")
|
||||
|
||||
async def ensure_connected(self) -> SupervisedMcpSession:
|
||||
"""Return this entry's live session, connecting it once when needed."""
|
||||
if self.session is not None:
|
||||
return self.session
|
||||
if self.connection_config is None:
|
||||
raise McpConnectionUnavailableError(
|
||||
f"MCP connection {self.name!r} is unavailable and cannot reconnect."
|
||||
)
|
||||
if self._connect_task is None:
|
||||
if self.state == "unavailable" and time.monotonic() < self._retry_after:
|
||||
raise McpConnectionUnavailableError(
|
||||
f"MCP connection {self.name!r} is temporarily unavailable."
|
||||
)
|
||||
self._connect_task = asyncio.create_task(
|
||||
self._connect(),
|
||||
name=f"mcp-connect-{self.name}",
|
||||
)
|
||||
task = self._connect_task
|
||||
try:
|
||||
return await asyncio.shield(task)
|
||||
finally:
|
||||
if self._connect_task is task and task.done():
|
||||
self._connect_task = None
|
||||
|
||||
async def _connect(self) -> SupervisedMcpSession:
|
||||
config = self.connection_config
|
||||
if config is None:
|
||||
raise McpConnectionUnavailableError(
|
||||
f"MCP connection {self.name!r} has no connection configuration."
|
||||
)
|
||||
self._set_state("connecting")
|
||||
session = SupervisedMcpSession(config)
|
||||
try:
|
||||
started = await session.start()
|
||||
except BaseException:
|
||||
with contextlib.suppress(BaseException):
|
||||
await session.aclose()
|
||||
self._retry_after = time.monotonic() + _RETRY_DELAY_SECONDS
|
||||
self._set_state("unavailable")
|
||||
raise
|
||||
if not started:
|
||||
await session.aclose()
|
||||
self._retry_after = time.monotonic() + _RETRY_DELAY_SECONDS
|
||||
self._set_state("unavailable")
|
||||
raise McpConnectionUnavailableError(f"MCP connection {self.name!r} could not connect.")
|
||||
self.session = session
|
||||
session.set_on_dead(self._on_dead)
|
||||
self._retry_after = 0.0
|
||||
self._set_state("connected")
|
||||
return session
|
||||
|
||||
async def ensure_catalog(self) -> list[Any]:
|
||||
"""Return the filtered catalog, listing it once on first use."""
|
||||
if (
|
||||
self._catalog is not None
|
||||
and self.session is not None
|
||||
and not self.session.is_dead
|
||||
and not self.session.is_unavailable
|
||||
):
|
||||
return self._catalog
|
||||
if self.session is not None and (self.session.is_dead or self.session.is_unavailable):
|
||||
self._catalog = None
|
||||
if self._catalog_task is None:
|
||||
self._catalog_task = asyncio.create_task(
|
||||
self._load_catalog(),
|
||||
name=f"mcp-catalog-{self.name}",
|
||||
)
|
||||
task = self._catalog_task
|
||||
try:
|
||||
return await asyncio.shield(task)
|
||||
finally:
|
||||
if self._catalog_task is task and task.done():
|
||||
self._catalog_task = None
|
||||
|
||||
async def _load_catalog(self) -> list[Any]:
|
||||
session = await self.ensure_connected()
|
||||
self._set_state("catalog_loading")
|
||||
try:
|
||||
catalog = await session.list_tools()
|
||||
except BaseException:
|
||||
if session.is_dead:
|
||||
self._set_state("unavailable")
|
||||
else:
|
||||
self._set_state("connected")
|
||||
raise
|
||||
self._catalog = list(catalog)
|
||||
self.tool_count = len(self._catalog)
|
||||
self._set_state("catalog_ready")
|
||||
return self._catalog
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Cancel pending initialization and close an opened session."""
|
||||
tasks = [task for task in (self._catalog_task, self._connect_task) if task is not None]
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
for task in tasks:
|
||||
with contextlib.suppress(BaseException):
|
||||
await task
|
||||
self._catalog_task = None
|
||||
self._connect_task = None
|
||||
if self.session is not None:
|
||||
with contextlib.suppress(BaseException):
|
||||
await self.session.aclose()
|
||||
self.session = None
|
||||
"""The session's reconnect config. Carries the bearer token; never log it."""
|
||||
return self.session.config
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -264,7 +105,6 @@ class McpConnectionSummary:
|
||||
purpose: str | None
|
||||
tool_count: int
|
||||
provider: str | None = None
|
||||
state: McpConnectionState = "configured"
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -283,7 +123,6 @@ class McpConnectionStatus:
|
||||
provider: str | None
|
||||
tool_count: int
|
||||
dead: bool
|
||||
state: McpConnectionState
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -326,21 +165,6 @@ class McpRegistry:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._entries: dict[str, McpConnectionEntry] = {}
|
||||
self._status_sink: Callable[[], None] | None = None
|
||||
self._warmup_task: asyncio.Task[None] | None = None
|
||||
|
||||
def register(self, request: McpConnectionRequest) -> McpConnectionEntry:
|
||||
"""Register an inert request without opening a network connection."""
|
||||
entry = McpConnectionEntry(
|
||||
name=request.config.name,
|
||||
connection_config=request.config,
|
||||
purpose=request.purpose or request.config.notes,
|
||||
result_transform=request.result_transform,
|
||||
provider=request.provider,
|
||||
)
|
||||
entry.set_status_sink(self._status_sink)
|
||||
self._entries[entry.name] = entry
|
||||
return entry
|
||||
|
||||
def add(
|
||||
self,
|
||||
@@ -367,16 +191,13 @@ class McpRegistry:
|
||||
raise ValueError("McpRegistry.add requires either 'session' or 'server'")
|
||||
session = SupervisedMcpSession.adopt(server, name=name, config=config)
|
||||
entry = McpConnectionEntry(
|
||||
name=name,
|
||||
connection_config=config or session.config,
|
||||
session=session,
|
||||
name=name,
|
||||
purpose=purpose,
|
||||
tool_count=tool_count,
|
||||
result_transform=result_transform,
|
||||
provider=provider,
|
||||
state="connected",
|
||||
)
|
||||
entry.set_status_sink(self._status_sink)
|
||||
self._entries[name] = entry
|
||||
return entry
|
||||
|
||||
@@ -396,7 +217,6 @@ class McpRegistry:
|
||||
purpose=entry.purpose,
|
||||
tool_count=entry.tool_count,
|
||||
provider=entry.provider,
|
||||
state=entry.state,
|
||||
)
|
||||
for entry in self._entries.values()
|
||||
]
|
||||
@@ -413,9 +233,7 @@ class McpRegistry:
|
||||
name=entry.name,
|
||||
provider=entry.provider,
|
||||
tool_count=entry.tool_count,
|
||||
dead=entry.state == "unavailable"
|
||||
or (entry.session is not None and entry.session.is_dead),
|
||||
state=entry.state,
|
||||
dead=entry.session.is_dead,
|
||||
)
|
||||
for entry in self._entries.values()
|
||||
]
|
||||
@@ -425,42 +243,6 @@ class McpRegistry:
|
||||
runner)."""
|
||||
self._entries.clear()
|
||||
|
||||
def set_status_sink(self, sink: Callable[[], None] | None) -> None:
|
||||
"""Receive a callback after any connection lifecycle transition."""
|
||||
self._status_sink = sink
|
||||
for entry in self._entries.values():
|
||||
entry.set_status_sink(sink)
|
||||
|
||||
def start_warmup(self, *, max_concurrency: int = 6) -> asyncio.Task[None]:
|
||||
"""Connect every configured entry in the background with a fixed bound."""
|
||||
if self._warmup_task is not None:
|
||||
return self._warmup_task
|
||||
|
||||
async def warm() -> None:
|
||||
semaphore = asyncio.Semaphore(max(1, max_concurrency))
|
||||
|
||||
async def connect(entry: McpConnectionEntry) -> None:
|
||||
async with semaphore:
|
||||
with contextlib.suppress(McpConnectionUnavailableError):
|
||||
await entry.ensure_connected()
|
||||
|
||||
await asyncio.gather(*(connect(entry) for entry in self._entries.values()))
|
||||
|
||||
self._warmup_task = asyncio.create_task(warm(), name="mcp-warmup")
|
||||
return self._warmup_task
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Stop warm-up and close only sessions this registry opened."""
|
||||
if self._warmup_task is not None:
|
||||
self._warmup_task.cancel()
|
||||
with contextlib.suppress(BaseException):
|
||||
await self._warmup_task
|
||||
self._warmup_task = None
|
||||
await asyncio.gather(
|
||||
*(entry.close() for entry in self._entries.values()),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._entries)
|
||||
|
||||
@@ -500,6 +282,6 @@ def resolve_mcp_call(
|
||||
if entry is None:
|
||||
return None
|
||||
provider = entry.provider
|
||||
raw_tool = args.get("tool") if tool_name in {CALL_MCP_TOOL, GET_MCP_TOOL_SCHEMA_TOOL} else ""
|
||||
raw_tool = args.get("tool") if tool_name == CALL_MCP_TOOL else ""
|
||||
tool = raw_tool if isinstance(raw_tool, str) else ""
|
||||
return McpCallInfo(connection=connection, tool=tool, provider=provider)
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -68,31 +67,6 @@ async def _call[T](client: Client, fn: Callable[[Client], Awaitable[T]]) -> T:
|
||||
return await fn(client)
|
||||
|
||||
|
||||
async def existing_request_ids(
|
||||
ctx: RunContextWrapper,
|
||||
request_ids: list[str],
|
||||
) -> set[str]:
|
||||
"""Return request IDs that exist in the current Caido project."""
|
||||
if not request_ids:
|
||||
return set()
|
||||
|
||||
client = await _ctx_client(ctx)
|
||||
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.
|
||||
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))
|
||||
return existing
|
||||
|
||||
|
||||
def _to_tool_json(value: Any) -> Any:
|
||||
"""Recursively convert SDK dataclasses/Pydantic objects to tool JSON values."""
|
||||
if value is None or isinstance(value, str | int | float | bool):
|
||||
|
||||
+68
-487
@@ -11,19 +11,15 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from pathlib import PurePosixPath
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents import RunContextWrapper, function_tool
|
||||
|
||||
from strix.tools.nullish import clean_optional
|
||||
from strix.tools.proxy.tools import existing_request_ids
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
from strix.fix.contracts import SourceIdentity
|
||||
from strix.report.state import ReportState
|
||||
|
||||
|
||||
@@ -172,8 +168,6 @@ _REQUIRED_FIELDS = {
|
||||
|
||||
_VALID_FIX_EFFORT = frozenset({"trivial", "low", "medium", "high"})
|
||||
_VALID_CONFIDENCE = frozenset({"high", "medium", "low"})
|
||||
_MAX_HTTP_EXCHANGE_IDS = 10
|
||||
_MAX_HTTP_EXCHANGE_ID_CHARS = 128
|
||||
|
||||
|
||||
def _validate_required_text(fields: dict[str, str]) -> list[str]:
|
||||
@@ -183,97 +177,6 @@ def _validate_required_text(fields: dict[str, str]) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def _normalize_http_exchange_ids(raw: Any) -> tuple[list[str] | None, list[str]]:
|
||||
"""Return distinct proxy exchange ids in their original order."""
|
||||
if raw is None:
|
||||
return None, []
|
||||
if not isinstance(raw, list):
|
||||
return None, ["http_exchange_ids must be a list of proxy request ids"]
|
||||
|
||||
normalized: list[str] = []
|
||||
errors: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for index, value in enumerate(raw):
|
||||
if not isinstance(value, str):
|
||||
errors.append(f"http_exchange_ids[{index}] must be a string")
|
||||
continue
|
||||
request_id = value.strip()
|
||||
if not request_id:
|
||||
errors.append(f"http_exchange_ids[{index}] cannot be empty")
|
||||
continue
|
||||
if len(request_id) > _MAX_HTTP_EXCHANGE_ID_CHARS:
|
||||
errors.append(
|
||||
f"http_exchange_ids[{index}] must be {_MAX_HTTP_EXCHANGE_ID_CHARS} "
|
||||
"characters or fewer"
|
||||
)
|
||||
continue
|
||||
if any(ord(char) < 0x21 or ord(char) > 0x7E for char in request_id):
|
||||
errors.append(f"http_exchange_ids[{index}] must contain only visible ASCII characters")
|
||||
continue
|
||||
if not request_id.isdigit():
|
||||
errors.append(f"http_exchange_ids[{index}] must be a numeric proxy request id")
|
||||
continue
|
||||
if request_id not in seen:
|
||||
seen.add(request_id)
|
||||
normalized.append(request_id)
|
||||
if len(normalized) > _MAX_HTTP_EXCHANGE_IDS:
|
||||
errors.append(
|
||||
f"http_exchange_ids can contain at most "
|
||||
f"{_MAX_HTTP_EXCHANGE_IDS} distinct request ids"
|
||||
)
|
||||
break
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
async def _verify_http_exchange_ids(
|
||||
ctx: RunContextWrapper,
|
||||
raw: Any,
|
||||
) -> tuple[list[str] | None, list[str], str | None]:
|
||||
"""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.
|
||||
"""
|
||||
request_ids, errors = _normalize_http_exchange_ids(raw)
|
||||
if request_ids is None or errors or not request_ids:
|
||||
return request_ids, errors, None
|
||||
|
||||
try:
|
||||
existing_ids = await existing_request_ids(ctx, request_ids)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning(
|
||||
"Could not verify HTTP exchange IDs against the current Caido project",
|
||||
exc_info=True,
|
||||
)
|
||||
return None, [], _HTTP_EXCHANGE_DROPPED_WARNING
|
||||
|
||||
missing_ids = [request_id for request_id in request_ids if request_id not in existing_ids]
|
||||
if missing_ids:
|
||||
return (
|
||||
None,
|
||||
[
|
||||
"http_exchange_ids do not exist in the current proxy project: "
|
||||
+ ", ".join(missing_ids)
|
||||
],
|
||||
None,
|
||||
)
|
||||
return request_ids, [], None
|
||||
|
||||
|
||||
def _with_warning(result: dict[str, Any], warning: str | None) -> dict[str, Any]:
|
||||
if warning and result.get("success"):
|
||||
result["warning"] = warning
|
||||
return result
|
||||
|
||||
|
||||
def _validate_cvss_breakdown(breakdown: Any) -> list[str]:
|
||||
"""Check the 8 CVSS metrics are all present with legal values."""
|
||||
if not isinstance(breakdown, dict) or not breakdown:
|
||||
@@ -345,11 +248,16 @@ def _validate_fix_verification(
|
||||
if str(fix_verification or "").strip():
|
||||
return []
|
||||
return [
|
||||
"fix_verification is REQUIRED when any code_location carries a 'fix_after'. "
|
||||
"Describe the checks you performed on this draft candidate. Separate executed "
|
||||
"checks from reasoned checks and list every gap. This statement does not make "
|
||||
"the candidate ready for automatic application. If the candidate fails a check, "
|
||||
"revise it or remove 'fix_after'."
|
||||
"fix_verification is REQUIRED when any code_location carries a 'fix_after' - "
|
||||
"a suggestion a reviewer can click to apply must be verified first. State, in "
|
||||
"order: (1) security closure - re-trace the source->sink path through the "
|
||||
"PATCHED code and say why it is now blocked; (2) bypass review - re-read the "
|
||||
"diff without your original rationale and name the equivalent sinks, sibling "
|
||||
"call sites, and alternate malicious input classes you checked; (3) preserved "
|
||||
"behavior - the legitimate inputs, APIs, and error semantics that still work; "
|
||||
"(4) how each was checked (executed vs. reasoned), naming any unrun check as "
|
||||
"an explicit gap. If you cannot make these statements, drop 'fix_after' and "
|
||||
"leave the location informational."
|
||||
]
|
||||
|
||||
|
||||
@@ -369,110 +277,6 @@ def _finding_class_of(report: dict[str, Any]) -> str:
|
||||
return "dynamic"
|
||||
|
||||
|
||||
def _fix_source_context(
|
||||
report_state: ReportState,
|
||||
) -> tuple[SourceIdentity | None, Path | None]:
|
||||
from strix.fix.contracts import SourceIdentity, SourceIdentityKind
|
||||
|
||||
raw_context = report_state.get_repository_context()
|
||||
if raw_context is None:
|
||||
return None, None
|
||||
context = cast("dict[str, object]", raw_context)
|
||||
commit = context.get("commitSha")
|
||||
if not isinstance(commit, str) or not commit:
|
||||
return None, None
|
||||
raw_targets = cast("object", report_state.run_record.get("targets_info"))
|
||||
targets = cast("list[object]", raw_targets) if isinstance(raw_targets, list) else []
|
||||
if len(targets) != 1:
|
||||
return SourceIdentity(kind=SourceIdentityKind.COMMIT, value=commit), None
|
||||
raw_target = targets[0]
|
||||
target = cast("dict[str, object]", raw_target) if isinstance(raw_target, dict) else {}
|
||||
raw_details = target.get("details")
|
||||
details = cast("dict[str, object]", raw_details) if isinstance(raw_details, dict) else {}
|
||||
repo_path = details.get("cloned_repo_path")
|
||||
repository = context.get("repositoryUri")
|
||||
return (
|
||||
SourceIdentity(
|
||||
kind=SourceIdentityKind.COMMIT,
|
||||
value=commit,
|
||||
repository=repository if isinstance(repository, str) else None,
|
||||
),
|
||||
Path(repo_path) if isinstance(repo_path, str) and repo_path else None,
|
||||
)
|
||||
|
||||
|
||||
def _build_fix_candidate(
|
||||
report_state: ReportState,
|
||||
report_fields: Mapping[str, object],
|
||||
) -> dict[str, object] | None:
|
||||
from strix.fix.contracts import candidate_from_legacy_report
|
||||
from strix.fix.locations import AnchorStatus, anchor_candidate
|
||||
|
||||
source_identity, repo_path = _fix_source_context(report_state)
|
||||
candidate = candidate_from_legacy_report(report_fields, source_identity=source_identity)
|
||||
if candidate is None:
|
||||
return None
|
||||
if repo_path is not None:
|
||||
try:
|
||||
anchored, results = anchor_candidate(repo_path, candidate)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Anchoring must never block the finding from being stored.
|
||||
candidate = candidate.model_copy(
|
||||
update={"known_gaps": [*candidate.known_gaps, f"Candidate anchoring failed: {exc}"]}
|
||||
)
|
||||
else:
|
||||
candidate = anchored
|
||||
gaps = [
|
||||
f"{result.location.file}: {result.status}"
|
||||
for result in results
|
||||
if result.status is not AnchorStatus.UNIQUE
|
||||
]
|
||||
if gaps:
|
||||
candidate = candidate.model_copy(
|
||||
update={"known_gaps": [*candidate.known_gaps, *gaps]}
|
||||
)
|
||||
return cast("dict[str, object]", candidate.model_dump(mode="json"))
|
||||
|
||||
|
||||
_FIX_CANDIDATE_FIELDS = frozenset(
|
||||
{
|
||||
"code_locations",
|
||||
"remediation_steps",
|
||||
"technical_analysis",
|
||||
"poc_description",
|
||||
"evidence",
|
||||
"fix_verification",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _refresh_fix_candidate(
|
||||
report_state: ReportState,
|
||||
report_id: str,
|
||||
changes: dict[str, Any],
|
||||
) -> None:
|
||||
if not _FIX_CANDIDATE_FIELDS.intersection(changes):
|
||||
return
|
||||
existing = next(
|
||||
(
|
||||
report
|
||||
for report in report_state.get_existing_vulnerabilities()
|
||||
if report.get("id") == report_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing is None:
|
||||
return
|
||||
candidate = _build_fix_candidate(report_state, {**existing, **changes})
|
||||
if candidate is not None:
|
||||
changes["fix_candidate"] = candidate
|
||||
if candidate is not None or existing.get("fix_candidate") or existing.get("fix_preparation"):
|
||||
changes["fix_preparation"] = {
|
||||
"state": "stale",
|
||||
"stop_reason": "The finding or draft candidate changed after preparation.",
|
||||
}
|
||||
|
||||
|
||||
_UPDATE_TEXT_FIELDS = (
|
||||
"title",
|
||||
"description",
|
||||
@@ -495,7 +299,7 @@ _UPDATE_TEXT_FIELDS = (
|
||||
)
|
||||
|
||||
|
||||
def _collect_update_changes( # noqa: PLR0912, PLR0915
|
||||
def _collect_update_changes( # noqa: PLR0912
|
||||
fields: dict[str, Any],
|
||||
) -> tuple[dict[str, Any], list[str]]:
|
||||
"""Validate the fields a revision replaces and return them with any errors."""
|
||||
@@ -564,12 +368,6 @@ def _collect_update_changes( # noqa: PLR0912, PLR0915
|
||||
if cwe:
|
||||
changes["cwe"] = cwe
|
||||
|
||||
raw_http_exchange_ids = fields.get("http_exchange_ids")
|
||||
http_exchange_ids, http_exchange_errors = _normalize_http_exchange_ids(raw_http_exchange_ids)
|
||||
errors.extend(http_exchange_errors)
|
||||
if raw_http_exchange_ids is not None and not http_exchange_errors:
|
||||
changes["http_exchange_ids"] = http_exchange_ids or []
|
||||
|
||||
return changes, errors
|
||||
|
||||
|
||||
@@ -580,7 +378,6 @@ _DYNAMIC_ONLY_UPDATE_FIELDS = (
|
||||
"method",
|
||||
"poc_description",
|
||||
"poc_script_code",
|
||||
"http_exchange_ids",
|
||||
)
|
||||
|
||||
# A dependency finding is rated in the context of the codebase that pins it, and
|
||||
@@ -753,26 +550,14 @@ def _do_update(
|
||||
class_error = _fit_revision_to_class(report_state, report_id, changes)
|
||||
if class_error is not None:
|
||||
return class_error
|
||||
_refresh_fix_candidate(report_state, report_id, changes)
|
||||
|
||||
try:
|
||||
updated = report_state.update_vulnerability_report(
|
||||
report_id,
|
||||
changes,
|
||||
update_reason=update_reason,
|
||||
updated_by_agent_id=agent_id,
|
||||
updated_by_agent_name=agent_name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("update_vulnerability_report persistence failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"Failed to revise report '{report_id}': {e!s}. "
|
||||
"The report still carries its previous content; retry the update."
|
||||
),
|
||||
"report_id": report_id,
|
||||
}
|
||||
updated = report_state.update_vulnerability_report(
|
||||
report_id,
|
||||
changes,
|
||||
update_reason=update_reason,
|
||||
updated_by_agent_id=agent_id,
|
||||
updated_by_agent_name=agent_name,
|
||||
)
|
||||
if updated is None:
|
||||
known = [r.get("id") for r in report_state.get_existing_vulnerabilities()]
|
||||
if report_id not in known:
|
||||
@@ -799,87 +584,6 @@ def _do_update(
|
||||
}
|
||||
|
||||
|
||||
def _do_delete(
|
||||
*,
|
||||
report_id: str,
|
||||
delete_reason: str,
|
||||
agent_id: str | None = None,
|
||||
agent_name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Withdraw a report an agent filed and has since disproved.
|
||||
|
||||
Deletion is for a finding that is not a vulnerability at all. A finding
|
||||
that is real but weaker than filed is revised, not deleted.
|
||||
"""
|
||||
report_id = (report_id or "").strip()
|
||||
delete_reason = (delete_reason or "").strip()
|
||||
if not report_id or not delete_reason:
|
||||
missing = "report_id" if not report_id else "delete_reason"
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"{missing} cannot be empty - name the report you are withdrawing and state "
|
||||
"what disproved it"
|
||||
),
|
||||
}
|
||||
|
||||
from strix.report.state import ReportRollbackError, get_global_report_state
|
||||
|
||||
report_state = get_global_report_state()
|
||||
if report_state is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Report state unavailable - no reports have been filed yet",
|
||||
}
|
||||
|
||||
try:
|
||||
deleted = report_state.delete_vulnerability_report(
|
||||
report_id,
|
||||
delete_reason=delete_reason,
|
||||
deleted_by_agent_id=agent_id,
|
||||
deleted_by_agent_name=agent_name,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("delete_vulnerability_report persistence failed")
|
||||
if isinstance(e, ReportRollbackError):
|
||||
outcome = (
|
||||
f"{e.cause!s}. The report is still on file, but its on-disk indexes could "
|
||||
"not be rewritten and may be stale until the next report is saved"
|
||||
)
|
||||
else:
|
||||
outcome = f"{e!s}. The report is still on file"
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to delete report '{report_id}': {outcome}; retry the deletion.",
|
||||
"report_id": report_id,
|
||||
}
|
||||
if deleted is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Report with id '{report_id}' not found",
|
||||
"report_id": report_id,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Vulnerability report %s deleted by %s: %s",
|
||||
report_id,
|
||||
agent_name or agent_id or "an agent",
|
||||
delete_reason[:200],
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"action": "deleted",
|
||||
"message": (
|
||||
f"Report '{report_id}' ({deleted.get('title')}) is withdrawn and no longer "
|
||||
"counts as a finding of this scan. Do not file it again unless new evidence "
|
||||
"proves it."
|
||||
),
|
||||
"report_id": report_id,
|
||||
"title": deleted.get("title"),
|
||||
"severity": deleted.get("severity"),
|
||||
}
|
||||
|
||||
|
||||
async def _do_create(
|
||||
*,
|
||||
title: str,
|
||||
@@ -902,7 +606,6 @@ async def _do_create(
|
||||
cve: str | None,
|
||||
cwe: str | None,
|
||||
code_locations: list[dict[str, Any]] | None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
confidence_rationale: str | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
@@ -948,10 +651,6 @@ async def _do_create(
|
||||
errors.extend(_validate_fix_verification(parsed_locations, fix_verification))
|
||||
cve, cwe, identifier_errors = _validate_identifiers(cve, cwe)
|
||||
errors.extend(identifier_errors)
|
||||
normalized_http_exchange_ids, http_exchange_errors = _normalize_http_exchange_ids(
|
||||
http_exchange_ids
|
||||
)
|
||||
errors.extend(http_exchange_errors)
|
||||
|
||||
if errors:
|
||||
return {"success": False, "error": "Validation failed", "errors": errors}
|
||||
@@ -1013,11 +712,7 @@ async def _do_create(
|
||||
"code_locations": parsed_locations,
|
||||
"fix_verification": fix_verification,
|
||||
"fix_pr_body": fix_pr_body,
|
||||
"http_exchange_ids": normalized_http_exchange_ids,
|
||||
}
|
||||
fix_candidate = _build_fix_candidate(report_state, report_fields)
|
||||
if fix_candidate:
|
||||
report_fields["fix_candidate"] = fix_candidate
|
||||
|
||||
dedupe = await check_duplicate(candidate, existing)
|
||||
if dedupe.get("is_duplicate"):
|
||||
@@ -1043,15 +738,9 @@ async def _do_create(
|
||||
agent_id=agent_id if isinstance(agent_id, str) else None,
|
||||
agent_name=agent_name if isinstance(agent_name, str) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.exception("create_vulnerability_report persistence failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"Failed to create vulnerability report: {e!s}. "
|
||||
"The finding was not stored; file it again."
|
||||
),
|
||||
}
|
||||
return {"success": False, "error": f"Failed to create vulnerability report: {e!s}"}
|
||||
else:
|
||||
logger.info(
|
||||
"Vulnerability report created: id=%s severity=%s cvss=%.1f title=%s",
|
||||
@@ -1107,7 +796,6 @@ async def create_vulnerability_report(
|
||||
cve: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
confidence_rationale: str | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
@@ -1352,23 +1040,12 @@ async def create_vulnerability_report(
|
||||
cve: ``CVE-YYYY-NNNNN`` if certain, else omit.
|
||||
cwe: ``CWE-NNN`` (most specific child) if certain, else omit.
|
||||
code_locations: White-box findings — list of location objects.
|
||||
http_exchange_ids: Proxy request IDs that prove this finding.
|
||||
Copy these IDs from ``list_requests`` or ``view_request``.
|
||||
For a finding validated over HTTP, capture and inspect the
|
||||
supporting exchanges and include their IDs here before filing.
|
||||
Include relevant baseline/control requests as well as the exploit.
|
||||
Omit only when the finding has no captured HTTP evidence (for
|
||||
example a static-only code finding). Never invent IDs or drop
|
||||
them to bypass a verification error; retry the capture instead.
|
||||
If the result carries a ``warning`` that the IDs were not
|
||||
stored, the finding is filed without them: attach them with
|
||||
``update_vulnerability_report`` once the proxy responds.
|
||||
Keep IDs out of ``evidence`` and all other report text.
|
||||
|
||||
**How ``fix_before`` / ``fix_after`` work**: they describe an
|
||||
initial fix candidate. A later preparation stage applies,
|
||||
tests, repairs, and independently verifies the candidate before
|
||||
Strix can offer an automatic pull request. Therefore:
|
||||
**How ``fix_before`` / ``fix_after`` work**: they're used as
|
||||
literal GitHub/GitLab PR suggestion blocks. When a reviewer
|
||||
accepts the suggestion, the platform replaces the **exact
|
||||
lines from ``start_line`` to ``end_line``** with
|
||||
``fix_after``. Therefore:
|
||||
|
||||
1. ``fix_before`` must be a **VERBATIM** copy of the source
|
||||
at those lines — same whitespace, indentation, line
|
||||
@@ -1389,9 +1066,9 @@ async def create_vulnerability_report(
|
||||
before SQL"``). Order primary fix first, supporting
|
||||
changes (imports, config) after.
|
||||
|
||||
**Informational vs candidate**:
|
||||
**Informational vs actionable**:
|
||||
- With ``fix_before`` / ``fix_after``: actionable fix
|
||||
candidate for the preparation stage.
|
||||
(renders as a PR suggestion block).
|
||||
- Without them: informational context (e.g. showing the
|
||||
source of tainted data, or a sink that doesn't need
|
||||
direct editing).
|
||||
@@ -1426,10 +1103,11 @@ async def create_vulnerability_report(
|
||||
that aren't part of the fix.
|
||||
- Duplicating the same change across multiple locations.
|
||||
fix_verification: REQUIRED whenever any ``code_locations`` entry
|
||||
carries a ``fix_after``. This field records the reporting
|
||||
agent's checks on the draft candidate. It is not an independent
|
||||
verification result. Before writing this field, work the gates
|
||||
**in order**:
|
||||
carries a ``fix_after``. A reviewer can apply that
|
||||
suggestion with one click, so an unverified fix ships
|
||||
straight into the codebase. Before writing this field, work
|
||||
the gates **in order** and never trade an earlier one for a
|
||||
later one:
|
||||
|
||||
1. **Security closure** — re-trace the source → sink path
|
||||
through the *patched* code and state why it is now
|
||||
@@ -1448,9 +1126,11 @@ async def create_vulnerability_report(
|
||||
|
||||
Then write what you did: the commands you ran and their
|
||||
results, and every gate you could only reason about rather
|
||||
than execute, marked explicitly as a gap. Do not claim that a
|
||||
gate passed because the candidate looks correct. If a gate
|
||||
fails, revise the candidate or drop ``fix_after``.
|
||||
than execute, marked explicitly as a gap. Do not claim a
|
||||
gate passed because it looks right. If a gate fails, revise
|
||||
the patch or drop ``fix_after`` and leave the location
|
||||
informational — never compensate for a failed security
|
||||
closure with a smaller diff or extra prose.
|
||||
|
||||
Also use this field to record the narrowest-complete-change
|
||||
judgement: prefer the smallest repository-native fix that
|
||||
@@ -1506,29 +1186,7 @@ async def create_vulnerability_report(
|
||||
A restrictive CSP that blocks inline script execution would
|
||||
reduce impact and lower the severity.
|
||||
fix_effort: "low"
|
||||
|
||||
Nice to have: for code findings, if the checkout has git history, a quick
|
||||
``git blame`` (quote the paths) on the vulnerable line is worth weaving into
|
||||
``technical_analysis`` — who last touched it, when, and in which commit, as
|
||||
part of the prose, not a separate section. Skip it if the line is
|
||||
uncommitted or the command fails.
|
||||
"""
|
||||
(
|
||||
http_exchange_ids,
|
||||
http_exchange_errors,
|
||||
http_exchange_warning,
|
||||
) = await _verify_http_exchange_ids(ctx, http_exchange_ids)
|
||||
if http_exchange_errors:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Validation failed",
|
||||
"errors": http_exchange_errors,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
agent_id, agent_name = _caller_identity(ctx)
|
||||
|
||||
result = await _do_create(
|
||||
@@ -1553,13 +1211,12 @@ async def create_vulnerability_report(
|
||||
cve=cve,
|
||||
cwe=cwe,
|
||||
code_locations=code_locations,
|
||||
http_exchange_ids=http_exchange_ids,
|
||||
fix_verification=fix_verification,
|
||||
fix_pr_body=fix_pr_body,
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
return json.dumps(_with_warning(result, http_exchange_warning), ensure_ascii=False, default=str)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
@function_tool(timeout=60, strict_mode=False)
|
||||
@@ -1588,7 +1245,6 @@ async def update_vulnerability_report(
|
||||
cve: str | None = None,
|
||||
cwe: str | None = None,
|
||||
code_locations: list[dict[str, Any]] | None = None,
|
||||
http_exchange_ids: list[str] | None = None,
|
||||
fix_verification: str | None = None,
|
||||
fix_pr_body: str | None = None,
|
||||
contextual_cvss_reasoning: str | None = None,
|
||||
@@ -1661,112 +1317,43 @@ async def update_vulnerability_report(
|
||||
cve: Replacement CVE id.
|
||||
cwe: Replacement CWE id.
|
||||
code_locations: Replacement code locations.
|
||||
http_exchange_ids: Replacement proxy request ids. Pass an empty
|
||||
list to remove all linked exchanges.
|
||||
fix_verification: Verification statement for an applyable fix.
|
||||
fix_pr_body: Replacement fix PR body.
|
||||
contextual_cvss_reasoning: Dependency findings only. What you
|
||||
observed in this codebase that justifies the contextual
|
||||
``cvss_breakdown``.
|
||||
"""
|
||||
(
|
||||
http_exchange_ids,
|
||||
http_exchange_errors,
|
||||
http_exchange_warning,
|
||||
) = await _verify_http_exchange_ids(ctx, http_exchange_ids)
|
||||
if http_exchange_errors:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Validation failed",
|
||||
"errors": http_exchange_errors,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
fields = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"impact": impact,
|
||||
"target": target,
|
||||
"technical_analysis": technical_analysis,
|
||||
"poc_description": poc_description,
|
||||
"poc_script_code": poc_script_code,
|
||||
"remediation_steps": remediation_steps,
|
||||
"evidence": evidence,
|
||||
"assumptions": assumptions,
|
||||
"counterevidence": counterevidence,
|
||||
"confidence": confidence,
|
||||
"confidence_rationale": confidence_rationale,
|
||||
"severity_change_conditions": severity_change_conditions,
|
||||
"fix_effort": fix_effort,
|
||||
"cvss_breakdown": cvss_breakdown,
|
||||
"endpoint": endpoint,
|
||||
"method": method,
|
||||
"cve": cve,
|
||||
"cwe": cwe,
|
||||
"code_locations": code_locations,
|
||||
"http_exchange_ids": http_exchange_ids,
|
||||
"fix_verification": fix_verification,
|
||||
"fix_pr_body": fix_pr_body,
|
||||
"contextual_cvss_reasoning": contextual_cvss_reasoning,
|
||||
}
|
||||
if http_exchange_warning and all(value is None for value in fields.values()):
|
||||
return json.dumps(
|
||||
{"success": False, "error": http_exchange_warning, "report_id": report_id},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
||||
agent_id, agent_name = _caller_identity(ctx)
|
||||
result = await asyncio.to_thread(
|
||||
_do_update,
|
||||
report_id=report_id,
|
||||
update_reason=update_reason,
|
||||
fields=fields,
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
return json.dumps(_with_warning(result, http_exchange_warning), ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
@function_tool(timeout=60)
|
||||
async def delete_vulnerability_report(
|
||||
ctx: RunContextWrapper,
|
||||
report_id: str,
|
||||
delete_reason: str,
|
||||
) -> str:
|
||||
"""Withdraw a vulnerability report that later testing disproved.
|
||||
|
||||
Use this when a finding you filed turns out not to be a vulnerability
|
||||
at all: the exploit only worked because of a mistake in your own test
|
||||
setup (a mixed-up session, a self-inflicted state change, a misread
|
||||
response), the behaviour is documented and intended, or the control
|
||||
you believed was missing is in fact enforced. The report is removed
|
||||
from the scan; it does not stay behind as a zero-severity entry.
|
||||
|
||||
Do NOT use this for a finding that is real but weaker than filed —
|
||||
revise it with ``update_vulnerability_report`` so the severity, the
|
||||
impact and the counterevidence come down together. Do NOT rewrite a
|
||||
report into a "retracted" or "false positive" note either: delete it.
|
||||
|
||||
Only withdraw a report you have re-tested yourself, whoever filed it.
|
||||
Call ``get_report`` first to read what it claims, then state in
|
||||
``delete_reason`` exactly what disproved it, so the scan history shows
|
||||
who withdrew the finding and why. A withdrawn report cannot be restored; if new
|
||||
evidence later proves the issue, file it again.
|
||||
|
||||
Args:
|
||||
report_id: Id of the report to withdraw (format ``vuln-NNNN``).
|
||||
delete_reason: What disproved the finding, in one or two
|
||||
sentences.
|
||||
"""
|
||||
agent_id, agent_name = _caller_identity(ctx)
|
||||
result = await asyncio.to_thread(
|
||||
_do_delete,
|
||||
report_id=report_id,
|
||||
delete_reason=delete_reason,
|
||||
fields={
|
||||
"title": title,
|
||||
"description": description,
|
||||
"impact": impact,
|
||||
"target": target,
|
||||
"technical_analysis": technical_analysis,
|
||||
"poc_description": poc_description,
|
||||
"poc_script_code": poc_script_code,
|
||||
"remediation_steps": remediation_steps,
|
||||
"evidence": evidence,
|
||||
"assumptions": assumptions,
|
||||
"counterevidence": counterevidence,
|
||||
"confidence": confidence,
|
||||
"confidence_rationale": confidence_rationale,
|
||||
"severity_change_conditions": severity_change_conditions,
|
||||
"fix_effort": fix_effort,
|
||||
"cvss_breakdown": cvss_breakdown,
|
||||
"endpoint": endpoint,
|
||||
"method": method,
|
||||
"cve": cve,
|
||||
"cwe": cwe,
|
||||
"code_locations": code_locations,
|
||||
"fix_verification": fix_verification,
|
||||
"fix_pr_body": fix_pr_body,
|
||||
"contextual_cvss_reasoning": contextual_cvss_reasoning,
|
||||
},
|
||||
agent_id=agent_id,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
@@ -2160,15 +1747,9 @@ async def _do_create_dependency( # noqa: PLR0912
|
||||
agent_id=agent_id if isinstance(agent_id, str) else None,
|
||||
agent_name=agent_name if isinstance(agent_name, str) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.exception("create_dependency_report persistence failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
f"Failed to create dependency report: {e!s}. "
|
||||
"The finding was not stored; file it again."
|
||||
),
|
||||
}
|
||||
return {"success": False, "error": f"Failed to create dependency report: {e!s}"}
|
||||
else:
|
||||
logger.info(
|
||||
"Dependency report created: id=%s cve=%s package=%s severity=%s",
|
||||
|
||||
+55
-289
@@ -1,12 +1,11 @@
|
||||
"""Security-focused web research tools (Exa or Perplexity)."""
|
||||
"""``web_search`` — Perplexity-backed security-focused web search."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from agents import RunContextWrapper, function_tool
|
||||
@@ -14,10 +13,6 @@ from agents import RunContextWrapper, function_tool
|
||||
from strix.config import load_settings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -46,7 +41,22 @@ Structure your response to be comprehensive yet concise, emphasizing the most cr
|
||||
security implications and details."""
|
||||
|
||||
|
||||
def _perplexity_content(api_key: str, query: str) -> str:
|
||||
def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error class needs its own sanitized return
|
||||
if not query or not query.strip():
|
||||
return {"success": False, "error": "Query cannot be empty"}
|
||||
|
||||
api_key = load_settings().integrations.perplexity_api_key
|
||||
if not api_key:
|
||||
logger.warning("web_search invoked without PERPLEXITY_API_KEY configured")
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"Web search is not configured for this scan "
|
||||
"(operator needs to set PERPLEXITY_API_KEY). Proceed without it"
|
||||
),
|
||||
}
|
||||
logger.info("web_search query (len=%d): %s", len(query), query[:120])
|
||||
|
||||
url = "https://api.perplexity.ai/chat/completions"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
@@ -56,269 +66,61 @@ def _perplexity_content(api_key: str, query: str) -> str:
|
||||
{"role": "user", "content": query},
|
||||
],
|
||||
}
|
||||
with requests.post(url, headers=headers, json=payload, timeout=300) as response:
|
||||
response.raise_for_status()
|
||||
return str(response.json()["choices"][0]["message"]["content"])
|
||||
|
||||
|
||||
_EXA_PAGE_MAX_CHARS = 20000
|
||||
_EXA_MAX_CONTENT_URLS = 10
|
||||
_EXA_SUMMARY_PROMPT = (
|
||||
"Summarize this page for a penetration tester. Keep concrete technical detail: "
|
||||
"affected products and exact versions, CVE and CWE identifiers, CVSS scores, "
|
||||
"exploitation preconditions, payloads or commands, and mitigations. "
|
||||
"Leave out marketing copy and navigation text."
|
||||
)
|
||||
|
||||
|
||||
def _exa_result_block(result: dict[str, Any]) -> str | None:
|
||||
result_url = str(result.get("url") or result.get("id") or "")
|
||||
if not result_url:
|
||||
return None
|
||||
title = str(result.get("title") or result_url)
|
||||
parts = [f"### {title}\n{result_url}"]
|
||||
summary = str(result.get("summary") or "").strip()
|
||||
if summary:
|
||||
parts.append(summary)
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _exa_page_block(result: dict[str, Any]) -> str | None:
|
||||
result_url = str(result.get("url") or result.get("id") or "")
|
||||
text = str(result.get("text") or "").strip()
|
||||
if not result_url or not text:
|
||||
return None
|
||||
if len(text) > _EXA_PAGE_MAX_CHARS:
|
||||
text = f"{text[:_EXA_PAGE_MAX_CHARS]}\n[truncated at {_EXA_PAGE_MAX_CHARS} characters]"
|
||||
title = str(result.get("title") or result_url)
|
||||
return f"### {title}\n{result_url}\n\n{text}"
|
||||
|
||||
|
||||
def _exa_blocks(
|
||||
results: list[Any],
|
||||
render: Callable[[dict[str, Any]], str | None],
|
||||
) -> list[str]:
|
||||
blocks: list[str] = []
|
||||
for result in results:
|
||||
if not isinstance(result, dict):
|
||||
continue
|
||||
block = render(cast("dict[str, Any]", result))
|
||||
if block:
|
||||
blocks.append(block)
|
||||
return blocks
|
||||
|
||||
|
||||
def _exa_post(api_key: str, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
headers = {"x-api-key": api_key, "Content-Type": "application/json"}
|
||||
with requests.post(endpoint, headers=headers, json=payload, timeout=300) as response:
|
||||
response.raise_for_status()
|
||||
body: dict[str, Any] = response.json()
|
||||
return body
|
||||
|
||||
|
||||
def _exa_content(api_key: str, query: str, search_type: str, num_results: int) -> str:
|
||||
body = _exa_post(
|
||||
api_key,
|
||||
"https://api.exa.ai/search",
|
||||
{
|
||||
"query": query,
|
||||
"type": search_type,
|
||||
"numResults": num_results,
|
||||
"contents": {"summary": {"query": _EXA_SUMMARY_PROMPT}},
|
||||
},
|
||||
)
|
||||
blocks = _exa_blocks(body.get("results") or [], _exa_result_block)
|
||||
if not blocks:
|
||||
raise ValueError("Exa response has no results")
|
||||
return "\n\n".join(blocks)
|
||||
|
||||
|
||||
def _normalize_url(url: str) -> str:
|
||||
"""Canonical form for matching: case-fold scheme and host only, drop a trailing slash."""
|
||||
parts = urlsplit(url.strip())
|
||||
return urlunsplit(
|
||||
(parts.scheme.lower(), parts.netloc.lower(), parts.path.rstrip("/"), parts.query, "")
|
||||
)
|
||||
|
||||
|
||||
def _exa_page_text(api_key: str, urls: list[str]) -> tuple[str, set[str]]:
|
||||
"""Fetch page text and report which of the requested URLs Exa returned."""
|
||||
body = _exa_post(api_key, "https://api.exa.ai/contents", {"urls": urls, "text": True})
|
||||
blocks: list[str] = []
|
||||
fetched: set[str] = set()
|
||||
results: list[Any] = body.get("results") or []
|
||||
for result in results:
|
||||
if not isinstance(result, dict):
|
||||
continue
|
||||
page = cast("dict[str, Any]", result)
|
||||
block = _exa_page_block(page)
|
||||
if not block:
|
||||
continue
|
||||
blocks.append(block)
|
||||
fetched.add(_normalize_url(str(page.get("url") or page.get("id") or "")))
|
||||
if not blocks:
|
||||
raise ValueError("Exa returned no page contents")
|
||||
return "\n\n".join(blocks), fetched
|
||||
|
||||
|
||||
def _resolve_provider( # noqa: PLR0911 - each provider/missing-key case needs its own return
|
||||
integrations: Any,
|
||||
) -> tuple[str, str] | dict[str, Any]:
|
||||
"""Pick the search provider and its key, or return a sanitized error dict."""
|
||||
provider = integrations.web_search_provider
|
||||
perplexity_key = integrations.perplexity_api_key
|
||||
exa_key = integrations.exa_api_key
|
||||
|
||||
if provider == "perplexity":
|
||||
if not perplexity_key:
|
||||
return _not_configured_error("PERPLEXITY_API_KEY")
|
||||
return ("perplexity", perplexity_key)
|
||||
if provider == "exa":
|
||||
if not exa_key:
|
||||
return _not_configured_error("EXA_API_KEY")
|
||||
return ("exa", exa_key)
|
||||
|
||||
if exa_key:
|
||||
return ("exa", exa_key)
|
||||
if perplexity_key:
|
||||
return ("perplexity", perplexity_key)
|
||||
return _not_configured_error("EXA_API_KEY or PERPLEXITY_API_KEY")
|
||||
|
||||
|
||||
def _not_configured_error(missing: str) -> dict[str, Any]:
|
||||
logger.warning("web_search invoked without %s configured", missing)
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"Web search is not configured for this scan "
|
||||
f"(operator needs to set {missing}). Proceed without it"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _guarded_call[T]( # noqa: PLR0911 - each error class needs its own sanitized return
|
||||
tool: str,
|
||||
rejected_hint: str,
|
||||
fetch: Callable[[], T],
|
||||
) -> T | dict[str, Any]:
|
||||
"""Run a provider call and translate any failure into a sanitized error dict."""
|
||||
try:
|
||||
return fetch()
|
||||
with requests.post(url, headers=headers, json=payload, timeout=300) as response:
|
||||
response.raise_for_status()
|
||||
content = response.json()["choices"][0]["message"]["content"]
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning("%s timed out", tool)
|
||||
return {"success": False, "error": f"{tool} timed out. Try again or narrow the request"}
|
||||
logger.warning("web_search timed out")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Web search timed out. Try again or shorten the query",
|
||||
}
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
status = exc.response.status_code if exc.response is not None else None
|
||||
logger.exception("%s HTTP error status=%s", tool, status)
|
||||
logger.exception("web_search HTTP error status=%s", status)
|
||||
if status is not None and 400 <= status < 500:
|
||||
return {"success": False, "error": rejected_hint}
|
||||
return {"success": False, "error": f"{tool} service is unavailable. Try again later"}
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"Web search rejected the query. Refine it "
|
||||
"(more specific, shorter, no unusual characters) and retry"
|
||||
),
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Web search service is unavailable. Try again later",
|
||||
}
|
||||
except requests.exceptions.RequestException:
|
||||
logger.exception("%s network error", tool)
|
||||
return {"success": False, "error": f"{tool} network error. Try again later"}
|
||||
logger.exception("web_search network error")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Web search network error. Try again later",
|
||||
}
|
||||
except (KeyError, IndexError, ValueError):
|
||||
logger.exception("%s response shape unexpected", tool)
|
||||
return {"success": False, "error": f"{tool} returned an unexpected response. Try again"}
|
||||
logger.exception("web_search response shape unexpected")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Web search returned an unexpected response. Try again",
|
||||
}
|
||||
except Exception:
|
||||
logger.exception("%s failed", tool)
|
||||
return {"success": False, "error": f"{tool} failed unexpectedly"}
|
||||
|
||||
|
||||
def _do_search(query: str) -> dict[str, Any]:
|
||||
if not query or not query.strip():
|
||||
return {"success": False, "error": "Query cannot be empty"}
|
||||
|
||||
integrations = load_settings().integrations
|
||||
resolved = _resolve_provider(integrations)
|
||||
if isinstance(resolved, dict):
|
||||
return resolved
|
||||
provider, api_key = resolved
|
||||
logger.info("web_search provider=%s query (len=%d): %s", provider, len(query), query[:120])
|
||||
|
||||
def fetch() -> str:
|
||||
if provider == "exa":
|
||||
return _exa_content(
|
||||
api_key,
|
||||
query,
|
||||
integrations.exa_search_type,
|
||||
integrations.exa_num_results,
|
||||
)
|
||||
return _perplexity_content(api_key, query)
|
||||
|
||||
outcome = _guarded_call(
|
||||
"Web search",
|
||||
(
|
||||
"Web search rejected the query. Refine it "
|
||||
"(more specific, shorter, no unusual characters) and retry"
|
||||
),
|
||||
fetch,
|
||||
)
|
||||
if isinstance(outcome, dict):
|
||||
return outcome
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"provider": provider,
|
||||
"content": outcome,
|
||||
}
|
||||
|
||||
|
||||
def _do_get_contents(urls: list[str]) -> dict[str, Any]:
|
||||
cleaned = [url.strip() for url in urls if url and url.strip()]
|
||||
if not cleaned:
|
||||
return {"success": False, "error": "Provide at least one URL"}
|
||||
if len(cleaned) > _EXA_MAX_CONTENT_URLS:
|
||||
logger.exception("web_search failed")
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Too many URLs. Pass at most {_EXA_MAX_CONTENT_URLS} per call",
|
||||
"error": "Web search failed unexpectedly",
|
||||
}
|
||||
|
||||
integrations = load_settings().integrations
|
||||
api_key = integrations.exa_api_key
|
||||
if not api_key:
|
||||
return _not_configured_error("EXA_API_KEY")
|
||||
if integrations.web_search_provider == "perplexity":
|
||||
logger.warning("web_get_contents invoked while the provider is pinned to Perplexity")
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"Page fetching needs the Exa provider "
|
||||
"(operator pinned STRIX_WEB_SEARCH_PROVIDER to perplexity). "
|
||||
"Use web_search instead"
|
||||
),
|
||||
"success": True,
|
||||
"query": query,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
logger.info("web_get_contents urls=%d", len(cleaned))
|
||||
outcome = _guarded_call(
|
||||
"Page fetch",
|
||||
"Page fetch was rejected. Check the URLs are complete, public, and correctly formed",
|
||||
lambda: _exa_page_text(api_key, cleaned),
|
||||
)
|
||||
if isinstance(outcome, dict):
|
||||
return outcome
|
||||
content, fetched = outcome
|
||||
missing = [url for url in cleaned if _normalize_url(url) not in fetched]
|
||||
result: dict[str, Any] = {
|
||||
"success": True,
|
||||
"urls": [url for url in cleaned if url not in missing],
|
||||
"provider": "exa",
|
||||
"content": content,
|
||||
}
|
||||
if missing:
|
||||
logger.warning(
|
||||
"web_get_contents returned %d of %d pages", len(cleaned) - len(missing), len(cleaned)
|
||||
)
|
||||
result["failed_urls"] = missing
|
||||
result["warning"] = (
|
||||
f"Exa returned no content for {len(missing)} of {len(cleaned)} requested URLs. "
|
||||
"Those pages are missing from the content below"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@function_tool(timeout=330)
|
||||
async def web_search(ctx: RunContextWrapper, query: str) -> str:
|
||||
"""Real-time web search (Exa or Perplexity) — your primary research tool.
|
||||
"""Real-time web search via Perplexity — your primary research tool.
|
||||
|
||||
Use it liberally for anything that's not in your training data:
|
||||
|
||||
@@ -348,12 +150,6 @@ async def web_search(ctx: RunContextWrapper, query: str) -> str:
|
||||
exploits, Kali-compatible tooling, and concrete code/command
|
||||
examples.
|
||||
|
||||
With the Exa provider you get a ranked list of results, each with a
|
||||
title, URL, and a short security-focused summary. Read the result
|
||||
you need, then call ``web_get_contents`` with its URL to pull the
|
||||
full page text when a summary is not enough. With Perplexity you get
|
||||
a single synthesized cited answer.
|
||||
|
||||
**Good example queries** (each is a full sentence, names a
|
||||
version/product, and asks one concrete thing):
|
||||
|
||||
@@ -381,33 +177,3 @@ async def web_search(ctx: RunContextWrapper, query: str) -> str:
|
||||
"""
|
||||
result = await asyncio.to_thread(_do_search, query)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
@function_tool(timeout=330)
|
||||
async def web_get_contents(ctx: RunContextWrapper, urls: list[str]) -> str:
|
||||
"""Fetch the full, cleaned text of specific web pages (Exa only).
|
||||
|
||||
Use this as the drill-down step after ``web_search``: when a result's
|
||||
summary is not enough, pass that result's URL here to read the whole
|
||||
page. Good for reading a full advisory, a CVE writeup,
|
||||
an exploit proof-of-concept, or vendor documentation end to end.
|
||||
|
||||
Prefer ``web_search`` first to find the right pages, then fetch only
|
||||
the few URLs worth reading in full — each page can be large, so avoid
|
||||
fetching many pages you do not need.
|
||||
|
||||
This tool needs the Exa provider (``EXA_API_KEY``). When the operator
|
||||
pins the provider to Perplexity, it returns an error and you should
|
||||
use ``web_search`` instead.
|
||||
|
||||
Some pages block extraction. When a page returns no content, the
|
||||
result lists it under ``failed_urls`` and the ``content`` field holds
|
||||
only the pages that came back. Check ``failed_urls`` before you
|
||||
conclude that a page had nothing useful.
|
||||
|
||||
Args:
|
||||
urls: The page URLs to fetch, at most 10 per call. Use complete,
|
||||
public URLs (for example the ones returned by ``web_search``).
|
||||
"""
|
||||
result = await asyncio.to_thread(_do_get_contents, urls)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
@@ -24,30 +24,3 @@ def _isolate_mcp_config(
|
||||
monkeypatch.setenv("STRIX_MCP_CONFIG", str(missing))
|
||||
monkeypatch.delenv("STRIX_MCP_ONLY", raising=False)
|
||||
monkeypatch.delenv("STRIX_MCP_EXCLUDE", raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _plain_terminal(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Make Rich output identical on every developer's machine.
|
||||
|
||||
Many CLI tests force ``isatty()`` to ``True`` to exercise the human-readable
|
||||
code path and then assert on the plain text. Rich picks its color system
|
||||
from ``TERM``, ``COLORTERM``, and ``FORCE_COLOR``, so on a real terminal
|
||||
those assertions would meet ANSI escape codes instead of the words they
|
||||
look for. A dumb terminal renders the same text without any styling.
|
||||
"""
|
||||
monkeypatch.setenv("TERM", "dumb")
|
||||
for name in ("COLORTERM", "FORCE_COLOR", "NO_COLOR", "TTY_COMPATIBLE"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_wallet_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Keep a developer's real mppx wallet out of the top-up tests.
|
||||
|
||||
``strix cloud billing topup`` chooses the Stripe Link flow or the
|
||||
preconfigured mppx wallet from these variables, so leaving them set would
|
||||
silently switch which branch a test runs.
|
||||
"""
|
||||
for name in ("MPPX_ACCOUNT", "MPPX_STRIPE_SECRET_KEY", "MPPX_STRIPE_PAYMENT_METHOD"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
@@ -1,258 +0,0 @@
|
||||
"""Tests for parent/child coordination once a non-interactive child has finished.
|
||||
|
||||
A non-interactive agent's loop returns after its terminal state, so nothing will
|
||||
ever read a message sent to it afterwards. Messaging it must say so instead of
|
||||
reporting delivery, waiting on it must return at once, and its completion report
|
||||
must carry the ids of the reports it actually filed so the parent does not have
|
||||
to go asking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import pytest
|
||||
from agents.tool_context import ToolContext
|
||||
|
||||
from strix.core.agents import AgentCoordinator
|
||||
from strix.report.state import ReportState, set_global_report_state
|
||||
from strix.tools.agents_graph.tools import agent_finish, send_message_to_agent, wait_for_agents
|
||||
|
||||
|
||||
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)
|
||||
state = ReportState(run_name="test-run")
|
||||
set_global_report_state(state)
|
||||
yield state
|
||||
set_global_report_state(None)
|
||||
|
||||
|
||||
async def _graph(*, interactive: bool) -> AgentCoordinator:
|
||||
coordinator = AgentCoordinator()
|
||||
await coordinator.register("root", "strix", parent_id=None)
|
||||
await coordinator.register("child", "Validator", parent_id="root")
|
||||
await coordinator.attach_runtime("root", resumable=interactive)
|
||||
await coordinator.attach_runtime("child", resumable=interactive)
|
||||
return coordinator
|
||||
|
||||
|
||||
async def _call(
|
||||
tool: Any, coordinator: AgentCoordinator, agent_id: str, args: dict[str, Any], **extra: Any
|
||||
) -> dict[str, Any]:
|
||||
ctx = ToolContext(
|
||||
context={"coordinator": coordinator, "agent_id": agent_id, **extra},
|
||||
tool_name=tool.name,
|
||||
tool_call_id="call-1",
|
||||
tool_arguments="{}",
|
||||
)
|
||||
raw: str = await tool.on_invoke_tool(ctx, json.dumps(args))
|
||||
return cast("dict[str, Any]", json.loads(raw))
|
||||
|
||||
|
||||
# --- send_message_to_agent -------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_to_finished_non_interactive_child_is_not_delivered() -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
await coordinator.set_status("child", "completed")
|
||||
|
||||
result = await _call(
|
||||
send_message_to_agent,
|
||||
coordinator,
|
||||
"root",
|
||||
{"target_agent_id": "child", "message": "did you file it?", "message_type": "query"},
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["delivery_status"] == "not_delivered"
|
||||
assert result["target_status"] == "completed"
|
||||
assert "list_reports" in result["error"]
|
||||
assert coordinator.pending_counts.get("child", 0) == 0
|
||||
assert coordinator.runtimes["child"].mailbox == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("status", ["stopped", "failed", "crashed"])
|
||||
async def test_every_terminal_non_interactive_status_is_unreachable(status: str) -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
await coordinator.set_status("child", status)
|
||||
|
||||
assert await coordinator.send("child", {"from": "root", "content": "hi"}) is False
|
||||
assert await coordinator.reachability("child") == (False, status)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("status", ["running", "waiting"])
|
||||
async def test_message_to_live_child_is_delivered(status: str) -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
await coordinator.set_status("child", status)
|
||||
|
||||
result = await _call(
|
||||
send_message_to_agent,
|
||||
coordinator,
|
||||
"root",
|
||||
{"target_agent_id": "child", "message": "wrap up"},
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["delivery_status"] == "delivered"
|
||||
assert coordinator.pending_counts["child"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_to_finished_interactive_child_still_wakes_it() -> None:
|
||||
# An interactive loop parks after finishing and resumes on a message.
|
||||
coordinator = await _graph(interactive=True)
|
||||
await coordinator.set_status("child", "completed")
|
||||
|
||||
result = await _call(
|
||||
send_message_to_agent,
|
||||
coordinator,
|
||||
"root",
|
||||
{"target_agent_id": "child", "message": "one more thing"},
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert coordinator.pending_counts["child"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_target_is_reported_as_not_found() -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
|
||||
result = await _call(
|
||||
send_message_to_agent,
|
||||
coordinator,
|
||||
"root",
|
||||
{"target_agent_id": "ghost", "message": "hello"},
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["target_status"] is None
|
||||
assert "not found" in result["error"]
|
||||
|
||||
|
||||
# --- wait_for_agents -------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_returns_at_once_when_no_child_can_answer() -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
await coordinator.set_status("child", "completed")
|
||||
# The completion report was already consumed in an earlier turn.
|
||||
|
||||
result = await _call(
|
||||
wait_for_agents,
|
||||
coordinator,
|
||||
"root",
|
||||
{"reason": "waiting for validator", "timeout_seconds": 240},
|
||||
)
|
||||
|
||||
assert result["wait_outcome"] == "no_active_agents"
|
||||
assert result["agents"] == [{"agent_id": "child", "name": "Validator", "status": "completed"}]
|
||||
assert coordinator.statuses["root"] == "running"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_delivers_a_pending_report_before_checking_liveness() -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
await coordinator.send("root", {"from": "child", "type": "completion", "content": "done"})
|
||||
await coordinator.set_status("child", "completed")
|
||||
|
||||
result = await _call(wait_for_agents, coordinator, "root", {"timeout_seconds": 5})
|
||||
|
||||
assert result["wait_outcome"] == "message_arrived"
|
||||
assert result["pending_messages"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_still_parks_while_a_child_is_running() -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
|
||||
result = await _call(wait_for_agents, coordinator, "root", {"timeout_seconds": 1})
|
||||
|
||||
assert result["wait_outcome"] == "timeout"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interactive_wait_parks_even_without_active_children() -> None:
|
||||
# In an interactive run a finished child can be woken later, so parking is
|
||||
# legitimate; the run loop's own auto-resume bounds the wait.
|
||||
coordinator = await _graph(interactive=True)
|
||||
await coordinator.set_status("child", "completed")
|
||||
|
||||
result = await _call(
|
||||
wait_for_agents, coordinator, "root", {"timeout_seconds": 5}, interactive=True
|
||||
)
|
||||
|
||||
assert result["wait_outcome"] == "waiting"
|
||||
|
||||
|
||||
# --- agent_finish ----------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_finish_lists_the_reports_the_child_filed(report_state: ReportState) -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
mine = report_state.add_vulnerability_report(
|
||||
title="IDOR on /api/audits", severity="high", agent_id="child", agent_name="Validator"
|
||||
)
|
||||
report_state.add_vulnerability_report(title="Root's own", severity="low", agent_id="root")
|
||||
|
||||
result = await _call(
|
||||
agent_finish,
|
||||
coordinator,
|
||||
"child",
|
||||
{"result_summary": "confirmed", "findings": ["IDOR confirmed"]},
|
||||
parent_id="root",
|
||||
)
|
||||
|
||||
assert result["filed_report_ids"] == [mine]
|
||||
delivered = coordinator.runtimes["root"].mailbox
|
||||
assert len(delivered) == 1
|
||||
assert delivered[0]["filed_report_ids"] == [mine]
|
||||
body = delivered[0]["content"]
|
||||
assert f"- {mine} [HIGH] IDOR on /api/audits" in body
|
||||
assert "Root's own" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_finish_states_explicitly_when_nothing_was_filed(
|
||||
report_state: ReportState,
|
||||
) -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
report_state.add_vulnerability_report(title="Someone else's", severity="low", agent_id="root")
|
||||
|
||||
result = await _call(
|
||||
agent_finish,
|
||||
coordinator,
|
||||
"child",
|
||||
{"result_summary": "nothing exploitable", "findings": ["ruled out X"]},
|
||||
parent_id="root",
|
||||
)
|
||||
|
||||
assert result["filed_report_ids"] == []
|
||||
body = coordinator.runtimes["root"].mailbox[0]["content"]
|
||||
assert "Vulnerability reports filed by this agent" in body
|
||||
assert body.index("filed by this agent") < body.index("- (none)")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_finish_without_report_state_still_completes() -> None:
|
||||
set_global_report_state(None)
|
||||
coordinator = await _graph(interactive=False)
|
||||
|
||||
result = await _call(
|
||||
agent_finish, coordinator, "child", {"result_summary": "done"}, parent_id="root"
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["filed_report_ids"] == []
|
||||
+33
-103
@@ -6,7 +6,6 @@ import io
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
@@ -17,7 +16,7 @@ import requests
|
||||
from rich.console import Console
|
||||
|
||||
from strix.interface import cloud, platform_cli
|
||||
from strix.interface.cloud import http, render, runner, workspaces
|
||||
from strix.interface.cloud import billing, http, payment_proxy, render, runner, workspaces
|
||||
from strix.interface.cloud.spec import GROUP_HELP, SPEC
|
||||
|
||||
|
||||
@@ -148,7 +147,7 @@ def test_read_groups_have_safe_defaults(group: str, verb: str) -> None:
|
||||
resolved = runner.resolve(group, [])
|
||||
assert resolved is not None
|
||||
command, remaining = resolved
|
||||
assert command is SPEC[group][verb]
|
||||
assert command is runner.SPEC[group][verb]
|
||||
assert remaining == []
|
||||
|
||||
|
||||
@@ -534,69 +533,6 @@ def test_insufficient_credits_exits_with_payment_code(monkeypatch: pytest.Monkey
|
||||
assert cloud.run_cloud(["scans", "start", "--domain-ids", "d1"]) == http.EXIT_PAYMENT
|
||||
|
||||
|
||||
def test_insufficient_credits_always_prints_topup_instruction(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
lambda *_a, **_k: FakeResponse(
|
||||
status_code=402,
|
||||
payload={"detail": "Out of credits.", "code": "scan_credit_limit_reached"},
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
argv = ["scans", "start", "--domain-ids", "d1", "--app-url", "https://app.strix.ai"]
|
||||
assert cloud.run_cloud(argv) == http.EXIT_PAYMENT
|
||||
output = " ".join(capsys.readouterr().out.split())
|
||||
assert "Error: Out of credits." in output
|
||||
assert "Next step:" in output
|
||||
assert "strix cloud billing topup --credits <count>" in output
|
||||
assert "https://app.strix.ai/settings/billing" in output
|
||||
assert "strix cloud billing credits" in output
|
||||
|
||||
|
||||
def test_insufficient_credits_shows_platform_hint_once(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
hint = "Buy credits at https://app.strix.ai/settings/billing. Then retry this request."
|
||||
payload = {
|
||||
"detail": f"Out of credits. {hint}",
|
||||
"code": "scan_credit_limit_reached",
|
||||
"hint": hint,
|
||||
"topup_url": "https://app.strix.ai/settings/billing",
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
http, "request", lambda *_a, **_k: FakeResponse(status_code=402, payload=payload)
|
||||
)
|
||||
assert cloud.run_cloud(["scans", "start", "--domain-ids", "d1", "--json"]) == http.EXIT_PAYMENT
|
||||
result = json.loads(capsys.readouterr().out)
|
||||
assert result["error"] == "Out of credits."
|
||||
assert result["next_step"] == hint
|
||||
assert result["topup_url"] == "https://app.strix.ai/settings/billing"
|
||||
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
assert cloud.run_cloud(["scans", "start", "--domain-ids", "d1"]) == http.EXIT_PAYMENT
|
||||
output = " ".join(capsys.readouterr().out.split())
|
||||
assert output.count(hint) == 1
|
||||
assert "Error: Out of credits." in output
|
||||
assert f"Next step: {hint}" in output
|
||||
|
||||
|
||||
def test_payment_required_without_body_names_the_topup_command(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
http, "request", lambda *_a, **_k: FakeResponse(status_code=402, payload={})
|
||||
)
|
||||
argv = ["scans", "start", "--domain-ids", "d1", "--json", "--app-url", "https://app.strix.ai"]
|
||||
assert cloud.run_cloud(argv) == http.EXIT_PAYMENT
|
||||
result = json.loads(capsys.readouterr().out)
|
||||
assert result["error"] == "Not enough credits to run this command."
|
||||
assert "strix cloud billing topup --credits <count>" in result["next_step"]
|
||||
assert "https://app.strix.ai/settings/billing" in result["next_step"]
|
||||
|
||||
|
||||
def test_data_rejects_non_object() -> None:
|
||||
assert cloud.run_cloud(["scans", "start", "--data", "[1,2]"]) == http.EXIT_USAGE
|
||||
assert cloud.run_cloud(["scans", "start", "--data", "not json"]) == http.EXIT_USAGE
|
||||
@@ -625,7 +561,7 @@ def test_stored_token_is_never_sent_to_a_different_platform_origin(
|
||||
lambda: {"api_token": "stored-secret", "app_url": "https://app.strix.ai"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
http.requests,
|
||||
"request",
|
||||
lambda *_args, **_kwargs: pytest.fail("a mismatched origin must not receive the token"),
|
||||
)
|
||||
@@ -640,7 +576,7 @@ def test_stored_token_requires_an_issuer_binding(monkeypatch: pytest.MonkeyPatch
|
||||
monkeypatch.setattr(http, "_app_url_override", "https://app.strix.ai")
|
||||
monkeypatch.setattr(http, "read_record", lambda: {"api_token": "legacy-secret"})
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
http.requests,
|
||||
"request",
|
||||
lambda *_args, **_kwargs: pytest.fail("an unbound token must not be sent"),
|
||||
)
|
||||
@@ -666,7 +602,7 @@ def test_stored_token_is_sent_only_to_its_bound_platform(
|
||||
seen.update(url=url, headers=kwargs["headers"])
|
||||
return FakeResponse(payload={"balance": 1})
|
||||
|
||||
monkeypatch.setattr(requests, "request", request)
|
||||
monkeypatch.setattr(http.requests, "request", request)
|
||||
response = http.request("GET", "/billing/credits")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -690,7 +626,7 @@ def test_explicit_token_can_target_an_explicit_platform(
|
||||
seen.update(url=url, headers=kwargs["headers"])
|
||||
return FakeResponse(payload={"balance": 1})
|
||||
|
||||
monkeypatch.setattr(requests, "request", request)
|
||||
monkeypatch.setattr(http.requests, "request", request)
|
||||
override_value = "explicit-preview-" + str(1)
|
||||
response = http.request("GET", "/billing/credits", token=override_value)
|
||||
|
||||
@@ -770,9 +706,9 @@ def test_topup_noninteractive_requires_explicit_payment_approval(
|
||||
monkeypatch.setattr(
|
||||
http, "request", lambda *_a, **_k: FakeResponse(status_code=402, payload=challenge)
|
||||
)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: False)
|
||||
monkeypatch.setattr(runner.sys.stdin, "isatty", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
subprocess,
|
||||
billing.subprocess,
|
||||
"run",
|
||||
lambda *_a, **_k: pytest.fail("wallet must not run without --yes"),
|
||||
)
|
||||
@@ -796,10 +732,10 @@ def test_topup_machine_output_never_prompts_even_with_terminal_stdin(
|
||||
monkeypatch.setattr(
|
||||
http, "request", lambda *_a, **_k: FakeResponse(status_code=402, payload=challenge)
|
||||
)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: stdout_tty)
|
||||
monkeypatch.setattr(runner.sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(runner.sys.stdout, "isatty", lambda: stdout_tty)
|
||||
monkeypatch.setattr(
|
||||
Console,
|
||||
runner.Console,
|
||||
"input",
|
||||
lambda *_a, **_k: pytest.fail("machine-readable top-up must not prompt"),
|
||||
)
|
||||
@@ -903,7 +839,7 @@ def test_topup_keeps_token_out_of_wallet_process_and_forwards_payment(
|
||||
upstream.update(method=method, url=url, **kwargs)
|
||||
return FakeResponse(payload=receipt, content=json.dumps(receipt).encode())
|
||||
|
||||
monkeypatch.setattr(requests, "request", fake_upstream_request)
|
||||
monkeypatch.setattr(payment_proxy.requests, "request", fake_upstream_request)
|
||||
|
||||
def fake_run(command: list[str], **kwargs: Any) -> Any:
|
||||
commands.append(command)
|
||||
@@ -969,7 +905,6 @@ def test_topup_wallet_failure_is_one_redacted_json_object(
|
||||
http, "request", lambda *_a, **_k: FakeResponse(status_code=402, payload=challenge)
|
||||
)
|
||||
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
|
||||
monkeypatch.setenv("MPPX_ACCOUNT", "agent")
|
||||
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
|
||||
monkeypatch.setattr(
|
||||
subprocess,
|
||||
@@ -1011,7 +946,6 @@ def test_topup_wallet_interruption_reports_unknown_payment_outcome(
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
|
||||
monkeypatch.setenv("MPPX_ACCOUNT", "agent")
|
||||
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
|
||||
monkeypatch.setattr(
|
||||
subprocess, "run", lambda *_a, **_k: (_ for _ in ()).throw(KeyboardInterrupt)
|
||||
@@ -1037,7 +971,6 @@ def test_topup_non_json_wallet_success_requires_balance_verification(
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
|
||||
monkeypatch.setenv("MPPX_ACCOUNT", "agent")
|
||||
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
|
||||
monkeypatch.setattr(
|
||||
subprocess,
|
||||
@@ -1064,7 +997,6 @@ def test_topup_rejects_parseable_wallet_error_as_a_success(
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
|
||||
monkeypatch.setenv("MPPX_ACCOUNT", "agent")
|
||||
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
|
||||
monkeypatch.setattr(
|
||||
subprocess,
|
||||
@@ -1104,7 +1036,6 @@ def test_topup_does_not_trust_an_unobserved_wallet_receipt(
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
|
||||
monkeypatch.setenv("MPPX_ACCOUNT", "agent")
|
||||
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
|
||||
monkeypatch.setattr(
|
||||
subprocess,
|
||||
@@ -1125,7 +1056,7 @@ def test_topup_does_not_trust_an_unobserved_wallet_receipt(
|
||||
def test_topup_human_mode_requires_a_bridge_confirmed_receipt(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -1135,10 +1066,9 @@ def test_topup_human_mode_requires_a_bridge_confirmed_receipt(
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(http, "api_token", lambda *_a, **_k: "tok")
|
||||
monkeypatch.setenv("MPPX_ACCOUNT", "agent")
|
||||
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
payment_proxy.requests,
|
||||
"request",
|
||||
lambda *_a, **_k: FakeResponse(status_code=200, content=b"<html>not a receipt</html>"),
|
||||
)
|
||||
@@ -1667,7 +1597,7 @@ def test_handoff_links_reject_non_http_schemes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: Any,
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(runner.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -1952,7 +1882,7 @@ def test_workspace_use_preserves_definitive_conflict(
|
||||
def test_group_help_lists_all_verbs_instead_of_default_verb_help(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
assert cloud.run_cloud(["workspaces", "-h"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "workspaces verbs" in output
|
||||
@@ -1976,7 +1906,7 @@ def test_workspace_alias_routes_to_workspaces(monkeypatch: pytest.MonkeyPatch) -
|
||||
def test_workspace_human_list_is_numbered_and_hides_ids(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -2024,7 +1954,7 @@ def test_integrations_human_list_exposes_installation_id_and_json_stays_full(
|
||||
],
|
||||
"bitbucket_oauth_enabled": True,
|
||||
}
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(http, "request", lambda *_a, **_k: FakeResponse(payload=payload))
|
||||
|
||||
assert cloud.run_cloud(["integrations", "list"]) == 0
|
||||
@@ -2042,7 +1972,7 @@ def test_integrations_human_list_exposes_installation_id_and_json_stays_full(
|
||||
def test_pr_review_human_list_prioritizes_actionable_fields(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -2117,7 +2047,7 @@ def test_pr_review_human_list_shows_pull_request_state(
|
||||
capsys: Any,
|
||||
pr_state: str,
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -2180,7 +2110,7 @@ def test_scan_human_list_identifies_internal_and_uploaded_targets(
|
||||
record: dict[str, Any],
|
||||
expected_targets: tuple[str, ...],
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -2344,7 +2274,7 @@ def test_human_lists_prioritize_actionable_fields(
|
||||
visible: tuple[str, ...],
|
||||
hidden: tuple[str, ...],
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(http, "request", lambda *_a, **_k: FakeResponse(payload=payload))
|
||||
|
||||
assert cloud.run_cloud(command) == 0
|
||||
@@ -2358,7 +2288,7 @@ def test_human_lists_prioritize_actionable_fields(
|
||||
def test_token_human_list_shows_lifecycle_status(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -2715,7 +2645,7 @@ def test_nonstandard_human_list_envelopes_are_actionable(
|
||||
visible: tuple[str, ...],
|
||||
hidden: tuple[str, ...],
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(http, "request", lambda *_a, **_k: FakeResponse(payload=payload))
|
||||
|
||||
assert cloud.run_cloud(command) == 0
|
||||
@@ -2765,7 +2695,7 @@ def test_chat_credentials_human_view_separates_attached_and_available_sources(
|
||||
}
|
||||
],
|
||||
}
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(http, "request", lambda *_a, **_k: FakeResponse(payload=payload))
|
||||
|
||||
command = ["chat", "credentials", "chat-id", "--scan-ids", "source-scan-id"]
|
||||
@@ -2991,7 +2921,7 @@ def test_named_human_list_views_match_api_fields(
|
||||
visible: tuple[str, ...],
|
||||
hidden: tuple[str, ...],
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(http, "request", lambda *_a, **_k: FakeResponse(payload=payload))
|
||||
|
||||
assert cloud.run_cloud(command) == 0
|
||||
@@ -3005,7 +2935,7 @@ def test_named_human_list_views_match_api_fields(
|
||||
def test_supply_chain_org_summary_human_view_shows_totals_and_repository_risk(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -3166,7 +3096,7 @@ def test_wrapped_detail_human_views_are_unwrapped_and_actionable(
|
||||
visible: tuple[str, ...],
|
||||
hidden: tuple[str, ...],
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(http, "request", lambda *_a, **_k: FakeResponse(payload=payload))
|
||||
|
||||
assert cloud.run_cloud(command) == 0
|
||||
@@ -3180,7 +3110,7 @@ def test_wrapped_detail_human_views_are_unwrapped_and_actionable(
|
||||
def test_trace_human_view_summarizes_events_and_preserves_selector(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
seen_query: dict[str, Any] = {}
|
||||
|
||||
def fake_trace_request(_method: str, _path: str, **kwargs: Any) -> FakeResponse:
|
||||
@@ -3276,7 +3206,7 @@ def test_trace_human_view_summarizes_events_and_preserves_selector(
|
||||
def test_paginated_human_list_shows_total_and_continuation_command(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -3378,7 +3308,7 @@ def test_page_pagination_explains_an_out_of_range_page() -> None:
|
||||
def test_human_detail_preserves_long_prose_beyond_table_cell_limit(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
description = (
|
||||
" ".join(["authorization context"] * 12) + " final-description-marker\nsecond-line-marker"
|
||||
)
|
||||
@@ -3456,7 +3386,7 @@ def test_large_vulnerability_detail_prioritizes_evidence_and_remediation() -> No
|
||||
def test_test_user_human_view_joins_latest_verification(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -3553,7 +3483,7 @@ def test_wide_knowledge_table_keeps_title_readable_with_long_identifiers() -> No
|
||||
def test_human_get_prioritizes_details_and_hides_internal_identity_fields(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
|
||||
@@ -6,16 +6,13 @@ import argparse
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from rich.console import Console
|
||||
|
||||
from strix.interface import cloud, platform_cli
|
||||
from strix.interface.cloud import http, render, runner, source_scan
|
||||
from strix.interface.cloud.source_upload import prepare_source
|
||||
from strix.interface.main import main as interface_main
|
||||
|
||||
|
||||
@@ -172,7 +169,7 @@ def test_connector_enrollment_command_is_complete_multiline_and_terminal_safe(
|
||||
" -e LABEL=before\x1b]52;c;copied\x07after \\\n"
|
||||
" ghcr.io/usestrix/connector:latest"
|
||||
)
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -296,8 +293,8 @@ def test_source_prompt_shows_paths_and_literal_confirmation(
|
||||
return "n"
|
||||
|
||||
monkeypatch.setattr(console, "input", answer)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(source_scan.sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(source_scan.sys.stdout, "isatty", lambda: True)
|
||||
args = argparse.Namespace(
|
||||
source=str(tmp_path),
|
||||
dry_run=False,
|
||||
@@ -327,7 +324,7 @@ def test_source_prompt_interruption_removes_temporary_archive(
|
||||
(tmp_path / "app.py").write_text("print('ok')\n", encoding="utf-8")
|
||||
console = Console(file=io.StringIO(), width=100)
|
||||
archive_paths: list[Path] = []
|
||||
original_prepare = prepare_source
|
||||
original_prepare = source_scan.prepare_source
|
||||
|
||||
def capture_bundle(*args: Any, **kwargs: Any) -> Any:
|
||||
bundle = original_prepare(*args, **kwargs)
|
||||
@@ -339,8 +336,8 @@ def test_source_prompt_interruption_removes_temporary_archive(
|
||||
|
||||
monkeypatch.setattr(source_scan, "prepare_source", capture_bundle)
|
||||
monkeypatch.setattr(console, "input", interrupt)
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(source_scan.sys.stdin, "isatty", lambda: True)
|
||||
monkeypatch.setattr(source_scan.sys.stdout, "isatty", lambda: True)
|
||||
args = argparse.Namespace(
|
||||
source=str(tmp_path),
|
||||
dry_run=False,
|
||||
@@ -397,7 +394,7 @@ def test_device_login_rejects_non_http_verification_url(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
platform_cli.requests,
|
||||
"post",
|
||||
lambda *_a, **_k: FakeResponse(
|
||||
{
|
||||
@@ -430,7 +427,7 @@ def test_boolean_query_values_are_lowercase_for_url_search_params(
|
||||
seen["params"] = kwargs.get("params")
|
||||
return FakeResponse({"items": []})
|
||||
|
||||
monkeypatch.setattr(requests, "request", fake_request)
|
||||
monkeypatch.setattr(http.requests, "request", fake_request)
|
||||
http.request("GET", "/test", query={"enabled": True, "disabled": False})
|
||||
assert seen["params"] == {"enabled": "true", "disabled": "false"}
|
||||
|
||||
@@ -484,7 +481,7 @@ def test_binary_response_refuses_to_write_to_a_terminal(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: Any,
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(runner.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -516,7 +513,7 @@ def test_binary_response_can_be_intentionally_redirected(
|
||||
return None
|
||||
|
||||
redirected = RedirectedStdout()
|
||||
monkeypatch.setattr(sys, "stdout", redirected)
|
||||
monkeypatch.setattr(runner.sys, "stdout", redirected)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
@@ -537,7 +534,7 @@ def test_redirected_binary_errors_never_append_diagnostics_to_stdout(
|
||||
def iter_content(self, *, chunk_size: int) -> Any:
|
||||
assert chunk_size == 1024 * 1024
|
||||
yield b"%PDF-partial"
|
||||
raise requests.ConnectionError("connection lost")
|
||||
raise http.requests.ConnectionError("connection lost")
|
||||
|
||||
response = (
|
||||
FakeResponse({"detail": "report rejected"}, status_code=500)
|
||||
@@ -648,7 +645,7 @@ def test_binary_download_streams_and_preserves_existing_file_on_failure(
|
||||
def iter_content(self, *, chunk_size: int) -> Any:
|
||||
assert chunk_size == 1024 * 1024
|
||||
yield b"partial"
|
||||
raise requests.ConnectionError("connection lost")
|
||||
raise http.requests.ConnectionError("connection lost")
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
@@ -773,7 +770,7 @@ def test_session_help_is_specific_and_human_whoami_shows_scopes(
|
||||
"scopes": ["scans:read", "organizations:read"],
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(platform_cli.sys.stdout, "isatty", lambda: True)
|
||||
assert cloud.run_cloud(["whoami", "--help"]) == 0
|
||||
who_help = capsys.readouterr().out
|
||||
assert "strix cloud whoami" in who_help
|
||||
@@ -787,7 +784,7 @@ def test_non_tty_whoami_and_logout_emit_json(
|
||||
) -> None:
|
||||
monkeypatch.delenv("STRIX_API_TOKEN", raising=False)
|
||||
monkeypatch.setattr(platform_cli, "AUTH_PATH", tmp_path / "auth.json")
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
|
||||
monkeypatch.setattr(platform_cli.sys.stdout, "isatty", lambda: False)
|
||||
platform_cli.save_record(
|
||||
{
|
||||
"api_token": "secret",
|
||||
@@ -802,7 +799,7 @@ def test_non_tty_whoami_and_logout_emit_json(
|
||||
assert json.loads(capsys.readouterr().out)["email"] == "[email protected]"
|
||||
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
platform_cli.requests,
|
||||
"delete",
|
||||
lambda *_args, **_kwargs: type("Response", (), {"status_code": 200})(),
|
||||
)
|
||||
@@ -834,7 +831,7 @@ def test_scope_picker_labels_match_the_server_presets() -> None:
|
||||
def test_noninteractive_login_never_prompts_for_workspace(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdin, "isatty", lambda: False)
|
||||
monkeypatch.setattr(platform_cli.sys.stdin, "isatty", lambda: False)
|
||||
console = Console(file=io.StringIO())
|
||||
console.input = lambda *_args, **_kwargs: pytest.fail("must not prompt") # type: ignore[method-assign]
|
||||
|
||||
@@ -899,10 +896,10 @@ def test_device_flow_slow_down_never_exceeds_the_poll_interval_cap(
|
||||
sleeps.append(seconds)
|
||||
now += seconds
|
||||
|
||||
monkeypatch.setattr(requests, "post", post)
|
||||
monkeypatch.setattr(platform_cli.requests, "post", post)
|
||||
monkeypatch.setattr(platform_cli, "_app_url", lambda: "https://example.test")
|
||||
monkeypatch.setattr(time, "monotonic", monotonic)
|
||||
monkeypatch.setattr(time, "sleep", sleep)
|
||||
monkeypatch.setattr(platform_cli.time, "monotonic", monotonic)
|
||||
monkeypatch.setattr(platform_cli.time, "sleep", sleep)
|
||||
|
||||
with pytest.raises(platform_cli.PlatformAuthError, match="expired"):
|
||||
platform_cli._run_device_flow(
|
||||
@@ -936,9 +933,9 @@ def test_device_flow_accepts_external_authkit_url_and_binds_token_origin(
|
||||
),
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr(requests, "post", lambda *_a, **_k: next(responses))
|
||||
monkeypatch.setattr(platform_cli.requests, "post", lambda *_a, **_k: next(responses))
|
||||
monkeypatch.setattr(platform_cli, "_app_url", lambda: "https://preview.strix.ai")
|
||||
monkeypatch.setattr(time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(platform_cli.time, "sleep", lambda _seconds: None)
|
||||
|
||||
record = platform_cli._run_device_flow(
|
||||
Console(file=io.StringIO()),
|
||||
@@ -987,7 +984,7 @@ def test_root_help_accepts_json_before_help_and_leaf_help_stays_specific(
|
||||
def test_non_tty_dispatcher_always_emits_structured_json(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: False)
|
||||
|
||||
assert cloud.run_cloud([]) == 0
|
||||
assert json.loads(capsys.readouterr().out)["command"] == "strix cloud"
|
||||
@@ -1023,7 +1020,7 @@ def test_source_upload_rejects_untrusted_destinations_before_reading_file(
|
||||
source.write_bytes(b"approved source")
|
||||
monkeypatch.setattr(http, "_app_url_override", "https://app.strix.ai")
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
http.requests,
|
||||
"put",
|
||||
lambda *_args, **_kwargs: pytest.fail("an untrusted URL must not receive source bytes"),
|
||||
)
|
||||
@@ -1062,7 +1059,7 @@ def test_source_upload_allows_only_managed_or_same_origin_storage(
|
||||
return response
|
||||
|
||||
monkeypatch.setattr(http, "_app_url_override", app_url)
|
||||
monkeypatch.setattr(requests, "put", put)
|
||||
monkeypatch.setattr(http.requests, "put", put)
|
||||
http.upload_file(signed_url, "upload-token", source)
|
||||
|
||||
assert request_options["allow_redirects"] is False
|
||||
@@ -1083,7 +1080,7 @@ def test_source_upload_refuses_redirects_without_following_them(
|
||||
return response
|
||||
|
||||
monkeypatch.setattr(http, "_app_url_override", "https://app.strix.ai")
|
||||
monkeypatch.setattr(requests, "put", put)
|
||||
monkeypatch.setattr(http.requests, "put", put)
|
||||
|
||||
with pytest.raises(http.CloudError, match="unexpected redirect"):
|
||||
http.upload_file(
|
||||
@@ -1098,7 +1095,7 @@ def test_source_upload_refuses_redirects_without_following_them(
|
||||
def test_one_time_api_token_has_save_now_warning(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(render.sys.stdout, "isatty", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
http,
|
||||
"request",
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from strix.interface import cloud
|
||||
from strix.interface.cloud import http, runner
|
||||
@@ -32,7 +30,7 @@ class FakeResponse:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _token(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_API_TOKEN", "idempotency-test-token")
|
||||
monkeypatch.setattr(time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(runner.time, "sleep", lambda _seconds: None)
|
||||
|
||||
|
||||
def test_scan_start_generates_and_sends_one_stable_key(
|
||||
@@ -199,7 +197,7 @@ def test_http_client_places_key_in_the_header(monkeypatch: pytest.MonkeyPatch) -
|
||||
seen.update(kwargs)
|
||||
return FakeResponse({"ok": True})
|
||||
|
||||
monkeypatch.setattr(requests, "request", request)
|
||||
monkeypatch.setattr(http.requests, "request", request)
|
||||
http.request("POST", "/scans", body={}, idempotency_key="header-key")
|
||||
assert seen["headers"]["Idempotency-Key"] == "header-key"
|
||||
assert seen["headers"]["Authorization"] == "Bearer idempotency-test-token"
|
||||
@@ -7,7 +7,6 @@ import urllib.request
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from strix.interface.cloud import payment_proxy
|
||||
|
||||
@@ -40,8 +39,7 @@ def _post(url: str, body: bytes, headers: dict[str, str] | None = None) -> bytes
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=2) as response: # noqa: S310
|
||||
body_bytes: bytes = response.read()
|
||||
return body_bytes
|
||||
return response.read()
|
||||
|
||||
|
||||
def test_bridge_bounds_decompressed_upstream_response(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@@ -52,7 +50,7 @@ def test_bridge_bounds_decompressed_upstream_response(monkeypatch: pytest.Monkey
|
||||
return response
|
||||
|
||||
monkeypatch.setattr(payment_proxy, "_MAX_UPSTREAM_RESPONSE_BYTES", 4)
|
||||
monkeypatch.setattr(requests, "request", fake_request)
|
||||
monkeypatch.setattr(payment_proxy.requests, "request", fake_request)
|
||||
|
||||
with payment_proxy.wallet_payment_bridge(
|
||||
upstream_url="https://app.example.test/api/v1/billing/topup",
|
||||
@@ -82,7 +80,7 @@ def test_bridge_forwards_only_the_approved_request_and_protected_headers(
|
||||
captured.append(kwargs)
|
||||
return _StreamingResponse([b'{"ok":true}'])
|
||||
|
||||
monkeypatch.setattr(requests, "request", fake_request)
|
||||
monkeypatch.setattr(payment_proxy.requests, "request", fake_request)
|
||||
with payment_proxy.wallet_payment_bridge(
|
||||
upstream_url="https://app.example.test/api/v1/billing/topup",
|
||||
api_token="strix-secret", # noqa: S106
|
||||
@@ -126,7 +124,7 @@ def test_bridge_limits_valid_wallet_attempts(monkeypatch: pytest.MonkeyPatch) ->
|
||||
calls += 1
|
||||
return _StreamingResponse([b"{}"])
|
||||
|
||||
monkeypatch.setattr(requests, "request", fake_request)
|
||||
monkeypatch.setattr(payment_proxy.requests, "request", fake_request)
|
||||
with payment_proxy.wallet_payment_bridge(
|
||||
upstream_url="https://app.example.test/api/v1/billing/topup",
|
||||
api_token="strix-secret", # noqa: S106
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from rich.console import Console
|
||||
|
||||
from strix.interface import cloud, platform_cli, platform_identity
|
||||
@@ -56,7 +55,7 @@ def test_http_workspace_pin_is_captured_once(
|
||||
sent.append(dict(kwargs["headers"]))
|
||||
return Response({})
|
||||
|
||||
monkeypatch.setattr(requests, "request", fake_request)
|
||||
monkeypatch.setattr(http.requests, "request", fake_request)
|
||||
http.configure()
|
||||
platform_cli.save_record(
|
||||
{
|
||||
@@ -117,7 +116,7 @@ def test_logout_keeps_local_token_when_remote_outcome_is_not_definitive(
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
requests,
|
||||
platform_cli.requests,
|
||||
"delete",
|
||||
lambda *_args, **_kwargs: Response({"detail": "unavailable"}, 503),
|
||||
)
|
||||
|
||||
@@ -655,44 +655,3 @@ def test_incomplete_upload_credentials_delete_the_reserved_upload(
|
||||
monkeypatch.setattr(http, "request", fake_request)
|
||||
assert cloud.run_cloud(["scans", "start", "--source", str(tmp_path), "--yes", "--json"]) == 1
|
||||
assert ("DELETE", "/uploads/upload-incomplete") in paths
|
||||
|
||||
|
||||
def test_archive_source_is_rejected_with_directory_guidance(tmp_path: Path) -> None:
|
||||
archive = tmp_path / "backend.zip"
|
||||
with zipfile.ZipFile(archive, "w") as bundle:
|
||||
bundle.writestr("app.py", "print('safe')\n")
|
||||
|
||||
with pytest.raises(http.CloudError, match="not an archive") as raised:
|
||||
source_upload.prepare_source(
|
||||
str(archive),
|
||||
include_hidden=False,
|
||||
include_sensitive=False,
|
||||
include_archives=False,
|
||||
exclude=[],
|
||||
)
|
||||
assert raised.value.next_step is not None
|
||||
assert "--source" in raised.value.next_step
|
||||
assert "--dry-run --show-files" in raised.value.next_step
|
||||
|
||||
|
||||
def test_oversize_archive_names_largest_files_and_exclude_guidance(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
(tmp_path / "app.py").write_text("print('safe')\n", encoding="utf-8")
|
||||
(tmp_path / "big.bin").write_bytes(os.urandom(4096))
|
||||
monkeypatch.setattr(source_upload, "MAX_ARCHIVE_BYTES", 1024)
|
||||
|
||||
with pytest.raises(http.CloudError, match=r"larger than the 0\.0 MiB upload limit") as raised:
|
||||
source_upload.prepare_source(
|
||||
str(tmp_path),
|
||||
include_hidden=False,
|
||||
include_sensitive=False,
|
||||
include_archives=False,
|
||||
exclude=[],
|
||||
)
|
||||
message = str(raised.value)
|
||||
assert message.index("big.bin") < message.index("app.py")
|
||||
assert raised.value.next_step is not None
|
||||
assert "--exclude" in raised.value.next_step
|
||||
assert "--dry-run --show-files" in raised.value.next_step
|
||||
assert not list(tmp_path.glob("strix-source-*.zip"))
|
||||
@@ -30,8 +30,6 @@ _LLM_ENV_KEYS = [
|
||||
"STRIX_FORCE_REQUIRED_TOOL_CHOICE",
|
||||
"LLM_TIMEOUT",
|
||||
"PERPLEXITY_API_KEY",
|
||||
"EXA_API_KEY",
|
||||
"STRIX_WEB_SEARCH_PROVIDER",
|
||||
# RuntimeSettings
|
||||
"STRIX_IMAGE",
|
||||
"STRIX_RUNTIME_BACKEND",
|
||||
@@ -82,17 +80,6 @@ def test_read_json_overrides_maps_to_nested_settings(tmp_path: Path) -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_read_json_overrides_maps_exa_and_provider(tmp_path: Path) -> None:
|
||||
path = tmp_path / "cli-config.json"
|
||||
path.write_text(
|
||||
json.dumps({"env": {"EXA_API_KEY": "exa-key", "STRIX_WEB_SEARCH_PROVIDER": "exa"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert loader._read_json_overrides(path) == {
|
||||
"integrations": {"exa_api_key": "exa-key", "web_search_provider": "exa"},
|
||||
}
|
||||
|
||||
|
||||
def test_read_json_overrides_skips_keys_already_in_environ(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
@@ -221,191 +208,6 @@ def test_persist_current_writes_env_block(tmp_path: Path, monkeypatch: pytest.Mo
|
||||
}
|
||||
|
||||
|
||||
def test_persist_current_keeps_file_values_when_env_is_unset(tmp_path: Path) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
assert loader.load_settings().llm.model == "file-model"
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}
|
||||
}
|
||||
|
||||
|
||||
def test_persist_current_env_overrides_file_value(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "file-model", "PERPLEXITY_API_KEY": "file-pplx"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("PERPLEXITY_API_KEY", "env-pplx")
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "file-model", "PERPLEXITY_API_KEY": "env-pplx"}
|
||||
}
|
||||
|
||||
|
||||
def test_linked_llm_model_change_drops_stored_key_and_base(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"env": {
|
||||
"STRIX_LLM": "file-model",
|
||||
"LLM_API_KEY": "file-key",
|
||||
"LLM_API_BASE": "http://file-base",
|
||||
"PERPLEXITY_API_KEY": "pplx",
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("STRIX_LLM", "env-model")
|
||||
|
||||
llm = loader.load_settings().llm
|
||||
assert llm.model == "env-model"
|
||||
assert llm.api_key is None
|
||||
assert llm.api_base is None
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "env-model", "PERPLEXITY_API_KEY": "pplx"}
|
||||
}
|
||||
|
||||
|
||||
def test_linked_llm_key_change_drops_stored_model(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("LLM_API_KEY", "new-key")
|
||||
|
||||
assert loader.load_settings().llm.model is None
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {"env": {"LLM_API_KEY": "new-key"}}
|
||||
|
||||
|
||||
def test_linked_llm_secondary_alias_in_env_is_not_a_change(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("LLM_API_KEY", "file-key")
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "unrelated-global-key")
|
||||
|
||||
llm = loader.load_settings().llm
|
||||
assert llm.model == "file-model"
|
||||
assert llm.api_key == "file-key"
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}
|
||||
}
|
||||
|
||||
|
||||
def test_linked_llm_unchanged_env_keeps_stored_key(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("STRIX_LLM", "file-model")
|
||||
|
||||
assert loader.load_settings().llm.api_key == "file-key"
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {
|
||||
"env": {"STRIX_LLM": "file-model", "LLM_API_KEY": "file-key"}
|
||||
}
|
||||
|
||||
|
||||
def test_persist_current_env_alias_replaces_other_alias_in_file(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(json.dumps({"env": {"OPENAI_API_KEY": "old-key"}}), encoding="utf-8")
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("LLM_API_KEY", "new-key")
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {"env": {"LLM_API_KEY": "new-key"}}
|
||||
|
||||
|
||||
def test_persist_current_empty_env_clears_file_value(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(
|
||||
json.dumps({"env": {"STRIX_LLM": "file-model", "PERPLEXITY_API_KEY": "pplx"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("PERPLEXITY_API_KEY", "")
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {"env": {"STRIX_LLM": "file-model"}}
|
||||
|
||||
|
||||
def test_persist_current_empty_primary_alias_does_not_save_sibling(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text(json.dumps({"env": {"PERPLEXITY_API_KEY": "pplx"}}), encoding="utf-8")
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("LLM_API_KEY", "")
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sibling-key")
|
||||
|
||||
assert loader.load_settings().llm.api_key == ""
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {"env": {"PERPLEXITY_API_KEY": "pplx"}}
|
||||
|
||||
|
||||
def test_persist_current_replaces_corrupt_file(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
target = tmp_path / "cli-config.json"
|
||||
target.write_text("{not json", encoding="utf-8")
|
||||
loader.apply_config_override(target)
|
||||
monkeypatch.setenv("STRIX_LLM", "env-model")
|
||||
|
||||
loader.persist_current()
|
||||
|
||||
assert json.loads(target.read_text(encoding="utf-8")) == {"env": {"STRIX_LLM": "env-model"}}
|
||||
|
||||
|
||||
def test_persist_current_sets_0600_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "persisted-model")
|
||||
target = tmp_path / "cli-config.json"
|
||||
|
||||
@@ -114,14 +114,7 @@ def test_config_file_loads_dedupe_model(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
for key in (
|
||||
"STRIX_LLM",
|
||||
"LLM_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"LLM_API_BASE",
|
||||
"STRIX_DEDUPE_MODEL",
|
||||
"STRIX_DEDUPE_REASONING_EFFORT",
|
||||
):
|
||||
for key in ("STRIX_LLM", "STRIX_DEDUPE_MODEL", "STRIX_DEDUPE_REASONING_EFFORT"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
path = tmp_path / "config.json"
|
||||
path.write_text(
|
||||
|
||||
@@ -32,7 +32,6 @@ from openai.types.responses import (
|
||||
from strix.config import codex, loader
|
||||
from strix.config.loader import load_settings
|
||||
from strix.config.models import StrixProvider, _NonStreamingModel, _TurnGuardModel
|
||||
from strix.llm.request_log import RequestLoggingModel
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -313,8 +312,7 @@ def test_get_model_keeps_streaming_by_default(
|
||||
|
||||
model = StrixProvider().get_model("openai/gpt-4o-mini")
|
||||
assert isinstance(model, _TurnGuardModel)
|
||||
assert isinstance(model._inner, RequestLoggingModel)
|
||||
assert model._inner._inner is inner
|
||||
assert model._inner is inner
|
||||
|
||||
|
||||
def test_get_model_guards_subscription_model_but_keeps_it_streaming(
|
||||
|
||||
@@ -12,7 +12,6 @@ would let it escape and surface a traceback on every teardown.
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -23,10 +22,6 @@ from requests.exceptions import ConnectionError as RequestsConnectionError
|
||||
from strix.runtime.docker_client import StrixDockerSandboxClient
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents.sandbox.session.sandbox_session import SandboxSession
|
||||
|
||||
|
||||
def _client_with_kill_error(exc: Exception) -> StrixDockerSandboxClient:
|
||||
"""A StrixDockerSandboxClient whose containers.get(...).kill() raises ``exc``."""
|
||||
client = StrixDockerSandboxClient.__new__(StrixDockerSandboxClient)
|
||||
@@ -36,10 +31,9 @@ def _client_with_kill_error(exc: Exception) -> StrixDockerSandboxClient:
|
||||
return client
|
||||
|
||||
|
||||
def _session(container_id: str | None = "abc123") -> SandboxSession:
|
||||
def _session() -> object:
|
||||
# delete() reads session._inner.state.container_id
|
||||
fake = SimpleNamespace(_inner=SimpleNamespace(state=SimpleNamespace(container_id=container_id)))
|
||||
return cast("SandboxSession", fake)
|
||||
return SimpleNamespace(_inner=SimpleNamespace(state=SimpleNamespace(container_id="abc123")))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -51,7 +45,7 @@ def _session(container_id: str | None = "abc123") -> SandboxSession:
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_swallows_best_effort_kill_errors(exc: Exception) -> None:
|
||||
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()."""
|
||||
@@ -68,7 +62,7 @@ async def test_delete_swallows_best_effort_kill_errors(exc: Exception) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_does_not_swallow_unrelated_errors() -> None:
|
||||
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"))
|
||||
@@ -77,11 +71,11 @@ async def test_delete_does_not_swallow_unrelated_errors() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_noop_without_container_id() -> None:
|
||||
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 = _session(container_id=None)
|
||||
session = SimpleNamespace(_inner=SimpleNamespace(state=SimpleNamespace(container_id=None)))
|
||||
|
||||
with patch.object(
|
||||
DockerSandboxClient, "delete", new=AsyncMock(return_value=session)
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from agents import RunConfig, Runner
|
||||
from openai import APIError, PermissionDeniedError
|
||||
|
||||
from strix.core import execution
|
||||
from strix.core.agents import AgentCoordinator
|
||||
from strix.llm import request_log
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
|
||||
def _request() -> httpx.Request:
|
||||
return httpx.Request("POST", "https://api.openai.com/v1/responses")
|
||||
|
||||
|
||||
def _midstream_api_error() -> APIError:
|
||||
return APIError("An error occurred while processing the request.", _request(), body=None)
|
||||
|
||||
|
||||
def _blocked_error() -> PermissionDeniedError:
|
||||
response = httpx.Response(
|
||||
403, request=_request(), headers={"x-request-id": "req_blocked_hdr"}, text="blocked"
|
||||
)
|
||||
return PermissionDeniedError("Output blocked by policy", response=response, body=None)
|
||||
|
||||
|
||||
class _FakeStream:
|
||||
def __init__(self, exc: BaseException | None = None) -> None:
|
||||
self._exc = exc
|
||||
self.run_loop_exception: BaseException | None = None
|
||||
self.seen_context: request_log.LlmCallContext | None = None
|
||||
|
||||
async def stream_events(self) -> AsyncIterator[Any]:
|
||||
self.seen_context = request_log.current_call_context()
|
||||
if self._exc is not None:
|
||||
raise self._exc
|
||||
items: tuple[Any, ...] = ()
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
|
||||
async def _run(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
streams: list[_FakeStream],
|
||||
coordinator: AgentCoordinator | None = None,
|
||||
) -> Any:
|
||||
monkeypatch.setattr(execution, "_TRANSIENT_MODEL_RETRY_BASE_DELAY_S", 0.0)
|
||||
monkeypatch.setattr(execution, "_TRANSIENT_MODEL_RETRY_MAX_DELAY_S", 0.0)
|
||||
calls = {"n": 0}
|
||||
|
||||
def _fake_run_streamed(*_args: Any, **_kwargs: Any) -> _FakeStream:
|
||||
stream = streams[calls["n"]]
|
||||
calls["n"] += 1
|
||||
return stream
|
||||
|
||||
monkeypatch.setattr(Runner, "run_streamed", _fake_run_streamed)
|
||||
coordinator = coordinator or AgentCoordinator()
|
||||
await coordinator.register("root", "strix", parent_id=None)
|
||||
return await execution._run_cycle(
|
||||
object(),
|
||||
coordinator,
|
||||
"root",
|
||||
input_data="task",
|
||||
run_config=cast("RunConfig", object()),
|
||||
context={},
|
||||
max_turns=5,
|
||||
session=None,
|
||||
interactive=False,
|
||||
event_sink=None,
|
||||
hooks=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_each_transient_replay_is_stamped_with_its_attempt_number(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
streams = [
|
||||
_FakeStream(exc=_midstream_api_error()),
|
||||
_FakeStream(exc=_midstream_api_error()),
|
||||
_FakeStream(),
|
||||
]
|
||||
await _run(monkeypatch, streams)
|
||||
assert [s.seen_context.retry_attempt for s in streams if s.seen_context] == [0, 1, 2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocked_provider_failure_text_carries_request_id(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
coordinator = AgentCoordinator()
|
||||
with pytest.raises(PermissionDeniedError):
|
||||
await _run(monkeypatch, [_FakeStream(exc=_blocked_error())], coordinator)
|
||||
assert coordinator.statuses["root"] == "failed"
|
||||
error = coordinator.errors["root"]
|
||||
assert "Output blocked by policy" in error
|
||||
assert error.endswith("[provider request id: req_blocked_hdr]")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agent_loop_binds_agent_context_and_resets(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
seen: dict[str, request_log.LlmCallContext] = {}
|
||||
|
||||
async def _fake_loop(**_kwargs: Any) -> None:
|
||||
seen["ctx"] = request_log.current_call_context()
|
||||
|
||||
monkeypatch.setattr(execution, "_run_agent_loop", _fake_loop)
|
||||
|
||||
class _Agent:
|
||||
name = "Recon Agent"
|
||||
|
||||
coordinator = AgentCoordinator()
|
||||
await execution.run_agent_loop(
|
||||
agent=_Agent(),
|
||||
initial_input="task",
|
||||
run_config=cast("RunConfig", object()),
|
||||
context={},
|
||||
max_turns=1,
|
||||
coordinator=coordinator,
|
||||
agent_id="agent-42",
|
||||
interactive=False,
|
||||
)
|
||||
assert seen["ctx"].agent_id == "agent-42"
|
||||
assert seen["ctx"].agent_name == "Recon Agent"
|
||||
assert request_log.current_call_context().agent_id is None
|
||||
@@ -1,854 +0,0 @@
|
||||
"""Tests for fix candidate anchoring and repository preparation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.fix.contracts import (
|
||||
BlockerKind,
|
||||
CandidateLocation,
|
||||
CheckResult,
|
||||
CheckStatus,
|
||||
CommandSpec,
|
||||
FixCandidateV1,
|
||||
FixEdit,
|
||||
FixPreparationRequestV1,
|
||||
PreparationBlocker,
|
||||
PreparationState,
|
||||
RepairOutcome,
|
||||
RepairStatus,
|
||||
ReproductionSpec,
|
||||
SourceIdentity,
|
||||
SourceIdentityKind,
|
||||
VerificationDecision,
|
||||
VerificationTarget,
|
||||
VerifierResult,
|
||||
candidate_from_legacy_report,
|
||||
)
|
||||
from strix.fix.locations import AnchorStatus, anchor_location
|
||||
from strix.fix.prepare import (
|
||||
PreparationContext,
|
||||
_network_isolation_prefix,
|
||||
build_git_manifest,
|
||||
prepare_fix,
|
||||
run_command,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _git(workspace: Path, *args: str) -> str:
|
||||
return subprocess.run( # noqa: S603
|
||||
["/usr/bin/git", *args],
|
||||
cwd=workspace,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def _workspace(tmp_path: Path) -> tuple[Path, str]:
|
||||
workspace = tmp_path / "repo"
|
||||
workspace.mkdir()
|
||||
_git(workspace, "init")
|
||||
_git(workspace, "config", "user.email", "[email protected]")
|
||||
_git(workspace, "config", "user.name", "Test")
|
||||
(workspace / "app.py").write_text("def result():\n return 'unsafe'\n", encoding="utf-8")
|
||||
_git(workspace, "add", "app.py")
|
||||
_git(workspace, "commit", "-m", "initial")
|
||||
return workspace, _git(workspace, "rev-parse", "HEAD")
|
||||
|
||||
|
||||
def _candidate(
|
||||
commit: str,
|
||||
*,
|
||||
reproduction: ReproductionSpec | None = None,
|
||||
) -> FixCandidateV1:
|
||||
if reproduction is None:
|
||||
reproduction = ReproductionSpec(
|
||||
instructions="Confirm that result returns safe.",
|
||||
command=CommandSpec(
|
||||
name="security reproduction",
|
||||
argv=[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"from app import result; assert result() == 'safe'",
|
||||
],
|
||||
),
|
||||
)
|
||||
return FixCandidateV1(
|
||||
source_identity=SourceIdentity(kind=SourceIdentityKind.COMMIT, value=commit),
|
||||
security_invariant="Return a safe value.",
|
||||
finding_locations=[
|
||||
CandidateLocation(
|
||||
file="app.py",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
snippet="def result():\n return 'unsafe'",
|
||||
)
|
||||
],
|
||||
draft_edits=[
|
||||
FixEdit(
|
||||
file="app.py",
|
||||
start_line=2,
|
||||
end_line=2,
|
||||
before=" return 'unsafe'",
|
||||
after=" return 'safe'",
|
||||
)
|
||||
],
|
||||
reproduction=reproduction,
|
||||
)
|
||||
|
||||
|
||||
def _request(candidate: FixCandidateV1, *, attempts: int = 2) -> FixPreparationRequestV1:
|
||||
return FixPreparationRequestV1(
|
||||
scan_id="scan-1",
|
||||
finding_id="finding-1",
|
||||
candidate=candidate,
|
||||
checks=[
|
||||
CommandSpec(
|
||||
name="compile",
|
||||
argv=[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"compile(open('app.py', encoding='utf-8').read(), 'app.py', 'exec')",
|
||||
],
|
||||
)
|
||||
],
|
||||
max_repair_attempts=attempts,
|
||||
)
|
||||
|
||||
|
||||
async def _noop_repair(
|
||||
context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
path = context.workspace / "app.py"
|
||||
if path.exists():
|
||||
path.write_text(
|
||||
path.read_text(encoding="utf-8").replace("'unsafe'", "'safe'"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return RepairOutcome(
|
||||
status=RepairStatus.COMPLETE,
|
||||
summary="The repository fix is ready for independent evaluation.",
|
||||
)
|
||||
|
||||
|
||||
async def _verified(
|
||||
_context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
return VerifierResult(
|
||||
decision=VerificationDecision.VERIFIED,
|
||||
summary="The invariant is closed.",
|
||||
security_invariant_closed=True,
|
||||
reproduction_executed=True,
|
||||
reproduction_summary="The vulnerable input is rejected.",
|
||||
sibling_paths_reviewed=["app.py"],
|
||||
preserved_behaviors=["The module compiles."],
|
||||
security_tests=[
|
||||
CheckResult(
|
||||
name="security reproduction",
|
||||
argv=[sys.executable, "-c", "assert True"],
|
||||
status=CheckStatus.PASSED,
|
||||
exit_code=0,
|
||||
duration_seconds=0,
|
||||
target=VerificationTarget.PATCHED,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_from_legacy_report_preserves_draft_and_checks() -> None:
|
||||
candidate = candidate_from_legacy_report(
|
||||
{
|
||||
"remediation_steps": "Reject unsafe input.",
|
||||
"poc_description": "Call the vulnerable function.",
|
||||
"fix_verification": "Reasoned about the patched branch.",
|
||||
"code_locations": [
|
||||
{
|
||||
"file": "src/app.py",
|
||||
"start_line": 9,
|
||||
"end_line": 9,
|
||||
"snippet": "sink(value)",
|
||||
"fix_before": "sink(value)",
|
||||
"fix_after": "sink(clean(value))",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert candidate is not None
|
||||
assert candidate.security_invariant == "Reject unsafe input."
|
||||
assert candidate.draft_edits[0].after == "sink(clean(value))"
|
||||
assert candidate.reported_checks[0].executed is False
|
||||
assert candidate.known_gaps == ["The reporting-agent verification is not independent."]
|
||||
|
||||
|
||||
def test_anchor_location_rewrites_invented_line_numbers(tmp_path: Path) -> None:
|
||||
(tmp_path / "app.py").write_text("first\nsecond\ntarget\nlast\n", encoding="utf-8")
|
||||
location = CandidateLocation(
|
||||
file="app.py",
|
||||
start_line=99,
|
||||
end_line=99,
|
||||
snippet="target",
|
||||
)
|
||||
|
||||
result = anchor_location(tmp_path, location)
|
||||
|
||||
assert result.status is AnchorStatus.UNIQUE
|
||||
assert result.location.start_line == 3
|
||||
assert result.location.end_line == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("content", "expected"),
|
||||
[
|
||||
("first\nlast\n", AnchorStatus.MISSING),
|
||||
("target\nmiddle\ntarget\n", AnchorStatus.AMBIGUOUS),
|
||||
],
|
||||
)
|
||||
def test_anchor_location_rejects_non_unique_source(
|
||||
tmp_path: Path,
|
||||
content: str,
|
||||
expected: AnchorStatus,
|
||||
) -> None:
|
||||
(tmp_path / "app.py").write_text(content, encoding="utf-8")
|
||||
edit = FixEdit(
|
||||
file="app.py",
|
||||
start_line=1,
|
||||
end_line=1,
|
||||
before="target",
|
||||
after="safe",
|
||||
)
|
||||
|
||||
assert anchor_location(tmp_path, edit).status is expected
|
||||
|
||||
|
||||
def test_anchor_location_detects_stale_file_digest(tmp_path: Path) -> None:
|
||||
original = "target\n"
|
||||
(tmp_path / "app.py").write_text("changed\n", encoding="utf-8")
|
||||
edit = FixEdit(
|
||||
file="app.py",
|
||||
start_line=1,
|
||||
end_line=1,
|
||||
before="target",
|
||||
after="safe",
|
||||
original_sha256=hashlib.sha256(original.encode()).hexdigest(),
|
||||
)
|
||||
|
||||
assert anchor_location(tmp_path, edit).status is AnchorStatus.STALE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_returns_ready_with_manifest(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.READY
|
||||
assert result.attempts == 1
|
||||
assert [entry.path for entry in result.final_file_manifest] == ["app.py"]
|
||||
assert result.security_reproduction is not None
|
||||
assert result.security_reproduction.target is VerificationTarget.PATCHED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_does_not_apply_draft_before_repair(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
observed_source: list[str] = []
|
||||
|
||||
async def repair(
|
||||
context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
observed_source.append((context.workspace / "app.py").read_text(encoding="utf-8"))
|
||||
(context.workspace / "app.py").write_text(
|
||||
"def result():\n return 'safe'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return RepairOutcome(
|
||||
status=RepairStatus.COMPLETE,
|
||||
summary="Implemented the repair from repository context.",
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=repair,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.READY
|
||||
assert observed_source == ["def result():\n return 'unsafe'\n"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quality_gate_retries_once_before_verification(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
compile_calls = 0
|
||||
repair_calls = 0
|
||||
verification_calls = 0
|
||||
|
||||
async def runner(_workspace: Path, command: CommandSpec) -> CheckResult:
|
||||
nonlocal compile_calls
|
||||
compile_calls += 1
|
||||
failed = compile_calls == 1
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.FAILED if failed else CheckStatus.PASSED,
|
||||
exit_code=1 if failed else 0,
|
||||
duration_seconds=0,
|
||||
required=command.required,
|
||||
)
|
||||
|
||||
async def repair(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
nonlocal repair_calls
|
||||
repair_calls += 1
|
||||
assert checks == [] if repair_calls == 1 else checks[0].status is CheckStatus.FAILED
|
||||
(context.workspace / "app.py").write_text(
|
||||
"def result():\n return 'safe'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return RepairOutcome(status=RepairStatus.COMPLETE, summary="Repair complete.")
|
||||
|
||||
async def verify(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
nonlocal verification_calls
|
||||
verification_calls += 1
|
||||
return await _verified(context, checks)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=repair,
|
||||
verify=verify,
|
||||
command_runner=runner,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.READY
|
||||
assert repair_calls == 2
|
||||
assert compile_calls == 2
|
||||
assert verification_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failed_quality_gate_never_runs_security_verifier(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
verifier_called = False
|
||||
|
||||
async def runner(_workspace: Path, command: CommandSpec) -> CheckResult:
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.FAILED,
|
||||
exit_code=1,
|
||||
duration_seconds=0,
|
||||
required=command.required,
|
||||
)
|
||||
|
||||
async def verify(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
nonlocal verifier_called
|
||||
verifier_called = True
|
||||
return await _verified(context, checks)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit), attempts=1),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=verify,
|
||||
command_runner=runner,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.FAILED
|
||||
assert verifier_called is False
|
||||
assert "quality gate" in result.stop_reason
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repository_baseline_failure_is_a_typed_blocker(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
|
||||
async def runner(_workspace: Path, command: CommandSpec) -> CheckResult:
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.FAILED,
|
||||
exit_code=1,
|
||||
duration_seconds=0,
|
||||
output="existing failure",
|
||||
required=command.required,
|
||||
baseline_status=CheckStatus.FAILED,
|
||||
baseline_output="existing failure",
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=_verified,
|
||||
command_runner=runner,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.BLOCKED
|
||||
assert result.blocker is not None
|
||||
assert result.blocker.kind is BlockerKind.REPOSITORY_BASELINE
|
||||
assert result.attempts == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_different_candidate_and_baseline_failures_are_repairable(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
repairs = 0
|
||||
|
||||
async def repair(
|
||||
_context: PreparationContext,
|
||||
_feedback: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
nonlocal repairs
|
||||
repairs += 1
|
||||
return await _noop_repair(_context, _feedback)
|
||||
|
||||
async def runner(_workspace: Path, command: CommandSpec) -> CheckResult:
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.FAILED,
|
||||
exit_code=1,
|
||||
duration_seconds=0,
|
||||
output="candidate-specific failure",
|
||||
required=command.required,
|
||||
baseline_status=CheckStatus.FAILED,
|
||||
baseline_output="different pre-existing failure",
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=repair,
|
||||
verify=_verified,
|
||||
command_runner=runner,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.FAILED
|
||||
assert result.blocker is None
|
||||
assert result.attempts == 2
|
||||
assert repairs == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unavailable_required_check_is_a_typed_blocker(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
|
||||
async def runner(_workspace: Path, command: CommandSpec) -> CheckResult:
|
||||
return CheckResult(
|
||||
name=command.name,
|
||||
argv=command.argv,
|
||||
status=CheckStatus.UNAVAILABLE,
|
||||
duration_seconds=0,
|
||||
output="compiler unavailable",
|
||||
required=command.required,
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=_verified,
|
||||
command_runner=runner,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.BLOCKED
|
||||
assert result.blocker is not None
|
||||
assert result.blocker.kind is BlockerKind.ENVIRONMENT
|
||||
assert result.attempts == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repairable_security_rejection_gets_one_retry(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
repair_feedback: list[list[str]] = []
|
||||
verifier_calls = 0
|
||||
|
||||
async def repair(
|
||||
context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
repair_feedback.append(
|
||||
[
|
||||
gap
|
||||
for attempt in context.feedback
|
||||
if attempt.verifier is not None
|
||||
for gap in attempt.verifier.gaps
|
||||
]
|
||||
)
|
||||
(context.workspace / "app.py").write_text(
|
||||
"def result():\n return 'safe'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return RepairOutcome(status=RepairStatus.COMPLETE, summary="Repair complete.")
|
||||
|
||||
async def verify(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
nonlocal verifier_calls
|
||||
verifier_calls += 1
|
||||
if verifier_calls == 1:
|
||||
return VerifierResult(
|
||||
decision=VerificationDecision.REJECTED,
|
||||
summary="A sibling path remains vulnerable.",
|
||||
gaps=["Harden the sibling path."],
|
||||
repairable=True,
|
||||
)
|
||||
return await _verified(context, checks)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=repair,
|
||||
verify=verify,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.READY
|
||||
assert result.attempts == 2
|
||||
assert repair_feedback == [[], ["Harden the sibling path."]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_blocker_stops_without_repair_retry(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
repair_calls = 0
|
||||
blocker = PreparationBlocker(
|
||||
kind=BlockerKind.EXTERNAL_CONFIGURATION,
|
||||
summary="A production account mapping is unavailable.",
|
||||
user_action="Provide the production account mapping.",
|
||||
)
|
||||
|
||||
async def repair(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
nonlocal repair_calls
|
||||
repair_calls += 1
|
||||
return await _noop_repair(context, checks)
|
||||
|
||||
async def verify(
|
||||
_context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
return VerifierResult(
|
||||
decision=VerificationDecision.INCONCLUSIVE,
|
||||
summary=blocker.summary,
|
||||
blocker=blocker,
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=repair,
|
||||
verify=verify,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.BLOCKED
|
||||
assert result.blocker == blocker
|
||||
assert repair_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_budget_exhaustion_fails_without_verification(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
verifier_called = False
|
||||
|
||||
async def exhausted(
|
||||
context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
(context.workspace / "app.py").write_text(
|
||||
"def result():\n return 'safe'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return RepairOutcome(
|
||||
status=RepairStatus.BUDGET_EXHAUSTED,
|
||||
summary="The repair hit its emergency turn ceiling after editing the patch.",
|
||||
turns_used=40,
|
||||
)
|
||||
|
||||
async def verify(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
nonlocal verifier_called
|
||||
verifier_called = True
|
||||
return await _verified(context, checks)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=exhausted,
|
||||
verify=verify,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.FAILED
|
||||
assert verifier_called is False
|
||||
assert result.changed_files == ["app.py"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_preserves_explicit_blocked_outcome(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
blocker = PreparationBlocker(
|
||||
kind=BlockerKind.CREDENTIAL,
|
||||
summary="A required credential is unavailable.",
|
||||
user_action="Provide a repository-scoped test credential.",
|
||||
)
|
||||
|
||||
async def blocked(
|
||||
_context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
return RepairOutcome(
|
||||
status=RepairStatus.BLOCKED,
|
||||
summary=blocker.summary,
|
||||
blocker=blocker,
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=blocked,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.BLOCKED
|
||||
assert result.blocker == blocker
|
||||
assert result.verifier is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_requires_verifier_security_test(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
|
||||
async def verify_without_test(
|
||||
_context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
return VerifierResult(
|
||||
decision=VerificationDecision.VERIFIED,
|
||||
summary="The verifier did not execute the issue-specific test.",
|
||||
security_invariant_closed=True,
|
||||
reproduction_executed=False,
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=verify_without_test,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.FAILED
|
||||
assert "did not execute a security test" in " ".join(result.gaps)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_requires_closed_security_invariant(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
|
||||
async def incomplete(
|
||||
context: PreparationContext,
|
||||
checks: list[CheckResult],
|
||||
) -> VerifierResult:
|
||||
verified = await _verified(context, checks)
|
||||
return verified.model_copy(update={"security_invariant_closed": False})
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=incomplete,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.FAILED
|
||||
assert "invariant" in " ".join(result.gaps)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_requires_a_source_change(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
|
||||
async def no_change(
|
||||
_context: PreparationContext,
|
||||
_checks: list[CheckResult],
|
||||
) -> RepairOutcome:
|
||||
return RepairOutcome(status=RepairStatus.COMPLETE, summary="No change.")
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=no_change,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.FAILED
|
||||
assert result.final_file_manifest == []
|
||||
assert "change repository source" in result.stop_reason
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_rejects_wrong_source_commit(tmp_path: Path) -> None:
|
||||
workspace, _commit = _workspace(tmp_path)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate("0" * 40)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.STALE
|
||||
assert result.blocker is not None
|
||||
assert result.blocker.kind is BlockerKind.SOURCE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_does_not_apply_escaping_draft_edit(tmp_path: Path) -> None:
|
||||
workspace, _commit = _workspace(tmp_path)
|
||||
outside = tmp_path / "outside.txt"
|
||||
outside.write_text("target\n", encoding="utf-8")
|
||||
(workspace / "link.txt").symlink_to(outside)
|
||||
_git(workspace, "add", "link.txt")
|
||||
_git(workspace, "commit", "-m", "add link")
|
||||
candidate = _candidate(_git(workspace, "rev-parse", "HEAD")).model_copy(
|
||||
update={
|
||||
"draft_edits": [
|
||||
FixEdit(
|
||||
file="link.txt",
|
||||
start_line=1,
|
||||
end_line=1,
|
||||
before="target",
|
||||
after="safe",
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(candidate),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.READY
|
||||
assert outside.read_text(encoding="utf-8") == "target\n"
|
||||
assert "link.txt" not in result.changed_files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_fix_rejects_dirty_workspace(tmp_path: Path) -> None:
|
||||
workspace, commit = _workspace(tmp_path)
|
||||
(workspace / "stray.txt").write_text("unrelated\n", encoding="utf-8")
|
||||
|
||||
result = await prepare_fix(
|
||||
_request(_candidate(commit)),
|
||||
workspace,
|
||||
repair=_noop_repair,
|
||||
verify=_verified,
|
||||
)
|
||||
|
||||
assert result.state is PreparationState.STALE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_git_manifest_lists_files_inside_new_directory(tmp_path: Path) -> None:
|
||||
workspace, _commit = _workspace(tmp_path)
|
||||
package = workspace / "pkg"
|
||||
package.mkdir()
|
||||
(package / "mod.py").write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
entries, _summary, _artifact = await build_git_manifest(workspace)
|
||||
|
||||
paths = {entry.path for entry in entries}
|
||||
assert "pkg/mod.py" in paths
|
||||
entry = next(entry for entry in entries if entry.path == "pkg/mod.py")
|
||||
assert entry.operation == "add"
|
||||
assert entry.resulting_sha256 is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_command_drops_ambient_credentials(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_AMBIENT_TOKEN", "hunter2")
|
||||
command = CommandSpec(
|
||||
name="env probe",
|
||||
argv=[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import os; print(os.environ.get('STRIX_AMBIENT_TOKEN', '<absent>'))",
|
||||
],
|
||||
)
|
||||
|
||||
sealed = await run_command(tmp_path, command, network_allowed=True)
|
||||
assert sealed.status is CheckStatus.PASSED
|
||||
assert "<absent>" in sealed.output
|
||||
|
||||
granted = await run_command(
|
||||
tmp_path,
|
||||
command,
|
||||
credentials_allowed=["STRIX_AMBIENT_TOKEN"],
|
||||
network_allowed=True,
|
||||
)
|
||||
assert granted.status is CheckStatus.PASSED
|
||||
assert "hunter2" in granted.output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_command_blocks_egress_when_network_not_allowed(tmp_path: Path) -> None:
|
||||
command = CommandSpec(
|
||||
name="egress probe",
|
||||
argv=[
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
"import socket, sys; s = socket.socket(); s.settimeout(3); "
|
||||
"sys.exit(0 if s.connect_ex(('1.1.1.1', 53)) == 0 else 1)"
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
result = await run_command(tmp_path, command)
|
||||
|
||||
if _network_isolation_prefix() is None:
|
||||
assert result.status is CheckStatus.UNAVAILABLE
|
||||
assert "not run" in result.output
|
||||
else:
|
||||
assert result.status is CheckStatus.FAILED
|
||||
+48
-166
@@ -19,7 +19,6 @@ from strix.config.settings import DEFAULT_MAX_TURNS
|
||||
from strix.interface.tui import runtime as go_tui
|
||||
from strix.interface.tui import sidecar
|
||||
from strix.interface.tui.runtime import GoTuiRuntime
|
||||
from strix.report.state import ReportState
|
||||
|
||||
|
||||
def args() -> argparse.Namespace:
|
||||
@@ -344,19 +343,15 @@ async def test_setup_preflights_model_before_starting(
|
||||
assert candidate.scope_mode == "diff"
|
||||
assert candidate.diff_base == "origin/main"
|
||||
|
||||
monkeypatch.setattr(go_tui, "persist_current", lambda: calls.append("persist"))
|
||||
monkeypatch.setattr(go_tui, "build_targets_info", build)
|
||||
monkeypatch.setattr(go_tui, "prepare_run", prepare)
|
||||
monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: calls.append("telemetry"))
|
||||
monkeypatch.setattr(runtime, "init_run_state", lambda: calls.append("state"))
|
||||
monkeypatch.setattr(runtime, "start_scan", lambda: calls.append("scan"))
|
||||
|
||||
# The controller runs these two in turn for every setup launch.
|
||||
await runtime.ensure_model_verified()
|
||||
await runtime.start_from_setup()
|
||||
|
||||
# The same steps, in the same order, as a direct launch's prepare_and_start.
|
||||
assert calls == ["preflight", "persist", "targets", "prepare", "telemetry", "state", "scan"]
|
||||
assert calls == ["preflight", "targets", "prepare", "telemetry", "state", "scan"]
|
||||
assert runtime.args.scan_mode == "quick"
|
||||
assert runtime.args.instruction == ""
|
||||
assert runtime.args.max_budget_usd == 8.5
|
||||
@@ -365,138 +360,35 @@ async def test_setup_preflights_model_before_starting(
|
||||
assert runtime.args.diff_base == "origin/main"
|
||||
|
||||
|
||||
def _setup_model(
|
||||
monkeypatch: pytest.MonkeyPatch, model: str | None = "openrouter/test-model"
|
||||
@pytest.mark.asyncio
|
||||
async def test_optimistic_setup_skips_model_preflight(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
runtime.controller.targets = [str(Path.cwd())]
|
||||
calls: list[str] = []
|
||||
|
||||
async def preflight(_model: str) -> None:
|
||||
calls.append("preflight")
|
||||
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"load_settings",
|
||||
lambda: SimpleNamespace(llm=SimpleNamespace(model=model)),
|
||||
lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
|
||||
)
|
||||
|
||||
|
||||
def _setup_messages(runtime: GoTuiRuntime) -> list[tuple[str, str]]:
|
||||
return [(message["level"], message["text"]) for message in runtime.controller.messages]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_model_check_reports_success_in_the_setup_log(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
calls: list[str] = []
|
||||
|
||||
async def preflight(model: str) -> None:
|
||||
calls.append(model)
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
monkeypatch.setattr(go_tui, "build_targets_info", lambda _args, **_kw: calls.append("targets"))
|
||||
monkeypatch.setattr(go_tui, "prepare_run", lambda _args: calls.append("prepare"))
|
||||
monkeypatch.setattr(go_tui, "telemetry_start", lambda _args: calls.append("telemetry"))
|
||||
monkeypatch.setattr(runtime, "init_run_state", lambda: calls.append("state"))
|
||||
monkeypatch.setattr(runtime, "start_scan", lambda: calls.append("scan"))
|
||||
|
||||
await runtime.check_setup_model()
|
||||
await runtime.start_from_setup(verify=False)
|
||||
|
||||
assert calls == ["openrouter/test-model"]
|
||||
assert runtime.model_verified is True
|
||||
assert _setup_messages(runtime) == [
|
||||
("info", "Verifying model connection..."),
|
||||
("info", "Model connection verified"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_model_check_reports_failure_without_leaving_setup(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
|
||||
async def preflight(_model: str) -> None:
|
||||
raise TimeoutError("connection timed out")
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
|
||||
await runtime.check_setup_model()
|
||||
|
||||
assert runtime.model_verified is False
|
||||
assert runtime.controller.setup_mode is True
|
||||
assert runtime.controller.scan_state == "setup"
|
||||
assert _setup_messages(runtime)[-1] == (
|
||||
"error",
|
||||
"Model connection failed: connection timed out",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_model_check_waits_for_a_configured_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
|
||||
_setup_model(monkeypatch, model=None)
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"preflight_model_connection",
|
||||
lambda _model: pytest.fail("nothing to check without a model"),
|
||||
)
|
||||
|
||||
await runtime.check_setup_model()
|
||||
|
||||
assert runtime.model_verified is False
|
||||
assert runtime.controller.messages == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_model_verified_reuses_the_startup_check(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
release = asyncio.Event()
|
||||
calls: list[str] = []
|
||||
|
||||
async def preflight(_model: str) -> None:
|
||||
calls.append("preflight")
|
||||
await release.wait()
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
runtime._setup_preflight = asyncio.create_task(runtime.check_setup_model())
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# A launch that arrives mid-check waits for it rather than racing a second
|
||||
# round trip.
|
||||
ensure = asyncio.create_task(runtime.ensure_model_verified())
|
||||
await asyncio.sleep(0)
|
||||
assert not ensure.done()
|
||||
release.set()
|
||||
await ensure
|
||||
|
||||
assert calls == ["preflight"]
|
||||
assert runtime.model_verified is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_model_verified_retries_after_a_failed_startup_check(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
outcomes = iter([TimeoutError("connection timed out"), None])
|
||||
calls: list[str] = []
|
||||
|
||||
async def preflight(_model: str) -> None:
|
||||
calls.append("preflight")
|
||||
outcome = next(outcomes)
|
||||
if outcome is not None:
|
||||
raise outcome
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
|
||||
await runtime.check_setup_model()
|
||||
assert runtime.model_verified is False
|
||||
|
||||
await runtime.ensure_model_verified()
|
||||
|
||||
assert calls == ["preflight", "preflight"]
|
||||
assert runtime.model_verified is True
|
||||
# No preflight: the scan launches straight through and any model error
|
||||
# surfaces once the agent runs.
|
||||
assert "preflight" not in calls
|
||||
assert calls == ["targets", "prepare", "telemetry", "state", "scan"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -508,8 +400,15 @@ async def test_confirmed_target_less_launch_mounts_workspace_without_targets(
|
||||
runtime.controller.workspace_mount = str(Path.home())
|
||||
prepared: list[argparse.Namespace] = []
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(go_tui, "persist_current", lambda: None)
|
||||
async def preflight(_model: str) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"load_settings",
|
||||
lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
|
||||
)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"build_targets_info",
|
||||
@@ -520,7 +419,7 @@ async def test_confirmed_target_less_launch_mounts_workspace_without_targets(
|
||||
monkeypatch.setattr(runtime, "init_run_state", lambda: None)
|
||||
monkeypatch.setattr(runtime, "start_scan", lambda: None)
|
||||
|
||||
await runtime.start_from_setup()
|
||||
await runtime.start_from_setup(verify=False)
|
||||
|
||||
assert prepared[0].workspace_mount == str(Path.home())
|
||||
assert prepared[0].targets_info == []
|
||||
@@ -543,8 +442,15 @@ async def test_setup_preserves_prepared_cli_targets(
|
||||
runtime = GoTuiRuntime(runtime_args)
|
||||
calls: list[str] = []
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(go_tui, "persist_current", lambda: calls.append("persist"))
|
||||
async def preflight(_model: str) -> None:
|
||||
calls.append("preflight")
|
||||
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"load_settings",
|
||||
lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
|
||||
)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"build_targets_info",
|
||||
@@ -559,7 +465,7 @@ async def test_setup_preserves_prepared_cli_targets(
|
||||
|
||||
assert runtime.controller.targets == ["https://example.com"]
|
||||
assert runtime.args.targets_info[0]["type"] == "web"
|
||||
assert calls == ["persist", "prepare", "telemetry", "state", "scan"]
|
||||
assert calls == ["preflight", "prepare", "telemetry", "state", "scan"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -892,17 +798,19 @@ async def test_setup_preflight_failure_does_not_start_scan(
|
||||
nonlocal started
|
||||
started = True
|
||||
|
||||
_setup_model(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
go_tui,
|
||||
"load_settings",
|
||||
lambda: SimpleNamespace(llm=SimpleNamespace(model="openrouter/test-model")),
|
||||
)
|
||||
monkeypatch.setattr(go_tui, "preflight_model_connection", preflight)
|
||||
monkeypatch.setattr(go_tui, "persist_current", mark_started)
|
||||
monkeypatch.setattr(go_tui, "build_targets_info", mark_started)
|
||||
monkeypatch.setattr(runtime, "init_run_state", mark_started)
|
||||
monkeypatch.setattr(runtime, "start_scan", mark_started)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Model connection failed: 401 Unauthorized"):
|
||||
await runtime.ensure_model_verified()
|
||||
await runtime.start_from_setup()
|
||||
|
||||
assert runtime.model_verified is False
|
||||
assert started is False
|
||||
assert runtime.scan_task is None
|
||||
|
||||
@@ -1028,29 +936,3 @@ async def test_prepare_and_start_runs_the_scan_after_preparation(
|
||||
|
||||
assert order == ["preflight", "persist", "prepare", "telemetry", "state", "scan"]
|
||||
assert runtime.controller.scan_state == "running"
|
||||
|
||||
|
||||
def test_sync_fingerprint_tracks_report_revisions(tmp_path: Path) -> None:
|
||||
runtime = GoTuiRuntime(args())
|
||||
runtime.report_state = ReportState(run_name="test-run")
|
||||
runtime.report_state.vulnerability_reports = [{"id": "vuln-0001", "title": "Old title"}]
|
||||
runtime.report_state.get_run_dir = lambda: tmp_path # type: ignore[method-assign]
|
||||
|
||||
report = runtime.report_state.vulnerability_reports[0]
|
||||
timestamp = "2026-09-09 10:00:00 UTC"
|
||||
|
||||
before = runtime._runtime_sync_fingerprint()
|
||||
report.update(
|
||||
{
|
||||
"title": "New title",
|
||||
"updated_at": timestamp,
|
||||
"update_history": [{"timestamp": timestamp, "fields": ["title"]}],
|
||||
}
|
||||
)
|
||||
first_revision = runtime._runtime_sync_fingerprint()
|
||||
assert first_revision != before
|
||||
|
||||
report["title"] = "Newer title"
|
||||
report["update_history"].append({"timestamp": timestamp, "fields": ["title"]})
|
||||
|
||||
assert runtime._runtime_sync_fingerprint() != first_revision
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user