Compare commits

...
Author SHA1 Message Date
yoni 00fcb4c93a fix: classify distinct check failures correctly 2026-09-28 12:41:23 +00:00
yoni df313c1438 refactor fix preparation gates 2026-09-28 12:13:19 +00:00
yoni c510f58ef3 fix: retry transient verifier inconclusive results 2026-09-28 01:53:04 +00:00
yoni a41000d5bf Retry unchanged repairs when verification changes 2026-09-25 21:57:34 +00:00
yoni d02b74c3c0 Preserve explicit blocked repair outcomes 2026-09-25 21:12:49 +00:00
yoni 9d525ade7f Add bounded fix verification feedback loop 2026-09-25 21:08:38 +00:00
yoni be1c2e4767 Withhold automatic fixes when repairs exceed the recorded draft
Repair can change files beyond the candidate's draft edits while the
manifest still verifies. Comparing the applied draft hashes against the
final manifest now demotes the result to ready_with_gaps, so SARIF and
other auto-apply consumers never offer a fix that omits verified
changes.
2026-09-25 07:15:11 +00:00
yoni 2b413da544 Keep prepared candidates and staleness consistent across revisions
A report revision whose locations can no longer form a candidate now
still supersedes any recorded preparation instead of leaving a ready
result pointing at superseded locations.

Preparation results carry the candidate that was actually verified so
SARIF emits the anchor-corrected edits rather than comparing the stored
draft's digest against a re-anchored one.
2026-09-25 07:06:21 +00:00
yoni 49eca20a1a Harden fix preparation against review findings
- Resolve edit/anchor/manifest paths and require workspace containment so
  committed symlinks cannot redirect reads or writes outside the checkout.
- Treat unreadable or non-UTF-8 anchor targets as missing instead of
  raising, and never let candidate anchoring block report persistence.
- Enforce the declared command policy: subprocess env is an allowlist plus
  credentials_allowed, and commands run in a network namespace (unshare)
  when network_allowed is false, or are rejected when isolation is
  unavailable.
- Require a clean worktree in addition to a matching HEAD commit so
  pre-existing uncommitted changes are not attributed to the fix.
- Expand untracked directories into per-file manifest entries.
- Surface failed optional checks as gaps instead of silent readiness.
- SARIF fixes emit only the verified candidate (digest must match the
  recorded fix_candidate), not the stale draft locations.
2026-09-25 06:50:29 +00:00
yoni f23afb438e Add verified fix preparation engine 2026-09-25 05:16:06 +00:00
devin-ai-integration[bot] ae38fe70cd Fill in blank tool-call ids so strict providers accept the history (#1355) 2026-09-23 18:34:22 -07:00
alex s 4c1be22150 Let agents delete a vulnerability report they filed (#1354) 2026-09-23 17:25:23 -07:00
devin-ai-integration[bot] 56f7d45388 feat(llm): structured per-attempt provider request log with provider request ids (#1353) 2026-09-22 20:49:41 -07:00
yoni-at-strix e158eab3f8 feat(mcp): initialize connections lazily (#1347)
* feat(mcp): initialize connections lazily

* fix(mcp): replace terminally dead sessions

* fix(mcp): improve targeted tool discovery

* fix(mcp): limit active tool fallback
2026-09-22 14:02:02 -04:00
Ahmed Allam 56e9ae982c runtime: read_only local sources become :ro bind mounts
A local_code target can mark its tree read_only; collect_local_sources
forwards the flag and build_bind_mounts mounts the tree read-only instead
of relying on host mode bits, skipping the per-metadata remounts since the
whole tree is already immutable. Used for pulled container image layouts.
2026-09-20 06:10:13 +03:00
Ahmed Allam 355a8bb437 fix(reporting): move the git blame hint to the end of the tool description 2026-09-18 21:39:35 +03:00
Ahmed Allam 77a0cf839b fix(reporting): make the git blame hint a casual inline note 2026-09-18 21:39:35 +03:00
Ahmed Allam cafa4b19fd fix(reporting): keep git blame guidance to the technical_analysis field 2026-09-18 21:39:35 +03:00
alex s 976835194d Prompt agents to include local Git blame in technical details (#1329)
* Enrich issue technical details with local Git blame

* Bound report history enrichment and require unambiguous repository identity

* test(history): drive attribution through the CLI scan setup and isolate git config

* Simplify Git blame attribution to existing reporting instructions

* Make local blame guidance reliable in live reporting
2026-09-18 13:05:53 -04:00
Ahmed Allam 4c1f00d1ee fix(runtime): tear the sandbox down when staging is cancelled
CancelledError is not an Exception, so a run cancelled during the extra-file
upload or unpack left a created-but-uncached sandbox running.
2026-09-17 21:53:35 +03:00
Ahmed Allam 46d7bdb290 fix(runtime): place extra files as agent-writable sandbox files on every backend
Extra files (knowledge trees, workspace files) reached the docker sandbox as
per-file read-only bind mounts whose parent directories docker created as
root, so the sandbox user could neither edit them nor create siblings. They
now travel as one tar archive uploaded after bring-up and unpacked as the
sandbox user, on every backend.
2026-09-17 21:53:35 +03:00
alex s 910c1ea4bb fix(build): keep the TUI sidecar hook importable on hatchling 1.32.1 (#1325)
hatchling 1.32.1 made BuildHookInterface a two-parameter generic, so
subscripting it with one argument raises TypeError when the hook module
is imported and every from-source build fails. Subclass the unsubscripted
interface, which works on both the old and new generic signatures.
2026-09-16 14:25:38 -04:00
alex sandRAJVARDHAN 65d495bb7f feat(config): STRIX_API_TYPE forces responses vs chat completions (#1324)
* Add api_type field to LlmSettings

Added 'api_type' field to LlmSettings for API path selection.

* Refactor API type handling in models.py

* Implement test for LlmSettings API type

Add test for API type override settings in LlmSettings.

* fix(tests): lint api_type test, cover the api_base override route, document STRIX_API_TYPE

* fix(models): keep LiteLLM chat-completions tool schema when STRIX_API_TYPE=responses

---------

Co-authored-by: RAJVARDHAN <[email protected]>
2026-09-16 11:13:30 -04:00
Ahmed Allam 2dadbb748a chore(deps): require litellm>=1.101.0 for gpt-6-astra max_completion_tokens mapping 2026-09-16 01:07:48 +03:00
Ahmed Allam 84f4108195 fix(web_search): send only the agent's query to Exa search
Exa /search is a neural search endpoint, not a chat model, so prepending
the Perplexity system prompt made Exa match the prompt's own vocabulary
(Kali, OWASP, apt, NIST) instead of the query. The system prompt stays
on the Perplexity path where it is a chat system message; the Exa
summary instruction is unchanged.
2026-09-13 19:40:50 +03:00
Elisabeth Rulke 0c4364a6a7 docs: add Vercel AI Gateway provider guide
Add Vercel AI Gateway as an LLM provider option, mirroring the existing
provider pages. New guide, an overview card, and a nav entry after
OpenRouter. Docs only.
2026-09-12 19:37:41 +03:00
Ahmed Allam 95e085eb6c feat(prompts): require http_exchange_ids for proxy-validated findings
Agents mostly left http_exchange_ids empty because the only nudge was the
parameter docstring. The REPORTING rules now state that a finding validated
through the proxy is not fully filed until the ids of its proving exchanges
(exploit plus baseline) are attached, copied from list_requests/view_request,
omitted only for findings with no captured HTTP at all, and attached after
the fact with update_vulnerability_report when needed. The Caido section
tells agents to note the ids as they test.
2026-09-10 03:50:17 +03:00
alex sandAhmed Allam 22959a7ba6 feat(reporting): link HTTP exchange evidence (#1281)
Co-authored-by: Ahmed Allam <[email protected]>
2026-09-09 07:50:23 -07:00
devin-ai-integration[bot]andAhmed Allam 52b1923347 fix(models): frontier model check matches the model name only, never the provider route (#1280)
Co-authored-by: Ahmed Allam <[email protected]>
2026-09-06 11:09:24 -07:00
Ahmed Allam ff5c8cc8e4 chore: release v1.6.2 2026-09-05 04:22:29 +03:00
Ahmed Allam afce7d95e8 fix(telemetry): classify setup-mode TUI preflight and preparation failures 2026-09-05 04:08:09 +03:00
Ahmed Allam 2e1db25786 feat(telemetry): classify error beacons by phase and exception class
error events now carry phase (startup/preflight/sandbox_init/agent_setup/
agent_loop) and the exception class name (plus its cause), never the message
or trace. Startup and preflight failures that exit(1) before the scan starts
are beaconed with a stable error_type instead of vanishing. scan_ended
distinguishes budget_exceeded, rate_limited, and headless agent_stopped
from user_exit.
2026-09-05 04:08:09 +03:00
devin-ai-integration[bot]andAhmed Allam f4b0416b71 docs: update README and CLI links (#1272)
Co-authored-by: Ahmed Allam <[email protected]>
2026-09-04 17:56:08 -07:00
Ahmed Allam c2c84f1131 chore(telemetry): drop unnecessary lock around loaded-skills set 2026-09-05 03:26:12 +03:00
Ahmed Allam bb7e82b6ea chore(telemetry): drop per-load skill_loaded beacons, send anonymous events
Report the distinct set of skills used once on scan_ended instead of one
skill_loaded event per skill per prompt render. Mark PostHog events with
$process_person_profile=false (distinct_id is a throwaway session id, so
person profiles were never useful) and tag them with $lib/$lib_version.
2026-09-05 03:26:12 +03:00
Ahmed Allam 9cc9de8cdc fix(warmup): drop docker from WARMUP_MODULES
The Docker checks import the Docker SDK on the main thread before the
warm-up join, so warming it saves nothing and leaves one module shared
between the two threads during the startup window.
2026-09-05 02:45:34 +03:00
Ahmed Allam a3bf864e1e test(warmup): assert wait_for_import_warmup blocks until the thread finishes 2026-09-05 02:45:34 +03:00
Ahmed Allam e60fd83931 refactor(warmup): drop the orphan purge and join the warm-up once before the engine imports 2026-09-05 02:45:34 +03:00
Ahmed Allam 7f46dd17d3 fix(cli): wait for the import warm-up before importing the agents SDK on the main thread
The warm-up thread imports strix.core.runner while warm_up_llm and
preflight_model_connection import agents.models.interface. Both walk the
agents SDK graph from different entry points, CPython fails one side to
break the import-lock cycle, and the orphan purge then removes agents.*
from sys.modules while the main thread is still importing it, crashing
strix -n with KeyError: 'agents.models'.
2026-09-05 02:45:34 +03:00
devin-ai-integration[bot] afa7c4a77f feat(web_search): add Exa as a web search provider alongside Perplexity (#1270) 2026-09-04 10:34:28 -07:00
oyasumi f6d9790ecb fix(viewer): show stopped run status 2026-09-04 01:10:10 +03:00
alex s 5d015df6b1 fix(cloud): print top-up instructions on 402 and guide oversize or archive --source (#1242)
- Every payment-required error now ends with a "Next step" line: the
  platform hint when one is sent, else the topup command and the billing
  URL for the configured platform. JSON output gets the same text as
  next_step. The platform hint is no longer repeated inside the error.
- An archive file passed to --source is rejected with guidance to pass
  the directory instead, which packs and excludes deps/build output.
- An oversize archive names its largest files and points to --exclude
  and --dry-run --show-files.
- uploads request help points to scans start --source for local code.
2026-09-02 15:26:37 -04:00
Ahmed Allam 1edafd3e80 fix(agents): stop parents waiting on finished non-interactive children
A non-interactive agent's loop returns after its terminal state, yet
send_message_to_agent kept reporting messages to it as delivered and the
parent then waited out wait_for_agents on a reply that could never come.

- AgentRuntime.resumable records whether the loop parks for wake-ups after a
  terminal state; run_agent_loop / _start_child_runner set it from interactive.
- AgentCoordinator.send returns False (nothing queued) for a terminal agent
  that is not resumable; send_message_to_agent surfaces target_status and
  delivery_status=not_delivered with a pointer to list_reports / get_report.
- wait_for_agents returns wait_outcome=no_active_agents at once when no other
  agent is running or waiting in a non-interactive run.
- agent_finish reads the reports the finishing agent filed from the report
  state and puts their ids in the completion report, the parent message
  (filed_report_ids) and its own return payload, so parents no longer have to
  infer what was filed from prose.
2026-09-02 22:11:12 +03:00
Ahmed Allam f1e24fe3f2 chore: release v1.6.1 2026-09-02 19:05:46 +03:00
Ahmed Allam e644f4a02c docs(readme): shorten the coding-agent skills paragraph 2026-09-02 18:46:33 +03:00
Ahmed Allam 1ebe1007e8 docs: keep the existing recommended model rows in the README 2026-09-02 18:35:45 +03:00
Ahmed Allam 53d2e5cfeb docs: note viewer steering, history, and report prerequisites 2026-09-02 18:35:45 +03:00
Ahmed Allam 7708f717d5 docs: trim crammed README sections and add cloud CLI and viewer docs pages 2026-09-02 18:35:45 +03:00
devin-ai-integration[bot] a8642de76c docs(readme): trim the strix cloud section to the essentials (#1237) 2026-09-02 07:19:56 -07:00
Ahmed Allam 75b89018d3 docs: use openrouter/z-ai/glm-5.3 as the default model in setup examples 2026-09-02 16:52:53 +03:00
Ahmed Allam 129f938094 fix(models): keep aggregator routes out of RECOMMENDED_MODEL_NAMES, family matching already accepts them 2026-09-02 16:52:53 +03:00
Ahmed Allam b438632e12 fix(models): keep list additions-only, restore gpt-5.4 examples, make openrouter/z-ai/glm-5.3 the top pick 2026-09-02 16:52:53 +03:00
Ahmed Allam 0ab7244807 feat(models): refresh the recommended model list and docs examples
Add Claude Fable 5.1, Gemini 3.7 Flash, and Z.ai GLM-5.3 / GLM-5.3-Flash
to RECOMMENDED_MODEL_NAMES, add a Z.ai GLM frontier family so GLM-5.x is
accepted through OpenRouter and Novita routes, and drop the superseded
GPT-5.4, GPT-5.3-codex, Opus 4.8, Sonnet 4.6, Gemini 3.6 Flash, and
Qwen3.7 entries. Update the README, docs provider pages, quickstart, and
CLI hint strings to the same current models, including DeepSeek V4,
Kimi K3, and GLM-5.3.
2026-09-02 16:52:53 +03:00
Ahmed Allam c514f712f4 fix(config): persist only the alias the runtime settings read
pydantic-settings takes the first alias present in the environment, even
when it is empty. persist_current() must save that same alias, so an empty
LLM_API_KEY does not let a non-empty OPENAI_API_KEY sibling land in the
file and restore a credential the run did not use.
2026-09-02 16:10:52 +03:00
Ahmed Allam 3e88e498b9 fix(config): drop the stored LLM connection when a linked env var changes
A new STRIX_LLM, LLM_API_KEY, or LLM_API_BASE exported in the shell must not
be combined with the key, base, or model still stored in cli-config.json.
Restore the pre-refactor rule: when any linked LLM connection var differs
from the stored value, discard the whole stored connection before loading
and before persisting. Unrelated stored settings are kept.
2026-09-02 16:10:52 +03:00
Ahmed Allam ce0db30252 fix(config): merge env into cli-config.json instead of overwriting it
persist_current rewrote the config file with only the env vars set in the
shell, so a run whose STRIX_LLM or LLM_API_KEY came from the file erased
them and the next launch failed with MISSING REQUIRED ENVIRONMENT
VARIABLES. Start from the stored env block, let a set env var override or
replace the aliases of its field, and let an empty env var clear it.
2026-09-02 16:10:52 +03:00
Ahmed Allam 941c960650 fix(ci): make the pre-commit mypy hook and the test suite pass on a fresh checkout 2026-09-02 15:15:51 +03:00
Ahmed Allam 46b4e6cb64 fix(tui): run environment and model checks on the no-target start screen
The interactive start screen skipped validate_environment() entirely, and
a bare prompt sent verify=false so the model preflight never ran. Both
kinds of setup launch now verify the model before leaving the start
screen, environment validation runs for every mode, and quitting setup
without a scan still shows the update notice.
2026-09-02 15:03:19 +03:00
yoni-at-strix b5c3807fef fix(mcp): keep the session on tool-call protocol errors and report quarantine truthfully (#1228) 2026-09-01 22:43:54 -04:00
alex s 42baa7c09e skills: point to the strix cloud CLI in every skill (#1227) 2026-09-01 18:12:29 -04:00
Ahmed Allam 8fdf6a5c09 chore: release v1.6.0 2026-09-01 23:29:06 +03:00
alex sandAhmed Allam 46cf2f52f3 report: add update_vulnerability_report so an agent can revise a filed finding (#1210)
Co-authored-by: Ahmed Allam <[email protected]>
2026-09-01 13:07:17 -07:00
alex s 3de9471431 Link CLI wallet (#1222) 2026-09-01 16:00:32 -04:00
alex s a071022182 Forward the workspace header through the wallet payment bridge (#1221) 2026-09-01 14:57:00 -04:00
alex s d26b1ab0de pentest skill cloud cli (#1220)
* Reference the strix cloud CLI in the penetration-testing skill

* Note scope selection and default billing scopes in the cloud login example

* Shorten the scopes note in the cloud login example
2026-09-01 14:50:02 -04:00
alex s de730119f0 feat(cli): strix cloud — managed platform CLI (login, scans, billing, and the rest of the API) (#1177)
* feat(cli): add strix login for managed platform sign-in (device flow)

* feat(cli): add --scopes flag to strix login

* docs: document strix login and managed billing in README, AGENTS, docs, and managed skill

* fix(cli): handle malformed login responses and credential file failures

* fix(cli): reject sign-in responses without an API token

* feat(login): interactive workspace and scope selection with presets

* fix(login): reject malformed API token values in sign-in responses

* fix(login): skip the scope prompt when stdin is not a terminal

* fix(login): tolerate malformed selection containers and remove unreadable credential files on logout

* fix(login): treat overflowing timing values as invalid

* fix(login): show the configured platform host in the sign-in banner

* fix(login): bound device flow timing values and clean up unreplaced secret temp files

* feat(cli): add the strix cloud command surface for the managed platform

* feat(cli): manage workspaces and hosted onboarding links from strix cloud

* fix(cli): report a leftover temporary secret file instead of hiding it

* feat(cli): pass a Stripe payment method to the top-up wallet client

* docs(cloud): recommend the Stripe agent wallet as the default payment path

* fix(cloud): preserve API auth during MPP payment

* fix(cloud): drop knowledge query and settings commands removed from the API

* fix(cloud): align agent commands with API contracts

* fix(cloud): send required PR review integration fields

* fix(cloud): preserve scopes when switching workspaces

* fix(cloud): make session command help non-destructive

* feat(cloud): improve human navigation and output

* feat(cli): add native shell completions

* feat(cloud): tailor human list and detail views

* feat(cloud): upload local source for managed scans

* fix(cloud): infer scan type from local targets

* Add agent-friendly managed cloud CLI

* Harden cloud CLI type boundaries

* Clarify cloud test user MFA options

* Correct cloud vulnerability status guidance

* Clarify chat file path handling

* Allow signed storage upload URLs

* Fix provider token request handling

* Improve cloud CLI human list views

* Make cloud CLI workflows actionable and safe

* Make cloud workspace switching session-safe

* Preserve CLI session metadata in JSON output

* Remove preview protection bypass plumbing from cloud CLI
2026-09-01 14:39:34 -04:00
devin-ai-integration[bot] 608ef4a37b Make MCP connections survive transient transport failures (#1184) 2026-09-01 08:11:43 -07:00
Ahmed Allam f901d2a8bf fix(runtime): drop staged extra files on any failure before the bundle is cached 2026-09-01 16:54:20 +03:00
Ahmed Allam 944274e12f fix(runtime): remove the extra-file staging dir on cleanup and failed bring-up 2026-09-01 16:54:20 +03:00
Ahmed Allam eeca404716 fix(runtime): stage extra-file bind mounts under the temp dir so remote docker daemons can resolve them 2026-09-01 16:54:20 +03:00
oyasumiandoyasumi 1df67c52e2 fix(viewer): harden PDF report rendering (#1192)
* fix(viewer): harden PDF report rendering

* test(viewer): cover crossed markdown emphasis

---------

Co-authored-by: oyasumi <[email protected]>
2026-08-31 20:22:33 -04:00
156 changed files with 29447 additions and 1678 deletions

No files matched your search

+15 -14
View File
@@ -1,3 +1,6 @@
# 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
@@ -9,21 +12,18 @@ repos:
- id: ruff-format
name: ruff-format
# MyPy for static type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.17.1
# 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
hooks:
- id: mypy
additional_dependencies: [
types-requests,
types-python-dateutil,
pydantic,
fastapi,
pytest,
hatchling,
"openai-agents[litellm]>=0.19.0,<0.20",
]
args: [--install-types, --non-interactive]
name: mypy
entry: uv run mypy
language: system
types_or: [python, pyi]
files: ^(strix|tests)/
require_serial: true
# Built-in hooks for basic file checks
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -62,5 +62,6 @@ ci:
autoupdate_branch: ""
autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate"
autoupdate_schedule: weekly
skip: []
# pre-commit.ci cannot run `language: system` hooks; mypy runs via `make check-all`.
skip: [mypy]
submodules: false
+18 -6
View File
@@ -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="openai/gpt-5.4" # any LiteLLM model id
export STRIX_LLM="openrouter/z-ai/glm-5.3" # any LiteLLM model id
export LLM_API_KEY="<key>"
strix -n -t ./ --scan-mode quick --max-budget 10 # headless scan; always use -n
```
@@ -38,13 +38,25 @@ Target-specific workflows built on the same engine:
- **Managed cloud (app.strix.ai):** no Docker, no LLM key, no local install; adds team dashboards, scheduling, PR reviews, and downloadable PDF/DOCX reports (Enterprise plan). Best in sandboxed/CI environments and for teams. Use it when local infra isn't available.
```bash
# token from Settings → API Access; register the target as an asset, then:
curl -sS https://app.strix.ai/api/v1/scans -H "Authorization: Bearer $STRIX_API_TOKEN" \
-H "Content-Type: application/json" -d '{"engagement_type":"live_test","domain_ids":["<uuid>"]}'
strix cloud login --scopes scans:read scans:write uploads:write billing:read
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 billing topup --credits 20 --yes # explicit approval after exit code 5
```
- API docs: https://docs.app.strix.ai (OpenAPI: https://docs.app.strix.ai/openapi.json).
- Account setup runs from the CLI too: `strix cloud workspaces list|create|use` (`workspace` is an alias and `use` accepts a displayed number, name, or ID), `strix cloud session scopes|scopes set`, `strix cloud org members invite`, `strix cloud billing subscribe --plan strix_cloud`, `strix cloud billing portal`, `strix cloud integrations install github`, and `strix cloud domains verify <id>`. Workspace switching preserves the server-side profile and can never widen past the login ceiling; ordinary switches do not reprompt. The last four end at a person: the command prints a link or a DNS record for the user to open or add, and it never completes the payment, installation, or DNS change for them.
- Every REST operation has a `strix cloud <resource> <verb>` command. Run `strix cloud` to list them. Output is JSON when stdout is not a terminal (or with `--json`), and there are no prompts without a TTY. Binary downloads are the exception: redirect raw bytes intentionally, or combine `--output FILE --json` for structured download metadata. Exit codes: `0` success, `1` error, `2` usage, `4` auth or plan limit, `5` payment required. `--token` or `STRIX_API_TOKEN` is a stateless override and never replaces stored auth; set `--workspace-id`/`STRIX_WORKSPACE_ID` for an override CLI session. `--data` adds extra request fields as JSON, and accepts `@file` or `-` for standard input.
- Local source uploads require `uploads:write`. For an agent/CI handoff, review `scans start --source . --dry-run --show-files --json`, capture `source.archive_sha256`, then rerun with the same `--source`, `--exclude`, and `--include-*` selection flags plus `--approve-sha256 HASH`. A changed snapshot is rejected. `--yes` approves only the snapshot built in that invocation, so reserve it for a deliberate human or one-shot approval rather than a digest-bound two-step handoff.
- Git ignores, hidden files, `.git`, symlinks, dependency/build output, secret-like filenames, and nested archives are excluded by default; `.strixignore` and `--exclude` narrow the manifest further (a trailing `/` excludes a directory subtree). Limits: 20,000 files, 25 MiB/file, 250 MiB expanded, 50 MiB compressed. Source-only infers `code_review`; source plus a domain infers `live_test`.
- The temporary local archive is always removed. A staged upload is deleted after a definitive rejection, but retained when a network error, `5xx`, malformed success response, or interruption leaves the scan launch ambiguous. JSON reports its `upload_id` with `launch_outcome_unknown: true`, or with `cleanup_unknown: true` when automatic deletion cannot be confirmed. Check `scans list` before retrying; if no scan is linked, run `uploads delete UPLOAD_ID`.
- Non-Enterprise scans consume the scope estimate (a default-tier source-only review currently starts at 60 credits); Enterprise scans are plan-included. A rejected launch does not consume credits.
- Human output is compact and numbered; non-TTY output and `--json` retain full records. Enable tab completion with `source <(strix completions zsh)` (or `bash`), or `strix completions fish | source`.
- The REST API works directly too: https://docs.app.strix.ai (OpenAPI: https://docs.app.strix.ai/openapi.json).
- CLI docs index for LLMs: https://docs.strix.ai/llms.txt (full: https://docs.strix.ai/llms-full.txt).
- CLI docs index for LLMs: https://docs.strix.ai/llms.txt (full: https://docs.strix.ai/llms-full.txt). Managed API docs for LLMs: https://docs.app.strix.ai/llms.txt.
- Only scan targets the user is authorized to test.
## Contributing to this repo
+1 -1
View File
@@ -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="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="your-api-key"
```
+58 -64
View File
@@ -17,6 +17,9 @@
<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://dcbadge.limes.pink/api/server/strix-ai)](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>
@@ -34,7 +37,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).
> **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).
---
@@ -82,7 +85,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="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="your-api-key"
# Run your first security assessment
@@ -94,9 +97,17 @@ strix --target ./app-directory
---
## ☁️ Strix Platform
## Ways to Run Strix
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.
- **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.
- **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
@@ -104,7 +115,13 @@ 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
[**Start your first pentest →**](https://app.strix.ai)
[**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)
---
@@ -116,7 +133,9 @@ Strix is agent-ready. Give Claude Code, Cursor, Codex, or any [SKILL.md-compatib
npx skills add usestrix/strix
```
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.
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.
---
@@ -172,18 +191,9 @@ strix view my-run-name
strix view --host 0.0.0.0 --port 8080 --no-open
```
`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.
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.
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.
See the [viewer documentation](https://docs.strix.ai/usage/viewer) for the options and for reaching the viewer from another machine.
---
@@ -209,18 +219,9 @@ 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 (.json / .yaml)
# OpenAPI / Swagger file, Postman export, or a live collection by id
strix --target ./openapi.yaml --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>"
strix --target postman://<collection-uuid> --target https://api.your-app.com
```
@@ -235,20 +236,10 @@ strix -t https://github.com/org/app -t https://your-app.com
# Targets from a file, one target per non-empty, non-comment line
strix --target-list ./targets.txt
# White-box source-aware scan (local repository)
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
```
See the [CLI reference](https://docs.strix.ai/usage/cli) for every option, including scan modes, diff scope, instruction files, and budgets.
### Headless Mode
Run Strix programmatically without interactive UI using the `-n/--non-interactive` flag - perfect for servers and automated jobs. The CLI prints real-time vulnerability findings and the final report before exiting. Exits with non-zero code when vulnerabilities are found.
@@ -287,51 +278,55 @@ jobs:
```
> [!TIP]
> 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.
> 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.
### Configuration
```bash
export STRIX_LLM="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
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 --target ./app-directory
strix auth status # show the active sign-in
strix auth logout # forget the sign-in
strix auth status # show the active sign-in, or logout to forget it
```
#### 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:
```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
```
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.
See the [cloud CLI documentation](https://docs.strix.ai/cloud/cli) for scopes, workspaces, billing, and source-upload options.
#### 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 servers. Each entry is either a local `stdio` server that Strix launches as a subprocess, or a remote `http` server:
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:
```json
[
{
"name": "local_fs",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
},
{
"name": "github",
"transport": "http",
@@ -342,20 +337,19 @@ Strix can connect to Model Context Protocol (MCP) servers you list and expose th
]
```
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`.
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.
**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.
+27 -4
View File
@@ -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., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`).
Model name in LiteLLM format (e.g., `openrouter/z-ai/glm-5.3`, `openai/gpt-5.4`).
</ParamField>
<ParamField path="LLM_API_KEY" type="string">
@@ -19,6 +19,12 @@ 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
@@ -80,6 +86,22 @@ 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>
@@ -145,7 +167,7 @@ strix --target ./app --config /path/to/config.json
```json
{
"env": {
"STRIX_LLM": "openai/gpt-5.4",
"STRIX_LLM": "openrouter/z-ai/glm-5.3",
"LLM_API_KEY": "sk-...",
"STRIX_REASONING_EFFORT": "high"
}
@@ -156,10 +178,11 @@ strix --target ./app --config /path/to/config.json
```bash
# Required
export STRIX_LLM="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="sk-..."
# Optional: Enable web search
# Optional: Enable web search (Exa preferred, Perplexity supported)
export EXA_API_KEY="..."
export PERPLEXITY_API_KEY="pplx-..."
# Optional: Custom timeouts
+103
View File
@@ -0,0 +1,103 @@
---
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
```
+19
View File
@@ -35,6 +35,25 @@ Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai).
2. Connect your repository or enter a target URL
3. Launch your first scan
## Scan Local Source
Send a local working tree to the managed white-box scanner without connecting a source-control provider:
```bash
# Review the exact file manifest and capture source.archive_sha256. Nothing is uploaded.
strix cloud scans start --source . --dry-run --show-files --json
SOURCE_SHA256="<reviewed source.archive_sha256>"
# Repeat the same source-selection flags and approve that exact snapshot.
strix cloud scans start --source . --approve-sha256 "$SOURCE_SHA256" --wait
```
In a Git repository, Strix includes tracked files and untracked files that are not ignored. Hidden files, `.git`, symlinks, dependencies and build output, secret-like filenames, and nested archives are excluded by default. Use `.strixignore` or repeat `--exclude GLOB` for project-specific exclusions. `--include-hidden`, `--include-sensitive`, and `--include-archives` are explicit opt-ins.
The CLI limits individual files, total expanded bytes, archive bytes, and file count. For an agent or CI handoff, repeat the same `--source`, `--exclude`, and `--include-*` flags with `--approve-sha256`; Strix refuses the upload if the rebuilt archive differs from the reviewed digest. `--yes` is a one-invocation approval for the snapshot built at that moment, not a digest-bound two-step approval.
The temporary local archive is always removed. After a definitive launch rejection, Strix also deletes the staged remote upload. If a network error, server error, or interruption makes the launch outcome ambiguous, it retains the upload and reports its ID; check `strix cloud scans list` before retrying, then delete an unlinked upload with `strix cloud uploads delete UPLOAD_ID`.
<Card title="Try Strix Cloud" icon="rocket" href="https://app.strix.ai">
Run your first pentest in minutes.
</Card>
+1 -1
View File
@@ -33,7 +33,7 @@ description: "Contribute to Strix development"
</Step>
<Step title="Configure LLM">
```bash
export STRIX_LLM="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="your-api-key"
```
</Step>
+5 -2
View File
@@ -25,7 +25,8 @@
"pages": [
"usage/cli",
"usage/scan-modes",
"usage/instructions"
"usage/instructions",
"usage/viewer"
]
},
{
@@ -35,6 +36,7 @@
"llm-providers/openai",
"llm-providers/anthropic",
"llm-providers/openrouter",
"llm-providers/vercel-ai-gateway",
"llm-providers/vertex",
"llm-providers/bedrock",
"llm-providers/azure",
@@ -77,7 +79,8 @@
{
"group": "Strix Cloud",
"pages": [
"cloud/overview"
"cloud/overview",
"cloud/cli"
]
}
]
+1 -1
View File
@@ -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="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="your-api-key"
# Scan
+2 -1
View File
@@ -36,13 +36,14 @@ npx skills use usestrix/strix@penetration-testing-with-strix | claude
Both use the same engine and produce the same validated findings and SARIF, so agents can pick per situation or combine them:
- **Open-source CLI (self-hosted)** — runs locally in a Docker sandbox with your own LLM key. Free, fully local, air-gap capable. Best for local dev loops and full control.
- **Managed cloud** — runs on Strix's infrastructure via the [app.strix.ai REST API](https://docs.app.strix.ai). No Docker, no LLM key, no local install; adds team dashboards, scheduling, PR reviews, and downloadable PDF/DOCX reports (Enterprise plan). Best in sandboxed/CI environments and for teams. Create an API token under **Settings → API Access**; the `managed-pentesting-with-strix` skill has the full flow.
- **Managed cloud** — runs on Strix's infrastructure. Drive it with the `strix cloud` CLI (every REST operation has a `strix cloud <resource> <verb>` command) or the [app.strix.ai REST API](https://docs.app.strix.ai) directly. No Docker, no LLM key; adds team dashboards, scheduling, PR reviews, and downloadable PDF/DOCX reports (Enterprise plan). Best in sandboxed/CI environments and for teams. Sign in with `strix cloud login` (browser device sign-in, account created on first use) or create a token in the dashboard under **Settings → API Access**. The `managed-pentesting-with-strix` skill has the full flow.
## Agent-Friendly Interfaces
Everything an agent needs is machine-readable:
- **Headless CLI** — `strix -n` runs without the TUI and exits with `0` (clean), `1` (error), or `2` (vulnerabilities found).
- **Cloud CLI** — `strix cloud` prints JSON when stdout is not a terminal (or with `--json`), never prompts without a TTY, and exits with `0` (success), `1` (error), `2` (usage), `4` (authentication required), or `5` (payment required). Credit top-ups pay the Stripe machine-payment challenge with an agent wallet (`strix cloud billing topup --credits N --yes`). Account setup also runs from the CLI: `strix cloud workspaces list|create|use`, `strix cloud org members invite`, `strix cloud billing subscribe`, `strix cloud billing portal`, and `strix cloud integrations install github`. The last three print a hosted link the user opens to finish the payment or approve the installation.
- **REST API** — the managed platform exposes a documented [OpenAPI](https://docs.app.strix.ai/openapi.json) at `https://app.strix.ai/api/v1` (scans, vulnerabilities, assets, PR reviews, schedules, webhooks) with bearer tokens and scopes.
- **Structured results** — every run writes `vulnerabilities.json`, `vulnerabilities.csv`, `findings.sarif` (SARIF 2.1.0), and per-finding Markdown under `strix_runs/<run-name>/`; the cloud exposes the same as JSON plus SARIF export.
- **Budget controls** — `--max-budget` and `--max-turns` give agents hard cost/time caps.
+1 -1
View File
@@ -37,7 +37,7 @@ Add these secrets to your repository:
| Secret | Description |
|--------|-------------|
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5.4`) |
| `STRIX_LLM` | Model name (e.g., `openrouter/z-ai/glm-5.3`) |
| `LLM_API_KEY` | API key for your LLM provider |
## Exit Codes
+3
View File
@@ -17,6 +17,9 @@ 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` |
+4 -1
View File
@@ -8,7 +8,7 @@ description: "Configure Strix with models via OpenRouter"
## Setup
```bash
export STRIX_LLM="openrouter/openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="sk-or-..."
```
@@ -18,9 +18,12 @@ 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
+13 -6
View File
@@ -9,14 +9,17 @@ Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibi
Set your model and API key:
| 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` |
| 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` |
```bash
export STRIX_LLM="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="your-api-key"
```
@@ -43,6 +46,9 @@ 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>
@@ -62,6 +68,7 @@ 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
+39
View File
@@ -0,0 +1,39 @@
---
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
View File
@@ -28,12 +28,12 @@ description: "Install Strix and run your first security scan"
Set your LLM provider:
```bash
export STRIX_LLM="openai/gpt-5.4"
export STRIX_LLM="openrouter/z-ai/glm-5.3"
export LLM_API_KEY="your-api-key"
```
<Tip>
For best results, use `openai/gpt-5.4`, `anthropic/claude-opus-4-6`, or `openai/gpt-5.2`.
For best results, use `openrouter/z-ai/glm-5.3` (the default pick), `openai/gpt-5.4`, `anthropic/claude-opus-4-6`, or `openai/gpt-5.2`.
</Tip>
## Run Your First Scan
+1 -1
View File
@@ -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 via Perplexity |
| Web Search | Real-time OSINT with Exa or Perplexity |
| Notes | Document findings during the scan |
| Reporting | Generate vulnerability reports with PoCs |
+2 -1
View File
@@ -96,7 +96,8 @@ agent task, so the agent knows where to read them.
Rules that apply to every workspace file:
- The file is read-only inside the sandbox.
- Strix copies the file into the sandbox. The agent can edit the copy, but the
file on your machine does not change.
- 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.
+49
View File
@@ -0,0 +1,49 @@
---
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.
+11 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "strix-agent"
version = "1.5.3"
version = "1.6.2"
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",
"litellm>=1.101.0",
"pydantic>=2.11.3",
"pydantic-settings>=2.13.0",
"rich",
@@ -43,6 +43,7 @@ dependencies = [
"requests>=2.32.0",
"cvss>=3.2",
"caido-sdk-client>=0.2.0",
"markdown-it-py>=3.0.0",
"reportlab>=4.0",
"pypdf>=5.0",
# Cap <49: 49.x drops the universal2 macOS wheel (arm64-only), which breaks
@@ -229,6 +230,7 @@ ignore = [
# Test doubles use fixture tokens/passwords and match a callee signature whose
# args they intentionally ignore.
"tests/test_viewer_auth.py" = ["S105", "S106", "ARG001"]
"tests/test_cloud_cli.py" = ["S105", "ARG001"]
"tests/test_codex_auth.py" = ["S105", "S106", "SLF001"]
# Hatchling loads the build hook by path, not as an importable package.
"scripts/tui_sidecar_hook.py" = ["INP001"]
@@ -248,6 +250,7 @@ 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
@@ -305,6 +308,9 @@ 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"]
@@ -411,6 +417,8 @@ known_third_party = ["pydantic", "litellm"]
# ============================================================================
[tool.bandit]
exclude_dirs = ["docs", "build", "dist"]
# 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"]
skips = ["B101", "B601", "B404", "B603", "B607"] # Skip assert, shell injection, subprocess import and partial path checks
severity = "medium"
+3
View File
@@ -346,6 +346,9 @@ 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 ""
+1 -1
View File
@@ -12,7 +12,7 @@ from typing import Any
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface[Any]):
class CustomBuildHook(BuildHookInterface): # type: ignore[type-arg]
"""Compile the Bubble Tea sidecar and ship it inside the wheel.
The sidecar is the only interactive interface, so every wheel is a
+2 -2
View File
@@ -11,7 +11,7 @@ metadata:
APIs fail differently from web UIs: there is no rendered surface to crawl, the interesting bugs are authorization-shaped rather than injection-shaped, and the same endpoint behaves differently per token. This workflow targets those specifics with Strix's autonomous agents, using the current [OWASP API Security Top 10 (2023)](https://owasp.org/API-Security/editions/2023/en/0x11-t10/) as the coverage checklist. For the web-app equivalent, the current edition is the OWASP Top 10:2025 — see **owasp-top-10-testing**.
Install, LLM setup, full CLI flags, and the managed-cloud path are in the **penetration-testing-with-strix** skill. Read it if `strix --version` fails or the target is not an API.
Install, LLM setup, full CLI flags, and the managed-cloud path are in the **penetration-testing-with-strix** skill. Read it if `strix --version` fails or the target is not an API. For a run with no Docker and no LLM key, the same binary drives the managed platform: `strix cloud login`, then `strix cloud scans start ...` (details in **managed-pentesting-with-strix**).
## 1. Gather what the agents need
@@ -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`. The file lands read-only in `/workspace`. 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`. 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`.
## 3. Verify findings
+1 -1
View File
@@ -11,7 +11,7 @@ metadata:
Entry point for "make my application secure" requests, where the target is not yet a single URL or repo. The job here is to pick the right test per asset, run it, and produce one ranked plan — not to run everything at maximum depth.
Install, LLM setup, all CLI flags, and the managed-cloud path live in the **penetration-testing-with-strix** skill. Read it first if `strix --version` fails.
Install, LLM setup, all CLI flags, and the managed-cloud path live in the **penetration-testing-with-strix** skill. Read it first if `strix --version` fails. For a run with no Docker and no LLM key, the same binary drives the managed platform: `strix cloud login`, then `strix cloud scans start ...` (details in **managed-pentesting-with-strix**).
Only test assets the user owns or is authorized to test. Confirm authorization before the first run, and prefer staging over production, because the agents send real exploit payloads and can change data.
@@ -113,11 +113,11 @@ Gate the pipeline on the exit code (see the budget/fail-open caveat above — gi
# Option B — Managed platform (no runner infra)
No workflow file, no Docker, no LLM key. Two ways to use it:
No workflow file, no Docker, no LLM key. Three ways to use it:
1. **PR-review app (zero code):** the user installs the Strix GitHub/GitLab/Bitbucket app and enables PR reviews for the repo in the app.strix.ai dashboard. Every PR is then reviewed automatically, with findings posted as PR comments. Nothing to add to the repo. This is the lowest-effort path — recommend it first when the user just wants PR gating.
2. **API-triggered from any pipeline:** if you want to trigger from an existing pipeline (or a system without the SCM app), call the API with a token that has `pr_reviews:write` (or `scans:write`). Store the token as a CI secret; ask the user to create it at **Settings → API Access**. Example GitHub Actions step:
2. **CLI-triggered from any pipeline:** if you want to trigger from an existing pipeline (or a system without the SCM app), use the same `strix` binary with a token that has `pr_reviews:write`. Store the token as a CI secret and ask the user to create it at **Settings → API Access**. Read the repository's `provider` and `installation_id` once with `strix cloud repos list`. Example GitHub Actions step:
```yaml
- name: Strix PR review (managed)
@@ -125,12 +125,25 @@ No workflow file, no Docker, no LLM key. Two ways to use it:
env:
STRIX_API_TOKEN: ${{ secrets.STRIX_API_TOKEN }}
run: |
curl -sS --fail https://app.strix.ai/api/v1/pr-reviews/start \
-H "Authorization: Bearer $STRIX_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"repository_full_name\":\"${{ github.repository }}\",\"pr_number\":${{ github.event.pull_request.number }}}"
curl -sSL https://strix.ai/install | bash
strix cloud pr-reviews start \
--provider github \
--installation-id "${{ vars.STRIX_INSTALLATION_ID }}" \
--repository-full-name "${{ github.repository }}" \
--pr-number "${{ github.event.pull_request.number }}"
```
To gate the build on results, poll the PR review / scan status and fail on unresolved criticals/highs. Full endpoints (PR reviews, scans, SARIF export, schedules for scheduled deep scans) are in the **managed-pentesting-with-strix** skill.
Output is JSON when stdout is not a terminal, and there are no prompts without a TTY. To gate the build on results, poll `strix cloud pr-reviews get <id> --json` and fail on unresolved criticals or highs. The raw REST endpoint (`POST /api/v1/pr-reviews/start`) works too when the pipeline cannot install the CLI.
3. **Source upload from a pipeline without an SCM app:** upload the checked-out tree as a cloud code review (`scans:write` and `uploads:write`). The two-step digest handoff keeps a human in control of what leaves the runner:
```bash
strix cloud scans start --source . --dry-run --show-files --json # review, capture source.archive_sha256
strix cloud scans start --source . --approve-sha256 "$SOURCE_SHA256" --wait
```
Exit codes: `0` success, `4` auth or plan limit, `5` payment required. Non-Enterprise scans consume credits.
Full CLI coverage (PR reviews, scans, SARIF export, schedules) is in the **managed-pentesting-with-strix** skill.
Recommend Option B for most teams (no maintenance, central dashboard); use Option A when scans must stay entirely within your own infrastructure.
@@ -11,7 +11,7 @@ metadata:
White-box security review with Strix: the agents read the source to build a model of routes, sinks, and authorization checks, then attempt real exploitation. Findings come with a proof-of-concept, so the output is a short list of proven issues rather than the hundreds of "potential" hits a pattern-matching scanner produces.
Install, LLM setup, all flags, and the managed-cloud path are in the **penetration-testing-with-strix** skill.
Install, LLM setup, all flags, and the managed-cloud path are in the **penetration-testing-with-strix** skill. For a run with no Docker and no LLM key, the same binary drives the managed platform: `strix cloud login`, then `strix cloud scans start ...` (details in **managed-pentesting-with-strix**).
## Run it
@@ -18,7 +18,7 @@ Get the findings from wherever the scan ran:
- **OSS CLI** — artifacts in `strix_runs/<run-name>/`:
- `vulnerabilities/*.md` — one finding per file: description, severity, PoC steps or script, affected code locations, remediation guidance.
- `vulnerabilities.json` — the same findings as JSON (ids, severity, CWE/CVE, `code_locations` with `fix_before`/`fix_after` suggestions when available).
- **Cloud (app.strix.ai)** — fetch the scan's `vulnerabilities[]` via `GET /api/v1/scans/{scanId}` (or `GET /api/v1/vulnerabilities` org-wide). Each carries `severity, cwe, endpoint, method, impact, technical_analysis, poc_description, poc_script_code` and, for code findings, `code_file`/`code_diff`/`code_before`/`code_after`. See the **managed-pentesting-with-strix** skill for auth.
- **Cloud (app.strix.ai)** — pull findings with the CLI: `strix cloud vulns list --scan-id <scan-id> --json` (or `strix cloud scans get <scan-id> --json | jq '.vulnerabilities'`, or `strix cloud vulns list --severity critical` org-wide). Each finding carries `severity, cwe, endpoint, method, impact, technical_analysis, poc_description, poc_script_code` and, for code findings, `code_file`/`code_diff`/`code_before`/`code_after`. After a fix is verified, mark it with `strix cloud vulns update <id> --status fixed`. See the **managed-pentesting-with-strix** skill for `strix cloud login` and scopes.
Order work by severity: critical → high → medium → low. Every Strix finding was validated with a working proof-of-concept, so do not dismiss findings as false positives without re-testing the PoC yourself.
+231 -62
View File
@@ -1,23 +1,59 @@
---
name: managed-pentesting-with-strix
description: Run a managed pentest of a web app or API through the app.strix.ai REST API — no local Docker, LLM key, or install needed. Create an API token, register domain/repository assets, launch and poll scans, triage vulnerabilities, export SARIF, download PDF/DOCX pentest reports for SOC 2 and other compliance evidence (Enterprise plan), start PR reviews, and set up schedules and webhooks. Use when the user wants continuous or scheduled pentesting-as-a-service, an auditor-ready pentest report, scans tracked in a team dashboard, or security testing from a sandboxed agent/CI environment with no infrastructure.
description: Run a managed pentest of a web app, API, repository, or local workspace on the app.strix.ai platform with the `strix cloud` CLI or REST API — no local Docker or LLM key needed. Safely review and upload local source, register assets, launch and poll scans, triage vulnerabilities, export SARIF, download compliance reports, start PR reviews, buy credits, and set up schedules or webhooks. Use for managed, continuous, scheduled, team-tracked, or sandboxed-agent security testing.
license: Apache-2.0
metadata:
author: usestrix
homepage: https://docs.app.strix.ai
---
# Strix Cloud API (managed, no local infra)
# Strix Cloud (managed, no local infra)
Use this when you want Strix's autonomous pentesting **without running Docker or an LLM yourself** — the scan runs on Strix's infrastructure and results are tracked in a team dashboard. This is the right choice in sandboxed/hosted agent and CI environments, for teams, and for scheduled/continuous testing (downloadable PDF/DOCX reports are an Enterprise-plan feature). For fully local, free, air-gapped, or BYO-LLM runs, use the open-source CLI in the **penetration-testing-with-strix** skill instead — both share the same engine and SARIF output, so you can mix them.
Full reference: **[docs.app.strix.ai](https://docs.app.strix.ai)** · OpenAPI: `https://docs.app.strix.ai/openapi.json`
There are two equivalent interfaces. Prefer the CLI:
## Setup
- **`strix cloud` CLI** — every REST operation has a command in the form `strix cloud <resource> <verb>`. Install with `curl -sSL https://strix.ai/install | bash`. Run `strix cloud` to list all resources and `strix cloud <resource> help` (or `-h`) to list a resource's verbs; a bare resource with a safe read operation runs its documented default.
- **REST API** — base URL `https://app.strix.ai/api/v1`, `Authorization: Bearer <token>` on every request. Full reference: **[docs.app.strix.ai](https://docs.app.strix.ai)** · agent index: `https://docs.app.strix.ai/llms.txt` · OpenAPI: `https://docs.app.strix.ai/openapi.json`.
- **Base URL:** `https://app.strix.ai/api/v1`
- **Auth:** every request sends `Authorization: Bearer <token>`. Tokens are **org-scoped**.
- **Get a token:** the user creates one in the dashboard at **Settings → API Access** (app.strix.ai). Ask them for it; never hardcode, log, or commit it. Store it in an env var or the CI secret store.
The CLI is equally usable by agents and people. Output is complete JSON when stdout is not a terminal, or when you pass `--json`; terminal tables favor names, branches, lifecycle states, and numbered selectors. 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 label credentials as active, expired, or revoked. Binary downloads are the exception: redirect raw bytes intentionally, or use `--output FILE --json` to write the file and receive structured metadata. There are no interactive prompts when stdin is not a terminal. Exit codes: `0` success, `1` request/runtime error, `2` invalid usage, `4` authentication or plan limit, `5` payment required.
Every resource group with a safe read operation has a useful default action, and `-h` or `help` always shows its verbs. Native tab completion includes resources, verbs, flags, workspace commands, and local paths:
```bash
source <(strix completions zsh) # current zsh session
source <(strix completions bash) # current bash session
strix completions fish | source # current fish session
```
Write commands take request fields as flags. Every write command also accepts one JSON object with `--data`, which is the way to send fields that have no flag:
```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
```
The platform enforces plan and role limits, and the CLI passes the platform message through. Report downloads need the Enterprise plan. Schedules need the Pro plan. Billing writes need an admin token. A blocked command exits with code `4`.
## Setup: sign in
Run the device sign-in. It creates the user's account and workspace on first use and stores a personal API token in `~/.strix/platform-auth.json`:
```bash
strix cloud login
# Non-interactive least-privilege example:
strix cloud login --scopes scans:read scans:write uploads:write billing:read vulnerabilities:read assets:read assets:write
# Or use a stable named profile:
strix cloud login --scope-profile recommended
```
The user approves the sign-in in the browser. With `--scopes` (and optionally `--workspace <name-or-id>`) there are no terminal prompts, so the command works from a non-interactive agent shell. In an interactive terminal without flags, the CLI offers a workspace picker and scope presets (Recommended, Full access, Minimal, Custom). Recommended covers ordinary scans, source uploads, workspace switching, and user-approved credit top-ups; it excludes `tokens:write`, which must be requested explicitly when credential management is required. Use explicit scopes for a narrower automation token.
- `strix cloud whoami` is the fast local status. `strix cloud session --json` verifies the remote device session; `strix cloud session scopes` shows both effective access and the immutable login ceiling.
- `strix cloud logout` revokes the remote session before removing the local token. On a network or server failure it keeps the token so the user can retry; `--local-only` deliberately skips revocation.
- Every other `strix cloud` command uses the stored token automatically. `--token <token>` or `STRIX_API_TOKEN` is a stateless per-command override and never overwrites the stored account. For an override that is itself a CLI session, also pass `--workspace-id` or set `STRIX_WORKSPACE_ID`.
- Never hardcode, log, or commit the token. Store it in an env var or the CI secret store.
- **Scopes (least-privilege):** assign only what the integration needs and rotate regularly:
| Scope | Grants |
@@ -28,15 +64,105 @@ Full reference: **[docs.app.strix.ai](https://docs.app.strix.ai)** · OpenAPI: `
| `schedules:read` / `:write` | read schedules · create/trigger recurring scans |
| `pr_reviews:write` | trigger PR security reviews |
| `webhooks:read` / `:write` | manage webhook subscriptions |
| `tokens:write` | create/revoke API tokens |
| `uploads:write` | upload local source or documents for a scan |
| `organizations:read` | read organization details (listing/switching the signed-in user's workspaces needs no API scope) |
| `organizations:write` | create/update workspaces (admin) |
| `tokens:write` | create/revoke ordinary API tokens (not needed to manage the current CLI session) |
| `knowledge:read` / `:write` | read/update organization knowledge |
| `audit:read` | read/export the Enterprise audit log |
| `billing:read` / `billing:write` | read credit balance & auto top-up settings · buy credits (admin) |
HTTP errors map to messages and exit codes: `401` bad/expired token (exit `4`), `402` out of credits (exit `5`), `403` scope/plan-tier limit (exit `4`), `422` validation error (exit `1`).
Create a time-limited automation token with `strix cloud tokens create`. Use
`--rbac-scopes` to restrict it to target IDs, tags, or business units; the value is a
JSON array of `{ "type": "target|tag|business_unit", "value": "..." }` objects:
```bash
export STRIX_API_TOKEN="<token>"
BASE=https://app.strix.ai/api/v1
auth=(-H "Authorization: Bearer $STRIX_API_TOKEN")
strix cloud tokens create --type service --name staging-ci \
--expires-at 2026-12-31T23:59:59Z \
--scopes scans:read scans:write \
--rbac-scopes '[{"type":"tag","value":"staging"}]'
```
All examples use `jq` to parse JSON. Handle HTTP errors: `401` bad/expired token, `402` out of credits, `403` scope/plan-tier limit, `422` validation error.
The token secret is returned once. Store it directly in a secret manager and do not
print or commit it. `--expires-at` and `--expires-in-days` are mutually exclusive.
## 0. Credits & top-ups
Non-Enterprise scans consume org credits. Enterprise engagements are plan-included and do not debit the wallet. Check the balance before a scan (`billing:read`):
```bash
strix cloud credits
```
When the balance is too low, buy credits with `strix cloud billing topup` (`billing:write`, admin token). The server answers the first request with **HTTP 402 and a machine-payment challenge** (Stripe Machine Payments Protocol). The CLI pays the challenge with the Stripe Link wallet client when Node.js is available — the user approves the spend in the [Link app](https://link.com/agents). The response returns the receipt (`credits_granted`, `duplicate`, `reference`) and the new balance.
A default-tier source-only code review currently starts at 60 credits. Source uploads are not free: they launch an ordinary `code_review` and use the same deterministic scope estimator. The service checks the full balance before launch, reserves credits atomically only after validation succeeds, and does not create or charge a rejected scan. Retests and Enterprise scans are exempt.
```bash
strix cloud billing topup --credits 20 --yes # explicit approval; skips the TTY prompt
strix cloud billing topup --credits 20 --no-pay # print the 402 challenge without paying
```
The default payment path is the Stripe Link wallet. When no wallet is connected, an interactive `strix cloud billing topup` starts the Link sign-in for the user and prints the verification link. The user approves the connection one time in the Link app, and then approves each payment there. No keys or variables are necessary. In a non-interactive process, the command stops and tells the user to connect the wallet at [link.com/agents](https://link.com/agents) or to use the hosted checkout link.
In a non-interactive agent or CI process, payment never proceeds unless the command includes `--yes`. Show the challenge or estimated spend to the user and obtain approval before adding it. `--no-pay` always stops after printing the challenge.
If the user does not want a wallet, create a hosted checkout link with `strix cloud billing subscribe --plan strix_top_up` and give the link to the user. The user pays in the browser.
Automatic top-ups (admin): `strix cloud billing auto-topup` shows the setting. Enable it with:
```bash
strix cloud billing auto-topup update --enabled --topup-credits 20 --monthly-cap-credits 200
```
An omitted `--monthly-cap-credits` keeps the stored cap. Pass `--no-monthly-cap` to remove the cap.
### Workspaces and account setup
Manage workspaces with a personal token from `strix cloud login`:
```bash
strix cloud workspaces list # numbered name/role/current list
strix cloud workspaces create --name "My Team" # admin + organizations:write
strix cloud workspaces use 2 # displayed number, exact name, or ID
strix cloud workspace use "My Team" # singular `workspace` alias also works
strix cloud session scopes # effective scopes + consent ceiling
strix cloud session scopes set minimal # narrow the session
strix cloud org members invite --email [email protected] --role analyst
```
`workspaces use` retargets the current personal token to a workspace the user already belongs to and stores the updated workspace metadata; the bearer secret and expiry stay unchanged. It does not reprompt during ordinary switches: the server preserves the chosen profile, enforces the immutable login ceiling, and caps effective scopes by the target role. Use `--scope-profile` or `--scopes` to narrow within that ceiling; broader consent requires `strix cloud login` again. The CLI pins each process to the workspace it started in, so concurrent shells fail with a recoverable conflict instead of silently crossing organizations.
### Handoffs a person must finish
Four steps end at the user. The command creates the link or the record and prints it. Strix opens the browser only in an interactive terminal. Pass `--no-browser` to print the URL only.
```bash
strix cloud billing subscribe --plan strix_cloud # hosted checkout page for the Cloud plan
strix cloud billing portal # billing portal for the card and the plan
strix cloud integrations install github # GitHub App or Slack installation page
strix cloud domains verify <domain-id> # DNS record to add, then run it again
```
Give the printed URL or DNS record to the user and wait. Do not claim that the payment, the installation, or the DNS change is complete. Confirm the result afterwards with `strix cloud credits`, `strix cloud integrations list`, or `strix cloud domains list`. All four commands need an admin token, except `domains verify`, which needs `assets:write`.
### Organization knowledge
Agents can manage the organization knowledge base without the dashboard (`knowledge:read` / `knowledge:write`):
```bash
strix cloud knowledge list --search authentication
strix cloud knowledge add --title "Authentication" --content "Staging uses SSO."
strix cloud knowledge update <document-id> --content "Staging uses SSO and TOTP."
strix cloud knowledge delete <document-id>
strix cloud knowledge policies add --key staging-only --content "Never test production."
strix cloud knowledge policies delete staging-only
strix cloud knowledge repos entries usestrix/strix
```
Knowledge policy writes require an admin token. Repository names are passed as normal `owner/name` values; the CLI handles URL encoding. The `costs` and `llm-settings` commands target on-prem installations and return `404` on app.strix.ai.
## 1. Register the target as an asset
@@ -44,109 +170,152 @@ Scans run against **registered assets**, not raw URLs. Register once, then reuse
```bash
# Domain (black-box / live target). Requires domain verification before external scanning.
# asset_type must be one of: web_app | api | attack_surface.
curl -sS "$BASE/domains" "${auth[@]}" -H "Content-Type: application/json" \
-d '{"domain":"staging.example.com","asset_type":"web_app"}' | jq '{id:.domain.id, status, reachable, verification}'
# --asset-type must be one of: web_app | api | attack_surface.
strix cloud domains add --domain staging.example.com --asset-type web_app
# Repository (white-box / code review). `full_name` is "owner/name".
# Send one repository object, or a bare JSON array for several — not an object
# wrapping a "repositories" key (that is rejected with 400).
curl -sS "$BASE/repositories" "${auth[@]}" -H "Content-Type: application/json" \
-d '[{"full_name":"org/app","provider":"github"}]' | jq '.repositories[] | {id, full_name}'
strix cloud repos add --data '{"full_name":"org/app","provider":"github"}'
```
Look up existing assets instead of re-adding: `GET /domains`, `GET /repositories` (both `assets:read`, paginated with `?page=&limit=`).
Look up existing assets instead of re-adding: `strix cloud domains list`, `strix cloud repos list` (both `assets:read`).
## 2. Launch a scan
`POST /scans` (`scans:write`). Provide at least one target via `domain_ids`, `repository_ids`, or `internal_targets` (internal infra needs a network connector — see docs).
`strix cloud scans start` (`scans:write`). Provide at least one target with `--domain-ids`, `--repository-ids`, or `--internal-targets` (internal infra needs a network connector — see docs).
```bash
scan_id=$(curl -sS "$BASE/scans" "${auth[@]}" -H "Content-Type: application/json" -d '{
"engagement_type": "live_test",
"domain_ids": ["<domain-uuid>"],
"focus": "IDOR, auth bypass, SSRF",
"context": "Staging. Test account creds are configured as a test user.",
"notify_on_completion": true
}' | jq -r .scan_id)
echo "$scan_id"
strix cloud scans start \
--engagement-type live_test \
--domain-ids <domain-uuid> \
--focus "IDOR, auth bypass, SSRF" \
--context "Staging. Test account creds are configured as a test user." \
--notify-on-completion
```
Useful `CreateScanRequest` fields:
Useful flags (each maps to a `CreateScanRequest` field):
| Field | Purpose |
| Flag | Purpose |
|---|---|
| `engagement_type` | `live_test` (default), `code_review`, `internal_infra`, `compliance_pentest` |
| `domain_ids` / `repository_ids` / `internal_targets` | targets (at least one) |
| `domain_paths` / `repository_branches` | narrow to specific paths / branches |
| `credentials` | authenticated scanning, incl. `mfa_method` (`totp`/`email_otp`/…) + `totp_secret` |
| `headers` | extra HTTP headers (API keys, for example) for the target |
| `focus` / `concerns` / `context` | steer the agents |
| `upload_ids` | attach uploaded source/docs archives for white-box context |
| `notify_on_completion` / `notification_emails` | email when done |
| `--engagement-type` | `live_test` (default), `code_review`, `internal_infra`, `compliance_pentest` |
| `--domain-ids` / `--repository-ids` / `--internal-targets` | targets (at least one) |
| `--domain-paths` / `--repository-branches` | narrow to specific paths / branches (JSON maps) |
| `--credentials` | authenticated scanning, incl. `mfa_method` (`totp`/`email_otp`/…) + `totp_secret` (JSON list) |
| `--headers` | extra target HTTP headers as a JSON array of header objects |
| `--focus` / `--concerns` / `--context` | free-form strings that steer the agents |
| `--upload-ids` | attach uploaded source/docs archives for white-box context |
| `--notify-on-completion` / `--notification-emails` | email when done |
Response is `{ scan_id, title, status }` with `status` = `pending`.
Without `--source`, the response is `{ scan_id, title, status }` with `status` = `pending`.
Local-source success wraps that platform response as
`{ source, upload_id, scan: { scan_id, title, status } }`, so automation can retain the exact
approved manifest and staged-upload identifier alongside the created scan.
## 3. Poll to completion
### Scan a local workspace in the cloud
`GET /scans/{scanId}` (`scans:read`). Status flow: `pending → running → completed` (or `failed` / `cancelled`). Poll on an interval — scans take minutes to hours. Do not block.
For an agent or CI workflow, bind approval to the exact source snapshot that was reviewed. Run
the dry run with the intended source-selection flags, review the manifest and selected paths,
and capture `source.archive_sha256`. Then repeat the same `--source`, every `--exclude`, and
any `--include-hidden`, `--include-sensitive`, or `--include-archives` flags with
`--approve-sha256`:
```bash
while :; do
s=$(curl -sS "$BASE/scans/$scan_id" "${auth[@]}" | jq -r .status)
echo "status=$s"; [[ "$s" =~ ^(completed|failed|cancelled)$ ]] && break
sleep 60
done
strix cloud scans start --source . --exclude 'private/' --dry-run --show-files --json
# After reviewing the output, capture its source.archive_sha256 value:
SOURCE_SHA256="<reviewed source.archive_sha256>"
# Repeat every source-selection flag unchanged; a source-only scan infers code_review.
strix cloud scans start --source . --exclude 'private/' \
--approve-sha256 "$SOURCE_SHA256" --wait
```
The CLI rebuilds the archive and refuses the upload if its SHA-256 no longer matches. `--yes`
has deliberately narrower semantics: it approves only the snapshot built during that one
invocation. Use it for a deliberate human or one-shot approval, not as the second half of a
digest-bound agent/CI review. Without a TTY, a source upload requires either matching
`--approve-sha256` approval or `--yes`; an interactive terminal can instead show the summary,
the selected filenames when `--show-files` is set, and a `[y/N]` confirmation for its current
snapshot.
The default selection is privacy-conscious: in a Git worktree it includes tracked files plus untracked files that are not ignored; it honors `.gitignore`, excludes every hidden path component, always excludes `.git`, symlinks, dependencies/build output, secret-like filenames, and nested archives. Add project exclusions to `.strixignore` (one exclude glob per line) or repeat `--exclude GLOB`; a trailing slash such as `private/` excludes that directory subtree.
The client refuses more than 20,000 files, a file over 25 MiB, more than 250 MiB expanded, or a ZIP over 50 MiB. The service then stream-inflates the ZIP and independently rejects malformed or unsupported entries, unsafe paths, too many entries, oversized entries, excessive expanded data, and oversized compressed input, so an untrusted client cannot bypass the ZIP-bomb controls by forging metadata.
Only use `--include-hidden`, `--include-sensitive`, or `--include-archives` after the dry-run manifest shows that the scan needs them. Hidden and sensitive files are separate opt-ins: for example, including `.env` requires both `--include-hidden` and `--include-sensitive`.
The CLI removes its private temporary local archive after every invocation. Once a remote
upload is staged, a definitive scan rejection causes the CLI to delete it. A network failure,
`5xx` response, malformed success response, or interruption after scan launch begins is
ambiguous—the platform may have accepted the scan—so the CLI retains the upload and returns
its `upload_id` with `launch_outcome_unknown: true`. If an automatic deletion attempt cannot
be confirmed, it instead returns the retained `upload_id` with `cleanup_unknown: true`.
Before retrying, run `strix cloud scans list` to avoid a duplicate scan or charge. If no scan
is linked to the retained upload, remove it with `strix cloud uploads delete UPLOAD_ID`;
linked uploads cannot be deleted.
With no explicit type, source alone infers `code_review`. Any domain target wins and infers `live_test`, so source plus a deployed domain is the normal white-box live-test workflow. Pass `--engagement-type` when you need to override the inference.
## 3. Wait for completion
Pass `--wait` to `scans start` to poll until the scan reaches a final state, or poll yourself with `strix cloud scans get <scan-id>` (`scans:read`). Bound automation with `--wait-timeout SECONDS`; timeout exits cleanly without cancelling the remote scan. Status flow: `pending → running → completed` (or `failed` / `cancelled`). Scans take minutes to hours — poll on an interval, do not block indefinitely.
## 4. Read findings
The scan-detail response includes `executive_summary`, `methodology`, `recommendations`, a `findings` severity roll-up, and a `vulnerabilities[]` array. Each vulnerability carries `title, severity, status, cvss, cwe, endpoint, method, impact, technical_analysis, poc_description, poc_script_code`, and (for code findings) `code_file`/`code_diff`/`code_before`/`code_after`.
```bash
curl -sS "$BASE/scans/$scan_id" "${auth[@]}" \
strix cloud scans get <scan-id> --json \
| jq '["critical","high","medium","low","info"] as $order
| .vulnerabilities
| sort_by(.severity as $s | $order | index($s))
| .[] | {title, severity, endpoint, cwe}'
```
Cloud severities are `critical | high | medium | low` and statuses are `open | in_progress | fixed | ignored`. Sort by an explicit severity order rather than `sort_by(.severity)`, which sorts alphabetically (critical, high, low, medium).
Cloud severities are `critical | high | medium | low` and statuses are `open | in_progress | snoozed | fixed | ignored | not_affected`. Sort by an explicit severity order rather than `sort_by(.severity)`, which sorts alphabetically (critical, high, low, medium).
Org-wide triage across scans: `GET /vulnerabilities` (`vulnerabilities:read`; filter by severity/status). Update triage state with the vulnerabilities `:write` endpoints. To remediate, hand off to the **fix-security-vulnerabilities-with-strix** skill.
Org-wide triage across scans: `strix cloud vulns list --severity critical` (`vulnerabilities:read`, and it also filters by `--status`, `--scan-id`, and more). Update triage state with `strix cloud vulns update <id> --status fixed`. To remediate, hand off to the **fix-security-vulnerabilities-with-strix** skill.
## 5. Export & report
```bash
# SARIF 2.1.0 for GitHub code scanning / ASPM ingestion
curl -sS "$BASE/scans/$scan_id/sarif" "${auth[@]}" -o findings.sarif
strix cloud scans sarif <scan-id> --output findings.sarif
# Report. The format and file type are query params (`Accept` is ignored):
# format=technical (default) | retest | attestation | executive_summary
# type=pdf (default) | docx
# Any report download requires the Enterprise plan; formats beyond `technical`,
# Report. Formats: technical (default) | retest | attestation | executive_summary
# Types: pdf (default) | docx
# Any report download requires the Enterprise plan. Formats beyond `technical`,
# DOCX, and white-label branding are Enterprise-only too. Scan must be completed.
curl -sS "$BASE/scans/$scan_id/report?format=technical&type=pdf" "${auth[@]}" -o strix-report.pdf
strix cloud scans report <scan-id> --format technical --type pdf --output strix-report.pdf
```
Downloads refuse to replace a file unless `--force` is explicit. Enterprise audit logs can be streamed as JSON or exported without trying to JSON-decode the body:
```bash
strix cloud audit list --format csv --all --output audit.csv
strix cloud audit list --format ndjson --all --output audit.ndjson
```
## 6. PR reviews
Trigger an automated security review of a pull request (`pr_reviews:write`); results appear as PR comments and in the dashboard:
Trigger an automated security review of a pull request (`pr_reviews:write`). Read the repository's `provider` and `installation_id` with `strix cloud repos list`; both identify the installed source-control integration. The results appear as PR comments and in the dashboard:
```bash
curl -sS "$BASE/pr-reviews/start" "${auth[@]}" -H "Content-Type: application/json" \
-d '{"repository_full_name":"org/app","pr_number":123}'
strix cloud pr-reviews start \
--provider github \
--installation-id <installation-id> \
--repository-full-name org/app \
--pr-number 123
```
List/inspect via `GET /pr-reviews` and `GET /pr-reviews/{id}`. Repo-level PR-review behavior is configured with the repository-settings endpoint.
List/inspect with `strix cloud pr-reviews list` and `strix cloud pr-reviews get <id>`. Repo-level PR-review behavior is configured with `strix cloud pr-reviews settings`.
## 7. Continuous testing (schedules & webhooks)
- **Schedules** (`schedules:write`, Pro plan): create recurring scans and trigger them on demand — the managed equivalent of a cron-driven CLI loop.
- **Webhooks** (`webhooks:write`): subscribe to pentest/vulnerability lifecycle events such as `scan.completed` and `vulnerability.created` to push results into Slack, ticketing, or your own pipeline instead of polling.
- **Schedules** (`schedules:write`, Pro plan): `strix cloud schedules create` makes recurring scans, and `strix cloud schedules trigger <id>` runs one on demand — the managed equivalent of a cron-driven CLI loop.
- **Webhooks** (`webhooks:write`): `strix cloud webhooks create` subscribes to pentest/vulnerability lifecycle events such as `scan.completed` and `vulnerability.created` to push results into Slack, ticketing, or your own pipeline instead of polling.
See the schedules and webhooks sections at [docs.app.strix.ai](https://docs.app.strix.ai) for payloads.
Network connectors are Enterprise-only. `strix cloud connectors create` may return a one-time enrollment command containing credentials; do not paste it into logs, and request it with `--include-command` only when the user is ready to install it. Browser checkout, source-control installation, DNS verification, connector installation, chat sharing, and publishing SARIF to an external provider are user handoffs or explicit external mutations—prepare the command/link, then obtain the appropriate approval before completing them.
## Safety
Only scan assets the user's organization owns or is authorized to test. External domain scans require verification (DNS/file/meta-tag) enforced by the platform — do not try to bypass it.
+1 -1
View File
@@ -13,7 +13,7 @@ The OWASP Top 10 is a taxonomy of risk categories, not a test suite — "OWASP T
**Use the current edition: [OWASP Top 10:2025](https://owasp.org/Top10/)** (8th installment, superseding 2021). Ask the user before targeting an older edition — some compliance checklists still reference 2021, and a report labelled with the wrong edition is misleading. Key differences from 2021: **SSRF is folded into A01**, **A03 Software Supply Chain Failures** expands the old "Vulnerable and Outdated Components", and **A10 Mishandling of Exceptional Conditions** is new; A02 Security Misconfiguration moved 5→2.
Install, LLM setup, and the managed-cloud alternative: **penetration-testing-with-strix**.
Install, LLM setup, and the managed-cloud alternative: **penetration-testing-with-strix**. For a run with no Docker and no LLM key, the same binary drives the managed platform: `strix cloud login`, then `strix cloud scans start ...` (details in **managed-pentesting-with-strix**).
## What is and is not testable by an agent
+21 -15
View File
@@ -12,7 +12,7 @@ metadata:
Strix runs autonomous AI pentesting agents that dynamically exploit a target and only report findings validated with a working proof-of-concept. There are **two ways to run it, built on the same engine and producing the same findings** — pick per situation, and mix them freely:
- **Open-source CLI** (self-hosted) — runs on your machine in a Docker sandbox with your own LLM key. Free, fully local, BYO-LLM, air-gap capable. Docs: [docs.strix.ai](https://docs.strix.ai).
- **Cloud API** (managed) — runs on Strix's infrastructure via `https://app.strix.ai/api/v1`. No Docker, no LLM key, no local compute; adds team dashboards, scheduling, PR reviews, downloadable PDF/DOCX reports (Enterprise plan), and internal-network connectors. Docs: [docs.app.strix.ai](https://docs.app.strix.ai). Full workflow in the **managed-pentesting-with-strix** skill.
- **Managed cloud** — runs on Strix's infrastructure, driven from the same CLI (`strix cloud ...`) or the REST API at `https://app.strix.ai/api/v1`. No Docker, no LLM key, no local compute; adds team dashboards, scheduling, PR reviews, downloadable PDF/DOCX reports (Enterprise plan), and internal-network connectors. Docs: [docs.app.strix.ai](https://docs.app.strix.ai). Full workflow in the **managed-pentesting-with-strix** skill.
## Which one? (decide, do not default)
@@ -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]` | Place a file from this machine into `/workspace` read-only before the scan, for a wordlist, a spec, or notes. Repeatable. |
| `--workspace-file PATH[:DEST]` | Copy a file from this machine into `/workspace` 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`. |
@@ -122,27 +122,33 @@ Artifacts land in `strix_runs/<run-name>/`:
---
# Option B — Cloud API (managed, no local infra)
# Option B — Managed cloud (no local infra)
Full details, asset registration, polling, reports, PR reviews, schedules, and webhooks are in the **managed-pentesting-with-strix** skill. Minimal launch-and-poll:
The same `strix` binary drives the managed platform. Every command starts with `strix cloud`. Full details — asset registration, source uploads, reports, PR reviews, schedules, webhooks, and billing — are in the **managed-pentesting-with-strix** skill. Minimal flow:
```bash
export STRIX_API_TOKEN="<token>" # org-scoped bearer, from Settings → API Access at app.strix.ai
BASE=https://app.strix.ai/api/v1
# 1. Sign in (device flow — the user confirms a code in the browser; this also
# creates the account and workspace when needed)
strix cloud login
# 1. Launch a scan against an already-registered domain/repo asset
scan_id=$(curl -sS "$BASE/scans" \
-H "Authorization: Bearer $STRIX_API_TOKEN" -H "Content-Type: application/json" \
-d '{"engagement_type":"live_test","domain_ids":["<domain-uuid>"]}' | jq -r .scan_id)
# If you need specific scopes, request them with --scopes:
# strix cloud login --scopes scans:read scans:write assets:read assets:write \
# vulnerabilities:read billing:read billing:write
# 2. Poll until terminal (pending → running → completed/failed/cancelled)
curl -sS "$BASE/scans/$scan_id" -H "Authorization: Bearer $STRIX_API_TOKEN" | jq '.status'
# 2. Register and verify the target domain (verification prints a DNS record for the user)
strix cloud domains add --domain staging.example.com --asset-type web_app
strix cloud domains verify <domain-id>
# 3. Read validated findings from the scan detail's `vulnerabilities[]`, or export SARIF
curl -sS "$BASE/scans/$scan_id/sarif" -H "Authorization: Bearer $STRIX_API_TOKEN" -o findings.sarif
# 3. Launch and wait
strix cloud scans start --engagement-type live_test --domain-ids <domain-id> --wait
# 4. Read validated findings
strix cloud vulns list --severity critical
```
Ask the user to create the token (and register the target as a domain/repository asset) if they have not. If Docker/local prerequisites are not already satisfied, use this path instead of trying to install infra.
For a local repository, `strix cloud scans start --source .` uploads the working tree (needs `uploads:write`) and infers a code review. When credits run out, `strix cloud billing topup` starts an agent-payable Stripe challenge — the managed skill covers the payment flow. Output is JSON when stdout is not a terminal, so the commands compose in scripts.
The raw REST API works too (`https://app.strix.ai/api/v1`, org-scoped bearer token — see [docs.app.strix.ai](https://docs.app.strix.ai)). If Docker or local prerequisites are not already satisfied, use this path instead of trying to install infra.
---
+1 -1
View File
@@ -11,7 +11,7 @@ metadata:
Black-box (and optionally source-assisted) penetration testing of a running web app with Strix's autonomous agents. Every reported finding is validated with a working exploit, so there are no signature-based false positives to triage.
Install, LLM setup, all CLI flags, and the managed-cloud alternative are covered in the **penetration-testing-with-strix** skill — read it if the target is not a running web app, or if `strix --version` fails. This skill is the web-app-specific workflow.
Install, LLM setup, all CLI flags, and the managed-cloud alternative are covered in the **penetration-testing-with-strix** skill — read it if the target is not a running web app, or if `strix --version` fails. For a run with no Docker and no LLM key, the same binary drives the managed platform: `strix cloud login`, then `strix cloud scans start ...` (details in **managed-pentesting-with-strix**). This skill is the web-app-specific workflow.
## 1. Confirm authorization and scope
+15 -2
View File
@@ -29,7 +29,13 @@ 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, list_mcps
from strix.tools.mcp import (
call_mcp,
describe_mcp,
get_mcp_tool_schema,
list_mcps,
search_mcp_tools,
)
from strix.tools.notes.tools import (
create_note,
delete_note,
@@ -50,8 +56,10 @@ 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,
)
from strix.tools.respond.tool import respond_to_user
from strix.tools.thinking.tool import think
@@ -68,7 +76,7 @@ from strix.tools.todo.tools import (
mark_todo_pending,
update_todo,
)
from strix.tools.web_search.tool import web_search
from strix.tools.web_search.tool import web_get_contents, web_search
if TYPE_CHECKING:
@@ -578,8 +586,11 @@ _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,
@@ -589,6 +600,8 @@ _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,
+25 -15
View File
@@ -77,18 +77,20 @@ 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 — 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.
- 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.
{% if system_prompt_context.mcp_connections %}
- Connected this run (call describe_mcp on one to see its tools):
- Connected this run (search one to find relevant 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 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.
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.
{% endif %}
AUTHORIZATION STATUS:
@@ -151,8 +153,9 @@ WHITE-BOX TESTING (code provided):
- Local execution, unit/integration testing, patch verification, and HTTP requests against locally started in-scope services are normal authorized white-box validation
- If dynamically running the code proves impossible after exhaustive attempts, pivot to comprehensive static analysis.
- Try to infer how to run the code based on its structure and content.
- Derive the code fix as PART OF reporting, not as a separate later pass: 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.
- 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.
COMBINED MODE (code + deployed target present):
- Treat this as static analysis plus dynamic testing simultaneously
@@ -238,8 +241,13 @@ 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
- 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
- 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.
- 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):
@@ -355,7 +363,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-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)
3. **WHITE-BOX**: Discovery → Validation → Reporting with an initial fix candidate. The later preparation phase repairs and verifies the candidate.
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
@@ -374,7 +382,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 its inline fix (`code_locations` + `fix_pr_body`) in one shot — no separate fixing agent
- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" that files the report and an initial fix candidate (`code_locations` + `fix_pr_body`)
VULNERABILITY WORKFLOW (MANDATORY FOR EVERY FINDING):
@@ -396,10 +404,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 fix inline: code_locations fix_before/fix_after + fix_pr_body,
applying/verifying the patch in the same turn if desired)
with the initial fix candidate: code_locations fix_before/fix_after
+ fix_pr_body)
↓
STOP - no separate fixing agent; the fix was derived once, at report time
STOP - the scan preparation phase handles repository-wide repair and verification
```
CRITICAL RULES:
@@ -435,7 +443,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, spawns a reporting agent that files the report with the fix inline (white-box) — no separate fixing agent
- **Valid Vulnerability**: Validation succeeds. The reporting agent files the report and an initial fix candidate for white-box findings.
PERSISTENCE IS MANDATORY:
- Real vulnerabilities take TIME - expect to need 2000+ steps minimum
@@ -492,6 +500,8 @@ 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:
+4 -1
View File
@@ -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",
"id_token_add_organizations": "true", # nosec B105 - boolean flag, not a secret
"codex_cli_simplified_flow": "true",
"originator": ORIGINATOR,
}
@@ -356,6 +356,8 @@ 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:
@@ -367,6 +369,7 @@ 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,
+62 -19
View File
@@ -10,11 +10,13 @@ from typing import TYPE_CHECKING, Any
from pydantic import AliasChoices, BaseModel
from strix.config.settings import Settings
from strix.config.settings import LlmSettings, Settings
from strix.utils.secret_files import write_secret_text
if TYPE_CHECKING:
from collections.abc import Mapping
from pydantic.fields import FieldInfo
@@ -25,6 +27,11 @@ _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.
@@ -54,22 +61,31 @@ def apply_config_override(path: Path) -> None:
def persist_current() -> None:
"""Write currently-set env vars to the active config file (0o600)."""
"""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.
"""
s = load_settings()
target = _override or _DEFAULT_PATH
target.parent.mkdir(parents=True, exist_ok=True)
env_block: dict[str, str] = {}
for sub_name in s.model_fields:
env_block = _drop_stale_llm_connection(_read_env_block(target))
for sub_name in type(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():
for alias in _aliases_for(finfo):
value = os.environ.get(alias.upper())
if value:
env_block[alias.upper()] = value
break
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]
write_secret_text(target, json.dumps({"env": env_block}, indent=2))
@@ -93,17 +109,9 @@ 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.
"""
if not path.exists():
env_block_upper = _drop_stale_llm_connection(_read_env_block(path))
if not env_block_upper:
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]] = {}
@@ -123,3 +131,38 @@ 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()}
+87 -55
View File
@@ -40,6 +40,7 @@ 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:
@@ -390,7 +391,7 @@ def _guard_event(
event: TResponseStreamEvent, rewriter: TurnCallIdRewriter, limiter: TurnToolCallLimiter
) -> TResponseStreamEvent | None:
if isinstance(event, ResponseOutputItemAddedEvent | ResponseOutputItemDoneEvent):
rewritten = rewriter.rewrite_item(event.item)
rewritten = rewriter.rewrite_item(event.item, event.output_index)
if not limiter.allow(rewritten):
return None
if rewritten is not event.item:
@@ -525,13 +526,37 @@ 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 = _CodexResponsesModel(
slug,
codex.get_subscription_client(),
reasoning_effort=llm.reasoning_effort,
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,
)
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
@@ -545,6 +570,23 @@ 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(
@@ -562,6 +604,8 @@ 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",
@@ -570,6 +614,7 @@ 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",
@@ -577,6 +622,8 @@ 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",
@@ -588,16 +635,27 @@ RECOMMENDED_MODEL_NAMES = (
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
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")),
# 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",
)
@@ -605,6 +663,7 @@ 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()
@@ -616,9 +675,11 @@ 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)
set_default_openai_api("chat_completions")
else:
set_default_openai_api("responses")
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)
_configure_extra_headers(llm)
@@ -755,12 +816,16 @@ 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)
@@ -793,6 +858,8 @@ 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)
@@ -831,11 +898,8 @@ def is_recommended_or_frontier_model(model_name: str) -> bool:
return False
if name in _RECOMMENDED_MODEL_NAME_SET:
return True
provider_name, bare_model_name = _split_model_provider(name)
return any(
_matches_frontier_family(provider_name, bare_model_name, provider_markers, prefixes)
for provider_markers, prefixes in FRONTIER_MODEL_FAMILIES
)
bare_model_name = name.rsplit("/", 1)[-1]
return _matches_model_prefix(bare_model_name, FRONTIER_MODEL_PREFIXES)
def _normalized_model_name(model_name: str) -> str:
@@ -847,28 +911,6 @@ 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)
@@ -886,16 +928,6 @@ 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
+29
View File
@@ -9,6 +9,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
ApiType = Literal["responses", "chat_completions"]
DEFAULT_MAX_TURNS = 500
@@ -23,6 +24,11 @@ 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"),
@@ -120,6 +126,10 @@ 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
@@ -128,6 +138,25 @@ 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",
+57 -18
View File
@@ -1,12 +1,17 @@
"""Keep tool-call ids unique within a conversation.
"""Keep tool-call ids present and 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. Rewriting duplicates to fresh unique ids keeps the history
valid for any provider.
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.
"""
from __future__ import annotations
@@ -15,6 +20,7 @@ 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
@@ -22,23 +28,29 @@ 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):
if isinstance(call_id, str) and call_id:
used.add(call_id)
elif isinstance(item, ResponseFunctionToolCall):
elif isinstance(item, ResponseFunctionToolCall) and item.call_id:
used.add(item.call_id)
return used
def dedupe_history_call_ids(items: list[Any]) -> tuple[list[Any], bool]:
"""Rewrite duplicate call ids in a conversation history.
"""Rewrite blank and duplicate call ids in a conversation history.
Outputs are paired with their call by order, so parallel calls that share
an id keep answering the right call after the rewrite.
an id — or are all missing one — keep answering the right call after the
rewrite.
"""
used: set[str] = set()
pending: dict[str, deque[str]] = defaultdict(deque)
@@ -49,22 +61,24 @@ def dedupe_history_call_ids(items: list[Any]) -> tuple[list[Any], bool]:
if not isinstance(item, dict):
rebuilt.append(item)
continue
call_id = item.get("call_id")
if not isinstance(call_id, str):
kind = item.get("type")
if kind not in ("function_call", "function_call_output"):
rebuilt.append(item)
continue
kind = item.get("type")
call_id = item.get("call_id")
key = _pairing_key(call_id)
if kind == "function_call":
effective = call_id
if call_id in used:
effective = key
if not effective or effective in used:
effective = new_call_id()
item = {**item, "call_id": effective} # noqa: PLW2901
changed = True
used.add(effective)
pending[call_id].append(effective)
elif kind == "function_call_output":
queue = pending.get(call_id)
pending[key].append(effective)
else:
queue = pending.get(key)
if queue:
effective = queue.popleft()
if effective != call_id:
@@ -83,7 +97,7 @@ def dedupe_input(model_input: str | list[Any]) -> str | list[Any]:
class TurnCallIdRewriter:
"""Rewrite a single turn's tool-call ids that collide with the history.
"""Rewrite a single turn's tool-call ids that are blank or 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
@@ -93,12 +107,37 @@ 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_item(self, item: Any) -> Any:
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."""
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)
@@ -114,4 +153,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) for item in items]
return [self.rewrite_item(item, position) for position, item in enumerate(items)]
+36 -1
View File
@@ -24,6 +24,8 @@ 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.
@@ -36,6 +38,10 @@ 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
@@ -175,6 +181,7 @@ 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())
@@ -184,6 +191,8 @@ 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:
@@ -275,10 +284,29 @@ 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."""
"""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.
"""
from_user = message.get("from") == "user"
if from_user and self._budget_paused:
await self.resume_from_budget_pause(exclude=target_agent_id)
@@ -286,6 +314,13 @@ 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
+48 -4
View File
@@ -33,6 +33,7 @@ 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
@@ -197,11 +198,50 @@ 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
@@ -635,7 +675,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=str(exc) or type(exc).__name__)
await coordinator.set_status(agent_id, "failed", error=request_log.failure_text(exc))
await notify_parent_on_terminal(coordinator, agent_id, "failed")
return None
@@ -657,6 +697,7 @@ 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] = []
@@ -784,6 +825,7 @@ 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
@@ -791,7 +833,9 @@ 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=str(exc))
await coordinator.set_status(
agent_id, "failed", error=request_log.failure_text(exc)
)
await notify_parent_on_terminal(coordinator, agent_id, "failed")
return None
if isinstance(exc, MaxTurnsExceeded):
@@ -805,7 +849,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=str(exc) or type(exc).__name__)
await coordinator.set_status(agent_id, status, error=request_log.failure_text(exc))
await notify_parent_on_terminal(coordinator, agent_id, status)
if not interactive:
raise
@@ -1006,7 +1050,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)
await coordinator.attach_runtime(child_id, session=session, resumable=interactive)
child_ctx: dict[str, Any] = dict(parent_ctx)
child_ctx["agent_id"] = child_id
+52 -69
View File
@@ -45,6 +45,7 @@ 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,
@@ -58,10 +59,8 @@ if TYPE_CHECKING:
from strix.runtime.status import StatusSink
from strix.tools.mcp import (
ConnectedMcpServer,
McpConnectionRequest,
McpRegistry,
SupervisedMcpSession,
)
@@ -70,8 +69,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"}), once when the connections are
# established and again each time a connection transitions to dead. An interface
# ({"name", "provider", "tool_count", "dead", "state"}), once when the connections
# are registered and again on lifecycle transitions. 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.
@@ -79,30 +78,21 @@ 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 status dicts (name/provider/tool_count/dead)."""
"""The run's MCP roster as non-secret lifecycle status dicts."""
return [
{
"name": status.name,
"provider": status.provider,
"tool_count": status.tool_count,
"dead": status.dead,
"state": status.state,
}
for status in registry.statuses()
]
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.
def _record_mcp_connections(connection_names: list[str]) -> None:
"""Record which MCP servers this run configured, 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
@@ -113,7 +103,14 @@ def _record_mcp_connections(connections: list[ConnectedMcpServer]) -> None:
report_state = get_global_report_state()
if report_state is None:
return
report_state.record_mcp_connections([connection.name for connection in connections])
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
def _persist_mcp_status(roster: list[dict[str, Any]]) -> None:
@@ -313,6 +310,7 @@ 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,
@@ -322,6 +320,7 @@ 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"]
@@ -338,7 +337,7 @@ async def run_strix_scan(
configure_spill_writer(_spill_to_workspace)
sessions_to_close: list[SQLiteSession] = []
mcp_sessions: list[SupervisedMcpSession] = []
mcp_registry: McpRegistry | None = None
try:
targets = scan_config.get("targets") or []
@@ -390,7 +389,6 @@ async def run_strix_scan(
from strix.tools.mcp import (
McpConnectionRequest,
McpRegistry,
attach_mcp_requests,
load_user_mcp_configs,
)
@@ -406,55 +404,37 @@ async def run_strix_scan(
else:
mcp_requests = mcp_connection_requests
if mcp_requests:
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")
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()
]
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()
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)
except Exception:
logger.exception("Failed to connect user MCP servers; continuing without them")
logger.exception("Failed to configure user MCP servers; continuing without them")
root_context = _merge_root_prompt_context(scope_context, extra_system_prompt_context)
root_instructions = _compose_root_instructions_override(
@@ -572,6 +552,7 @@ 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,
@@ -609,6 +590,7 @@ 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")
@@ -621,6 +603,7 @@ 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")
@@ -647,9 +630,9 @@ async def run_strix_scan(
for s in sessions_to_close:
with contextlib.suppress(Exception):
s.close()
for mcp_session in mcp_sessions:
if mcp_registry is not None:
with contextlib.suppress(Exception):
await mcp_session.aclose()
await mcp_registry.close()
with contextlib.suppress(Exception):
await coordinator._maybe_snapshot()
if cleanup_on_exit:
+64
View File
@@ -0,0 +1,64 @@
"""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",
]
+331
View File
@@ -0,0 +1,331 @@
"""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."],
)
+109
View File
@@ -0,0 +1,109 @@
"""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,
)
+755
View File
@@ -0,0 +1,755 @@
"""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,
)
+28 -2
View File
@@ -105,14 +105,15 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
report_state.set_scan_config(scan_config)
report_state.save_run_data()
def display_vulnerability(report: dict[str, Any]) -> None:
def display_vulnerability(report: dict[str, Any], *, updated: bool = False) -> None:
report_id = report.get("id", "unknown")
vuln_text = format_vulnerability_report(report)
suffix = " (updated)" if updated else ""
vuln_panel = Panel(
vuln_text,
title=f"[bold red]{report_id.upper()}",
title=f"[bold red]{report_id.upper()}{suffix}",
title_align="left",
border_style="red",
padding=(1, 2),
@@ -121,7 +122,32 @@ 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()
+11 -2
View File
@@ -98,6 +98,14 @@ 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
""",
)
@@ -163,8 +171,9 @@ Examples:
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'). The file is "
"read-only inside the sandbox and lands outside every target directory.",
"(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.",
)
parser.add_argument(
+169
View File
@@ -0,0 +1,169 @@
"""`strix cloud` — the managed Strix platform (app.strix.ai) from the terminal.
Every command maps to one operation of the public REST API. Output is JSON
when stdout is not a terminal, so agents can parse every result. Exit codes:
0 success, 1 error, 2 invalid usage, 4 authentication required, 5 payment
required.
"""
from __future__ import annotations
import json
import sys
from rich.console import Console
from rich.markup import escape
from strix.interface.cloud import http
from strix.interface.cloud.render import json_mode
from strix.interface.cloud.runner import resolve, run
from strix.interface.cloud.session import run_session
from strix.interface.cloud.spec import DEFAULT_VERBS, GROUP_HELP, SPEC
from strix.interface.cloud.workspaces import run_workspace_use
from strix.interface.platform_cli import run_login
from strix.interface.terminal_text import sanitize_terminal_text
_USAGE_HEADER = """[bold]Usage:[/] strix cloud <command> [arguments]
[bold]Session commands:[/]
login Sign in to the managed platform and store an API token
logout Remove the stored API token
whoami Show the stored account, workspace, and token state
session Inspect or narrow the remote CLI session
credits Show the credit balance of the workspace
[bold]Resource commands:[/]"""
_USAGE_FOOTER = """
Run [bold]strix cloud <command> help[/] to list its verbs. Common read-only
commands may also run their default verb when no verb is given.
Every REST resource command accepts [bold]--json[/] and [bold]--token[/]. Write
commands accept [bold]--data[/] with a JSON object of extra request fields.
Login is an interactive device flow; [bold]whoami[/] and [bold]logout[/] also
produce JSON automatically when output is redirected.
API reference: https://docs.app.strix.ai"""
_HELP_TOKENS = frozenset({"-h", "--help", "help"})
def _is_help_request(argv: list[str]) -> bool:
"""Recognize a help token with an optional JSON-output flag in either order."""
return sum(argument in _HELP_TOKENS for argument in argv) == 1 and all(
argument in _HELP_TOKENS or argument == "--json" for argument in argv
)
def run_cloud(argv: list[str]) -> int:
"""Run a managed-cloud command without ever leaking a Ctrl-C traceback."""
try:
return _run_cloud(argv)
except KeyboardInterrupt:
if json_mode(flag="--json" in argv):
sys.stdout.write(json.dumps({"error": "Interrupted.", "interrupted": True}) + "\n")
else:
Console(stderr=True).print("[yellow]Interrupted.[/]")
return 130
def _run_cloud(argv: list[str]) -> int: # noqa: PLR0911, PLR0912
"""Entry point for ``strix cloud …``. Returns a process exit code."""
console = Console()
as_json = json_mode(flag="--json" in argv)
if not argv or _is_help_request(argv):
if as_json:
_print_usage_json()
else:
_print_usage(console)
return 0
if argv == ["--json"]:
_print_usage_json()
return 0
group, rest = argv[0], argv[1:]
if group == "workspace":
group = "workspaces"
if group in ("login", "logout", "whoami"):
return _run_session(console, group, rest)
if group == "session":
return run_session(rest)
if group == "credits":
group, rest = "billing", ["credits", *rest]
if group == "workspaces" and rest and rest[0] == "use":
try:
return run_workspace_use(rest[1:])
except http.CloudError as exc:
if "--json" in rest:
sys.stdout.write(json.dumps({"error": str(exc)}) + "\n")
else:
console.print(f"[red]Error:[/] {escape(sanitize_terminal_text(exc))}")
return exc.exit_code
if group not in SPEC:
if as_json:
sys.stdout.write(json.dumps({"error": f"unknown command: {group}"}) + "\n")
return 2
console.print(f"[red]Unknown command:[/] {escape(sanitize_terminal_text(group))}")
_print_usage(console)
return 2
group_help = _is_help_request(rest)
resolved = None if group_help else resolve(group, rest)
if resolved is None:
help_tokens: set[str] = set(_HELP_TOKENS) if group_help else set()
invalid = [arg for arg in rest if arg != "--json" and arg not in help_tokens]
_print_verbs(console, group, as_json=as_json, error="unknown verb" if invalid else None)
return 2 if invalid else 0
cmd, remaining = resolved
verb_label = " ".join(rest[: len(rest) - len(remaining)]) or DEFAULT_VERBS.get(group, "")
return run(group, verb_label, cmd, remaining)
def _run_session(_console: Console, group: str, rest: list[str]) -> int:
if rest and rest[0] == "help":
rest = ["--help", *rest[1:]]
session_argv = {
"login": rest,
"logout": ["logout", *rest],
"whoami": ["status", *rest],
}
return run_login(session_argv[group])
def _print_usage(console: Console) -> None:
console.print(_USAGE_HEADER)
for group in SPEC:
console.print(f" {group:<14}{GROUP_HELP.get(group, '')}")
console.print(_USAGE_FOOTER)
def _print_verbs(
console: Console, group: str, *, as_json: bool = False, error: str | None = None
) -> None:
if as_json:
verbs: list[dict[str, str]] = [
{"name": verb, "help": command.help} for verb, command in SPEC[group].items()
]
if group == "workspaces":
verbs.append({"name": "use", "help": "Switch the stored token to another workspace."})
payload: dict[str, object] = {
"command": f"strix cloud {group}",
"verbs": verbs,
}
if error:
payload["error"] = error
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
return
console.print(f"[bold]strix cloud {group}[/] verbs:")
for verb, cmd in SPEC[group].items():
console.print(f" {verb:<28}{cmd.help}")
if group == "workspaces":
console.print(f" {'use':<28}Switch the stored token to another workspace.")
def _print_usage_json() -> None:
payload = {
"command": "strix cloud",
"session_commands": ["login", "logout", "whoami", "session", "credits"],
"resource_commands": [{"name": group, "help": GROUP_HELP.get(group, "")} for group in SPEC],
}
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
+18
View File
@@ -0,0 +1,18 @@
"""Argument parsing that reports managed-cloud usage errors through one contract."""
from __future__ import annotations
import argparse
from typing import NoReturn
import strix.interface.cloud.http as http # noqa: PLR0402
class CloudArgumentParser(argparse.ArgumentParser):
"""Raise a typed usage error instead of printing argparse prose and exiting."""
def error(self, message: str) -> NoReturn:
raise http.CloudError(
f"invalid arguments for {self.prog}: {message}",
exit_code=http.EXIT_USAGE,
)
+718
View File
@@ -0,0 +1,718 @@
"""Billing top-up and agent-wallet execution for ``strix cloud``."""
from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import webbrowser
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
import strix.interface.cloud.http as http # noqa: PLR0402
from strix.interface.cloud.payment_proxy import WalletUpstreamResponse, wallet_payment_bridge
from strix.interface.cloud.render import emit
from strix.interface.terminal_text import sanitize_terminal_text
if TYPE_CHECKING:
import argparse
from rich.console import Console
_MAX_WALLET_DETAIL_CHARS = 2_000
# Keep the wallet client on the exact protocol implementation used by the
# platform. This version is also old enough to remain installable in npm
# environments that apply a short package-publication safety window.
_MPPX_PACKAGE = "[email protected]"
# Stripe's own wallet client. It runs the complete challenge flow: it creates a
# spend request, waits for the person to approve it in the Link app, and retries
# the payment with the approved credential.
_LINK_CLI_PACKAGE = "@stripe/[email protected]"
_LINK_CLI_CLIENT_NAME = "Strix CLI"
_LINK_LOGIN_TIMEOUT_S = 300
# Poll every 2 seconds while the person approves the spend request in the Link
# app. 150 attempts give the person 5 minutes.
_LINK_APPROVAL_POLL_INTERVAL_S = 2
_LINK_APPROVAL_MAX_ATTEMPTS = 150
# Bound every wallet subprocess so a stalled npm download or wallet request
# cannot block the top-up command forever. The poll step gets the full
# approval window plus this margin.
_WALLET_STEP_TIMEOUT_S = 300
_LINK_APPROVAL_TIMEOUT_S = (
_LINK_APPROVAL_POLL_INTERVAL_S * _LINK_APPROVAL_MAX_ATTEMPTS + _WALLET_STEP_TIMEOUT_S
)
_NPM_REGISTRY = "https://registry.npmjs.org"
_WALLET_ENV_NAMES = frozenset(
{
"ALL_PROXY",
"APPDATA",
"COLORTERM",
"COMSPEC",
"FORCE_COLOR",
"HOME",
"HTTPS_PROXY",
"HTTP_PROXY",
"LANG",
"LC_ALL",
"LC_CTYPE",
"LOCALAPPDATA",
"NO_COLOR",
"NO_PROXY",
"PATH",
"PATHEXT",
"SSL_CERT_DIR",
"SSL_CERT_FILE",
"SYSTEMROOT",
"TEMP",
"TERM",
"TMP",
"TMPDIR",
"USERPROFILE",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
"XDG_STATE_HOME",
"all_proxy",
"http_proxy",
"https_proxy",
"no_proxy",
}
)
_AUTHORIZATION_SECRET = re.compile(r"(?i)((?:bearer|payment)\s+)[^\s\"']+")
_LOOPBACK_NO_PROXY = ("127.0.0.1", "localhost", "::1")
@dataclass(frozen=True)
class _WalletClientResult:
process: subprocess.CompletedProcess[str]
upstream_responses: tuple[WalletUpstreamResponse, ...]
def run_topup( # noqa: PLR0911, PLR0912, PLR0915
console: Console,
args: argparse.Namespace,
body: dict[str, Any],
*,
as_json: bool,
token: str | None,
) -> int:
"""Handle the HTTP 402 challenge and optional agent-wallet payment."""
response = http.request("POST", "/billing/topup", token=token, body=body)
if response.status_code != 402:
emit(console, http.check(response), as_json=as_json)
return http.EXIT_OK
challenge = http.parsed(response)
if getattr(args, "no_pay", False):
emit(
console,
{"error": "Payment required", "challenge": challenge},
as_json=as_json,
)
return http.EXIT_PAYMENT
credit_count = body.get("credits")
if not getattr(args, "yes", False):
if as_json or not (sys.stdin.isatty() and sys.stdout.isatty()):
emit(
console,
{
"error": (
"Payment requires explicit approval in non-interactive mode. "
"Review the challenge, then re-run with --yes to authorize payment."
),
"challenge": challenge,
},
as_json=as_json,
)
return http.EXIT_PAYMENT
answer = console.input(f"Buy {credit_count} credit(s) now? [y/N]: ").strip().lower()
if answer not in ("y", "yes"):
console.print("[yellow]Payment cancelled.[/]")
return http.EXIT_PAYMENT
npx = shutil.which("npx")
if npx is None:
message = (
"Payment requires a wallet client. Install Node.js and run the command again, "
"or pay the challenge with an MPP wallet client."
)
if as_json:
emit(
console,
{"error": message, "challenge": challenge},
as_json=True,
)
else:
emit(console, challenge, as_json=False)
console.print(f"[yellow]Payment required.[/] {message}")
return http.EXIT_PAYMENT
payment_method = getattr(args, "payment_method", None) or os.environ.get(
"MPPX_STRIPE_PAYMENT_METHOD"
)
use_link_wallet = payment_method is None and not _mppx_wallet_configured()
if use_link_wallet:
setup_error = _prepare_link_wallet(console, npx, as_json=as_json)
if setup_error is not None:
emit(
console,
{"error": setup_error, "challenge": challenge},
as_json=as_json,
)
return http.EXIT_PAYMENT
try:
wallet_result = _run_wallet_client(
console,
npx,
args,
body,
token=token,
payment_method=payment_method,
use_link_wallet=use_link_wallet,
capture_output=as_json,
)
except KeyboardInterrupt:
emit(
console,
{
"error": (
"Payment was interrupted after the wallet started. The outcome is unknown; "
"run `strix cloud billing credits` and check the balance before retrying."
),
"interrupted": True,
"payment_outcome_unknown": True,
},
as_json=as_json,
)
return 130
except OSError:
emit(
console,
{
"error": "Could not start the wallet client securely.",
"challenge": challenge,
},
as_json=as_json,
)
return http.EXIT_PAYMENT
result = wallet_result.process
confirmed_receipt = _confirmed_topup_receipt(wallet_result.upstream_responses)
if confirmed_receipt is not None:
emit(console, confirmed_receipt, as_json=as_json)
return http.EXIT_OK
stdout = str(getattr(result, "stdout", "") or "").strip()
stderr = str(getattr(result, "stderr", "") or "").strip()
if not as_json:
console.print(
"[yellow]The wallet exited without a confirmed receipt. The payment outcome is "
"unknown; run `strix cloud billing credits` before retrying.[/]"
)
detail = _wallet_detail(stderr or stdout or "")
if detail:
console.print(f"[dim]Wallet output: {detail}[/]")
return http.EXIT_PAYMENT
if result.returncode == 0:
try:
receipt = json.loads(stdout)
except (TypeError, ValueError):
emit(
console,
{
"error": (
"The wallet reported success but did not return JSON. Check the credit "
"balance before retrying payment."
),
"detail": _wallet_detail(stdout or stderr or "No wallet output was returned."),
"payment_outcome_unknown": True,
},
as_json=True,
)
return http.EXIT_PAYMENT
if not _valid_topup_receipt(receipt):
emit(
console,
{
"error": (
"The wallet returned an invalid top-up receipt. Check the credit balance "
"before retrying payment."
),
"detail": _wallet_detail(stdout),
"payment_outcome_unknown": True,
},
as_json=True,
)
return http.EXIT_PAYMENT
emit(
console,
{
"error": (
"The wallet returned a receipt, but the Strix billing endpoint did not "
"confirm it. Check the credit balance before retrying payment."
),
"detail": _wallet_detail(stdout),
"payment_outcome_unknown": True,
},
as_json=True,
)
return http.EXIT_PAYMENT
emit(
console,
{
"error": (
"The wallet exited without a confirmed receipt. The payment outcome is unknown; "
"run `strix cloud billing credits` and check the balance before retrying."
),
"detail": _wallet_detail(
stderr or stdout or f"Wallet client exited with status {result.returncode}."
),
"wallet_exit_code": result.returncode,
"payment_outcome_unknown": True,
},
as_json=True,
)
return http.EXIT_PAYMENT
def _run_wallet_client(
console: Console,
npx: str,
args: argparse.Namespace,
body: dict[str, Any],
*,
token: str | None,
payment_method: str | None,
use_link_wallet: bool,
capture_output: bool,
) -> _WalletClientResult:
"""Run the wallet through the loopback bridge without exposing the API token."""
upstream_url = f"{http.app_url()}/api/v1/billing/topup"
body_json = json.dumps(body)
wallet_env = _wallet_environment()
upstream_responses: list[WalletUpstreamResponse] = []
with tempfile.TemporaryDirectory(prefix="strix-wallet-") as wallet_cwd:
wallet_root = Path(wallet_cwd)
user_config = wallet_root / "user.npmrc"
global_config = wallet_root / "global.npmrc"
user_config.touch(mode=0o600)
global_config.touch(mode=0o600)
npx_prefix = _npx_prefix(npx, wallet_root)
with wallet_payment_bridge(
upstream_url=upstream_url,
api_token=http.api_token(token),
workspace_id=http.expected_workspace_id(token_override=token is not None),
expected_body=body_json.encode(),
timeout=getattr(args, "timeout", None),
response_observer=upstream_responses.append,
) as wallet_url:
if use_link_wallet:
process = _run_link_wallet_flow(
console,
npx_prefix,
wallet_url,
body,
body_json,
wallet_env,
wallet_root,
quiet=capture_output,
)
else:
command = [
*npx_prefix,
_MPPX_PACKAGE,
wallet_url,
"--fail",
"-J",
body_json,
]
if payment_method:
command += ["-M", f"paymentMethod={payment_method}"]
try:
process = subprocess.run( # noqa: S603
command,
check=False,
capture_output=capture_output,
text=True,
env=wallet_env,
cwd=wallet_root,
timeout=_LINK_APPROVAL_TIMEOUT_S,
)
except subprocess.TimeoutExpired as timeout_error:
process = subprocess.CompletedProcess(
args=command,
returncode=1,
stdout=_decoded_stream(timeout_error.stdout),
stderr=(
"The wallet step did not complete within "
f"{_LINK_APPROVAL_TIMEOUT_S} seconds."
),
)
return _WalletClientResult(process=process, upstream_responses=tuple(upstream_responses))
def _run_link_wallet_flow(
console: Console,
npx_prefix: list[str],
wallet_url: str,
body: dict[str, Any],
body_json: str,
wallet_env: dict[str, str],
wallet_root: Path,
*,
quiet: bool,
) -> subprocess.CompletedProcess[str]:
"""Create the spend request, wait for approval in the Link app, then pay."""
def run_step(
arguments: list[str],
progress_message: str,
timeout: int = _WALLET_STEP_TIMEOUT_S,
) -> subprocess.CompletedProcess[str]:
command = [*npx_prefix, _LINK_CLI_PACKAGE, *arguments]
def run() -> subprocess.CompletedProcess[str]:
try:
return subprocess.run( # noqa: S603
command,
check=False,
capture_output=True,
text=True,
env=wallet_env,
cwd=wallet_root,
timeout=timeout,
)
except subprocess.TimeoutExpired as timeout_error:
return subprocess.CompletedProcess(
args=command,
returncode=1,
stdout=_decoded_stream(timeout_error.stdout),
stderr=f"The wallet step did not complete within {timeout} seconds.",
)
if quiet:
return run()
with console.status(progress_message):
return run()
created = run_step(
[
"mpp",
"pay",
wallet_url,
"--method",
"POST",
"--data",
body_json,
"--context",
_payment_context(body),
"--format",
"json",
],
"Starting the Stripe Link wallet…",
)
spend_request = _pending_spend_request(created.stdout)
if spend_request is None:
return created
request_id, approval_url = spend_request
if not quiet:
console.print(f"[yellow]Approve the payment in the Link app:[/] {approval_url}")
if sys.stdin.isatty() and sys.stdout.isatty() and approval_url.startswith("https://"):
with suppress(Exception):
webbrowser.open(approval_url)
polled = run_step(
[
"spend-request",
"retrieve",
request_id,
"--interval",
str(_LINK_APPROVAL_POLL_INTERVAL_S),
"--max-attempts",
str(_LINK_APPROVAL_MAX_ATTEMPTS),
"--format",
"jsonl",
],
"Waiting for the approval in the Link app…",
timeout=_LINK_APPROVAL_TIMEOUT_S,
)
if _final_spend_request_status(polled.stdout) != "approved":
return polled
return run_step(
[
"mpp",
"pay",
wallet_url,
"--spend-request-id",
request_id,
"--method",
"POST",
"--data",
body_json,
"--format",
"json",
],
"Completing the payment…",
)
def _decoded_stream(stream: str | bytes | None) -> str:
"""Return captured subprocess output as text."""
if stream is None:
return ""
if isinstance(stream, bytes):
return stream.decode(errors="replace")
return stream
def _embedded_json_documents(text: str) -> list[Any]:
"""Extract JSON documents from wallet output that can contain other text."""
documents: list[Any] = []
decoder = json.JSONDecoder()
position = 0
while position < len(text):
start_candidates = [
index for index in (text.find("[", position), text.find("{", position)) if index != -1
]
if not start_candidates:
break
start = min(start_candidates)
try:
document, end = decoder.raw_decode(text, start)
except ValueError:
position = start + 1
continue
documents.append(document)
position = end
return documents
def _spend_request_records(stdout: str) -> list[dict[str, Any]]:
"""Parse spend-request records from JSON or JSON-lines wallet output."""
records: list[dict[str, Any]] = []
for candidate in _embedded_json_documents((stdout or "").strip()):
items = candidate if isinstance(candidate, list) else [candidate]
for item in items:
if not isinstance(item, dict):
continue
record = cast("dict[str, Any]", item)
data = record.get("data")
if isinstance(data, dict):
record = cast("dict[str, Any]", data)
records.append(record)
return records
def _pending_spend_request(stdout: str) -> tuple[str, str] | None:
"""Find a spend request that waits for approval in the Link app."""
for record in _spend_request_records(stdout):
request_id = record.get("id")
approval_url = record.get("approval_url")
if (
record.get("status") == "pending_approval"
and isinstance(request_id, str)
and request_id
and isinstance(approval_url, str)
):
return request_id, approval_url
return None
def _final_spend_request_status(stdout: str) -> str | None:
"""Return the last reported status from the approval poll output."""
status: str | None = None
for record in _spend_request_records(stdout):
value = record.get("status")
if isinstance(value, str):
status = value
return status
def _npx_prefix(npx: str, wallet_root: Path) -> list[str]:
"""Install the wallet client from a fixed registry without lifecycle scripts."""
return [
npx,
"--yes",
f"--registry={_NPM_REGISTRY}",
"--ignore-scripts",
f"--userconfig={wallet_root / 'user.npmrc'}",
f"--globalconfig={wallet_root / 'global.npmrc'}",
f"--cache={_wallet_npm_cache()}",
]
def _wallet_npm_cache() -> Path:
"""Keep one private npm cache so the pinned wallet client installs once."""
cache = Path.home() / ".strix" / "wallet-npm-cache"
cache.mkdir(mode=0o700, parents=True, exist_ok=True)
return cache
def _payment_context(body: dict[str, Any]) -> str:
"""Describe the purchase for the person who approves it in the Link app."""
credits_requested = body.get("credits")
return (
f"Strix scan credits. The Strix command line interface asks to buy "
f"{credits_requested} scan credit(s) for the selected Strix workspace on "
"app.strix.ai. Strix spends the credits on managed penetration test scans "
"that the user starts."
)
def _mppx_wallet_configured() -> bool:
"""Report whether the person already configured the mppx wallet client."""
return bool(os.environ.get("MPPX_ACCOUNT") or os.environ.get("MPPX_STRIPE_SECRET_KEY"))
def _run_link_cli(
npx: str,
arguments: list[str],
*,
capture_output: bool,
timeout: float | None = None,
) -> subprocess.CompletedProcess[str]:
"""Run one Stripe Link wallet command in an isolated npm environment."""
with tempfile.TemporaryDirectory(prefix="strix-wallet-") as wallet_cwd:
wallet_root = Path(wallet_cwd)
(wallet_root / "user.npmrc").touch(mode=0o600)
(wallet_root / "global.npmrc").touch(mode=0o600)
return subprocess.run( # noqa: S603
[*_npx_prefix(npx, wallet_root), _LINK_CLI_PACKAGE, *arguments],
check=False,
capture_output=capture_output,
text=True,
env=_wallet_environment(),
cwd=wallet_root,
timeout=timeout,
)
def _link_wallet_authenticated(npx: str) -> bool:
"""Report whether a Link wallet is already connected to this machine."""
try:
result = _run_link_cli(
npx,
["auth", "status", "--format", "json"],
capture_output=True,
timeout=_LINK_LOGIN_TIMEOUT_S,
)
except (OSError, subprocess.SubprocessError):
return False
try:
payload = json.loads(result.stdout or "null")
except (TypeError, ValueError):
return False
if isinstance(payload, list):
payload = payload[0] if payload else None
return bool(isinstance(payload, dict) and payload.get("authenticated"))
def _prepare_link_wallet(console: Console, npx: str, *, as_json: bool) -> str | None:
"""Connect a Link wallet when none is present. Return an error message on failure."""
if _link_wallet_authenticated(npx):
return None
manual_setup = (
"Payment needs a Stripe Link wallet. Run `strix cloud billing topup` in an "
"interactive terminal to connect one, or set up the wallet at "
"https://link.com/agents. For a browser checkout instead, run "
"`strix cloud billing subscribe --plan strix_top_up`."
)
if as_json or not (sys.stdin.isatty() and sys.stdout.isatty()):
return manual_setup
console.print(
"[yellow]No Stripe Link wallet is connected.[/] Strix starts the Link sign-in now. "
"Approve the connection in the Link app, then Strix continues the payment. "
"The user approves every payment in the Link app."
)
try:
_run_link_cli(
npx,
[
"auth",
"login",
"--client-name",
_LINK_CLI_CLIENT_NAME,
"--interval",
"3",
"--timeout",
str(_LINK_LOGIN_TIMEOUT_S),
],
capture_output=False,
timeout=_LINK_LOGIN_TIMEOUT_S + 30,
)
except (OSError, subprocess.SubprocessError):
return manual_setup
if _link_wallet_authenticated(npx):
return None
return manual_setup
def _wallet_environment() -> dict[str, str]:
"""Pass only platform essentials and explicit wallet variables to npm/mppx."""
environment = {
name: value
for name, value in os.environ.items()
if name in _WALLET_ENV_NAMES or name.startswith(("LINK_", "MPPX_"))
}
for name in ("NO_PROXY", "no_proxy"):
entries = [entry.strip() for entry in environment.get(name, "").split(",") if entry.strip()]
normalized = {entry.lower().strip("[]") for entry in entries}
entries.extend(host for host in _LOOPBACK_NO_PROXY if host not in normalized)
environment[name] = ",".join(entries)
return environment
def _wallet_detail(value: str) -> str:
"""Bound and redact third-party wallet diagnostics before returning JSON."""
redacted = _AUTHORIZATION_SECRET.sub(r"\1[redacted]", sanitize_terminal_text(value))
if len(redacted) <= _MAX_WALLET_DETAIL_CHARS:
return redacted
return redacted[: _MAX_WALLET_DETAIL_CHARS - 1] + "…"
def _valid_topup_receipt(value: Any) -> bool:
"""Require the documented success shape before reporting a paid top-up."""
if not isinstance(value, dict):
return False
fields = cast("dict[str, Any]", value)
credits_granted = fields.get("credits_granted")
balance = fields.get("balance")
return (
isinstance(credits_granted, int)
and not isinstance(credits_granted, bool)
and credits_granted >= 0
and isinstance(fields.get("duplicate"), bool)
and isinstance(fields.get("reference"), str)
and bool(fields["reference"])
and isinstance(balance, int)
and not isinstance(balance, bool)
and balance >= 0
)
def _confirmed_topup_receipt(
responses: tuple[WalletUpstreamResponse, ...],
) -> dict[str, Any] | None:
"""Return a receipt only when the trusted bridge observed its successful response."""
for response in reversed(responses):
if not 200 <= response.status_code < 300:
continue
try:
receipt = json.loads(response.body)
except (TypeError, ValueError):
continue
if _valid_topup_receipt(receipt):
return cast("dict[str, Any]", receipt)
return None
+408
View File
@@ -0,0 +1,408 @@
"""HTTP client for the managed Strix platform API (app.strix.ai)."""
from __future__ import annotations
import ipaddress
import math
import os
import re
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import SplitResult, urlsplit
import requests
from strix.config import load_settings
from strix.interface.platform_cli import read_record
if TYPE_CHECKING:
from pathlib import Path
_DEFAULT_TIMEOUT_S = 120
_SUPABASE_STORAGE_HOST = re.compile(r"^[a-z0-9-]+\.supabase\.co$")
_STORAGE_PATH_PREFIX = "/storage/v1/"
_app_url_override: str | None = None
_token_override_active = False
_workspace_id_override: str | None = None
_timeout_s: float = _DEFAULT_TIMEOUT_S
EXIT_OK = 0
EXIT_ERROR = 1
EXIT_USAGE = 2
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.
`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:
super().__init__(message)
self.exit_code = exit_code
self.payload = payload
self.next_step = next_step
class CloudTransportError(CloudError):
"""A request may have reached the platform, but no response was received."""
def configure(
*,
base_url: str | None = None,
timeout: float | None = None,
token_override: bool = False,
workspace_id: str | None = None,
) -> None:
"""Set the platform URL and the request timeout for this process."""
global _app_url_override, _timeout_s, _token_override_active # noqa: PLW0603
global _workspace_id_override # noqa: PLW0603
_app_url_override = base_url.rstrip("/") if base_url else None
_token_override_active = token_override
explicit_workspace = workspace_id or os.environ.get("STRIX_WORKSPACE_ID")
if explicit_workspace:
_workspace_id_override = explicit_workspace.strip()
elif not token_override and not os.environ.get("STRIX_API_TOKEN"):
record = read_record()
stored_workspace = record.get("organization_id") if record is not None else None
_workspace_id_override = (
stored_workspace.strip()
if isinstance(stored_workspace, str) and stored_workspace.strip()
else None
)
else:
_workspace_id_override = None
if timeout is not None:
if not math.isfinite(timeout) or timeout <= 0:
raise CloudError(
"request timeout must be a finite number greater than 0.",
exit_code=EXIT_USAGE,
)
_timeout_s = timeout
def app_url() -> str:
if _app_url_override:
return _app_url_override
viewer = load_settings().viewer
configured = viewer.app_url.rstrip("/")
explicitly_configured = bool(os.environ.get("STRIX_APP_URL")) or "app_url" in getattr(
viewer, "model_fields_set", set[str]()
)
if explicitly_configured or _token_override_active or os.environ.get("STRIX_API_TOKEN"):
return configured
record = read_record()
stored = record.get("app_url") if record is not None else None
if isinstance(stored, str) and stored:
try:
_parse_origin_url(stored, label="stored platform URL")
except CloudError:
pass
else:
return stored.rstrip("/")
return configured
def api_token(override: str | None = None) -> str:
token = override or os.environ.get("STRIX_API_TOKEN")
if not token:
record = read_record()
if record is not None:
stored = record.get("api_token")
if isinstance(stored, str):
_validate_stored_token_origin(record)
token = stored
if not token or not token.strip():
raise CloudError(
"not signed in. Run `strix cloud login`, or set STRIX_API_TOKEN.",
exit_code=EXIT_AUTH,
)
return token.strip()
def _validate_stored_token_origin(record: dict[str, Any]) -> None:
"""Never send a stored bearer token to an origin other than its issuer."""
stored_url = record.get("app_url")
if not isinstance(stored_url, str) or not stored_url:
raise CloudError(
"the stored sign-in is not bound to a trusted platform. Run `strix cloud login` "
"again before using it.",
exit_code=EXIT_AUTH,
)
try:
stored_origin = _origin(_parse_origin_url(stored_url, label="stored platform URL"))
active_origin = _origin(_parse_origin_url(app_url(), label="configured platform URL"))
except CloudError as exc:
raise CloudError(
"the stored sign-in has an invalid platform binding. Run `strix cloud login` again.",
exit_code=EXIT_AUTH,
) from exc
if stored_origin != active_origin:
raise CloudError(
"the stored sign-in belongs to a different platform. Refusing to send its token; "
"run `strix cloud login` for the configured platform or supply an explicit token.",
exit_code=EXIT_AUTH,
)
def request(
method: str,
path: str,
*,
token: str | None = None,
query: dict[str, Any] | None = None,
body: dict[str, Any] | None = None,
stream: bool = False,
idempotency_key: str | None = None,
) -> requests.Response:
url = f"{app_url()}/api/v1{path}"
headers = {
"Authorization": f"Bearer {api_token(token)}",
}
workspace_id = expected_workspace_id(token_override=token is not None)
if workspace_id:
headers["X-Strix-Workspace"] = workspace_id
if idempotency_key is not None:
headers["Idempotency-Key"] = idempotency_key
try:
response = requests.request(
method,
url,
headers=headers,
params={
key: ("true" if value else "false") if isinstance(value, bool) else value
for key, value in (query or {}).items()
if value is not None
}
or None,
json=body,
timeout=_timeout_s,
stream=stream,
allow_redirects=False,
)
except requests.RequestException as exc:
raise CloudTransportError(f"could not reach {app_url()}: {exc}") from exc
return response
def expected_workspace_id(*, token_override: bool) -> str | None:
"""Pin every request in this process to the workspace selected at startup."""
if _workspace_id_override:
return _workspace_id_override
if token_override or _token_override_active or os.environ.get("STRIX_API_TOKEN"):
return None
return None
def upload_file(signed_url: str, upload_token: str, path: Path) -> None:
"""Stream a file to a platform-issued storage URL."""
_validate_upload_url(signed_url)
response: requests.Response | None = None
try:
with path.open("rb") as stream:
response = requests.put(
signed_url,
data=stream,
headers={
"Authorization": f"Bearer {upload_token}",
"Content-Type": "application/zip",
},
timeout=_timeout_s,
allow_redirects=False,
)
except (OSError, requests.RequestException) as exc:
raise CloudError(f"source upload failed: {exc}") from exc
try:
if 300 <= response.status_code < 400:
raise CloudError("source upload refused an unexpected redirect")
if not response.ok:
detail = ""
try:
payload = response.json()
if isinstance(payload, dict):
fields = cast("dict[str, Any]", payload)
detail = str(fields.get("message") or fields.get("error") or "")
except ValueError:
pass
raise CloudError(detail or f"source upload failed (HTTP {response.status_code})")
finally:
response.close()
def _validate_upload_url(signed_url: str) -> None:
"""Allow uploads only to the trusted app origin or managed Supabase storage."""
# Supabase signed upload URLs carry their signature in the query string.
# Keep every origin/path restriction below, but allow that opaque query on
# this one platform-issued URL type.
target = _parse_origin_url(
signed_url,
label="source upload URL",
allow_query=True,
)
if not target.path.startswith(_STORAGE_PATH_PREFIX):
raise CloudError("source upload refused a URL outside the storage API")
configured_app = _parse_origin_url(app_url(), label="configured platform URL")
if _origin(target) == _origin(configured_app):
return
if _is_loopback_host(configured_app.hostname or "") and _is_loopback_host(
target.hostname or ""
):
return
hostname = target.hostname or ""
if (
target.scheme == "https"
and target.port in (None, 443)
and _SUPABASE_STORAGE_HOST.fullmatch(hostname)
):
return
raise CloudError(
"source upload refused an untrusted storage origin; only the configured platform "
"origin and managed Supabase storage are allowed"
)
def _parse_origin_url(
value: str,
*,
label: str,
allow_query: bool = False,
) -> SplitResult:
try:
parsed = urlsplit(value)
port = parsed.port
except (TypeError, ValueError) as exc:
raise CloudError(f"{label} is invalid") from exc
hostname = parsed.hostname
if (
parsed.scheme not in {"http", "https"}
or not hostname
or parsed.username is not None
or parsed.password is not None
or (parsed.query and not allow_query)
or parsed.fragment
or "\\" in value
or any(character.isspace() for character in value)
or "%" in parsed.netloc
):
raise CloudError(f"{label} is invalid")
try:
hostname.encode("ascii")
except UnicodeEncodeError as exc:
raise CloudError(f"{label} contains a non-ASCII hostname") from exc
if port is not None and not 1 <= port <= 65535:
raise CloudError(f"{label} is invalid")
return parsed
def _origin(parsed: SplitResult) -> tuple[str, str, int]:
default_port = 443 if parsed.scheme == "https" else 80
return parsed.scheme, (parsed.hostname or "").lower(), parsed.port or default_port
def _is_loopback_host(hostname: str) -> bool:
normalized = hostname.lower().rstrip(".")
if normalized == "localhost" or normalized.endswith(".localhost"):
return True
try:
return ipaddress.ip_address(normalized).is_loopback
except ValueError:
return False
def parsed(response: requests.Response) -> Any:
content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
try:
return response.json()
except ValueError:
return response.text
return response.text
def check(response: requests.Response) -> Any:
data = parsed(response)
if 200 <= response.status_code < 300:
content_type = response.headers.get("content-type", "").lower()
if "application/json" not in content_type:
raise CloudError(
"the server returned a non-JSON response. Check STRIX_APP_URL and preview "
"access, then retry."
)
try:
return response.json()
except ValueError as exc:
raise CloudError(
"the server returned malformed JSON. Check STRIX_APP_URL and preview "
"access, then retry."
) from exc
detail = ""
error_code = ""
if isinstance(data, dict):
raw = cast("dict[str, Any]", data)
detail = str(raw.get("detail") or raw.get("error") or "")
error_code = str(raw.get("code") or raw.get("error_code") or "")
nested_error = raw.get("error")
if isinstance(nested_error, dict):
nested = cast("dict[str, Any]", nested_error)
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 response.status_code in (401, 403):
raise CloudError(message, exit_code=EXIT_AUTH, 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)
+286
View File
@@ -0,0 +1,286 @@
"""Loopback bridge for wallet clients that only accept secrets in argv.
The ``mppx`` CLI accepts custom HTTP headers through ``-H`` only. Passing a
Strix API token that way exposes it to process-listing tools. This module keeps
the token in the Strix process and injects it while forwarding the wallet's few
requests (challenge probes and the paid retry) to the fixed billing endpoint.
"""
from __future__ import annotations
import secrets
import threading
from contextlib import contextmanager, suppress
from dataclasses import dataclass, field
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import TYPE_CHECKING, Any
import requests
if TYPE_CHECKING:
from collections.abc import Callable, Generator
_DEFAULT_REQUEST_TIMEOUT_S = 120.0
_MAX_REQUEST_BODY_BYTES = 64 * 1024
_MAX_UPSTREAM_RESPONSE_BYTES = 1024 * 1024
_MAX_WALLET_REQUESTS = 3
_HOP_BY_HOP_HEADERS = frozenset(
{
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"proxy-connection",
"te",
"trailer",
"transfer-encoding",
"upgrade",
}
)
@dataclass
class _BridgeState:
upstream_url: str
authorization: str
workspace_id: str | None
expected_body: bytes
path: str
timeout: float
response_observer: Callable[[WalletUpstreamResponse], None] | None = None
request_count: int = 0
lock: threading.Lock = field(default_factory=threading.Lock)
def claim_request(self) -> bool:
"""Allow only the challenge probes and the one paid retry."""
with self.lock:
if self.request_count >= _MAX_WALLET_REQUESTS:
return False
self.request_count += 1
return True
class _ResponseTooLargeError(Exception):
"""The fixed billing endpoint returned more data than a wallet needs."""
@dataclass(frozen=True)
class WalletUpstreamResponse:
"""A bounded upstream response observed by the trusted loopback bridge."""
status_code: int
body: bytes
def _bounded_response_body(response: requests.Response) -> bytes:
content_length = response.headers.get("Content-Length")
if content_length:
try:
if int(content_length) > _MAX_UPSTREAM_RESPONSE_BYTES:
raise _ResponseTooLargeError
except ValueError:
pass
chunks: list[bytes] = []
total = 0
for chunk in response.iter_content(chunk_size=64 * 1024):
if not chunk:
continue
total += len(chunk)
if total > _MAX_UPSTREAM_RESPONSE_BYTES:
raise _ResponseTooLargeError
chunks.append(chunk)
return b"".join(chunks)
def _connection_header_names(handler: BaseHTTPRequestHandler) -> set[str]:
value = handler.headers.get("Connection", "")
return {item.strip().lower() for item in value.split(",") if item.strip()}
def _forward_request_headers(handler: BaseHTTPRequestHandler) -> dict[str, str]:
blocked = {
*_HOP_BY_HOP_HEADERS,
*_connection_header_names(handler),
"content-length",
"forwarded",
"host",
"true-client-ip",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
"x-real-ip",
"x-strix-authorization",
"x-strix-workspace",
"x-vercel-forwarded-for",
}
return {name: value for name, value in handler.headers.items() if name.lower() not in blocked}
def _send_json_error(handler: BaseHTTPRequestHandler, status: int, message: str) -> None:
body = f'{{"error": "{message}"}}'.encode()
handler.close_connection = True
handler.send_response(status)
handler.send_header("Content-Type", "application/json")
handler.send_header("Content-Length", str(len(body)))
handler.send_header("Cache-Control", "no-store")
handler.send_header("Connection", "close")
handler.end_headers()
with suppress(BrokenPipeError, ConnectionResetError):
handler.wfile.write(body)
def _make_handler(state: _BridgeState) -> type[BaseHTTPRequestHandler]:
class WalletBridgeHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
"""Do not write wallet request metadata to stderr."""
del format, args
def do_POST(self) -> None: # noqa: PLR0911, PLR0912
if self.path != state.path:
_send_json_error(self, 404, "Not found")
return
if self.headers.get("Transfer-Encoding"):
_send_json_error(self, 400, "Chunked request bodies are not supported")
return
try:
content_length = int(self.headers.get("Content-Length", ""))
except ValueError:
_send_json_error(self, 411, "A valid Content-Length is required")
return
if content_length < 0 or content_length > _MAX_REQUEST_BODY_BYTES:
_send_json_error(self, 413, "Request body is too large")
return
body = self.rfile.read(content_length)
if body != state.expected_body:
_send_json_error(self, 403, "Request body did not match the approved top-up")
return
if not state.claim_request():
_send_json_error(self, 429, "Wallet request limit reached")
return
headers = _forward_request_headers(self)
headers["X-Strix-Authorization"] = state.authorization
if state.workspace_id:
headers["X-Strix-Workspace"] = state.workspace_id
try:
response = requests.request(
"POST",
state.upstream_url,
headers=headers,
data=body,
timeout=state.timeout,
allow_redirects=False,
stream=True,
)
try:
response_body = _bounded_response_body(response)
response_status = response.status_code
response_headers = dict(response.headers)
finally:
response.close()
except _ResponseTooLargeError:
_send_json_error(self, 502, "Strix billing response was too large")
return
except requests.RequestException:
_send_json_error(self, 502, "Could not reach the Strix billing endpoint")
return
if state.response_observer is not None:
with suppress(Exception):
state.response_observer(
WalletUpstreamResponse(status_code=response_status, body=response_body)
)
if 300 <= response_status < 400:
_send_json_error(self, 502, "Strix billing refused an unexpected redirect")
return
self.send_response(response_status)
response_connection_headers = {
item.strip().lower()
for item in response_headers.get("Connection", "").split(",")
if item.strip()
}
blocked_response_headers = {
*_HOP_BY_HOP_HEADERS,
*response_connection_headers,
"cache-control",
"content-encoding",
"content-length",
"location",
}
for name, value in response_headers.items():
if (
name.lower() not in blocked_response_headers
and "\r" not in value
and "\n" not in value
):
self.send_header(name, value)
self.send_header("Content-Length", str(len(response_body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
with suppress(BrokenPipeError, ConnectionResetError):
self.wfile.write(response_body)
def do_GET(self) -> None:
_send_json_error(self, 405, "Method not allowed")
def do_PUT(self) -> None:
_send_json_error(self, 405, "Method not allowed")
def do_PATCH(self) -> None:
_send_json_error(self, 405, "Method not allowed")
def do_DELETE(self) -> None:
_send_json_error(self, 405, "Method not allowed")
return WalletBridgeHandler
@contextmanager
def wallet_payment_bridge(
*,
upstream_url: str,
api_token: str,
workspace_id: str | None = None,
expected_body: bytes,
timeout: float | None = None,
response_observer: Callable[[WalletUpstreamResponse], None] | None = None,
) -> Generator[str]:
"""Yield a one-run loopback URL that injects the Strix API token upstream.
The random path prevents accidental cross-process requests and limits local
denial-of-service races. It is not an authentication boundary against a
same-user process that can inspect another process's argv.
"""
capability = secrets.token_urlsafe(32)
path = f"/topup/{capability}"
state = _BridgeState(
upstream_url=upstream_url,
authorization=f"Bearer {api_token}",
workspace_id=workspace_id,
expected_body=expected_body,
path=path,
timeout=timeout or _DEFAULT_REQUEST_TIMEOUT_S,
response_observer=response_observer,
)
server = ThreadingHTTPServer(("127.0.0.1", 0), _make_handler(state))
server.daemon_threads = True
thread = threading.Thread(
target=server.serve_forever,
kwargs={"poll_interval": 0.05},
name="strix-wallet-bridge",
daemon=True,
)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}{path}"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=1)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+167
View File
@@ -0,0 +1,167 @@
"""Inspect and safely narrow a managed Strix CLI session."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, cast
from rich.console import Console
from rich.markup import escape
import strix.interface.cloud.http as http # noqa: PLR0402
from strix.interface.cloud.arguments import CloudArgumentParser
from strix.interface.cloud.render import emit, json_mode
from strix.interface.platform_cli import read_record, save_record
from strix.interface.terminal_text import sanitize_terminal_text
if TYPE_CHECKING:
import argparse
def run_session(argv: list[str]) -> int:
console = Console()
normalized = ["show", *argv] if not argv or argv[0].startswith("-") else list(argv)
if normalized[0] == "help":
normalized = ["--help", *normalized[1:]]
if normalized[0] in {"-h", "--help"}:
_print_help(console)
return 0
verb = normalized.pop(0)
if verb == "scopes" and normalized and normalized[0] == "set":
normalized.pop(0)
return _run_scopes_set(console, normalized)
if verb not in {"show", "scopes"}:
console.print(f"[red]Unknown session command:[/] {escape(sanitize_terminal_text(verb))}")
_print_help(console)
return http.EXIT_USAGE
return _run_show(console, normalized, scopes_only=verb == "scopes")
def _common(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--json", action="store_true", help="Print the raw JSON response.")
parser.add_argument("--show-scopes", action="store_true", help="Print every granted scope.")
parser.add_argument("--token", default=None, help="API token override.")
parser.add_argument("--workspace-id", default=None, metavar="ORG_ID")
parser.add_argument("--app-url", default=None, metavar="URL")
parser.add_argument("--timeout", default=None, type=float, metavar="SECONDS")
def _configure(args: argparse.Namespace) -> bool:
external = args.token is not None or bool(os.environ.get("STRIX_API_TOKEN", "").strip())
http.configure(
base_url=args.app_url,
timeout=args.timeout,
token_override=bool(args.token),
workspace_id=args.workspace_id,
)
return external
def _run_show(console: Console, argv: list[str], *, scopes_only: bool) -> int:
parser = CloudArgumentParser(prog=f"strix cloud session {'scopes' if scopes_only else 'show'}")
_common(parser)
as_json = json_mode(flag="--json" in argv)
try:
args = parser.parse_args(argv)
_configure(args)
payload = http.check(http.request("GET", "/cli/session", token=args.token))
except SystemExit as exc:
return int(exc.code or 0)
except http.CloudError as exc:
return _error(console, exc, as_json=as_json)
if not isinstance(payload, dict):
return _error(console, http.CloudError("invalid CLI session response"), as_json=as_json)
record = cast("dict[str, Any]", payload)
if as_json:
emit(console, record, as_json=True)
return http.EXIT_OK
scopes = _string_list(record.get("scopes"))
ceiling = _string_list(record.get("scope_ceiling"))
profile = str(record.get("scope_profile") or "custom").title()
if not scopes_only:
device_name = escape(str(record.get("device_name") or "this device"))
console.print(f"[green]Active CLI session[/] on [bold]{device_name}[/]")
console.print(f" Workspace: {escape(str(record.get('organization_id') or 'unknown'))}")
console.print(f" Access: {profile} · {len(scopes)} scopes granted · {len(ceiling)} maximum")
if args.show_scopes or scopes_only:
console.print(f" Granted: [dim]{escape(' '.join(scopes))}[/]")
console.print(f" Ceiling: [dim]{escape(' '.join(ceiling))}[/]")
return http.EXIT_OK
def _run_scopes_set(console: Console, argv: list[str]) -> int:
parser = CloudArgumentParser(
prog="strix cloud session scopes set",
description="Change scopes within the access approved at browser sign-in.",
)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("profile", nargs="?", choices=("minimal", "recommended", "full"))
mode.add_argument("--scopes", nargs="+", metavar="SCOPE")
_common(parser)
as_json = json_mode(flag="--json" in argv)
try:
args = parser.parse_args(argv)
external = _configure(args)
body = (
{"scope_profile": args.profile}
if args.profile
else {"scope_profile": "custom", "scopes": args.scopes}
)
payload = http.check(http.request("PATCH", "/cli/session", token=args.token, body=body))
except SystemExit as exc:
return int(exc.code or 0)
except http.CloudError as exc:
return _error(console, exc, as_json=as_json)
if not isinstance(payload, dict):
return _error(console, http.CloudError("invalid CLI session response"), as_json=as_json)
result = cast("dict[str, Any]", payload)
if not external:
stored = read_record()
if stored is not None:
stored.update(
{
key: result[key]
for key in ("scopes", "requested_scopes", "scope_ceiling", "scope_profile")
if key in result
}
)
save_record(stored)
if as_json:
emit(console, result, as_json=True)
else:
scopes = _string_list(result.get("scopes"))
profile = str(result.get("scope_profile") or "custom").title()
console.print(f"[green]✓ CLI access updated.[/] {profile} · {len(scopes)} scopes granted")
if args.show_scopes:
console.print(f" Scopes: [dim]{escape(' '.join(scopes))}[/]")
return http.EXIT_OK
def _string_list(value: Any) -> list[str]:
if not isinstance(value, list):
return []
items = cast("list[Any]", cast("Any", value))
return [str(item) for item in items]
def _error(console: Console, error: http.CloudError, *, as_json: bool) -> int:
if as_json:
raw_payload: Any = error.payload
error_payload = cast("dict[str, Any]", raw_payload)
payload = dict(error_payload) if isinstance(raw_payload, dict) else {}
payload["error"] = str(error)
if payload.get("detail") == payload.get("error"):
payload.pop("detail", None)
emit(console, payload, as_json=True)
else:
console.print(f"[red]Error:[/] {escape(sanitize_terminal_text(error))}")
return error.exit_code
def _print_help(console: Console) -> None:
console.print("[bold]strix cloud session[/] commands:")
console.print(" show Show the remote CLI session (default).")
console.print(" scopes Show granted scopes and consent ceiling.")
console.print(" scopes set PROFILE Use minimal, recommended, or full.")
console.print(" scopes set --scopes SCOPE… Use a custom set within the ceiling.")
+403
View File
@@ -0,0 +1,403 @@
"""Local-source approval, upload, and scan-launch lifecycle."""
from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import quote
from rich.markup import escape
import strix.interface.cloud.http as http # noqa: PLR0402
from strix.interface.cloud.render import emit
from strix.interface.cloud.source_upload import prepare_source, remove_bundle
from strix.interface.terminal_text import sanitize_terminal_text
if TYPE_CHECKING:
import argparse
from typing import NoReturn
from rich.console import Console
from strix.interface.cloud.source_upload import SourceBundle
_SHA256 = re.compile(r"^[0-9a-fA-F]{64}$")
@dataclass
class LocalSourceScan:
"""Own one local bundle and its staged upload through a scan launch."""
bundle: SourceBundle | None = None
upload_id: str | None = None
idempotency_key: str | None = None
_launch_started: bool = False
def prepare_and_attach(
self,
console: Console,
args: argparse.Namespace,
body: dict[str, Any],
*,
as_json: bool,
token: str | None,
) -> bool:
"""Prepare source, emit a dry run, or upload and attach it to ``body``.
Returns ``True`` when a dry run was emitted and request execution should stop.
"""
self.bundle = prepare_scan_source(console, args, as_json=as_json)
if self.bundle is None:
return False
if getattr(args, "dry_run", False):
emit(
console,
{"source": self.bundle.summary(show_files=getattr(args, "show_files", False))},
as_json=as_json,
view="source_manifest",
)
return True
self.upload_id = _upload_scan_source(self.bundle, token=token)
existing = body.get("upload_ids")
body["upload_ids"] = [
*(existing if isinstance(existing, list) else []),
self.upload_id,
]
return False
def mark_launch_started(self) -> None:
"""Record that the scan-creation request may have reached the platform."""
self._launch_started = self.upload_id is not None
def handle_request_failure(self, error: BaseException, *, token: str | None) -> None:
"""Clean or retain a staged upload according to request ambiguity."""
if self.upload_id is None:
return
if self._launch_started:
if isinstance(error, KeyboardInterrupt):
raise _interrupted_source_upload_error(
self.upload_id, self.idempotency_key
) from None
if isinstance(error, Exception):
raise _retained_source_upload_error(
self.upload_id, error, self.idempotency_key
) from error
return
try:
_delete_upload(self.upload_id, token=token)
except (http.CloudError, KeyboardInterrupt) as cleanup_error:
if isinstance(error, Exception):
raise _source_cleanup_error(self.upload_id, error, cleanup_error) from error
interrupted = http.CloudError("source upload interrupted.", exit_code=130)
raise _source_cleanup_error(self.upload_id, interrupted, cleanup_error) from None
def handle_response_failure(
self,
error: BaseException,
*,
definitive: bool,
token: str | None,
) -> None:
"""Clean a rejected upload or retain one whose scan result is ambiguous."""
if self.upload_id is None:
return
if definitive:
try:
_delete_upload(self.upload_id, token=token)
except (http.CloudError, KeyboardInterrupt) as cleanup_error:
if isinstance(error, Exception):
raise _source_cleanup_error(self.upload_id, error, cleanup_error) from error
interrupted = http.CloudError("source upload interrupted.", exit_code=130)
raise _source_cleanup_error(self.upload_id, interrupted, cleanup_error) from None
return
if isinstance(error, Exception):
raise _retained_source_upload_error(
self.upload_id, error, self.idempotency_key
) from error
def wrap_result(self, result: Any, args: argparse.Namespace) -> Any:
"""Attach the approved source manifest to a successful scan response."""
if self.bundle is None:
return result
return {
"source": self.bundle.summary(show_files=getattr(args, "show_files", False)),
"upload_id": self.upload_id,
"scan": result,
}
def close(self) -> None:
"""Remove the private temporary bundle, if one was built."""
if self.bundle is not None:
remove_bundle(self.bundle)
def prepare_scan_source(
console: Console, args: argparse.Namespace, *, as_json: bool
) -> SourceBundle | None:
"""Build and approve the exact local-source snapshot for one invocation."""
source = getattr(args, "source", None)
source_flags = (
"dry_run",
"show_files",
"include_hidden",
"include_sensitive",
"include_archives",
"approve_sha256",
)
if source is None:
if any(getattr(args, name, False) for name in source_flags) or getattr(args, "exclude", []):
raise http.CloudError("source upload options require --source DIRECTORY.")
return None
bundle = prepare_source(
source,
include_hidden=bool(getattr(args, "include_hidden", False)),
include_sensitive=bool(getattr(args, "include_sensitive", False)),
include_archives=bool(getattr(args, "include_archives", False)),
exclude=cast("list[str]", getattr(args, "exclude", [])),
)
keep_bundle = False
try:
approved_digest = _validate_source_digest_approval(args, bundle)
if getattr(args, "dry_run", False):
keep_bundle = True
return bundle
if getattr(args, "yes", False) or approved_digest is not None:
keep_bundle = True
return bundle
if as_json or not (sys.stdin.isatty() and sys.stdout.isatty()):
_source_approval_error(
"source upload requires explicit approval in non-interactive mode. "
"Review with --dry-run --show-files, then rerun with "
"--approve-sha256 <reviewed hash>; use --yes only for a deliberate "
"one-shot approval of the snapshot built by that invocation."
)
console.print(
"[bold]Local source upload[/]\n"
f" {len(bundle.manifest.files):,} file(s), "
f"{_format_bytes(bundle.manifest.total_bytes)} "
f"({_format_bytes(bundle.archive_bytes)} compressed)\n"
f" {sum(bundle.manifest.excluded.values()):,} path(s) excluded\n"
" Only the selected files will be sent to Strix Cloud."
)
if getattr(args, "show_files", False):
console.print(f"\n[bold]Selected files ({len(bundle.manifest.files):,})[/]")
for selected in bundle.manifest.files:
console.print(
f" {escape(sanitize_terminal_text(selected.archive_name))}", soft_wrap=True
)
answer = (
console.input("Upload this source and start the scan? [y/N]: ", markup=False)
.strip()
.lower()
)
if answer not in ("y", "yes"):
_source_approval_error("source upload cancelled.")
keep_bundle = True
return bundle
finally:
if not keep_bundle:
remove_bundle(bundle)
def _validate_source_digest_approval(args: argparse.Namespace, bundle: SourceBundle) -> str | None:
approved_digest = getattr(args, "approve_sha256", None)
if approved_digest is None:
return None
if not isinstance(approved_digest, str) or not _SHA256.fullmatch(approved_digest):
_source_approval_error("--approve-sha256 must be exactly 64 hexadecimal characters.")
if bundle.archive_sha256 != approved_digest.lower():
_source_approval_error(
"source archive SHA-256 does not match --approve-sha256; review a fresh "
"--dry-run before uploading."
)
return approved_digest
def _source_approval_error(message: str) -> NoReturn:
raise http.CloudError(message)
def _upload_scan_source(bundle: SourceBundle, *, token: str | None) -> str:
file_name = f"strix-source-{bundle.archive_sha256[:12]}.zip"
requested = http.check(
http.request(
"POST",
"/uploads/request",
token=token,
body={
"file_name": file_name,
"file_size": bundle.archive_bytes,
"category": "repository",
},
)
)
if not isinstance(requested, dict):
raise http.CloudError("the platform returned an invalid source upload response.")
fields = cast("dict[str, Any]", requested)
upload_id = fields.get("upload_id")
signed_url = fields.get("signed_url")
upload_token = fields.get("token")
if not all(isinstance(value, str) and value for value in (upload_id, signed_url, upload_token)):
error = http.CloudError("the platform did not return complete source upload credentials.")
if isinstance(upload_id, str) and upload_id:
try:
_delete_upload(upload_id, token=token)
except (http.CloudError, KeyboardInterrupt) as cleanup_error:
raise _source_cleanup_error(upload_id, error, cleanup_error) from error
raise error
try:
http.upload_file(cast("str", signed_url), cast("str", upload_token), bundle.archive_path)
completed = http.check(
http.request(
"POST",
"/uploads/complete",
token=token,
body={"upload_id": upload_id},
)
)
_validate_completed_upload(completed, expected_id=cast("str", upload_id))
except BaseException as error:
try:
_delete_upload(cast("str", upload_id), token=token)
except (http.CloudError, KeyboardInterrupt) as cleanup_error:
if isinstance(error, Exception):
raise _source_cleanup_error(cast("str", upload_id), error, cleanup_error) from error
interrupted = http.CloudError("source upload interrupted.", exit_code=130)
raise _source_cleanup_error(
cast("str", upload_id), interrupted, cleanup_error
) from None
raise
return cast("str", upload_id)
def _validate_completed_upload(completed: Any, *, expected_id: str) -> None:
fields = cast("dict[str, Any]", completed) if isinstance(completed, dict) else {}
if fields.get("id") != expected_id:
raise http.CloudError("the platform returned an invalid source upload completion response.")
def _delete_upload(upload_id: str, *, token: str | None) -> None:
response = http.request("DELETE", f"/uploads/{quote(upload_id, safe='')}", token=token)
if response.status_code == 404 or 200 <= response.status_code < 300:
return
http.check(response)
def _source_cleanup_note(upload_id: str, cleanup_error: BaseException) -> str:
return (
f"Cleanup of source upload {upload_id} could not be confirmed: {cleanup_error}. "
f"Retry with `strix cloud uploads delete {upload_id}`."
)
def _source_cleanup_error(
upload_id: str, error: Exception, cleanup_error: BaseException
) -> http.CloudError:
"""Report a staged source object whenever automatic deletion is uncertain."""
message = f"{error} {_source_cleanup_note(upload_id, cleanup_error)}"
payload: dict[str, Any] = {}
exit_code = http.EXIT_ERROR
if isinstance(error, http.CloudError):
exit_code = error.exit_code
raw_payload: Any = error.payload
if isinstance(raw_payload, dict):
payload.update(cast("dict[str, Any]", raw_payload))
elif raw_payload is not None:
payload["detail"] = raw_payload
payload.update(
{
"error": message,
"upload_id": upload_id,
"upload_retained": True,
"cleanup_unknown": True,
}
)
return http.CloudError(message, exit_code=exit_code, payload=payload)
def _interrupted_source_upload_error(
upload_id: str, idempotency_key: str | None = None
) -> http.CloudError:
retry_note = _idempotency_retry_note(idempotency_key)
message = (
"Interrupted while starting the scan. The launch outcome is unknown, so source upload "
f"{upload_id} was retained. Check `strix cloud scans list` before retrying; if no scan "
f"was created, run `strix cloud uploads delete {upload_id}`.{retry_note}"
)
payload: dict[str, Any] = {
"error": message,
"interrupted": True,
"upload_id": upload_id,
"upload_retained": True,
"launch_outcome_unknown": True,
}
_attach_idempotency_recovery(payload, idempotency_key)
return http.CloudError(message, exit_code=130, payload=payload)
def _retained_source_upload_error(
upload_id: str,
error: Exception,
idempotency_key: str | None = None,
) -> http.CloudError:
"""Preserve source when the platform may already have accepted its scan."""
retry_note = _idempotency_retry_note(idempotency_key)
message = (
f"{error} The scan launch outcome is unknown, so source upload {upload_id} was retained. "
"Check `strix cloud scans list` before retrying; if no scan was created, clean it up "
f"with `strix cloud uploads delete {upload_id}`. Linked uploads cannot be deleted."
f"{retry_note}"
)
payload: dict[str, Any] = {}
exit_code = http.EXIT_ERROR
if isinstance(error, http.CloudError):
exit_code = error.exit_code
raw_payload: Any = error.payload
error_payload = cast("dict[str, Any]", raw_payload)
if isinstance(raw_payload, dict):
payload.update(error_payload)
elif raw_payload is not None:
payload["detail"] = raw_payload
payload.update(
{
"error": message,
"upload_id": upload_id,
"upload_retained": True,
"launch_outcome_unknown": True,
}
)
_attach_idempotency_recovery(payload, idempotency_key)
return http.CloudError(message, exit_code=exit_code, payload=payload)
def _idempotency_retry_note(idempotency_key: str | None) -> str:
if not idempotency_key:
return ""
return (
" An exact retry is safe only with the same request body and "
f"`--idempotency-key {idempotency_key}`."
)
def _attach_idempotency_recovery(payload: dict[str, Any], idempotency_key: str | None) -> None:
if not idempotency_key:
return
payload.update(
{
"idempotency_key": idempotency_key,
"retry_safe": True,
"retry_same_request": True,
}
)
def _format_bytes(value: int) -> str:
if value < 1024:
return f"{value} B"
if value < 1024 * 1024:
return f"{value / 1024:.1f} KB"
return f"{value / (1024 * 1024):.1f} MB"
+734
View File
@@ -0,0 +1,734 @@
"""Privacy-conscious local source packaging for managed scans."""
from __future__ import annotations
import fnmatch
import hashlib
import os
import shutil
import stat
import subprocess # nosec B404
import tempfile
import zipfile
from collections import Counter
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING
import strix.interface.cloud.http as http # noqa: PLR0402
if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Protocol
class _ScandirIterator(Iterator[os.DirEntry[str]], Protocol):
def close(self) -> None: ...
MAX_FILES = 20_000
MAX_FILE_BYTES = 25 * 1024 * 1024
MAX_TOTAL_BYTES = 250 * 1024 * 1024
MAX_ARCHIVE_BYTES = 50 * 1024 * 1024
MAX_CANDIDATE_PATHS = 200_000
MAX_IGNORE_BYTES = 64 * 1024
MAX_IGNORE_PATTERNS = 1_000
MAX_IGNORE_PATTERN_CHARS = 1_024
_ALWAYS_EXCLUDED_DIRS = frozenset(
{
".git",
".hg",
".svn",
"node_modules",
"vendor",
"venv",
".venv",
"env",
"__pycache__",
".tox",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"dist",
"build",
"coverage",
"target",
".next",
".nuxt",
".gradle",
}
)
_SENSITIVE_NAMES = frozenset(
{
"id_rsa",
"id_dsa",
"id_ecdsa",
"id_ed25519",
"credentials.json",
"service-account.json",
"service_account.json",
".env",
".npmrc",
".pypirc",
".netrc",
".git-credentials",
"application_default_credentials.json",
}
)
_SENSITIVE_PATTERNS = (
"*.pem",
"*.key",
"*.p12",
"*.pfx",
"*.keystore",
"*.jks",
"secrets.*",
"secret.*",
".env.*",
)
_SENSITIVE_PATH_SUFFIXES = (
(".aws", "credentials"),
(".aws", "config"),
(".docker", "config.json"),
(".config", "gcloud", "credentials.db"),
(".azure", "accesstokens.json"),
(".azure", "azureprofile.json"),
(".kube", "config"),
)
_ARCHIVE_SUFFIXES = (
".zip",
".tar",
".tgz",
".tar.gz",
".tar.bz2",
".tar.xz",
".7z",
".rar",
".gz",
".bz2",
".xz",
".jar",
".war",
".whl",
".nupkg",
".apk",
".ipa",
)
_ARCHIVE_MAGIC_PREFIXES = (
b"PK\x03\x04",
b"PK\x05\x06",
b"PK\x07\x08",
b"\x1f\x8b",
b"BZh",
b"\xfd7zXZ\x00",
b"7z\xbc\xaf\x27\x1c",
b"Rar!\x1a\x07",
)
@dataclass(frozen=True)
class SelectedFile:
path: Path
archive_name: str
size: int
device: int
inode: int
mtime_ns: int
ctime_ns: int
@dataclass(frozen=True)
class SourceManifest:
source: Path
files: tuple[SelectedFile, ...]
excluded: Counter[str]
include_hidden: bool
include_sensitive: bool
include_archives: bool
@property
def total_bytes(self) -> int:
return sum(item.size for item in self.files)
def as_dict(
self,
*,
show_files: bool,
archive_bytes: int | None = None,
archive_sha256: str | None = None,
) -> dict[str, object]:
result: dict[str, object] = {
"source": str(self.source),
"file_count": len(self.files),
"uncompressed_bytes": self.total_bytes,
"excluded_count": sum(self.excluded.values()),
"excluded_by_reason": dict(sorted(self.excluded.items())),
"include_hidden": self.include_hidden,
"include_sensitive": self.include_sensitive,
"include_archives": self.include_archives,
}
if archive_bytes is not None:
result["archive_bytes"] = archive_bytes
if archive_sha256 is not None:
result["archive_sha256"] = archive_sha256
if show_files:
result["files"] = [item.archive_name for item in self.files]
return result
@dataclass(frozen=True)
class SourceBundle:
manifest: SourceManifest
archive_path: Path
archive_bytes: int
archive_sha256: str
def summary(self, *, show_files: bool) -> dict[str, object]:
return self.manifest.as_dict(
show_files=show_files,
archive_bytes=self.archive_bytes,
archive_sha256=self.archive_sha256,
)
def prepare_source(
value: str,
*,
include_hidden: bool,
include_sensitive: bool,
include_archives: bool,
exclude: list[str],
) -> SourceBundle:
"""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,
include_hidden=include_hidden,
include_sensitive=include_sensitive,
include_archives=include_archives,
exclude=exclude,
)
if not manifest.files:
raise http.CloudError("no files remain after applying source upload exclusions.")
with tempfile.NamedTemporaryFile(prefix="strix-source-", suffix=".zip", delete=False) as handle:
archive_path = Path(handle.name)
try:
_write_archive(archive_path, manifest.files)
except BaseException:
archive_path.unlink(missing_ok=True)
raise
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)
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,
*,
include_hidden: bool = False,
include_sensitive: bool = False,
include_archives: bool = False,
exclude: list[str] | None = None,
) -> SourceManifest:
excluded: Counter[str] = Counter()
selected: list[SelectedFile] = []
patterns = [*_load_ignore_patterns(source), *(exclude or [])]
_validate_patterns(patterns)
total_bytes = 0
for relative in _candidate_paths(
source,
include_hidden=include_hidden,
patterns=patterns,
excluded=excluded,
):
archive_name = relative.as_posix()
reason = _exclusion_reason(
relative,
include_hidden=include_hidden,
include_sensitive=include_sensitive,
include_archives=include_archives,
patterns=patterns,
)
if reason:
excluded[reason] += 1
continue
path = source / relative
try:
info = path.lstat()
except OSError:
excluded["unreadable"] += 1
continue
if not stat.S_ISREG(info.st_mode):
excluded["symlink_or_non_file"] += 1
continue
if not include_archives and _has_archive_magic(path):
excluded["nested_archive"] += 1
continue
if info.st_size > MAX_FILE_BYTES:
raise http.CloudError(
f"{archive_name} is larger than the 25 MB per-file limit; exclude it explicitly."
)
selected.append(
SelectedFile(
path=path,
archive_name=archive_name,
size=info.st_size,
device=info.st_dev,
inode=info.st_ino,
mtime_ns=info.st_mtime_ns,
ctime_ns=info.st_ctime_ns,
)
)
total_bytes += info.st_size
if len(selected) > MAX_FILES:
raise http.CloudError(
f"source contains more than {MAX_FILES:,} files; narrow --source or add exclusions."
)
if total_bytes > MAX_TOTAL_BYTES:
raise http.CloudError(
"selected source is larger than the 250 MB expanded-size limit; narrow --source "
"or add --exclude patterns."
)
selected.sort(key=lambda item: item.archive_name)
return SourceManifest(
source,
tuple(selected),
excluded,
include_hidden,
include_sensitive,
include_archives,
)
def remove_bundle(bundle: SourceBundle) -> None:
bundle.archive_path.unlink(missing_ok=True)
def _candidate_paths(
source: Path,
*,
include_hidden: bool,
patterns: list[str],
excluded: Counter[str],
) -> Iterator[Path]:
git_root = _git_root(source)
if git_root is not None:
git = shutil.which("git")
if git is not None:
yield from _git_candidate_paths(git, git_root, source)
return
yield from _walk_candidate_paths(
source,
include_hidden=include_hidden,
patterns=patterns,
excluded=excluded,
)
def _git_candidate_paths(git: str, git_root: Path, source: Path) -> Iterator[Path]:
"""Stream Git's NUL-delimited manifest without buffering an unbounded repository."""
relative_source = source.relative_to(git_root)
command = [
git,
"-C",
str(git_root),
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
]
if relative_source != Path():
command.append(relative_source.as_posix())
try:
process = subprocess.Popen( # noqa: S603 # nosec B603
command,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
except OSError as exc:
raise http.CloudError(f"could not enumerate Git source files: {exc}") from exc
assert process.stdout is not None
buffer = b""
count = 0
try:
while chunk := process.stdout.read(64 * 1024):
buffer += chunk
records = buffer.split(b"\0")
buffer = records.pop()
for raw in records:
relative = _git_relative_path(raw, relative_source)
if relative is None:
continue
count += 1
_check_candidate_limit(count)
yield relative
if buffer:
raise http.CloudError("Git returned a malformed source file manifest.")
if process.wait() != 0:
raise http.CloudError("Git could not enumerate the source directory.")
finally:
process.stdout.close()
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
def _git_relative_path(raw: bytes, relative_source: Path) -> Path | None:
repo_relative = Path(os.fsdecode(raw))
try:
relative = repo_relative.relative_to(relative_source)
except ValueError:
return None
if relative.is_absolute() or ".." in relative.parts:
raise http.CloudError("Git returned an unsafe source path.")
return relative
def _walk_candidate_paths(
source: Path,
*,
include_hidden: bool,
patterns: list[str],
excluded: Counter[str],
) -> Iterator[Path]:
"""Walk top-down so excluded dependency, VCS, and hidden trees are never traversed."""
count = 0
stack: list[tuple[Path, _ScandirIterator]] = []
try:
stack.append((source, os.scandir(source)))
while stack:
root_path, entries = stack[-1]
try:
entry = next(entries)
except StopIteration:
entries.close()
stack.pop()
continue
count += 1
_check_candidate_limit(count)
path = root_path / entry.name
relative = path.relative_to(source)
try:
is_directory = entry.is_dir(follow_symlinks=False)
is_symlink = entry.is_symlink()
except OSError:
excluded["unreadable"] += 1
continue
if is_directory:
reason = _pruned_directory_reason(
relative,
include_hidden=include_hidden,
patterns=patterns,
)
if reason:
excluded[reason] += 1
continue
try:
stack.append((path, os.scandir(path)))
except OSError:
excluded["unreadable"] += 1
continue
if is_symlink:
excluded["symlink_or_non_file"] += 1
continue
yield relative
except OSError as exc:
raise http.CloudError(f"could not enumerate source directory {source}: {exc}") from exc
finally:
for _, entries in stack:
entries.close()
def _pruned_directory_reason(
relative: Path,
*,
include_hidden: bool,
patterns: list[str],
) -> str | None:
lower_parts = tuple(part.lower() for part in relative.parts)
if any(part == ".git" for part in lower_parts):
return "git_metadata"
if any(part in _ALWAYS_EXCLUDED_DIRS for part in lower_parts):
return "dependency_or_build_output"
if not include_hidden and any(part.startswith(".") for part in relative.parts):
return "hidden"
if any(_matches_user_pattern(relative, pattern) for pattern in patterns):
return "user_pattern"
return None
def _check_candidate_limit(count: int) -> None:
if count > MAX_CANDIDATE_PATHS:
raise http.CloudError(
f"source enumeration exceeded {MAX_CANDIDATE_PATHS:,} paths before filtering; "
"narrow --source or add directory exclusions."
)
def _git_root(source: Path) -> Path | None:
git = shutil.which("git")
if git is None:
return None
result = subprocess.run( # noqa: S603 # nosec B603
[git, "-C", str(source), "rev-parse", "--show-toplevel"],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
return None
try:
return Path(result.stdout.strip()).resolve()
except OSError:
return None
def _exclusion_reason( # noqa: PLR0911
relative: Path,
*,
include_hidden: bool,
include_sensitive: bool,
include_archives: bool,
patterns: list[str],
) -> str | None:
parts = relative.parts
lower_parts = tuple(part.lower() for part in parts)
if any(part == ".git" for part in lower_parts):
return "git_metadata"
if any(part in _ALWAYS_EXCLUDED_DIRS for part in lower_parts[:-1]):
return "dependency_or_build_output"
if not include_hidden and any(part.startswith(".") for part in parts):
return "hidden"
if any(_matches_user_pattern(relative, pattern) for pattern in patterns):
return "user_pattern"
name = relative.name.lower()
if not include_sensitive and (
name in _SENSITIVE_NAMES
or any(fnmatch.fnmatch(name, pattern) for pattern in _SENSITIVE_PATTERNS)
or any(
lower_parts[-len(suffix) :] == suffix
for suffix in _SENSITIVE_PATH_SUFFIXES
if len(lower_parts) >= len(suffix)
)
):
return "sensitive_filename"
if not include_archives and name.endswith(_ARCHIVE_SUFFIXES):
return "nested_archive"
return None
def _matches_user_pattern(relative: Path, pattern: str) -> bool:
"""Match exclude globs, including intuitive trailing-slash directory rules."""
relative_posix = relative.as_posix()
posix = PurePosixPath(relative_posix)
if pattern.endswith("/"):
directory_pattern = pattern.rstrip("/")
if not directory_pattern:
return False
return (
posix.match(directory_pattern)
or fnmatch.fnmatch(relative_posix, directory_pattern)
or any(
PurePosixPath(parent.as_posix()).match(directory_pattern)
or fnmatch.fnmatch(parent.as_posix(), directory_pattern)
for parent in posix.parents
if parent != PurePosixPath(".")
)
)
return posix.match(pattern) or fnmatch.fnmatch(relative_posix, pattern)
def _write_archive(destination: Path, files: tuple[SelectedFile, ...]) -> None:
with zipfile.ZipFile(
destination, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6
) as archive:
for item in files:
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(item.path, flags)
except OSError as exc:
raise http.CloudError(f"could not safely read {item.archive_name}: {exc}") from exc
with os.fdopen(descriptor, "rb") as source_file:
current = os.fstat(source_file.fileno())
if (
not stat.S_ISREG(current.st_mode)
or current.st_size != item.size
or current.st_dev != item.device
or current.st_ino != item.inode
or current.st_mtime_ns != item.mtime_ns
or current.st_ctime_ns != item.ctime_ns
):
raise http.CloudError(
f"{item.archive_name} changed while the source archive was being built; "
"retry."
)
info = zipfile.ZipInfo(item.archive_name)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
with archive.open(info, "w", force_zip64=True) as target:
remaining = item.size
while remaining:
chunk = source_file.read(min(1024 * 1024, remaining))
if not chunk:
raise http.CloudError(
f"{item.archive_name} changed while the source archive was being "
"built; retry."
)
target.write(chunk)
remaining -= len(chunk)
final = os.fstat(source_file.fileno())
if (
source_file.read(1)
or not stat.S_ISREG(final.st_mode)
or final.st_size != item.size
or final.st_dev != item.device
or final.st_ino != item.inode
or final.st_mtime_ns != item.mtime_ns
or final.st_ctime_ns != item.ctime_ns
):
raise http.CloudError(
f"{item.archive_name} changed while the source archive was being "
"built; retry."
)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _has_archive_magic(path: Path) -> bool:
"""Recognize common archive containers even when their suffix is disguised."""
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(path, flags)
with os.fdopen(descriptor, "rb") as stream:
header = stream.read(512)
except OSError:
return False
return header.startswith(_ARCHIVE_MAGIC_PREFIXES) or header[257:262] == b"ustar"
def _load_ignore_patterns(source: Path) -> list[str]:
path = source / ".strixignore"
raw_text = _read_ignore_file(path)
if raw_text is None:
return []
if len(raw_text) > MAX_IGNORE_BYTES:
raise http.CloudError(f"{path} is larger than the {MAX_IGNORE_BYTES:,}-byte limit.")
try:
lines = raw_text.decode("utf-8").splitlines()
except UnicodeDecodeError as exc:
raise http.CloudError(f"{path} must be UTF-8 text.") from exc
patterns: list[str] = []
for line_number, raw in enumerate(lines, start=1):
value = raw.strip()
if not value or value.startswith("#"):
continue
if value.startswith("!"):
raise http.CloudError(
f"{path}:{line_number}: negated patterns are not supported; use exclude-only globs."
)
patterns.append(value)
if len(patterns) > MAX_IGNORE_PATTERNS:
raise http.CloudError(
f"{path} contains more than {MAX_IGNORE_PATTERNS:,} exclusion patterns."
)
return patterns
def _read_ignore_file(path: Path) -> bytes | None:
"""Read a bounded regular ignore file without blocking on a FIFO or device."""
try:
descriptor = os.open(
path,
os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0),
)
except FileNotFoundError:
return None
except OSError as exc:
raise http.CloudError(f"could not read {path}: {exc}") from exc
try:
info = os.fstat(descriptor)
except OSError as exc:
os.close(descriptor)
raise http.CloudError(f"could not inspect {path}: {exc}") from exc
if not stat.S_ISREG(info.st_mode):
os.close(descriptor)
raise http.CloudError(f"{path} must be a regular file.")
try:
stream = os.fdopen(descriptor, "rb")
except OSError as exc:
os.close(descriptor)
raise http.CloudError(f"could not read {path}: {exc}") from exc
try:
return stream.read(MAX_IGNORE_BYTES + 1)
except OSError as exc:
raise http.CloudError(f"could not read {path}: {exc}") from exc
finally:
stream.close()
def _validate_patterns(patterns: list[str]) -> None:
if len(patterns) > MAX_IGNORE_PATTERNS:
raise http.CloudError(
f"source upload accepts at most {MAX_IGNORE_PATTERNS:,} exclusion patterns."
)
for pattern in patterns:
if len(pattern) > MAX_IGNORE_PATTERN_CHARS:
raise http.CloudError(
"source exclusion patterns must be at most "
f"{MAX_IGNORE_PATTERN_CHARS:,} characters each."
)
if "\x00" in pattern:
raise http.CloudError("source exclusion patterns cannot contain NUL bytes.")
File diff suppressed because it is too large. Load diff
+291
View File
@@ -0,0 +1,291 @@
"""`strix cloud workspaces use` — switch the stored token to another workspace.
The command lists the workspaces of the account, finds the requested one by
ID or by exact name, asks the platform to rotate that token in place, and
stores the returned workspace metadata. The bearer secret and expiry stay the
same; the account's role in the target workspace limits the granted scopes.
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, cast
from rich.console import Console
from rich.markup import escape
import strix.interface.cloud.http as http # noqa: PLR0402
from strix.interface.cloud.arguments import CloudArgumentParser
from strix.interface.cloud.render import emit, json_mode
from strix.interface.platform_cli import AUTH_PATH, read_record, save_record
from strix.interface.platform_identity import read_or_create_identity
from strix.interface.terminal_text import sanitize_terminal_text
if TYPE_CHECKING:
import argparse
def run_workspace_use(argv: list[str]) -> int:
"""Entry point for ``strix cloud workspaces use``. Returns an exit code."""
console = Console()
parser = CloudArgumentParser(
prog="strix cloud workspaces use",
description="Switch the stored API token to another workspace.",
)
parser.add_argument(
"workspace",
metavar="WORKSPACE",
help="Workspace number from `workspaces list`, ID, or exact name.",
)
scope_mode = parser.add_mutually_exclusive_group()
scope_mode.add_argument(
"--scopes",
nargs="+",
metavar="SCOPE",
default=None,
help=(
"Use a custom scope set within the login-approved ceiling. "
"Without this option, preserve the server-side scope preference."
),
)
scope_mode.add_argument(
"--scope-profile",
choices=("minimal", "recommended", "full"),
default=None,
help="Change to a profile within the authority approved at login.",
)
parser.add_argument("--show-scopes", action="store_true", help="Print every granted scope.")
parser.add_argument("--json", action="store_true", help="Print the raw JSON response.")
parser.add_argument("--token", default=None, help="API token override.")
parser.add_argument(
"--workspace-id",
default=None,
metavar="ORG_ID",
help="Expected workspace for an override CLI token.",
)
parser.add_argument("--app-url", default=None, metavar="URL", help="Platform URL override.")
parser.add_argument(
"--timeout", default=None, type=float, metavar="SECONDS", help="Request timeout in seconds."
)
as_json = json_mode(flag="--json" in argv)
try:
args = parser.parse_args(argv)
except SystemExit as exc:
return exc.code if isinstance(exc.code, int) else 2
except http.CloudError as exc:
_emit_cloud_error(console, exc, as_json=as_json)
return exc.exit_code
as_json = json_mode(flag=bool(args.json))
try:
http.configure(
base_url=args.app_url,
timeout=args.timeout,
token_override=bool(args.token),
workspace_id=args.workspace_id,
)
return _use(console, args, as_json=as_json)
except http.CloudError as exc:
_emit_cloud_error(console, exc, as_json=as_json)
return exc.exit_code
def _use( # noqa: PLR0912, PLR0915
console: Console, args: argparse.Namespace, *, as_json: bool
) -> int:
workspace = _find_workspace(args.workspace, token=args.token)
stored_record: dict[str, Any] = read_record() or {}
# An override token may belong to a different account. Never mix its new
# workspace state with identity or scope preferences from the stored sign-in.
external_token = args.token is not None or bool(os.environ.get("STRIX_API_TOKEN", "").strip())
record: dict[str, Any] = {} if external_token else dict(stored_record)
body: dict[str, Any] = {}
if args.scopes:
body["scopes"] = args.scopes
body["scope_profile"] = "custom"
elif args.scope_profile:
body["scope_profile"] = args.scope_profile
if not external_token:
try:
body.update(read_or_create_identity())
except (OSError, ValueError) as exc:
raise http.CloudError(f"could not load the CLI device identity: {exc}") from exc
switched = _switch_workspace_token(
str(workspace["id"]),
token=args.token,
body=body or None,
)
if not isinstance(switched, dict):
raise _workspace_switch_unknown("the platform returned an invalid response")
switched_record = cast("dict[str, Any]", switched)
switched_token = switched_record.get("api_token")
if not isinstance(switched_token, str) or not switched_token.strip():
raise _workspace_switch_unknown("the platform response omitted the token")
switched_scopes = switched_record.get("scopes")
switched_scope_items = cast("list[Any]", cast("Any", switched_scopes))
if not isinstance(switched_scopes, list) or not all(
isinstance(scope, str) for scope in switched_scope_items
):
raise _workspace_switch_unknown("the platform response contained invalid scopes")
validated_scopes = cast("list[str]", switched_scope_items)
record.update(
{
"api_token": switched_token,
"organization_id": switched_record.get("organization_id", workspace["id"]),
"organization_name": switched_record.get(
"organization_name", workspace.get("name", "")
),
"expires_at": switched_record.get("expires_at") or stored_record.get("expires_at"),
"scopes": validated_scopes,
"requested_scopes": switched_record.get("requested_scopes", validated_scopes),
"scope_ceiling": switched_record.get("scope_ceiling", []),
"scope_profile": switched_record.get("scope_profile", "custom"),
"token_id": switched_record.get("token_id"),
"credential_source": switched_record.get("credential_source", "api"),
"device_name": switched_record.get("device_name"),
"app_url": http.app_url(),
}
)
if switched_record.get("email"):
record["email"] = switched_record["email"]
if not external_token:
try:
save_record(record)
except OSError as exc:
raise http.CloudError(
"the platform switched the token, but the local workspace metadata could not be "
f"stored in {AUTH_PATH}: {exc}. The bearer is still valid; fix the file and safely "
"rerun the same workspace use command.",
payload={
"workspace_switched": True,
"local_record_updated": False,
"retry_safe": True,
},
) from exc
result = {
"workspace_id": record["organization_id"],
"workspace_name": record["organization_name"],
"scopes": record["scopes"],
"requested_scopes": record.get("requested_scopes", record["scopes"]),
"scope_ceiling": record.get("scope_ceiling", []),
"scope_profile": record.get("scope_profile", "custom"),
"expires_at": record.get("expires_at"),
"token_id": record.get("token_id"),
"credential_source": record.get("credential_source", "api"),
"device_name": record.get("device_name"),
"stored": not external_token,
}
if as_json:
emit(console, result, as_json=True)
return http.EXIT_OK
workspace_name = escape(sanitize_terminal_text(record["organization_name"]))
console.print(f"[green]✓ Switched to workspace [bold]{workspace_name}[/].[/]")
scopes = record.get("scopes")
if isinstance(scopes, list) and scopes:
scope_items = cast("list[Any]", cast("Any", scopes))
scope_names = [scope for scope in scope_items if isinstance(scope, str)]
if scope_names and args.show_scopes:
rendered_scopes = escape(sanitize_terminal_text(" ".join(scope_names)))
console.print(f" Scopes: [dim]{rendered_scopes}[/]")
elif scope_names:
profile = str(record.get("scope_profile") or "custom").title()
console.print(f" Access: [dim]{profile} · {len(scope_names)} scopes granted[/]")
if external_token:
console.print(" Token: [dim]override used for this command only; not stored[/]")
else:
console.print(f" Token: stored in [dim]{escape(sanitize_terminal_text(AUTH_PATH))}[/]")
return http.EXIT_OK
def _switch_workspace_token(
workspace_id: str,
*,
token: str | None,
body: dict[str, Any] | None,
) -> Any:
"""Switch in place, distinguishing definitive rejections from lost outcomes."""
try:
response = http.request(
"POST",
f"/workspaces/{workspace_id}/token",
token=token,
body=body,
)
except http.CloudError as exc:
raise _workspace_switch_unknown(str(exc)) from exc
# Client/auth/conflict responses prove the rotation did not return success.
# A 5xx or malformed success may arrive after the database commit, but the
# server preserves the bearer so replaying this exact command is safe.
if response.status_code in {400, 401, 403, 404, 409, 422}:
return http.check(response)
try:
return http.check(response)
except http.CloudError as exc:
raise _workspace_switch_unknown(str(exc)) from exc
def _workspace_switch_unknown(detail: str) -> http.CloudError:
return http.CloudError(
"workspace switch outcome is unknown: "
f"{sanitize_terminal_text(detail)}. The bearer secret is unchanged; safely rerun the "
"same workspace use command, or list workspaces to check the current one.",
payload={
"switch_outcome_unknown": True,
"retry_safe": True,
},
)
def _emit_cloud_error(console: Console, error: http.CloudError, *, as_json: bool) -> None:
if as_json:
raw_payload: Any = error.payload
error_payload = cast("dict[str, Any]", raw_payload)
payload = dict(error_payload) if isinstance(raw_payload, dict) else {}
payload["error"] = str(error)
emit(console, payload, as_json=True)
return
console.print(f"[red]Error:[/] {escape(sanitize_terminal_text(error))}")
def _find_workspace(selector: str, *, token: str | None) -> dict[str, Any]:
listed = http.check(http.request("GET", "/workspaces", token=token))
listed_record = cast("dict[str, Any]", listed) if isinstance(listed, dict) else {}
items = listed_record.get("workspaces")
item_values = cast("list[Any]", cast("Any", items)) if isinstance(items, list) else []
workspaces = [
cast("dict[str, Any]", cast("Any", item)) for item in item_values if isinstance(item, dict)
]
if not workspaces:
raise http.CloudError("no workspaces found for this account.")
wanted = selector.strip()
if wanted.isdigit():
index = int(wanted)
if 1 <= index <= len(workspaces):
return workspaces[index - 1]
raise http.CloudError(
f"workspace number must be between 1 and {len(workspaces)}. "
"Run `strix cloud workspaces` to see the numbered list."
)
by_id = [w for w in workspaces if w.get("id") == wanted]
if by_id:
return by_id[0]
by_name = [w for w in workspaces if str(w.get("name", "")).casefold() == wanted.casefold()]
if len(by_name) == 1:
return by_name[0]
if len(by_name) > 1:
numbers = ", ".join(
str(index)
for index, workspace in enumerate(workspaces, start=1)
if workspace in by_name
)
raise http.CloudError(
f"multiple workspaces are named {wanted!r}. Use its list number: {numbers}"
)
names = ", ".join(
f"{index}: {workspace.get('name')}" for index, workspace in enumerate(workspaces, start=1)
)
raise http.CloudError(f"no workspace matches {wanted!r}. Your workspaces: {names}")
+373
View File
@@ -0,0 +1,373 @@
"""Shell completion scripts and candidates for the Strix CLI."""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
from strix.interface.cloud.spec import DEFAULT_VERBS, SPEC, Cmd
from strix.interface.terminal_text import has_terminal_control, sanitize_terminal_text
_ROOT_COMMANDS = ("cloud", "auth", "view", "completions", "completion")
_SESSION_COMMANDS = ("login", "logout", "whoami", "session", "credits")
_COMMON_FLAGS = (
"--json",
"--token",
"--workspace-id",
"--app-url",
"--timeout",
"-h",
"--help",
)
_COMMON_VALUE_FLAGS = frozenset({"--token", "--workspace-id", "--app-url", "--timeout"})
_WORKSPACE_USE_FLAGS = (*_COMMON_FLAGS, "--scopes", "--scope-profile", "--show-scopes")
def run_completions(argv: list[str]) -> int:
"""Print a shell integration script or hidden completion candidates."""
if argv and argv[0] == "--candidates":
for candidate in completion_candidates(argv[1:]):
sys.stdout.write(candidate + "\n")
return 0
if not argv or argv[0] in ("-h", "--help", "help"):
sys.stdout.write(
"Usage: strix completions <zsh|bash|fish>\n\n"
"Enable tab completion for the current shell:\n"
" zsh: source <(strix completions zsh)\n"
" bash: source <(strix completions bash)\n"
" fish: strix completions fish | source\n"
)
return 0
shell = argv[0].lower()
scripts = {"zsh": _zsh_script, "bash": _bash_script, "fish": _fish_script}
generator = scripts.get(shell)
if generator is None:
sys.stderr.write(
f"Unknown shell: {sanitize_terminal_text(shell)}. Choose zsh, bash, or fish.\n"
)
return 2
sys.stdout.write(generator())
return 0
def completion_candidates(words: list[str]) -> list[str]:
"""Return candidates for words after the ``strix`` executable."""
prior, current = _split_cursor(words)
if not prior:
candidates = _matching(_ROOT_COMMANDS, current)
elif prior[0] != "cloud":
candidates = []
else:
candidates = _cloud_candidates(prior[1:], current)
# The line-oriented shell protocol cannot represent these names safely.
# Omitting them is preferable to returning a sanitized path that does not exist.
return [candidate for candidate in candidates if not has_terminal_control(candidate)]
def _split_cursor(words: list[str]) -> tuple[list[str], str]:
if not words:
return [], ""
return words[:-1], words[-1]
def _cloud_candidates(prior: list[str], current: str) -> list[str]: # noqa: PLR0911
groups = (*_SESSION_COMMANDS, *SPEC, "workspace")
if not prior:
return _matching(groups, current)
group = "workspaces" if prior[0] == "workspace" else prior[0]
rest = prior[1:]
if group in _SESSION_COMMANDS:
return _session_candidates(group, rest, current)
commands = SPEC.get(group)
if commands is None:
return _matching(groups, current)
default_verb = DEFAULT_VERBS.get(group)
default_is_active = (rest and rest[0].startswith("-")) or (not rest and current.startswith("-"))
if default_verb is not None and default_is_active:
return _command_candidates(commands[default_verb], rest, current)
command_paths = sorted(
((verb.split(), cmd) for verb, cmd in commands.items()),
key=lambda item: len(item[0]),
reverse=True,
)
for path, cmd in command_paths:
if rest[: len(path)] == path:
command_candidates = _command_candidates(cmd, rest[len(path) :], current)
if rest == path:
nested_words = {
candidate_path[len(path)]
for candidate_path, _candidate_cmd in command_paths
if len(candidate_path) > len(path) and candidate_path[: len(path)] == path
}
return sorted({*command_candidates, *_matching(nested_words, current)})
return command_candidates
if group == "workspaces" and rest[:1] == ["use"]:
return _flag_candidates(
_WORKSPACE_USE_FLAGS,
rest[1:],
current,
value_flags=_COMMON_VALUE_FLAGS | {"--scopes"},
)
verb_paths = [path for path, _cmd in command_paths]
if group == "workspaces":
verb_paths.append(["use"])
matching_paths = [path for path in verb_paths if path[: len(rest)] == rest]
if not matching_paths:
return []
next_words = sorted({path[len(rest)] for path in matching_paths if len(path) > len(rest)})
return _matching(next_words, current)
def _session_candidates(group: str, prior: list[str], current: str) -> list[str]:
if group == "session":
if not prior:
return _matching(("show", "scopes", "help", *_COMMON_FLAGS, "--show-scopes"), current)
if prior[:1] == ["scopes"] and len(prior) == 1:
return _matching(("set", *_COMMON_FLAGS, "--show-scopes"), current)
if prior[:2] == ["scopes", "set"]:
return _matching(
("minimal", "recommended", "full", "--scopes", *_COMMON_FLAGS, "--show-scopes"),
current,
)
return _flag_candidates(
(*_COMMON_FLAGS, "--show-scopes"),
prior,
current,
value_flags=_COMMON_VALUE_FLAGS | {"--scopes"},
)
flags = _session_flags(group)
value_flags: frozenset[str] = frozenset()
if group == "login":
value_flags = frozenset({"--scopes", "--scope-profile", "--workspace", "--device-name"})
elif group == "credits":
value_flags = _COMMON_VALUE_FLAGS
return _flag_candidates(flags, prior, current, value_flags=value_flags)
def _session_flags(group: str) -> tuple[str, ...]:
if group == "login":
return (
"--no-browser",
"--scopes",
"--scope-profile",
"--workspace",
"--device-name",
"-h",
"--help",
)
if group == "whoami":
return ("--json", "--show-scopes", "-h", "--help")
if group == "logout":
return ("--json", "--local-only", "-h", "--help")
if group == "credits":
return _COMMON_FLAGS
return ("-h", "--help")
def _command_candidates(cmd: Cmd, prior: list[str], current: str) -> list[str]:
filesystem = _filesystem_candidates(cmd, prior, current)
if filesystem is not None:
return filesystem
return _flag_candidates(
_command_flags(cmd),
prior,
current,
value_flags=_command_value_flags(cmd),
)
def _flag_candidates(
flags: tuple[str, ...],
prior: list[str],
current: str,
*,
value_flags: frozenset[str],
) -> list[str]:
if prior and prior[-1] in value_flags and not current.startswith("-"):
return []
return _matching(flags, current)
def _command_flags(cmd: Cmd) -> tuple[str, ...]:
flags: list[str] = list(_COMMON_FLAGS)
for param in cmd.query + cmd.body:
flag = "--" + (param.flag or _kebab(param.name))
flags.append(flag)
if param.kind == "bool":
flags.append("--no-" + flag.removeprefix("--"))
if cmd.method in ("POST", "PUT", "PATCH"):
flags.append("--data")
if cmd.idempotent:
flags.append("--idempotency-key")
if cmd.binary or cmd.path == "/audit":
flags.extend(("--output", "--force"))
if cmd.link:
flags.append("--no-browser")
if cmd.wait_path or cmd.wait_self:
flags.extend(("--wait", "--wait-timeout"))
if cmd.path == "/billing/topup":
flags.extend(("--yes", "--no-pay", "--payment-method"))
if cmd.path == "/scans" and cmd.method == "POST":
flags.extend(
(
"--source",
"--approve-sha256",
"--dry-run",
"--yes",
"--show-files",
"--exclude",
"--include-hidden",
"--include-sensitive",
"--include-archives",
)
)
if cmd.path == "/billing/auto-topup" and cmd.method == "PUT":
flags.append("--no-monthly-cap")
return tuple(dict.fromkeys(flags))
def _command_value_flags(cmd: Cmd) -> frozenset[str]:
flags = set(_COMMON_VALUE_FLAGS)
for param in cmd.query + cmd.body:
if param.kind != "bool":
flags.add("--" + (param.flag or _kebab(param.name)))
if cmd.method in ("POST", "PUT", "PATCH"):
flags.add("--data")
if cmd.idempotent:
flags.add("--idempotency-key")
if cmd.binary or cmd.path == "/audit":
flags.add("--output")
if cmd.wait_path or cmd.wait_self:
flags.add("--wait-timeout")
if cmd.path == "/billing/topup":
flags.add("--payment-method")
if cmd.path == "/scans" and cmd.method == "POST":
flags.update(("--source", "--approve-sha256", "--exclude"))
return frozenset(flags)
def _filesystem_candidates( # noqa: PLR0911
cmd: Cmd, prior: list[str], current: str
) -> list[str] | None:
inline = (
("--source=", True, ""),
("--output=", False, ""),
("--data=@", False, "@"),
)
for option, directories_only, marker in inline:
if current.startswith(option):
value = current.removeprefix(option)
return [
option + candidate.removeprefix(marker)
for candidate in _path_candidates(
marker + value,
directories_only=directories_only,
marker=marker,
)
]
if not prior or current.startswith("-"):
return None
option = prior[-1]
if option == "--source" and cmd.path == "/scans" and cmd.method == "POST":
return _path_candidates(current, directories_only=True)
if option == "--output" and (cmd.binary or cmd.path == "/audit"):
return _path_candidates(current)
if option == "--data" and cmd.method in ("POST", "PUT", "PATCH"):
if not current:
return ["@"]
if current.startswith("@"):
return _path_candidates(current, marker="@")
return []
return None
def _path_candidates(
value: str,
*,
directories_only: bool = False,
marker: str = "",
) -> list[str]:
raw = value.removeprefix(marker) if marker else value
ends_with_separator = raw.endswith(("/", "\\"))
expanded = Path(raw or ".").expanduser()
directory = expanded if ends_with_separator else expanded.parent
name_prefix = "" if ends_with_separator else expanded.name
raw_base = raw if ends_with_separator else raw[: len(raw) - len(name_prefix)]
try:
entries = directory.iterdir()
matches = [
entry
for entry in entries
if entry.name.startswith(name_prefix) and (not directories_only or entry.is_dir())
]
except OSError:
return []
candidates: list[str] = []
for entry in sorted(matches, key=lambda item: item.name.casefold()):
candidate = marker + raw_base + entry.name
if entry.is_dir():
candidate += "/"
candidates.append(candidate)
return candidates
def _kebab(value: str) -> str:
output: list[str] = []
for char in value:
if char.isupper():
output.extend(("-", char.lower()))
else:
output.append("-" if char == "_" else char)
return "".join(output)
def _matching(candidates: Any, prefix: str) -> list[str]:
return sorted({str(candidate) for candidate in candidates if str(candidate).startswith(prefix)})
def _zsh_script() -> str:
return r"""#compdef strix
_strix() {
local -a candidates
candidates=("${(@f)$($words[1] completions --candidates "${words[@]:2}")}")
_describe 'strix' candidates
}
compdef _strix strix
"""
def _bash_script() -> str:
return r"""_strix_completion() {
local -a candidates
local candidate
while IFS= read -r candidate; do
candidates+=("$candidate")
done < <(strix completions --candidates "${COMP_WORDS[@]:1:$COMP_CWORD}")
COMPREPLY=("${candidates[@]}")
for candidate in "${COMPREPLY[@]}"; do
if [[ $candidate == */ ]]; then
if type compopt >/dev/null 2>&1; then
compopt -o nospace
fi
break
fi
done
}
complete -F _strix_completion strix
"""
def _fish_script() -> str:
return r"""function __strix_candidates
set -l words (commandline -opc)
set -e words[1]
command strix completions --candidates $words (commandline -ct)
end
complete -c strix -f -a '(__strix_candidates)'
"""
+30 -6
View File
@@ -8,17 +8,29 @@ from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from strix.config import codex, load_settings
from strix.config import IntegrationSettings, 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()
@@ -33,6 +45,7 @@ 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
@@ -46,8 +59,7 @@ def validate_environment() -> None:
if not settings.llm.api_base:
missing_optional_vars.append("LLM_API_BASE")
if not settings.integrations.perplexity_api_key:
missing_optional_vars.append("PERPLEXITY_API_KEY")
missing_optional_vars.extend(_missing_web_search_vars(settings.integrations))
if missing_required_vars:
error_text = Text()
@@ -70,7 +82,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., 'openai/gpt-5.4' or "
" - Model name to use (e.g., 'openrouter/z-ai/glm-5.3' or "
"'anthropic/claude-opus-4-7')\n",
style="white",
)
@@ -89,7 +101,14 @@ 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 (enables real-time research)\n",
" - 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",
style="white",
)
elif var == "STRIX_REASONING_EFFORT":
@@ -102,7 +121,7 @@ def validate_environment() -> None:
)
error_text.append("\nExample setup:\n", style="white")
error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white")
error_text.append("export STRIX_LLM='openrouter/z-ai/glm-5.3'\n", style="dim white")
if missing_optional_vars:
for var in missing_optional_vars:
@@ -116,6 +135,8 @@ 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",
@@ -134,6 +155,7 @@ 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)",
@@ -161,6 +183,7 @@ 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")
@@ -208,6 +231,7 @@ 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)
+52 -14
View File
@@ -41,7 +41,8 @@ from strix.interface.update_check import (
from strix.interface.utils import (
build_final_stats_text,
)
from strix.telemetry import posthog, scarf
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.logging import configure_dependency_logging
@@ -62,6 +63,14 @@ import logging # noqa: E402
logger = logging.getLogger(__name__)
_ROOT_SUBCOMMAND_HELP = """
Additional commands:
strix cloud ... Use the managed Strix platform
strix auth ... Manage model-subscription sign-in
strix view [RUN] View a completed or running scan
strix completions SHELL Generate zsh, bash, or fish tab completion
"""
def _exception_messages(exc: BaseException) -> tuple[str, ...]:
messages: list[str] = []
@@ -324,6 +333,11 @@ 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)
@@ -383,22 +397,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 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.
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.
"""
validate_environment()
if not args.non_interactive:
return
set_scan_phase("preflight")
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)
@@ -410,6 +424,13 @@ def main() -> None:
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
try:
parse_arguments()
except SystemExit as exc:
Console().print(_ROOT_SUBCOMMAND_HELP.strip(), markup=False)
raise SystemExit(exc.code) from None
# `strix view [<run>]` is a viewer-only subcommand, dispatched before the
# scan argument parser (which requires a target) and before any scan setup.
if len(sys.argv) > 1 and sys.argv[1] == "view":
@@ -425,7 +446,18 @@ def main() -> None:
sys.exit(run_auth(sys.argv[2:]))
from strix.llm.warmup import start_import_warmup
# Generate native shell completion scripts before scan argument parsing.
if len(sys.argv) > 1 and sys.argv[1] in ("completion", "completions"):
from strix.interface.completions import run_completions
sys.exit(run_completions(sys.argv[2:]))
# `strix cloud …` drives the managed platform (app.strix.ai) and exits;
# it needs no target, Docker, or scan setup.
if len(sys.argv) > 1 and sys.argv[1] == "cloud":
from strix.interface.cloud import run_cloud
sys.exit(run_cloud(sys.argv[2:]))
start_import_warmup()
@@ -439,10 +471,12 @@ def main() -> None:
check_docker_installed()
pull_docker_image()
validate_environment()
# 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:
# Everything below imports the scan engine; do not race the warm-up thread.
wait_for_import_warmup()
if args.non_interactive:
_bootstrap_scan(args)
from strix.report.state import get_global_report_state
@@ -453,18 +487,21 @@ 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:
except Exception as exc:
exit_reason = "error"
posthog.error("unhandled_exception")
scarf.error("unhandled_exception")
report_error("unhandled_exception", exc)
raise
finally:
report_state = get_global_report_state()
@@ -483,6 +520,7 @@ 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)
+798
View File
@@ -0,0 +1,798 @@
"""`strix cloud login` — managed platform sign-in (app.strix.ai).
Signing in runs an OAuth 2.0 device authorization flow in the browser, creates
the Strix account and workspace when they do not exist yet, and stores a
personal API token in ``~/.strix/platform-auth.json``. The token drives the
managed REST API (scans, credits, top-ups) without a dashboard visit.
"""
from __future__ import annotations
import argparse
import contextlib
import json
import sys
import time
import webbrowser
from pathlib import Path
from typing import Any, NoReturn, cast
from urllib.parse import urlparse, urlsplit, urlunsplit
import requests
from rich.console import Console
from rich.markup import escape
from rich.panel import Panel
from rich.text import Text
from strix.config import load_settings
from strix.interface.platform_identity import read_or_create_identity
from strix.interface.terminal_text import sanitize_terminal_text
from strix.interface.url_safety import is_safe_web_url
from strix.utils.secret_files import write_secret_text
AUTH_PATH = Path.home() / ".strix" / "platform-auth.json"
_HTTP_TIMEOUT_S = 30
_DEFAULT_POLL_INTERVAL_S = 5
_MAX_POLL_INTERVAL_S = 60
_MAX_EXPIRES_IN_S = 30 * 60
_ROLE_RANK = {"viewer": 0, "analyst": 1, "admin": 2}
class PlatformAuthError(Exception):
"""Raised when the device authorization flow fails."""
class _SessionUsageError(Exception):
"""A session subcommand received invalid arguments."""
class _SessionArgumentParser(argparse.ArgumentParser):
def error(self, message: str) -> NoReturn:
raise _SessionUsageError(f"invalid arguments for {self.prog}: {message}")
def _terminal_markup(value: object) -> str:
return escape(sanitize_terminal_text(value))
def _app_url() -> str:
return load_settings().viewer.app_url.rstrip("/")
def read_record() -> dict[str, Any] | None:
try:
data = json.loads(AUTH_PATH.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
if not isinstance(data, dict):
return None
record = cast("dict[str, Any]", data)
if not record.get("api_token"):
return None
return record
def save_record(record: dict[str, Any]) -> None:
write_secret_text(AUTH_PATH, json.dumps(record, indent=2))
def logout() -> bool:
try:
AUTH_PATH.unlink()
except FileNotFoundError:
return True
except OSError:
return False
return True
def run_login(argv: list[str]) -> int:
"""Entry point for ``strix cloud login``. Returns a process exit code."""
console = Console()
subcommand = argv[0] if argv else None
if subcommand == "status":
return _status(console, argv[1:])
if subcommand == "logout":
return _logout(console, argv[1:])
return _login(console, argv)
def _login(console: Console, argv: list[str]) -> int:
parser = argparse.ArgumentParser(prog="strix cloud login", add_help=True)
parser.add_argument(
"--no-browser",
action="store_true",
help="Do not open the browser. Print the verification URL instead.",
)
scope_mode = parser.add_mutually_exclusive_group()
scope_mode.add_argument(
"--scopes",
nargs="+",
metavar="SCOPE",
default=None,
help=(
"API scopes for the token, for example scans:read billing:write. "
"The server always includes a minimum scope set. "
"Without this option, an interactive picker opens after the browser step."
),
)
scope_mode.add_argument(
"--scope-profile",
choices=("minimal", "recommended", "full"),
default=None,
help="Scope profile to approve. Defaults to an interactive choice in a TTY.",
)
parser.add_argument(
"--device-name",
default=None,
metavar="NAME",
help="Privacy-safe label shown for this CLI session in the dashboard.",
)
parser.add_argument(
"--workspace",
metavar="WORKSPACE",
default=None,
help=(
"Workspace that receives the token, by ID or by exact name. "
"Without this option, an interactive picker opens when you have "
"more than one workspace."
),
)
previous_record = read_record()
try:
args = parser.parse_args(argv)
except SystemExit as exc: # argparse already printed the message
return exc.code if isinstance(exc.code, int) else 2
console.print()
host = urlparse(_app_url()).netloc or _app_url()
console.print(f"[bold]Signing in to the Strix platform[/] [dim]({_terminal_markup(host)})[/]")
console.print(
"[dim]This creates your account and workspace when needed, and stores an API token.[/]"
)
console.print()
try:
record = _run_device_flow(
console,
open_browser=not args.no_browser,
scopes=args.scopes,
scope_profile=args.scope_profile,
workspace=args.workspace,
device_name=args.device_name,
)
except PlatformAuthError as exc:
console.print(f"[red]Sign-in failed:[/] {_terminal_markup(exc)}")
return 1
except KeyboardInterrupt:
console.print("\n[yellow]Sign-in cancelled.[/]")
return 130
try:
save_record(record)
except OSError as exc:
console.print(
f"[red]Sign-in succeeded, but the token could not be stored:[/] {_terminal_markup(exc)}"
)
console.print(
f"[dim]Check that {_terminal_markup(AUTH_PATH.parent)} is writable, "
"then run `strix cloud login` again.[/]"
)
return 1
_revoke_replaced_legacy_session(previous_record, record)
_print_success(console, record)
return 0
def _run_device_flow( # noqa: PLR0912, PLR0915
console: Console,
*,
open_browser: bool,
scopes: list[str] | None = None,
scope_profile: str | None = None,
workspace: str | None = None,
device_name: str | None = None,
) -> dict[str, Any]:
app_url = _app_url()
interactive = workspace is not None or (
sys.stdin.isatty() and scopes is None and scope_profile is None
)
try:
identity = read_or_create_identity(device_name=device_name)
except (OSError, ValueError) as exc:
raise PlatformAuthError(f"could not prepare the CLI device identity: {exc}") from exc
try:
response = requests.post(
f"{app_url}/api/v1/cli/login",
timeout=_HTTP_TIMEOUT_S,
allow_redirects=False,
)
except requests.RequestException as exc:
raise PlatformAuthError(f"could not reach {app_url}: {exc}") from exc
if not 200 <= response.status_code < 300:
raise PlatformAuthError(_error_detail(response))
authorization = _json_object(response)
user_code = str(authorization.get("user_code") or "")
verification_uri = str(
authorization.get("verification_uri_complete")
or authorization.get("verification_uri")
or ""
)
device_code = str(authorization.get("device_code") or "")
expires_in = _as_positive_int(
authorization.get("expires_in"), default=300, maximum=_MAX_EXPIRES_IN_S
)
interval = _as_positive_int(
authorization.get("interval"),
default=_DEFAULT_POLL_INTERVAL_S,
maximum=_MAX_POLL_INTERVAL_S,
)
if not device_code or not verification_uri:
raise PlatformAuthError("the server returned an incomplete device authorization")
if not is_safe_web_url(verification_uri, trusted_origin=app_url):
raise PlatformAuthError("the server returned an invalid verification URL")
console.print(
Panel.fit(
Text.assemble(
("Confirmation code: ", "dim"),
(sanitize_terminal_text(user_code), "bold cyan"),
),
title="Verify this device",
)
)
console.print("Open this URL in your browser and confirm the code:")
console.print(sanitize_terminal_text(verification_uri), markup=False, soft_wrap=True)
if open_browser:
with contextlib.suppress(Exception):
webbrowser.open(verification_uri)
console.print("[dim]Waiting for browser confirmation…[/]")
poll_body: dict[str, Any] = {"device_code": device_code, **identity}
if interactive:
poll_body["interactive"] = True
elif scopes:
poll_body["scopes"] = scopes
elif scope_profile:
poll_body["scope_profile"] = scope_profile
deadline = time.monotonic() + expires_in
while time.monotonic() < deadline:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
time.sleep(min(interval, remaining))
try:
poll = requests.post(
f"{app_url}/api/v1/cli/login/poll",
json=poll_body,
timeout=_HTTP_TIMEOUT_S,
allow_redirects=False,
)
except requests.RequestException:
continue
if 200 <= poll.status_code < 300:
return _finish_login(
console,
app_url,
poll,
scopes=scopes,
scope_profile=scope_profile,
workspace=workspace,
)
delta = _handle_poll_error(poll)
if delta is None:
break
interval = min(interval + delta, _MAX_POLL_INTERVAL_S)
raise PlatformAuthError("the sign-in request expired. Run `strix cloud login` again.")
def _handle_poll_error(poll: requests.Response) -> int | None:
"""Return the interval increase, or None when the device code expired."""
error = ""
with contextlib.suppress(ValueError, AttributeError):
error = str(poll.json().get("error", ""))
if error == "authorization_pending":
return 0
if error == "slow_down":
return 5
if error == "access_denied":
raise PlatformAuthError("the sign-in request was denied in the browser")
if error == "expired_token":
return None
raise PlatformAuthError(_error_detail(poll))
def _finish_login(
console: Console,
app_url: str,
poll: requests.Response,
*,
scopes: list[str] | None,
scope_profile: str | None,
workspace: str | None,
) -> dict[str, Any]:
result = _json_object(poll)
if result.get("selection_required"):
return _complete_selection(
console,
app_url,
result,
scopes=scopes,
scope_profile=scope_profile,
workspace=workspace,
)
return _bind_login_record(_require_api_token(result), app_url)
def _signed_in_record(
response: requests.Response,
*,
app_url: str,
) -> dict[str, Any]:
return _bind_login_record(
_require_api_token(_json_object(response)),
app_url,
)
def _require_api_token(record: dict[str, Any]) -> dict[str, Any]:
api_token = record.get("api_token")
if not isinstance(api_token, str) or not api_token.strip():
raise PlatformAuthError("the server returned a sign-in response without an API token")
return record
def _bind_login_record(record: dict[str, Any], app_url: str) -> dict[str, Any]:
"""Bind a stored credential to its issuer and preserve its scope preference."""
parsed = urlsplit(app_url)
if (
parsed.scheme not in {"http", "https"}
or not parsed.netloc
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
or "\\" in app_url
or any(character.isspace() for character in app_url)
or "%" in parsed.netloc
):
raise PlatformAuthError("the configured platform URL is invalid")
bound = dict(record)
bound["app_url"] = urlunsplit(
(parsed.scheme.lower(), parsed.netloc.lower(), parsed.path.rstrip("/"), "", "")
)
preference: Any = record.get("requested_scopes", record.get("scopes"))
preference_items = cast("list[Any]", preference)
if isinstance(preference, list) and all(isinstance(scope, str) for scope in preference_items):
bound["requested_scopes"] = list(dict.fromkeys(cast("list[str]", preference_items)))
return bound
def _complete_selection(
console: Console,
app_url: str,
selection: dict[str, Any],
*,
scopes: list[str] | None,
scope_profile: str | None,
workspace: str | None,
) -> dict[str, Any]:
organizations = _dict_items(selection.get("organizations"))
catalog = _dict_items(selection.get("scopes"))
selection_token = str(selection.get("selection_token") or "")
if not selection_token or not organizations:
raise PlatformAuthError("the server returned an incomplete selection response")
chosen_org = _choose_workspace(console, organizations, workspace)
role = str(chosen_org.get("role") or "admin")
chosen_scopes = scopes
chosen_profile = scope_profile
if chosen_scopes is None and chosen_profile is None and sys.stdin.isatty():
chosen_profile, chosen_scopes = _choose_scopes(console, catalog, role)
body: dict[str, Any] = {
"selection_token": selection_token,
"organization_id": chosen_org.get("id"),
}
if chosen_scopes is not None:
body["scopes"] = chosen_scopes
body["scope_profile"] = "custom"
elif chosen_profile is not None:
body["scope_profile"] = chosen_profile
try:
response = requests.post(
f"{app_url}/api/v1/cli/login/complete",
json=body,
timeout=_HTTP_TIMEOUT_S,
allow_redirects=False,
)
except requests.RequestException as exc:
raise PlatformAuthError(f"could not reach {app_url}: {exc}") from exc
if not 200 <= response.status_code < 300:
raise PlatformAuthError(_error_detail(response))
return _signed_in_record(
response,
app_url=app_url,
)
def _dict_items(value: Any) -> list[dict[str, Any]]:
if not isinstance(value, list):
return []
items = cast("list[Any]", cast("Any", value))
return [cast("dict[str, Any]", cast("Any", item)) for item in items if isinstance(item, dict)]
def _choose_workspace(
console: Console, organizations: list[dict[str, Any]], workspace: str | None
) -> dict[str, Any]:
if workspace is not None:
wanted = workspace.strip().casefold()
by_id = [org for org in organizations if str(org.get("id", "")).casefold() == wanted]
if by_id:
return by_id[0]
by_name = [
org for org in organizations if str(org.get("name", "")).strip().casefold() == wanted
]
if len(by_name) == 1:
return by_name[0]
if len(by_name) > 1:
matching_ids = ", ".join(str(org.get("id", "")) for org in by_name)
raise PlatformAuthError(
f"multiple workspaces are named {workspace!r}; use an exact workspace ID: "
f"{matching_ids}"
)
names = ", ".join(str(org.get("name", "")) for org in organizations)
raise PlatformAuthError(f"no workspace matches {workspace!r}. Your workspaces: {names}")
if len(organizations) == 1:
return organizations[0]
if not sys.stdin.isatty():
choices = ", ".join(f"{org.get('name', '')} ({org.get('id', '')})" for org in organizations)
raise PlatformAuthError(
"more than one workspace is available; rerun with --workspace NAME_OR_ID. "
f"Available workspaces: {choices}"
)
console.print()
console.print("[bold]Select a workspace for the API token:[/]")
for index, org in enumerate(organizations, start=1):
name = _terminal_markup(org.get("name", ""))
org_role = _terminal_markup(org.get("role", ""))
console.print(f" [cyan]{index}[/]. {name} [dim]({org_role})[/]")
while True:
answer = console.input(f"Workspace [1-{len(organizations)}] (1): ").strip() or "1"
if answer.isdigit() and 1 <= int(answer) <= len(organizations):
return organizations[int(answer) - 1]
console.print("[yellow]Enter a number from the list.[/]")
def _choose_scopes(
console: Console, catalog: list[dict[str, Any]], role: str
) -> tuple[str, list[str] | None]:
"""Prompt for a named scope profile or a custom scope list."""
rank = _ROLE_RANK.get(role, 2)
allowed = [
item for item in catalog if _ROLE_RANK.get(str(item.get("min_role", "viewer")), 0) <= rank
]
if not allowed:
return "recommended", None
console.print()
console.print("[bold]Select token scopes:[/]")
console.print(
" [cyan]1[/]. Recommended [dim](scans, findings, schedules, assets, uploads, "
"workspace switching, billing/top-ups; no token creation)[/]"
)
console.print(" [cyan]2[/]. Full access [dim](every scope your role allows)[/]")
console.print(" [cyan]3[/]. Minimal [dim](scan read/write and billing read)[/]")
console.print(" [cyan]4[/]. Custom [dim](pick individual scopes)[/]")
while True:
answer = console.input("Scopes [1-4] (1): ").strip() or "1"
if answer == "1":
return "recommended", None
if answer == "2":
return "full", None
if answer == "3":
return "minimal", None
if answer == "4":
return "custom", _choose_custom_scopes(console, allowed)
console.print("[yellow]Enter a number from 1 to 4.[/]")
def _choose_custom_scopes(console: Console, allowed: list[dict[str, Any]]) -> list[str]:
selected = {
str(item["scope"])
for item in allowed
if item.get("scope") and (item.get("default") or item.get("minimum"))
}
while True:
console.print()
for index, item in enumerate(allowed, start=1):
scope = str(item.get("scope", ""))
mark = "[green]x[/]" if scope in selected else " "
required = " [dim](always included)[/]" if item.get("minimum") else ""
rendered_scope = _terminal_markup(scope)
description = _terminal_markup(item.get("description", ""))
console.print(
f" [{mark}] [cyan]{index:>2}[/]. {rendered_scope}{required}"
f"\n [dim]{description}[/]"
)
answer = console.input(
"Toggle scopes by number (comma separated), or press Enter to confirm: "
).strip()
if not answer:
return sorted(selected)
for part in answer.replace(",", " ").split():
if not part.isdigit() or not 1 <= int(part) <= len(allowed):
console.print(
f"[yellow]Ignored {_terminal_markup(part)!r}: not a number from the list.[/]"
)
continue
item = allowed[int(part) - 1]
scope = str(item.get("scope", ""))
if item.get("minimum"):
console.print(f"[yellow]{_terminal_markup(scope)} is always included.[/]")
continue
if scope in selected:
selected.discard(scope)
else:
selected.add(scope)
def _json_object(response: requests.Response) -> dict[str, Any]:
try:
data = response.json()
except ValueError as exc:
raise PlatformAuthError("the server returned a response that is not JSON") from exc
if not isinstance(data, dict):
raise PlatformAuthError("the server returned an unexpected response shape")
return cast("dict[str, Any]", data)
def _as_positive_int(value: Any, *, default: int, maximum: int) -> int:
try:
parsed = int(value)
except (TypeError, ValueError, OverflowError):
return default
if parsed <= 0:
return default
return min(parsed, maximum)
def _error_detail(response: requests.Response) -> str:
with contextlib.suppress(ValueError, AttributeError):
detail = response.json().get("detail")
if detail:
return str(detail)
return f"HTTP {response.status_code}"
def _session_headers(record: dict[str, Any]) -> dict[str, str]:
headers = {"Authorization": f"Bearer {record['api_token']}"}
workspace_id = record.get("organization_id")
if isinstance(workspace_id, str) and workspace_id:
headers["X-Strix-Workspace"] = workspace_id
return headers
def _revoke_stored_session(record: dict[str, Any]) -> tuple[bool, str | None]:
"""Revoke one server session; return (definitively_inactive, error)."""
app_url = record.get("app_url")
if not isinstance(app_url, str) or not app_url:
return False, (
"the stored sign-in has no trusted platform URL; use --local-only to remove it"
)
try:
response = requests.delete(
f"{app_url.rstrip('/')}/api/v1/cli/session",
headers=_session_headers(record),
timeout=_HTTP_TIMEOUT_S,
allow_redirects=False,
)
except requests.RequestException as exc:
return False, f"could not revoke the remote CLI session: {exc}"
if response.status_code in {200, 204, 401}:
return True, None
return False, f"could not revoke the remote CLI session: {_error_detail(response)}"
def _print_logout_failure(console: Console, message: str, *, as_json: bool) -> int:
if as_json:
sys.stdout.write(json.dumps({"error": message, "removed": False}) + "\n")
else:
console.print(f"[red]Sign-out failed:[/] {_terminal_markup(message)}")
console.print("[dim]The local token was kept so you can safely retry.[/]")
return 1
def _revoke_replaced_legacy_session(
previous: dict[str, Any] | None, current: dict[str, Any]
) -> None:
"""Best-effort cleanup when the first device-aware login replaces a legacy token."""
if not previous or previous.get("api_token") == current.get("api_token"):
return
if previous.get("app_url") != current.get("app_url"):
return
with contextlib.suppress(KeyError, requests.RequestException):
requests.delete(
f"{previous['app_url']}/api/v1/cli/session",
headers=_session_headers(previous),
timeout=_HTTP_TIMEOUT_S,
allow_redirects=False,
)
def _print_success(console: Console, record: dict[str, Any]) -> None:
email = record.get("email", "")
organization = record.get("organization_name") or record.get("organization_id", "")
console.print()
console.print("[green]✓ Signed in to the Strix platform.[/]")
if email:
console.print(f" Account: [bold]{_terminal_markup(email)}[/]")
if organization:
console.print(f" Workspace: [bold]{_terminal_markup(organization)}[/]")
scopes = record.get("scopes")
if isinstance(scopes, list) and scopes:
console.print(f" Access: [dim]{_terminal_markup(_scope_summary(record))}[/]")
console.print(f" Token: stored in [dim]{_terminal_markup(AUTH_PATH)}[/]")
console.print()
console.print(
"[dim]The managed platform is ready. Run `strix cloud` to list the commands. "
"See https://docs.app.strix.ai for the API reference.[/]"
)
def _status(console: Console, argv: list[str]) -> int: # noqa: PLR0912
parser = _SessionArgumentParser(
prog="strix cloud whoami",
description="Show the stored managed-platform account, workspace, scopes, and expiry.",
)
parser.add_argument("--json", action="store_true", help="Print the session as JSON.")
parser.add_argument("--show-scopes", action="store_true", help="Print every granted scope.")
as_json = "--json" in argv or not sys.stdout.isatty()
try:
args = parser.parse_args(argv)
except _SessionUsageError as exc:
if as_json:
sys.stdout.write(json.dumps({"error": str(exc)}) + "\n")
else:
console.print(f"[red]Error:[/] {_terminal_markup(exc)}")
return 2
except SystemExit as exc:
return exc.code if isinstance(exc.code, int) else 2
as_json = bool(args.json) or not sys.stdout.isatty()
record = read_record()
if record is None:
if as_json:
sys.stdout.write(json.dumps({"signed_in": False, "error": "Not signed in"}) + "\n")
return 1
console.print("[yellow]Not signed in.[/] Run [bold]strix cloud login[/] to sign in.")
return 1
email = record.get("email", "unknown")
organization = record.get("organization_name") or record.get("organization_id", "")
expires_at = record.get("expires_at", "")
if as_json:
payload = {
"signed_in": True,
"email": email,
"organization_id": record.get("organization_id"),
"organization_name": record.get("organization_name"),
"scopes": record.get("scopes", []),
"expires_at": expires_at or None,
**({"app_url": record["app_url"]} if record.get("app_url") else {}),
}
sys.stdout.write(json.dumps(payload, indent=2, default=str) + "\n")
return 0
console.print(f"[green]Signed in[/] as [bold]{_terminal_markup(email)}[/]")
if organization:
console.print(f" Workspace: {_terminal_markup(organization)}")
if expires_at:
console.print(f" Token expires: {_terminal_markup(expires_at)}")
if record.get("app_url"):
console.print(f" Platform: {_terminal_markup(record['app_url'])}")
scopes = record.get("scopes")
if isinstance(scopes, list) and scopes:
scope_items = cast("list[Any]", cast("Any", scopes))
if args.show_scopes:
console.print(
f" Scopes: {_terminal_markup(' '.join(str(scope) for scope in scope_items))}"
)
else:
console.print(f" Access: {_terminal_markup(_scope_summary(record))}")
return 0
def _scope_summary(record: dict[str, Any]) -> str:
scopes = record.get("scopes")
scope_items = cast("list[Any]", cast("Any", scopes)) if isinstance(scopes, list) else []
count = len(scope_items)
profile = str(record.get("scope_profile") or "custom").replace("_", " ").title()
return f"{profile} · {count} scope{'s' if count != 1 else ''} granted"
def _logout(console: Console, argv: list[str]) -> int: # noqa: PLR0911, PLR0912
parser = _SessionArgumentParser(
prog="strix cloud logout",
description="Revoke this CLI session and remove its token from this machine.",
)
parser.add_argument("--json", action="store_true", help="Print the result as JSON.")
parser.add_argument(
"--local-only",
action="store_true",
help="Remove only the local token, leaving the remote session active.",
)
as_json = "--json" in argv or not sys.stdout.isatty()
try:
args = parser.parse_args(argv)
except _SessionUsageError as exc:
if as_json:
sys.stdout.write(json.dumps({"error": str(exc)}) + "\n")
else:
console.print(f"[red]Error:[/] {_terminal_markup(exc)}")
return 2
except SystemExit as exc:
return exc.code if isinstance(exc.code, int) else 2
as_json = bool(args.json) or not sys.stdout.isatty()
if read_record() is None and not AUTH_PATH.exists():
if as_json:
sys.stdout.write(json.dumps({"signed_in": False, "removed": False}) + "\n")
return 0
console.print("[yellow]Not signed in.[/]")
return 0
record = read_record()
remotely_revoked = False
if record is not None and not args.local_only:
remotely_revoked, revoke_error = _revoke_stored_session(record)
if revoke_error:
return _print_logout_failure(console, revoke_error, as_json=as_json)
if not logout():
if as_json:
sys.stdout.write(
json.dumps(
{
"error": "Could not remove the stored API token",
"signed_in": True,
"removed": False,
}
)
+ "\n"
)
return 1
console.print(
f"[red]Could not remove the stored API token.[/] Delete "
f"{_terminal_markup(AUTH_PATH)} manually."
)
return 1
if as_json:
sys.stdout.write(
json.dumps(
{
"signed_in": False,
"removed": True,
"remotely_revoked": remotely_revoked,
"local_only": bool(args.local_only),
}
)
+ "\n"
)
return 0
if args.local_only:
console.print(
"[yellow]Local sign-out only.[/] The remote CLI session is still active; "
"revoke it from API Access if needed."
)
else:
console.print("[green]Signed out.[/] The CLI session was revoked and removed locally.")
return 0
+46
View File
@@ -0,0 +1,46 @@
"""Stable, privacy-safe identity for this Strix CLI installation."""
from __future__ import annotations
import json
import platform
from pathlib import Path
from typing import Any, cast
from uuid import uuid4
from strix.utils.secret_files import write_secret_text
IDENTITY_PATH = Path.home() / ".strix" / "cli-identity.json"
def _default_device_name(instance_id: str) -> str:
system = {"Darwin": "macOS", "Windows": "Windows", "Linux": "Linux"}.get(
platform.system(), "Computer"
)
return f"{system} CLI · {instance_id[:8]}"
def read_or_create_identity(*, device_name: str | None = None) -> dict[str, str]:
"""Return one installation ID, optionally updating its user-facing label."""
record: dict[str, Any] = {}
try:
raw = json.loads(IDENTITY_PATH.read_text(encoding="utf-8"))
if isinstance(raw, dict):
record = cast("dict[str, Any]", raw)
except (OSError, json.JSONDecodeError):
pass
instance_id = record.get("client_instance_id")
if not isinstance(instance_id, str) or len(instance_id) < 8:
instance_id = str(uuid4())
label = device_name.strip() if device_name is not None else record.get("device_name")
if not isinstance(label, str) or not label.strip():
label = _default_device_name(instance_id)
label = " ".join(label.split())
if not 1 <= len(label) <= 80:
raise ValueError("device name must be 1-80 printable characters")
identity = {"client_instance_id": instance_id, "device_name": label}
write_secret_text(IDENTITY_PATH, json.dumps(identity, indent=2))
return identity
+21
View File
@@ -0,0 +1,21 @@
"""Safe rendering of untrusted text in a terminal."""
from __future__ import annotations
import re
_TERMINAL_CONTROL = re.compile(r"[\x00-\x1f\x7f-\x9f]")
def has_terminal_control(value: object) -> bool:
"""Return whether text contains bytes that can alter terminal state/protocols."""
return _TERMINAL_CONTROL.search(str(value)) is not None
def sanitize_terminal_text(value: object) -> str:
"""Make C0/C1 control bytes visible so they cannot operate a terminal."""
return _TERMINAL_CONTROL.sub(
lambda match: f"\\x{ord(match.group()):02x}",
str(value),
)
+29 -16
View File
@@ -36,7 +36,8 @@ if TYPE_CHECKING:
_STOPPABLE_AGENT_STATUSES = frozenset({"running", "waiting", "budget_paused"})
ChangeCallback = Callable[[], None]
StartCallback = Callable[[bool], Awaitable[None]]
StartCallback = Callable[[], Awaitable[None]]
VerifyCallback = Callable[[], Awaitable[None]]
QuitCallback = Callable[[], Awaitable[None]]
@@ -51,6 +52,7 @@ 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:
@@ -99,7 +101,6 @@ 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
@@ -112,6 +113,7 @@ 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
@@ -190,7 +192,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,
@@ -328,12 +330,6 @@ 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)
@@ -344,27 +340,44 @@ 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(verify)
await self._begin_scan()
return {"started": True}
async def _begin_scan(self, verify: bool) -> None:
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:
if self._on_start is None:
raise RuntimeError("Scan start is unavailable")
self._start_in_progress = True
try:
await self._on_start(verify)
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
finally:
self._start_in_progress = False
self.setup_mode = False
@@ -384,7 +397,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(self._pending_verify)
await self._begin_scan()
return {"approved": approved}
async def _send_message(self, payload: dict[str, Any]) -> dict[str, Any]:
@@ -370,6 +370,17 @@ 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
+9 -12
View File
@@ -45,23 +45,20 @@ 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}))
}
// 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 {
// 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 {
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 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.
// 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.
return *m, tea.Sequence(commands...)
}
@@ -94,25 +94,6 @@ 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 {
@@ -160,10 +141,6 @@ 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)
@@ -273,9 +250,8 @@ func TestSetupPromptWithTargetLaunches(t *testing.T) {
t.Fatalf("missing %s in %v", want, 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)
if _, found := startPayloadFlag(t, envelopes, "mount_working_dir"); found {
t.Fatalf("a targeted prompt must not ask to mount the working directory: %v", types)
}
// The target and instruction must reach the backend before setup.start
// closes the setup guard.
+10 -6
View File
@@ -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(warn)
content += "\n\n" + splashModelWarning(m.snapshot.Model, warn)
}
panel := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(green).Padding(1, 6).Align(lipgloss.Center).Render(content)
// #splash_screen background is solid black.
@@ -419,12 +419,16 @@ func (m Model) splashView() string {
lipgloss.WithWhitespaceBackground(black))
}
// splashModelWarning ports SplashScreen._build_model_warning_text.
func splashModelWarning(model string) string {
// 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 {
yellow := lipgloss.Color("#eab308")
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")
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)
}
// chatPaneKey identifies everything the bordered trace depends on.
@@ -194,3 +194,28 @@ func TestVulnerabilityReportRendersCalibrationFields(t *testing.T) {
"Fix Verification", "bypass review reasoned only",
)
}
func TestVulnerabilityReportUpdateRendersReportAndReason(t *testing.T) {
out := ansi.Strip(Tool(tool("update_vulnerability_report",
map[string]any{
"report_id": "vuln-0009",
"update_reason": "built a working unauthenticated file write against the endpoint",
"poc_script_code": "curl -X PATCH https://target/files/uuid",
},
map[string]any{
"success": true,
"action": "updated",
"report_id": "vuln-0009",
"severity": "critical",
"cvss_score": 9.3,
"updated_fields": []any{"poc_script_code"},
},
"completed")))
requireContains(t, out,
"Vulnerability Report Updated",
"vuln-0009",
"built a working unauthenticated file write",
"CRITICAL",
"9.3",
)
}
@@ -84,6 +84,10 @@ func Tool(data map[string]any) string {
return renderViewImage(args, result)
case "create_vulnerability_report":
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":
+46 -3
View File
@@ -12,15 +12,57 @@ import (
// ---------------------------------------------------------------------------
func renderVulnerabilityReport(args map[string]any, result any) string {
return renderReport(args, result, "Vulnerability Report", "Creating report...")
}
// A revision names the report it changes and carries only the fields it
// replaces, so it renders the same sections with the ones it left alone absent.
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"))
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
b.WriteString("🐞 " + Bold(ReportHdr).Render(heading))
field := func(label, value string) {
if value != "" {
b.WriteString("\n\n" + Bold(Field).Render(label+": ") + value)
}
}
reportID := StringValue(args["report_id"])
field("Report", reportID)
title := StringValue(args["title"])
field("Title", title)
@@ -59,6 +101,7 @@ func renderVulnerabilityReport(args map[string]any, result any) string {
}
}
section("Reason", StringValue(args["update_reason"]))
section("Description", StringValue(args["description"]))
section("Impact", StringValue(args["impact"]))
section("Technical Analysis", StringValue(args["technical_analysis"]))
@@ -76,8 +119,8 @@ func renderVulnerabilityReport(args map[string]any, result any) string {
// was verified belongs next to it rather than in the artifact alone.
section("Fix Verification", StringValue(args["fix_verification"]))
if title == "" {
b.WriteString("\n " + Dim().Render("Creating report..."))
if title == "" && reportID == "" {
b.WriteString("\n " + Dim().Render(pending))
}
return "\n\n" + b.String() + "\n\n"
}
+83 -16
View File
@@ -37,6 +37,7 @@ 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
@@ -48,6 +49,11 @@ 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."""
@@ -63,11 +69,14 @@ 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)
@@ -102,9 +111,61 @@ class GoTuiRuntime:
self.report_state.vulnerability_found_callback = lambda _report: (
self.controller.notify_changed()
)
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 start_from_setup(self, verify: bool = True) -> None:
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:
candidate = deepcopy(self.args)
candidate.scan_mode = self.controller.scan_mode
candidate.instruction = self.controller.instruction
@@ -121,16 +182,7 @@ class GoTuiRuntime:
if isinstance(target, dict) and target.get("original")
]
targets_changed = self.controller.targets != existing_targets
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
persist_current()
# 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
@@ -140,7 +192,11 @@ class GoTuiRuntime:
candidate.target = list(self.controller.targets)
candidate.target_list = []
build_targets_info(candidate)
prepare_run(candidate)
try:
prepare_run(candidate)
except Exception as exc:
report_error("scan_preparation_failed", exc)
raise
telemetry_start(candidate)
vars(self.args).update(vars(candidate))
@@ -154,13 +210,21 @@ 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"
@@ -199,6 +263,9 @@ 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"
@@ -280,7 +347,9 @@ class GoTuiRuntime:
if self.report_state is not None:
usage = dict(self.report_state.get_total_llm_usage())
vulnerabilities = [
report.get("id", index) if isinstance(report, dict) else index
(report.get("id", index), _revision_count(report))
if isinstance(report, dict)
else index
for index, report in enumerate(self.report_state.vulnerability_reports)
]
return json.dumps(
@@ -373,9 +442,7 @@ class GoTuiRuntime:
)
process, backend_socket = await launch_tui_process(command, env, cwd)
await self.server.start(backend_socket)
if not self.controller.setup_mode:
self.controller.begin_preparation()
prepare_task = asyncio.create_task(self.prepare_and_start())
prepare_task = self._start_preparation()
sync_task = asyncio.create_task(self.sync_state())
return_code = await wait_process(process)
check_return_code(return_code)
+85
View File
@@ -0,0 +1,85 @@
"""Validation for URLs printed or opened on behalf of a remote service."""
from __future__ import annotations
import ipaddress
from urllib.parse import SplitResult, urlsplit
from strix.interface.terminal_text import has_terminal_control
def is_safe_web_url(
value: object,
*,
trusted_origin: str | None = None,
require_trusted_origin: bool = False,
) -> bool:
"""Accept a strict HTTP(S) URL, optionally only on a pre-trusted origin."""
parsed = _parse(value)
if parsed is None:
return False
trusted = _parse(trusted_origin) if trusted_origin is not None else None
same_origin = trusted is not None and _origin(parsed) == _origin(trusted)
if require_trusted_origin:
return same_origin
if same_origin:
return True
return _is_safe_external_https(parsed)
def _is_safe_external_https(parsed: SplitResult) -> bool:
"""Reject local, numeric-looking, or otherwise ambiguous external hosts."""
hostname = (parsed.hostname or "").lower().rstrip(".")
if (
parsed.scheme != "https"
or hostname == "localhost"
or hostname.endswith((".localhost", ".local"))
):
return False
try:
return ipaddress.ip_address(hostname).is_global
except ValueError:
pass
labels = hostname.split(".")
return len(labels) >= 2 and not all(_looks_numeric(label) for label in labels)
def _parse(value: object) -> SplitResult | None:
if not isinstance(value, str) or not value or has_terminal_control(value):
return None
if "\\" in value or any(character.isspace() for character in value):
return None
try:
parsed = urlsplit(value)
port = parsed.port
except ValueError:
return None
hostname = parsed.hostname
if (
parsed.scheme not in {"http", "https"}
or not hostname
or parsed.username is not None
or parsed.password is not None
or parsed.fragment
or "%" in parsed.netloc
):
return None
try:
hostname.encode("ascii")
except UnicodeEncodeError:
return None
return parsed if port is None or 1 <= port <= 65535 else None
def _origin(parsed: SplitResult) -> tuple[str, str, int]:
default_port = 443 if parsed.scheme == "https" else 80
return parsed.scheme, (parsed.hostname or "").lower().rstrip("."), parsed.port or default_port
def _looks_numeric(label: str) -> bool:
lowered = label.lower()
if lowered.startswith("0x"):
return len(lowered) > 2 and all(
character in "0123456789abcdef" for character in lowered[2:]
)
return bool(lowered) and all(character.isdigit() for character in lowered)
+7 -2
View File
@@ -19,6 +19,7 @@ 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
@@ -208,7 +209,9 @@ 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"):
text.append("\n Fix:")
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:")
if loc.get("fix_before"):
text.append("\n - ", style="dim")
text.append(loc["fix_before"], style="dim")
@@ -1322,6 +1325,7 @@ 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")),
}
)
@@ -1602,7 +1606,8 @@ def check_docker_connection() -> Any:
try:
return docker.from_env()
except DockerException:
except DockerException as exc:
report_error("docker_unavailable", exc)
console = Console()
error_text = Text()
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
+13 -1
View File
@@ -87,7 +87,7 @@ def run_view(argv: list[str]) -> None:
posthog.viewer_opened(source="cli", live=live)
state_label = "[#eab308]live[/]" if live else "[#22c55e]finished[/]"
state_label = _state_label(summary)
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,6 +107,18 @@ 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)
+38 -3
View File
@@ -8,6 +8,7 @@ import {
Radar,
Rocket,
ArrowUpRight,
Building2,
History,
} from "lucide-react";
import type { Vulnerability, VulnerabilitySeverity } from "@/types/issues";
@@ -35,7 +36,7 @@ import {
type LoadedRun,
type RunsPayload,
} from "@/data/serverSource";
import { SIGNUP_URL, ctaUrl, trackCta } from "@/lib/cta";
import { SIGNUP_URL, DEMO_URL, ctaUrl, trackCta } from "@/lib/cta";
import { runTitle } from "@/lib/target-utils";
import Sidebar from "@/components/Sidebar";
import PastRunsView from "@/components/PastRunsView";
@@ -706,6 +707,31 @@ 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) => (
@@ -789,14 +815,23 @@ 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]">Re-run this pentest on managed infra in the cloud.</p>
<p className="mt-0.5 text-xs text-[#666]">Run this pentest again in Strix Cloud.</p>
<div className="mt-3 flex flex-wrap gap-2.5">
<ProInlineCta
label="Re-run in Strix Pro with more depth"
desc="Run this pentest on managed infra with more depth."
desc="More depth, validated findings, and autofix."
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,23 +48,40 @@ 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(SIGNUP_URL, slug)}
href={ctaUrl(href, slug)}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackCta(slug, surface)}
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"
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"
}
>
<Icon className="h-4 w-4 text-[#888] transition-colors group-hover:text-white" aria-hidden="true" />
<Icon
className={
primary
? "h-4 w-4 text-black"
: "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, ctaUrl, trackCta } from "@/lib/cta";
import { SIGNUP_URL, PRICING_URL, DEMO_URL, ctaUrl, trackCta } from "@/lib/cta";
/**
* Dialog shown when a platform feature is clicked in the sidebar: a short
@@ -141,6 +141,19 @@ 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>
@@ -0,0 +1,49 @@
"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,6 +12,7 @@ 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";
@@ -116,7 +117,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", "list_reports", "get_report"],
reporting: ["create_vulnerability_report", "update_vulnerability_report", "delete_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"],
@@ -151,6 +152,7 @@ const RENDERER_OVERRIDES: Partial<Record<string, ComponentType<ToolRendererProps
view_image: ViewImageRenderer,
list_reports: ReportListRenderer,
get_report: ReportListRenderer,
delete_vulnerability_report: VulnReportDeleteRenderer,
};
/**
+58 -25
View File
@@ -20,6 +20,7 @@ from datetime import datetime
from io import BytesIO
from typing import TYPE_CHECKING, Any
from markdown_it import MarkdownIt
from pypdf import PdfReader, PdfWriter
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
@@ -49,6 +50,8 @@ from strix.interface.viewer.transcript import (
if TYPE_CHECKING:
from pathlib import Path
from markdown_it.token import Token
# Palette lifted from the cloud report theme (styles/base.ts, docx/theme.ts).
_INK = colors.HexColor("#000000")
@@ -72,11 +75,21 @@ _SANS_BOLD = "Helvetica-Bold"
_MONO = "Courier"
_PAGE_W, _PAGE_H = A4
_INLINE_MD = MarkdownIt("commonmark", {"html": False, "linkify": False}).disable(
["autolink", "image", "link"]
)
_UNSAFE_TEXT_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\ud800-\udfff\ufffe\uffff]")
def _normalize_text(value: Any) -> str:
"""Normalize characters that ReportLab cannot safely serialize."""
text = str(value).replace("\r\n", "\n").replace("\r", "\n")
return _UNSAFE_TEXT_RE.sub("\ufffd", text)
def _esc(value: Any) -> str:
"""Escape a value for reportlab's Paragraph markup."""
return html.escape(str(value)).replace("\n", "<br/>")
return html.escape(_normalize_text(value)).replace("\n", "<br/>")
class _NumberedCanvas(pdfcanvas.Canvas): # type: ignore[misc] # reportlab base is untyped
@@ -253,7 +266,10 @@ def _duration(start: Any, end: Any) -> str:
end_dt = _parse_time(end)
if not start_dt or not end_dt:
return "n/a"
seconds = int((end_dt - start_dt).total_seconds())
try:
seconds = int((end_dt - start_dt).total_seconds())
except (OverflowError, TypeError):
return "n/a"
if seconds < 0:
return "n/a"
hours, remainder = divmod(seconds, 3600)
@@ -265,10 +281,18 @@ def _duration(start: Any, end: Any) -> str:
return f"{secs}s"
def _severity_badge(styles: dict[str, ParagraphStyle], severity: str) -> Table:
def _normalize_severity(value: Any) -> str:
severity = str(value or "").lower().strip()
if severity == "informational":
return "info"
return severity if severity in {*_SEVERITY_COLORS, "info"} else "low"
def _severity_badge(styles: dict[str, ParagraphStyle], severity: Any) -> Table:
"""A colored pill matching .severity-badge in the cloud report."""
severity = _normalize_severity(severity)
color = _SEVERITY_COLORS.get(severity, _MUTED)
cell = Paragraph(severity.upper(), styles["badge"])
cell = Paragraph(_esc(severity.upper()), styles["badge"])
table = Table([[cell]], colWidths=[len(severity) * 6.5 + 20])
table.setStyle(
TableStyle(
@@ -406,27 +430,36 @@ def _cover(
def _inline_md(text: str) -> str:
"""Convert inline markdown (bold, italic, `code`) to reportlab markup.
"""Render a safe subset of inline Markdown as ReportLab markup."""
tokens = _INLINE_MD.parseInline(_normalize_text(text))[0].children or []
return "".join(_inline_token_markup(token) for token in tokens)
Code spans are stashed as placeholders before bold/italic run, so bold that
wraps a code span (``**`x`**``) works and code contents are never mangled.
"""
codes: list[str] = []
def _stash(match: re.Match[str]) -> str:
codes.append(match.group(1))
return f"\x00{len(codes) - 1}\x00"
def _inline_token_markup(token: Token) -> str:
fixed_markup = {
"strong_open": "<b>",
"strong_close": "</b>",
"em_open": "<i>",
"em_close": "</i>",
"hardbreak": "<br/>",
"softbreak": " ",
}.get(token.type)
if fixed_markup is not None:
return fixed_markup
if token.type == "code_inline":
return f'<font face="{_MONO}" color="#b31d28">{html.escape(token.content)}</font>'
# Unsupported token content remains escaped so parser extensions cannot
# expose ReportLab tags.
return html.escape(token.content)
seg = html.escape(re.sub(r"`([^`]+)`", _stash, text))
seg = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", seg)
seg = re.sub(r"__(.+?)__", r"<b>\1</b>", seg)
seg = re.sub(r"\*(.+?)\*", r"<i>\1</i>", seg)
def _restore(match: re.Match[str]) -> str:
inner = html.escape(codes[int(match.group(1))])
return f'<font face="{_MONO}" color="#b31d28">{inner}</font>'
return re.sub(r"\x00(\d+)\x00", _restore, seg)
def _markdown_paragraph(text: str, style: ParagraphStyle) -> Paragraph:
"""Build a Markdown paragraph, falling back to escaped source text."""
source = _normalize_text(text)
try:
return Paragraph(_inline_md(source), style)
except ValueError:
return Paragraph(_esc(source), style)
def _strip_leading_heading(md: str) -> str:
@@ -447,12 +480,12 @@ def _markdown_flowables( # noqa: PLR0915 - cohesive block parser, splitting hur
def flush_para() -> None:
if para:
flow.append(Paragraph(_inline_md(" ".join(para)), styles["body"]))
flow.append(_markdown_paragraph(" ".join(para), styles["body"]))
para.clear()
def flush_bullets() -> None:
for marker, item in bullets:
flow.append(Paragraph(f"{marker}&nbsp;{_inline_md(item)}", styles["bullet"]))
flow.append(_markdown_paragraph(f"{marker}\u00a0{item}", styles["bullet"]))
bullets.clear()
lines = md.replace("\r\n", "\n").split("\n")
@@ -479,7 +512,7 @@ def _markdown_flowables( # noqa: PLR0915 - cohesive block parser, splitting hur
if heading:
flush_para()
flush_bullets()
flow.append(Paragraph(_inline_md(heading.group(2)), styles["md_heading"]))
flow.append(_markdown_paragraph(heading.group(2), styles["md_heading"]))
i += 1
continue
ordered = re.match(r"^(\d+)\.\s+(.*)$", stripped)
@@ -532,7 +565,7 @@ def _finding_flowables(
styles: dict[str, ParagraphStyle], index: int, vuln: dict[str, Any]
) -> list[Flowable]:
title = vuln.get("title") or "Untitled finding"
severity = str(vuln.get("severity") or "").lower().strip() or "low"
severity = _normalize_severity(vuln.get("severity"))
meta_bits = []
if vuln.get("cvss") is not None:
@@ -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
@@ -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 Ao(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var fh={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 Mo(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var hh={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 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={};/**
*/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={};/**
* @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 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
*/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
* @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 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={};/**
*/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={};/**
* @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 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}/**
*/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}/**
* @license React
* react-dom-client.production.js
*
@@ -38,427 +38,432 @@
*
* 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 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(`
*/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(`
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,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
`+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
* @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 L_=(...e)=>e.filter((t,r,a)=>!!t&&t.trim()!==""&&a.indexOf(t)===r).join(" ").trim();/**
*/const I_=(...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 Uk=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/**
*/const qk=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 Hk=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,a)=>a?a.toUpperCase():r.toLowerCase());/**
*/const Pk=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 ly=e=>{const t=Hk(e);return t.charAt(0).toUpperCase()+t.slice(1)};/**
*/const cy=e=>{const t=Pk(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 $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"};/**
*/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"};/**
* @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=>{for(const t in e)if(t.startsWith("aria-")||t==="role"||t==="title")return!0;return!1};/**
*/const Gk=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 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]]));/**
*/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]]));/**
* @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 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};/**
*/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};/**
* @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 Fk=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],wp=Te("arrow-left",Fk);/**
*/const Yk=[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]],Ep=Ce("arrow-left",Yk);/**
* @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=[["path",{d:"M7 7h10v10",key:"1tivn9"}],["path",{d:"M7 17 17 7",key:"1vkiza"}]],z_=Te("arrow-up-right",Gk);/**
*/const Xk=[["path",{d:"M7 7h10v10",key:"1tivn9"}],["path",{d:"M7 17 17 7",key:"1vkiza"}]],B_=Ce("arrow-up-right",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 Vk=[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]],Yk=Te("arrow-up",Vk);/**
*/const Kk=[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]],Zk=Ce("arrow-up",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 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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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 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);/**
*/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);/**
* @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:"M18 6 7 17l-5-5",key:"116fxf"}],["path",{d:"m22 10-7.5 7.5L13 16",key:"ke71qq"}]],nC=Te("check-check",tC);/**
*/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);/**
* @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:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Vs=Te("check",rC);/**
*/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);/**
* @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 iC=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],po=Te("chevron-down",iC);/**
*/const lC=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],Vs=Ce("check",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 aC=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],sC=Te("chevron-right",aC);/**
*/const oC=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],po=Ce("chevron-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 lC=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],U_=Te("chevron-up",lC);/**
*/const cC=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],uC=Ce("chevron-right",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:"m7 15 5 5 5-5",key:"1hf1tw"}],["path",{d:"m7 9 5-5 5 5",key:"sgt6xg"}]],cC=Te("chevrons-up-down",oC);/**
*/const dC=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],q_=Ce("chevron-up",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 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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"m9 12 2 2 4-4",key:"dzmm74"}]],Eu=Te("circle-check",fC);/**
*/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);/**
* @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=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]],mC=Te("circle-dot",hC);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9",key:"1dfufj"}]],bC=Te("circle-slash",xC);/**
*/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);/**
* @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"}]],$_=Te("circle",yC);/**
*/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);/**
* @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:"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);/**
*/const EC=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],F_=Ce("circle",EC);/**
* @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=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],P_=Te("clock",_C);/**
*/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);/**
* @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:"m16 18 6-6-6-6",key:"eg8j8"}],["path",{d:"m8 6-6 6 6 6",key:"ppft3o"}]],EC=Te("code",wC);/**
*/const SC=[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]],V_=Ce("clock",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 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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]],GC=Te("info",FC);/**
*/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);/**
* @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=[["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);/**
*/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);/**
* @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=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],Ps=Te("loader-circle",XC);/**
*/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);/**
* @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=[["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);/**
*/const WC=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]],Ps=Ce("loader-circle",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 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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Y_=Te("plus",iT);/**
*/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);/**
* @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:"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);/**
*/const oT=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Z_=Ce("plus",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 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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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 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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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 pT=[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]],gT=Te("search",pT);/**
*/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);/**
* @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 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);/**
*/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);/**
* @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 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);/**
*/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);/**
* @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"}]],_T=Te("shield",vT);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],K_=Te("terminal",ST);/**
*/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);/**
* @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:"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);/**
*/const TT=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],J_=Ce("terminal",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 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);/**
*/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);/**
* @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:"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);/**
*/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);/**
* @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.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);/**
*/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);/**
* @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 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);/**
*/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);/**
* @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:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Sp=Te("x",DT);/**
*/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);/**
* @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:"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
*/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
`;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 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:`
`;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:`
`}),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 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
`});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
`},{type:"element",tagName:"ol",properties:{},children:e.wrap(d,!0)},{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:`
`}]}}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:`
`});++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 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:`
`}),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:`
`,encode:["`"],...d.current()})),g()}return p+=d.move(`
`),o&&(p+=d.move(o+`
`)),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:`
`)),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:`
`,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}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
`,...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
`,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 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(`
`))}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(`
`))&&(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 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(`
`)&&(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(`
`).map(S=>{const w=S===""?`
`:f!=="text"?eD(S,f):S.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");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
`:f!=="text"?rD(S,f):S.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");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
* @license React
* use-sync-external-store-shim.production.js
*
@@ -466,7 +471,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 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}/**
*/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}/**
* @license React
* use-sync-external-store-shim/with-selector.production.js
*
@@ -474,34 +479,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 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
*/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
`).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)??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(`
`)),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(`
`):[],g=o?o.split(`
`):[],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(`
`):[],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(`
`);return r.length>t?a+`
...`: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
...`: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
`)){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 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,{})}));
`)}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
+2 -2
View File
@@ -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-Bpn8GiSb.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-qwPOPAGC.css">
<script type="module" crossorigin src="./assets/index-DbPDzbcJ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CrLE9rrV.css">
</head>
<body>
<div id="root"></div>
File diff suppressed because it is too large. Load diff
+18 -40
View File
@@ -1,20 +1,22 @@
"""Background pre-import of the heavy scan dependencies.
The scan engine's import graph (the agents SDK, OpenAI client, LiteLLM, the
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.
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.
"""
from __future__ import annotations
import importlib
import logging
import sys
import threading
@@ -24,45 +26,17 @@ 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:
@@ -72,11 +46,15 @@ def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.
runtime that has no local Docker) warm a narrower set.
"""
global _thread # noqa: PLW0603
with _lock:
if _thread is not None:
return _thread
if _thread is None:
_thread = threading.Thread(
target=_warm, args=(modules,), name="strix-import-warmup", daemon=True
)
_thread.start()
return _thread
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()
+33 -30
View File
@@ -28,9 +28,10 @@ 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 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.
* 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.
* 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.
@@ -56,6 +57,10 @@ 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__)
@@ -583,46 +588,44 @@ def _result_properties(
def _build_fixes(report: dict[str, Any]) -> list[dict[str, Any]] | None:
"""Build SARIF ``fixes`` from a finding's code-location fix pairs.
"""Build SARIF ``fixes`` from a verified prepared finding.
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.
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.
"""
raw_locations = report.get("code_locations")
if not isinstance(raw_locations, list):
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():
return None
artifact_changes: list[dict[str, Any]] = []
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)
for edit in candidate.draft_edits:
uri = _sarif_uri(edit.file)
if uri is None:
continue
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
deleted_region: dict[str, Any] = {
"startLine": edit.start_line,
"endLine": edit.end_line,
}
artifact_changes.append(
{
"artifactLocation": {"uri": uri},
"replacements": [
{
"deletedRegion": deleted_region,
"insertedContent": {"text": fix_after},
"insertedContent": {"text": edit.after},
}
],
}
+338 -42
View File
@@ -36,6 +36,23 @@ _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:
@@ -44,6 +61,59 @@ def _strix_version() -> str | None:
return None
# Content a revision may replace. The identity of the finding (id, timestamp,
# finding_class) and its original author stay put. dependency_metadata is
# replaced whole, so a caller carries the package identity over itself.
UPDATABLE_REPORT_FIELDS = frozenset(
{
"title",
"dependency_metadata",
"severity",
"description",
"impact",
"target",
"technical_analysis",
"poc_description",
"poc_script_code",
"remediation_steps",
"evidence",
"assumptions",
"counterevidence",
"confidence",
"confidence_rationale",
"severity_change_conditions",
"fix_effort",
"cvss",
"cvss_breakdown",
"endpoint",
"method",
"cve",
"cwe",
"code_locations",
"http_exchange_ids",
"fix_verification",
"fix_pr_body",
"fix_candidate",
"fix_preparation",
}
)
_LOWERCASE_REPORT_FIELDS = frozenset({"severity", "confidence", "fix_effort"})
# Fields that only describe another field. A revision may raise the rating or
# replace the locations without restating the reasoning behind the old one, and
# that leftover reasoning then contradicts the finding it annotates
# ("confidence: high" beside a rationale calling the evidence unconfirmed). When
# the field they describe changes and the update carries no replacement, they
# are dropped rather than kept.
_DEPENDENT_REPORT_FIELDS: dict[str, tuple[str, ...]] = {
"confidence": ("confidence_rationale",),
"severity": ("severity_change_conditions",),
"cvss": ("cvss_breakdown",),
"code_locations": ("fix_verification",),
}
def _clean_title(title: str) -> str:
"""Return a single-line finding title.
@@ -117,7 +187,7 @@ def get_global_report_state() -> Optional["ReportState"]:
return _global_report_state
def set_global_report_state(report_state: "ReportState") -> None:
def set_global_report_state(report_state: Optional["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.
@@ -169,6 +239,8 @@ 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
@@ -236,6 +308,12 @@ class ReportState:
)
self.vulnerability_reports = [r for r in data if isinstance(r, dict)]
for r in self.vulnerability_reports:
# A finding written before the class was persisted still carries the
# metadata of its class, so name the class it always had.
if not r.get("finding_class"):
r["finding_class"] = (
"dependency_cve" if r.get("dependency_metadata") else "dynamic"
)
title = r.get("title")
stale_md = False
if isinstance(title, str):
@@ -276,14 +354,17 @@ 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 = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
report_id = self._next_report_id()
report: dict[str, Any] = {
"id": report_id,
@@ -334,6 +415,8 @@ 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:
@@ -341,22 +424,224 @@ 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,
fields: dict[str, Any],
*,
update_reason: str | None = None,
updated_by_agent_id: str | None = None,
updated_by_agent_name: str | None = None,
) -> dict[str, Any] | None:
"""Apply a revision to an existing report, keeping its id.
A field that only describes a field this update replaces is dropped when
the update carries no replacement for it, so the revised report cannot
state a new rating beside the superseded reasoning for the old one.
Returns the revised report, or ``None`` when the id is unknown or when
nothing in ``fields`` changes it.
"""
report = next((r for r in self.vulnerability_reports if r.get("id") == report_id), None)
if report is None:
logger.warning("cannot update unknown vulnerability report %s", report_id)
return None
changed: dict[str, Any] = {}
for key, raw_value in fields.items():
if key not in UPDATABLE_REPORT_FIELDS or raw_value is None:
continue
value = raw_value
if isinstance(value, str):
value = _clean_title(value) if key == "title" else value.strip()
if key in _LOWERCASE_REPORT_FIELDS:
value = value.lower()
if not value:
continue
if report.get(key) == value:
continue
changed[key] = value
superseded = {
dependent
for primary, dependents in _DEPENDENT_REPORT_FIELDS.items()
if primary in changed
for dependent in dependents
if dependent not in changed and report.get(dependent) not in (None, "", [], {})
}
if not changed and not superseded:
logger.info("update for %s carried no new content; keeping it as is", report_id)
return None
entry: dict[str, Any] = {
"timestamp": datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"),
"fields": sorted(changed),
}
if superseded:
entry["dropped_fields"] = sorted(superseded)
if update_reason and update_reason.strip():
entry["reason"] = update_reason.strip()[:500]
if updated_by_agent_id:
entry["agent_id"] = updated_by_agent_id
if updated_by_agent_name:
entry["agent_name"] = updated_by_agent_name
for key in ("severity", "cvss", "confidence"):
if key in changed and report.get(key) is not None:
entry[f"previous_{key}"] = report[key]
raw_history = report.get("update_history")
history: list[dict[str, Any]] = (
[e for e in raw_history if isinstance(e, dict)] if isinstance(raw_history, list) else []
)
history.append(entry)
revised = {**report, **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)
# The markdown on disk still shows the superseded evidence, so let the
# writer re-render it.
self._saved_vuln_ids.discard(report_id)
logger.info(
"Updated vulnerability report %s (%s)",
report_id,
", ".join(entry["fields"]) or "no field replaced",
)
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)
@@ -540,47 +825,54 @@ class ReportState:
return None
def _save_artifacts(self) -> None:
"""Write scan artifacts under ``run_dir``."""
run_dir = self.get_run_dir()
"""Write scan artifacts under ``run_dir``; a write failure is logged."""
try:
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)
self._write_artifacts()
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).
@@ -629,6 +921,9 @@ 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()
@@ -649,6 +944,7 @@ 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:
+51 -2
View File
@@ -332,7 +332,10 @@ 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"):
lines.append("\n **Suggested Fix:**")
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("```diff")
if loc.get("fix_before"):
lines.extend(f"- {ln}" for ln in str(loc["fix_before"]).splitlines())
@@ -347,13 +350,59 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append("")
if report.get("fix_verification"):
lines.append("## Fix Verification\n")
lines.append("## Reported Candidate Checks\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"]))
lines.append("")
lines.extend(render_update_history(report.get("update_history")))
return "\n".join(lines)
def render_update_history(history: Any) -> list[str]:
"""Render the audit trail of every revision a report has received."""
if not isinstance(history, list):
return []
entries: list[dict[str, Any]] = [
cast("dict[str, Any]", e) for e in history if isinstance(e, dict)
]
if not entries:
return []
lines = ["## Update History\n"]
for entry in entries:
author = str(entry.get("agent_name") or entry.get("agent_id") or "an agent")
raw_fields = entry.get("fields")
fields: list[Any] = raw_fields if isinstance(raw_fields, list) else []
changed = ", ".join(str(field) for field in fields)
timestamp = str(entry.get("timestamp") or "unknown")
lines.append(f"**{timestamp}** — {author} updated: {changed}")
raw_dropped = entry.get("dropped_fields")
if isinstance(raw_dropped, list) and raw_dropped:
dropped = ", ".join(str(field) for field in raw_dropped)
lines.append(f" Dropped as superseded: {dropped}")
for key, label in (
("previous_severity", "severity"),
("previous_cvss", "CVSS"),
("previous_confidence", "confidence"),
):
if entry.get(key) is not None:
lines.append(f" Previous {label}: {entry[key]}")
if entry.get("reason"):
lines.append(f" Reason: {entry['reason']}")
lines.append("")
return lines
+104 -86
View File
@@ -3,23 +3,27 @@
from __future__ import annotations
import asyncio
import io
import logging
import os
import sys
import tarfile
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any
from agents.sandbox.entries import BaseEntry, File, LocalDir
from agents.sandbox.entries import BaseEntry, LocalDir
from agents.sandbox.manifest import Environment, Manifest
from strix.config import load_settings
from strix.core.paths import run_dir_for, runtime_state_dir
from strix.runtime.backends import backend_supports_bind_mounts, get_backend
from strix.runtime.caido_bootstrap import bootstrap_caido
from strix.runtime.caido_handle import CaidoBootstrapHandle
if TYPE_CHECKING:
from agents.sandbox.session import BaseSandboxSession
from strix.runtime.status import StatusSink
@@ -37,6 +41,13 @@ _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
@@ -59,8 +70,9 @@ 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}"
bind_mounts.append({"source": str(resolved), "target": target, "read_only": False})
if src.get("protect_metadata"):
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.extend(_metadata_mounts(resolved, target))
return bind_mounts
@@ -132,87 +144,82 @@ def _extra_file_content(extra_file: dict[str, Any]) -> bytes | None:
return None
def build_extra_file_entries(
def build_extra_file_archive(
extra_files: list[dict[str, Any]],
local_sources: list[dict[str, Any]] | None = None,
) -> dict[str | Path, BaseEntry]:
"""Map extra files to in-memory ``File`` manifest entries.
) -> bytes | None:
"""Pack extra files into one tar archive rooted at ``/workspace``.
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.
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.
"""
source_roots = _source_root_rels(local_sources)
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
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()
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.
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
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.
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.
"""
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,
}
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}"
)
return mounts
def _metadata_mounts(tree: Path, target: str) -> list[dict[str, Any]]:
@@ -262,10 +269,9 @@ 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 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.
"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.
"""
def report(phase: str) -> None:
@@ -283,16 +289,12 @@ async def create_or_reuse(
if backend_supports_bind_mounts(backend_name):
bind_mounts = build_bind_mounts(local_sources)
entries: dict[str | Path, BaseEntry] = {}
if extra_files:
staging_dir = runtime_state_dir(run_dir_for(scan_id)) / "extra_files"
bind_mounts.extend(
build_extra_file_bind_mounts(extra_files, staging_dir, local_sources)
)
else:
bind_mounts = []
entries = build_manifest_entries(local_sources)
if extra_files:
entries.update(build_extra_file_entries(extra_files, local_sources))
extra_file_archive = (
build_extra_file_archive(extra_files, local_sources) if extra_files else None
)
# Caido runs as an in-container sidecar; HTTP(S) traffic from any
# process started via ``session.exec`` (the SDK's Shell tool, etc.)
@@ -329,6 +331,14 @@ async def create_or_reuse(
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"
@@ -360,6 +370,14 @@ async def create_or_reuse(
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)
async def cleanup(scan_id: str) -> None:
"""Tear down ``scan_id``'s container and drop its cache entry.
+7 -6
View File
@@ -1,6 +1,5 @@
import logging
import re
import threading
from collections import Counter
from collections.abc import Iterator
from pathlib import Path
@@ -8,7 +7,6 @@ from typing import TypeGuard
import yaml
from strix.telemetry import posthog, scarf
from strix.utils.resource_paths import get_strix_resource_path
@@ -241,16 +239,19 @@ 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)
threading.Thread(target=_send, daemon=True).start()
def get_loaded_skill_names() -> list[str]:
"""Distinct skills loaded so far in this process (custom skills collapse to ``"custom"``)."""
return sorted(_LOADED_SKILLS)
def _candidate_skill_files(skill_name: str) -> list[Path]:
+2 -2
View File
@@ -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 and error types (not messages or stack traces)\
**Session Errors:** Duration, the failure category, the scan phase, and the exception class name (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 are loaded\
**Feature Usage:** Which built-in skills were used during a scan (reported once, at scan end)\
**Aggregate Metrics:** Vulnerability counts by severity and weakness category (CWE)
### What We **Never** Collect
+12
View File
@@ -1,7 +1,19 @@
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",
]
+41 -1
View File
@@ -5,7 +5,7 @@ import platform
import sys
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from typing import Any
from typing import Any, cast
from uuid import uuid4
@@ -54,3 +54,43 @@ 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
+19 -13
View File
@@ -4,10 +4,14 @@ 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,
)
@@ -35,7 +39,12 @@ def _send(event: str, properties: dict[str, Any]) -> bool:
"api_key": _POSTHOG_PUBLIC_API_KEY,
"event": event,
"distinct_id": SESSION_ID,
"properties": properties,
"properties": {
**properties,
"$lib": "strix-cli",
"$lib_version": get_version(),
"$process_person_profile": False,
},
}
with requests.post(f"{_POSTHOG_HOST}/capture/", json=payload, timeout=SEND_TIMEOUT):
pass
@@ -82,16 +91,6 @@ 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
@@ -130,6 +129,7 @@ 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,6 +180,12 @@ def viewer_agent_steered() -> None:
_send("viewer_agent_steered", {**base_props()})
def error(error_type: str) -> None:
props = {**base_props(), "error_type": error_type}
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))
_send("error", props)
+8 -12
View File
@@ -7,10 +7,13 @@ 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,
)
@@ -90,17 +93,6 @@ 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
@@ -140,14 +132,18 @@ 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) -> None:
def error(error_type: str, exc: BaseException | None = None) -> 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)
+93 -3
View File
@@ -15,6 +15,7 @@ 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
@@ -28,6 +29,40 @@ 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,
@@ -38,6 +73,7 @@ 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.
@@ -63,6 +99,12 @@ 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)
@@ -149,8 +191,11 @@ 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. Messages to any registered agent wake it, regardless of
flow. In interactive runs a message wakes the target 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.
@@ -195,10 +240,23 @@ 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": f"Target agent '{target_agent_id}' not found or message delivery failed",
"error": error,
"target_agent_id": target_agent_id,
"target_status": status,
"delivery_status": "not_delivered",
},
ensure_ascii=False,
default=str,
@@ -364,6 +422,31 @@ 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)
@@ -610,6 +693,9 @@ 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:
@@ -623,6 +709,7 @@ 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,
@@ -632,6 +719,7 @@ async def agent_finish(
"content": report,
"type": "completion",
"priority": "high",
"filed_report_ids": filed_report_ids,
},
)
parent_notified = True
@@ -642,10 +730,11 @@ async def agent_finish(
await notify_parent_on_terminal(coordinator, me, "completed")
logger.info(
"agent_finish: %s success=%s findings=%d parent_notified=%s",
"agent_finish: %s success=%s findings=%d filed_reports=%d parent_notified=%s",
me,
success,
len(findings or []),
len(filed_report_ids),
parent_notified,
)
@@ -656,6 +745,7 @@ 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),
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more