From cbb0f57058a97cc78d19c3cdd0171d1b41d498fd Mon Sep 17 00:00:00 2001 From: yoni-at-strix Date: Wed, 26 Aug 2026 16:31:08 -0700 Subject: [PATCH] Reach MCP tools on demand instead of registering every one (#1175) --- pyproject.toml | 2 + strix/agents/factory.py | 4 + strix/agents/prompts/system_prompt.jinja | 16 + strix/core/runner.py | 102 +- strix/interface/tui/internal/render/mcp.go | 12 + .../interface/tui/internal/render/registry.go | 5 + .../tui/internal/render/render_test.go | 25 +- strix/interface/tui/live_view.py | 49 +- strix/interface/tui/runtime.py | 14 - .../live/tool-renderers/McpRenderer.tsx | 22 +- .../components/live/tool-renderers/index.ts | 5 +- .../viewer/frontend/src/types/events.ts | 7 +- .../{index-C9c1WbvP.js => index-CYf9nnT3.js} | 2 +- strix/interface/viewer/static/index.html | 2 +- strix/tools/mcp/__init__.py | 39 +- strix/tools/mcp/agent_tools.py | 173 +++ strix/tools/mcp/client.py | 360 +++--- strix/tools/mcp/config.py | 6 +- strix/tools/mcp/loader.py | 13 +- strix/tools/mcp/naming.py | 59 +- strix/tools/mcp/registry.py | 216 ++++ tests/conftest.py | 26 + tests/test_mcp_client.py | 1083 +++++++++++------ tests/test_runner_mcp.py | 142 +++ tests/test_runner_root_prompt.py | 68 ++ 25 files changed, 1744 insertions(+), 708 deletions(-) rename strix/interface/viewer/static/assets/{index-C9c1WbvP.js => index-CYf9nnT3.js} (92%) create mode 100644 strix/tools/mcp/agent_tools.py create mode 100644 strix/tools/mcp/registry.py create mode 100644 tests/conftest.py create mode 100644 tests/test_runner_mcp.py diff --git a/pyproject.toml b/pyproject.toml index 72041a3b..151b9a18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -243,6 +243,8 @@ ignore = [ "tests/test_report_pdf.py" = ["S105", "S106"] # Fake MCP server matches the SDK's MCPServer signature; its args are unused. "tests/test_mcp_client.py" = ["S105", "S106", "ARG002"] +# MCP connection request in a test carries a dummy bearer token. +"tests/test_runner_root_prompt.py" = ["S106"] # 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"] diff --git a/strix/agents/factory.py b/strix/agents/factory.py index b6b15f9c..d284dfd3 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -29,6 +29,7 @@ from strix.tools.agents_graph.tools import ( from strix.tools.coverage.tools import list_coverage, record_coverage, update_coverage from strix.tools.finish.tool import finish_scan from strix.tools.load_skill.tool import load_skill +from strix.tools.mcp import call_mcp, describe_mcp, list_mcps from strix.tools.notes.tools import ( create_note, delete_note, @@ -587,6 +588,9 @@ _BASE_TOOLS: tuple[Tool, ...] = ( list_sitemap, view_sitemap_entry, scope_rules, + list_mcps, + describe_mcp, + call_mcp, view_agent_graph, send_message_to_agent, wait_for_agents, diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index 99898c2e..66eaa7b8 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -75,6 +75,22 @@ AUTHORIZED TARGETS: {% endfor %} {% endif %} +{% 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. +{% if system_prompt_context.mcp_connections %} +- Connected this run (call describe_mcp on one to see its tools): +{% for connection in system_prompt_context.mcp_connections %} + - {{ connection.name }} ({{ connection.tool_count }} tools){% if connection.purpose %}: {{ connection.purpose }}{% endif %} +{% endfor %} +{% endif %} +- Reach for a connection whenever the target itself cannot give you information a connection could: its database schema and access policies, real deployment or infrastructure configuration, known issues or prior findings, or server logs. In those cases call list_mcps early to see what is available, and prefer a connection's authoritative data over inferring from the target's responses. Do not wait to be told a connection exists. + 1. Call list_mcps() to discover the available connections. + 2. Call describe_mcp(connection="") to inspect one connection's tools, each with its name, description, and JSON input schema. + 3. Call call_mcp(connection="", tool="", arguments={...}) to run one, passing an arguments object that matches the schema (omit arguments for a tool that takes none). +- Do not assume a connection or tool exists; discover it with list_mcps and describe it with describe_mcp before calling. +{% endif %} + AUTHORIZATION STATUS: - You have FULL AUTHORIZATION for authorized security validation on in-scope targets to help secure the target systems/app - All permission checks have been COMPLETED and APPROVED - never question your authority diff --git a/strix/core/runner.py b/strix/core/runner.py index 28acebd6..98a28ca3 100644 --- a/strix/core/runner.py +++ b/strix/core/runner.py @@ -58,7 +58,7 @@ if TYPE_CHECKING: from agents.result import RunResultBase from strix.runtime.status import StatusSink - from strix.tools.mcp import ConnectedMcpServer + from strix.tools.mcp import ConnectedMcpServer, McpConnectionRequest logger = logging.getLogger(__name__) @@ -91,23 +91,6 @@ def _record_mcp_connections(connections: list[ConnectedMcpServer]) -> None: report_state.record_mcp_connections([connection.name for connection in connections]) -def _mcp_connection_notes(connections: list[ConnectedMcpServer]) -> str | None: - """A block describing the connections the user left notes on, for the agent. - - Only connections with notes are listed, so the note describes the connection - once rather than being repeated onto every tool. Returns ``None`` when no - connection has notes. - """ - noted = [(c.name, c.notes) for c in connections if c.notes] - if not noted: - return None - lines = "\n".join(f"- `{name}.*` tools: {notes}" for name, notes in noted) - return ( - "The user connected these MCP servers for this run and left notes on how " - f"to use each:\n{lines}" - ) - - def _merge_root_prompt_context( scope_context: dict[str, Any], extra_system_prompt_context: dict[str, Any] | None, @@ -173,6 +156,7 @@ async def run_strix_scan( root_instructions_override: str | None = None, extra_system_prompt_context: dict[str, Any] | None = None, status_sink: StatusSink | None = None, + mcp_connection_requests: list[McpConnectionRequest] | None = None, ) -> RunResultBase | None: """Run or resume one Strix scan against a sandbox. @@ -184,6 +168,11 @@ async def run_strix_scan( ``extra_system_prompt_context`` is merged into the root agent's scan context before prompt rendering. Child agents keep the standard scan prompt and context. + ``mcp_connection_requests`` supplies the run's MCP connections from any + source: when given, the engine connects those requests; when ``None`` (the + command-line default) it reads ``~/.strix/mcp-servers.json`` itself. Either + way the engine does the connecting, so the caller passes inert configs plus + metadata and never live sessions. """ def report(phase: str) -> None: @@ -346,6 +335,61 @@ async def run_strix_scan( coordinator.set_budget_extender(hooks.extend_budget) scope_context = build_scope_context(scan_config) + + # Attach the run's MCP connections and hold their live sessions in a + # per-run registry. The connections are source-agnostic: a caller + # (the SaaS/pro product) can supply them as mcp_connection_requests, and + # when it does not the command-line path reads them from + # ~/.strix/mcp-servers.json here. Either way one shared engine routine + # does the connecting and populating. Nothing is registered as an agent + # tool: every agent reaches these connections on demand through the + # list_mcps / describe_mcp / call_mcp tools, guided by brief static prompt + # guidance when any connection exists. Fail-open: a missing config, or a + # server that will not connect, must never break a run. + from strix.tools.mcp import ( + McpConnectionRequest, + McpRegistry, + attach_mcp_requests, + load_user_mcp_configs, + ) + + mcp_registry = McpRegistry() + try: + if mcp_connection_requests is None: + # Command-line default: read the user's file and wrap each config + # in a bare request (no provider or transform), so this path is + # exactly the old behavior. + mcp_requests = [ + McpConnectionRequest(config=config) for config in load_user_mcp_configs() + ] + else: + mcp_requests = mcp_connection_requests + if mcp_requests: + connections = await attach_mcp_requests(mcp_requests, mcp_registry) + mcp_servers = [c.server 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() + ] + except Exception: + logger.exception("Failed to connect user MCP servers; continuing without them") + root_context = _merge_root_prompt_context(scope_context, extra_system_prompt_context) root_instructions = _compose_root_instructions_override( root_instructions_override, @@ -357,27 +401,6 @@ async def run_strix_scan( system_prompt_context=root_context, ) - # Connect any MCP servers the user listed in ~/.strix/mcp-servers.json and - # register their tools before the agent is built. Fail-open: a missing - # config, or a server that will not connect, must never break a run. - from strix.tools.mcp import connect_mcp_servers, load_user_mcp_configs - - try: - user_mcp_configs = load_user_mcp_configs() - if user_mcp_configs: - connections = await connect_mcp_servers(user_mcp_configs) - mcp_servers = [c.server 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)) - notes_block = _mcp_connection_notes(connections) - if notes_block: - root_task = f"{root_task}\n\n{notes_block}" - except Exception: - logger.exception("Failed to connect user MCP servers; continuing without them") - root_agent = build_strix_agent( name="Root Agent", skills=skills, @@ -429,6 +452,7 @@ async def run_strix_scan( "coordinator": coordinator, "sandbox_session": bundle["session"], "caido_client": bundle["caido_client"], + "mcp_registry": mcp_registry, "agent_id": root_id, "parent_id": None, "interactive": interactive, diff --git a/strix/interface/tui/internal/render/mcp.go b/strix/interface/tui/internal/render/mcp.go index 24e0b42a..3d1f2c25 100644 --- a/strix/interface/tui/internal/render/mcp.go +++ b/strix/interface/tui/internal/render/mcp.go @@ -33,3 +33,15 @@ func renderMcpTool(connection, toolName string, args map[string]any, status stri b.WriteString(style.Render(icon)) return b.String() } + +// renderMcpInspect renders describe_mcp: a request to inspect one connection's +// catalog rather than a call to a tool on it. There is no underlying tool, so +// the connection is the whole subject and leads. Same icon and colors as a tool +// call so the two read as one family while scrolling a transcript. +func renderMcpInspect(connection, status string) string { + var b strings.Builder + b.WriteString(mcpIcon + Dim().Render("Inspecting MCP server ") + Bold(Mint).Render(connection) + "\n") + icon, style := statusIcon(status) + b.WriteString(style.Render(icon)) + return b.String() +} diff --git a/strix/interface/tui/internal/render/registry.go b/strix/interface/tui/internal/render/registry.go index 9e1ccdb3..ec58f781 100644 --- a/strix/interface/tui/internal/render/registry.go +++ b/strix/interface/tui/internal/render/registry.go @@ -57,6 +57,11 @@ func Tool(data map[string]any) string { // nothing here. The tag is only ever set from the connections the run made, // so it is the one thing that can tell such a call apart from a built-in. if connection := StringValue(data["mcp_connection"]); connection != "" { + // describe_mcp inspects a connection's catalog rather than calling a tool + // on it, so there is no underlying tool and the connection is the subject. + if name == "describe_mcp" { + return renderMcpInspect(connection, status) + } toolName := StringValue(data["mcp_tool"]) if toolName == "" { toolName = name diff --git a/strix/interface/tui/internal/render/render_test.go b/strix/interface/tui/internal/render/render_test.go index c326b0c0..2c2b5bcd 100644 --- a/strix/interface/tui/internal/render/render_test.go +++ b/strix/interface/tui/internal/render/render_test.go @@ -227,7 +227,9 @@ func TestGenericToolOmitsRawResult(t *testing.T) { } func TestMcpToolLeadsWithActionAndNamesTheServer(t *testing.T) { - data := tool("local_fs_read_file", map[string]any{"path": "/etc/hosts"}, "file body", "completed") + // call_mcp is the dispatch tool; the connection and the server's own tool + // name are tagged onto the event from its arguments. + data := tool("call_mcp", map[string]any{"path": "/etc/hosts"}, "file body", "completed") data["mcp_connection"] = "local_fs" data["mcp_tool"] = "read_file" @@ -244,11 +246,26 @@ func TestMcpToolLeadsWithActionAndNamesTheServer(t *testing.T) { } } -func TestMcpToolWithoutTaggedNameFallsBackToFullName(t *testing.T) { - data := tool("local_fs_read_file", nil, nil, "running") +func TestMcpToolWithoutTaggedToolFallsBackToDispatchName(t *testing.T) { + // A call_mcp whose underlying tool could not be read still renders as an MCP + // row, falling back to the dispatch tool name. + data := tool("call_mcp", nil, nil, "running") data["mcp_connection"] = "local_fs" - requireContains(t, ansi.Strip(Tool(data)), "local_fs_read_file", "In progress") + requireContains(t, ansi.Strip(Tool(data)), mcpIcon+"call_mcp", "local_fs", "In progress") +} + +func TestMcpDescribeInspectsConnection(t *testing.T) { + // describe_mcp inspects a connection; the connection is the subject and the + // dispatch tool name is not shown as if it were a server tool. + data := tool("describe_mcp", nil, nil, "completed") + data["mcp_connection"] = "local_fs" + + out := ansi.Strip(Tool(data)) + requireContains(t, out, mcpIcon, "Inspecting MCP server", "local_fs", "Done") + if strings.Contains(out, "describe_mcp") { + t.Fatalf("describe_mcp must read as inspecting the connection, not name the dispatch tool:\n%s", out) + } } func TestCollapseToolShellPreviewAndExpand(t *testing.T) { diff --git a/strix/interface/tui/live_view.py b/strix/interface/tui/live_view.py index a52c14be..7e8f2534 100644 --- a/strix/interface/tui/live_view.py +++ b/strix/interface/tui/live_view.py @@ -8,17 +8,13 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from collections.abc import Iterable from pathlib import Path from agents.tool import ToolOutputImage from strix.core.paths import runtime_state_dir from strix.interface.tui.history import load_session_history - -# Imported from the naming module rather than the mcp package so a projection -# never pulls in the MCP client and the agents SDK behind it. -from strix.tools.mcp.naming import resolve_mcp_tool +from strix.tools.mcp import resolve_mcp_call class TuiLiveView: @@ -31,27 +27,23 @@ class TuiLiveView: self._user_instruction: str | None = None self._user_instruction_at: str | None = None self._user_instruction_shown = False - self._mcp_connections: tuple[str, ...] = () - def set_mcp_connections(self, names: Iterable[str]) -> None: - """The MCP servers this run connected, so its tool calls can name theirs. - - A server's tools are offered to the model under a name built from the - connection name and the tool's own name. That name cannot be split back - apart on its own, so tool calls are matched against these names instead. - """ - self._mcp_connections = tuple(str(name) for name in names) - - def _mcp_tool_fields(self, tool_name: str) -> dict[str, str]: + def _mcp_tool_fields(self, tool_name: str, args: dict[str, Any]) -> dict[str, str]: """Event fields naming the MCP server a tool call went out to, if any. - Empty for every built-in tool, which is what tells an interface to render - the call as one of its own rather than as a call to a user's server. + Delegates to the shared engine resolver :func:`resolve_mcp_call` so a + dispatch call is attributed the same way here and in strix-pro's tracer. + The projection has no live registry, so it passes none: it reports the + connection and tool read from the call's arguments and leaves the provider + out. Empty for every other tool, which is what tells an interface to + render the call as one of its own rather than as a call to a user's + server. ``describe_mcp`` resolves with an empty tool, which tells both + renderers to present the row as inspecting the connection itself. """ - origin = resolve_mcp_tool(tool_name, self._mcp_connections) - if origin is None: + info = resolve_mcp_call(tool_name, args) + if info is None: return {} - return {"mcp_connection": origin.connection, "mcp_tool": origin.tool} + return {"mcp_connection": info.connection, "mcp_tool": info.tool} def set_user_instruction(self, text: str | None, *, timestamp: str | None = None) -> None: """Open the transcript with what the user asked for. @@ -98,8 +90,7 @@ class TuiLiveView: def hydrate_from_run_dir(self, run_dir: Path) -> None: # Armed before the agents are added so the root agent's arrival puts the - # user's opening message ahead of the replayed history, and before the - # history is replayed so its MCP tool calls are attributed too. + # user's opening message ahead of the replayed history. self._load_run_record(run_dir) state_dir = runtime_state_dir(run_dir) agents_path = state_dir / "agents.json" @@ -128,16 +119,13 @@ class TuiLiveView: self._hydrate_sdk_session_history(run_dir, statuses.keys()) def _load_run_record(self, run_dir: Path) -> None: - """Take the user's opening message and the run's MCP servers off the record.""" + """Take the user's opening message off the record.""" try: record = json.loads((run_dir / "run.json").read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return if not isinstance(record, dict): return - connections = record.get("mcp_connections") - if isinstance(connections, list): - self.set_mcp_connections(name for name in connections if isinstance(name, str)) instruction = record.get("user_instruction") if not isinstance(instruction, str): return @@ -348,7 +336,7 @@ class TuiLiveView: "status": "running", "agent_id": agent_id, "call_id": call_id, - **self._mcp_tool_fields(call["tool_name"]), + **self._mcp_tool_fields(call["tool_name"], call["args"]), } if existing is None: event = self._append_event(agent_id, "tool", tool_data, timestamp=timestamp) @@ -371,6 +359,10 @@ class TuiLiveView: event_key = (agent_id, call_id) event = self._tool_event_by_agent_and_call_id.get(event_key) if event is None: + # No prior call event to update, so its arguments are gone and the + # connection an MCP call went out to cannot be recovered. The matching + # call event, when there is one, already carries the MCP fields; this + # arrives only when the call was never projected, so it stays generic. event = self._append_event( agent_id, "tool", @@ -380,7 +372,6 @@ class TuiLiveView: "status": "completed", "agent_id": agent_id, "call_id": call_id, - **self._mcp_tool_fields(output["tool_name"]), }, timestamp=timestamp, ) diff --git a/strix/interface/tui/runtime.py b/strix/interface/tui/runtime.py index 6a49f72a..7e716628 100644 --- a/strix/interface/tui/runtime.py +++ b/strix/interface/tui/runtime.py @@ -207,23 +207,9 @@ class GoTuiRuntime: self.controller.notify_changed() def capture_event(self, agent_id: str, event: Any) -> None: - self._refresh_mcp_connections() self.live_view.ingest_sdk_event(agent_id, event) self.controller.notify_changed() - def _refresh_mcp_connections(self) -> None: - """Hand the projection the MCP servers the scan connected. - - The scan records them as it connects, which is before the agent can call - anything, and the projection needs them to say which server a tool call - went out to. Read on the way in rather than pushed, so no tool call can - be projected before they arrive. - """ - if self.report_state is None: - return - connections = self.report_state.run_record.get("mcp_connections") or [] - self.live_view.set_mcp_connections(connections) - async def _sync_agent_state(self) -> bool: parent_of, statuses, names, errors = await self.coordinator.graph_snapshot() changed = False diff --git a/strix/interface/viewer/frontend/src/components/live/tool-renderers/McpRenderer.tsx b/strix/interface/viewer/frontend/src/components/live/tool-renderers/McpRenderer.tsx index e17c268f..105ac77c 100644 --- a/strix/interface/viewer/frontend/src/components/live/tool-renderers/McpRenderer.tsx +++ b/strix/interface/viewer/frontend/src/components/live/tool-renderers/McpRenderer.tsx @@ -47,13 +47,29 @@ export default function McpRenderer({ const lines = argLines(args); const failed = status === "failed" || status === "error"; const error = failed ? errorText(result) : null; + // describe_mcp inspects a connection's catalog rather than calling a tool on + // it, so the connection is the subject and there is no underlying tool. + const inspecting = toolName === "describe_mcp"; return (
- {mcpTool || toolName} - via MCP server - {mcpConnection && {mcpConnection}} + {inspecting ? ( + <> + Inspecting MCP server + {mcpConnection && ( + {mcpConnection} + )} + + ) : ( + <> + + {mcpTool || toolName} + + via MCP server + {mcpConnection && {mcpConnection}} + + )}
{lines.length > 0 && ( diff --git a/strix/interface/viewer/frontend/src/components/live/tool-renderers/index.ts b/strix/interface/viewer/frontend/src/components/live/tool-renderers/index.ts index 4c7e088d..bda50125 100644 --- a/strix/interface/viewer/frontend/src/components/live/tool-renderers/index.ts +++ b/strix/interface/viewer/frontend/src/components/live/tool-renderers/index.ts @@ -181,8 +181,9 @@ function resolveCategory(toolName: string): ToolCategory | null { /** * A call to a tool from one of the user's MCP servers is placed by the - * connection it was tagged with, ahead of every name-keyed lookup below: its - * name belongs to that server and matches nothing in this table. + * connection it was tagged with, ahead of every name-keyed lookup below. Every + * MCP call goes through the `call_mcp` / `describe_mcp` dispatch tools, so the + * connection tag, not the tool name, is what routes it to the MCP renderer. */ export function getToolRenderer( toolName: string, diff --git a/strix/interface/viewer/frontend/src/types/events.ts b/strix/interface/viewer/frontend/src/types/events.ts index b58660e8..3f4f1133 100644 --- a/strix/interface/viewer/frontend/src/types/events.ts +++ b/strix/interface/viewer/frontend/src/types/events.ts @@ -101,9 +101,10 @@ export interface ToolRendererProps { status: "running" | "completed" | "failed" | "error"; /** * Set only on a call to a tool from an MCP server the user connected: the name - * they gave that connection, and the server's own name for the tool. The - * engine resolves both, because `toolName` is the two glued together and - * cannot be split back apart here. + * they gave that connection, and the server's own name for the tool. Every MCP + * call goes through the `call_mcp` / `describe_mcp` dispatch tools, so the + * engine reads both out of the call's arguments; `describe_mcp` inspects a + * connection and leaves `mcpTool` empty. */ mcpConnection?: string | null; mcpTool?: string | null; diff --git a/strix/interface/viewer/static/assets/index-C9c1WbvP.js b/strix/interface/viewer/static/assets/index-CYf9nnT3.js similarity index 92% rename from strix/interface/viewer/static/assets/index-C9c1WbvP.js rename to strix/interface/viewer/static/assets/index-CYf9nnT3.js index bac9288b..b6f6e45c 100644 --- a/strix/interface/viewer/static/assets/index-C9c1WbvP.js +++ b/strix/interface/viewer/static/assets/index-CYf9nnT3.js @@ -502,6 +502,6 @@ https://github.com/highlightjs/highlight.js/issues/2277`),Ht=ye,ft=Le),Qe===void ... 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,f=d?d.error??null:null,h=d?d.status_code??null:null,p=d?d.response_time_ms??null:null,g=d?d.body:null,y=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(([b,_])=>m.jsxs("div",{className:"text-[#555] pl-5",children:[b,": ",_p(String(_),150)]},b))]}),c&&m.jsx(wi,{className:"text-[#888]",children:wp(c,4)}),f&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1.5",children:_p(f,150)}),h!=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(h)}`,children:h}),p!=null&&m.jsxs("span",{className:"text-[#555] ml-2",children:[p,"ms"]})]}),y&&m.jsx(wi,{className:"text-[#666]",children:wp(y,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,f=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(([h,p])=>m.jsxs("div",{children:[m.jsxs("span",{className:"text-orange-400/60",children:[h,":"]})," ",m.jsx("span",{className:"text-[#777]",children:_p(typeof p=="string"?p:JSON.stringify(p),150)})]},h))}),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"]})]}),f&&m.jsx(wi,{className:"text-[#666]",children:wp(f,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?"Agent failed":"Agent completed"}),r&&m.jsx("div",{className:"mt-1.5",children:m.jsx(Yt,{text:r,maxLines:20})}),o&&o.length>0&&m.jsx("div",{className:"mt-1.5 space-y-0.5",children:o.map((c,d)=>m.jsxs("div",{className:"text-[13px] text-[#888]",children:[m.jsx("span",{className:"text-red-400/50 mr-1",children:"•"}),typeof c=="string"?c:JSON.stringify(c)]},d))})]})}if(e==="send_message_to_agent"){const r=t.message??"",a=t.target_agent_id??t.agent_id??"";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:"message"}),a&&m.jsxs("span",{className:"text-[#888] text-[13px]",children:["to ",a.slice(0,16)]})]}),r&&m.jsx("div",{className:"mt-1.5",children:m.jsx(Yt,{text:r,maxLines:20})})]})}if(e==="wait_for_agents"){const r=t.reason??"";return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:"waiting"}),r&&m.jsx("span",{className:"text-[#888] text-[13px] truncate",children:r})]})}if(e==="stop_agent"){const r=t.target_agent_id??"",a=t.cascade!==!1,s=t.reason??"";return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:"text-red-400/80 font-semibold text-sm",children:"stopping"}),r&&m.jsx("span",{className:"text-[#888] text-[13px]",children:r.slice(0,16)}),a&&m.jsx("span",{className:"text-[#555] text-[13px] italic",children:"+ descendants"})]}),s&&m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:s})]})}return e==="view_agent_graph"?m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:"viewing agents graph"}):m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:e.replace(/_/g," ")})}function P7({args:e,result:t}){const r=e.query??e.search_query??"",a=t,s=a?a.content??null:null,o=a&&!a.success?a.message??null:null;return m.jsxs("div",{children:[m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"Searching the web"}),r&&m.jsx("div",{className:"text-[#888] text-[13px] mt-0.5",children:r}),o&&m.jsx("div",{className:"text-red-400/70 text-[13px] mt-1.5",children:o}),s&&m.jsx("div",{className:"mt-2",children:m.jsx(Yt,{text:s,maxLines:15})})]})}const F7=50,h_=200,m_=25,p_=24,G7=/\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*\x07)/g,V7=/\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]/g;function Y7(e){return e.replace(G7,"")}function Um(e){const t=Y7(e);return t.length>h_?t.slice(0,h_-3)+"...":t}function X7(e){return e.replace(V7,"").trim()}function K7(e){const t=e.split(` `);if(t.length<=F7)return t.map(Um).join(` `);const r=t.length-m_-p_;return[...t.slice(0,m_).map(Um),`... ${r} lines truncated ...`,...t.slice(-p_).map(Um)].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]",children:o.title??"(untitled)"}),m.jsxs("span",{className:"text-[#555] ml-1",children:["(",o.category??"general",")"]}),(o.by_you||o.agent_name)&&m.jsxs("span",{className:"text-[#666] ml-1 text-xs",children:["by ",o.by_you?"you":o.agent_name]}),o.content&&m.jsx("div",{className:"ml-3",children:m.jsx(ua,{text:o.content})})]},c))}):m.jsx("div",{className:"mt-1 text-[#555] text-xs",children:"No notes"})]})}return m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note"})}const nU={create_todo:{label:"Task added",Icon:Y_},list_todos:{label:"Plan",Icon:nC},update_todo:{label:"Task updated",Icon:nT},mark_todo_done:{label:"Task completed",Icon:H_},mark_todo_pending:{label:"Task reopened",Icon:fT},delete_todo:{label:"Task removed",Icon:CT}};function rU({status:e}){return e==="done"?m.jsx(H_,{className:"w-3.5 h-3.5 text-emerald-400/70 shrink-0"}):e==="in_progress"?m.jsx(mC,{className:"w-3.5 h-3.5 text-purple-400/70 shrink-0 animate-pulse"}):m.jsx($_,{className:"w-3.5 h-3.5 text-[#444] shrink-0"})}function iU({todos:e,highlightId:t}){return m.jsx("div",{className:"space-y-0",children:e.map((r,a)=>{const s=r.status??"pending",o=t&&r.id===t;return m.jsxs("div",{className:`flex items-start gap-2.5 py-1.5 px-2 -mx-2 rounded-md transition-colors ${o?"bg-purple-500/[0.08]":""}`,children:[m.jsx("div",{className:"mt-[1px]",children:m.jsx(rU,{status:s})}),m.jsx("span",{className:`text-[13px] leading-snug ${s==="done"?"text-[#555] line-through":s==="in_progress"?"text-[#bbb]":"text-[#999]"}`,children:r.title??"(untitled)"})]},r.id??a)})})}function aU({toolName:e,args:t,result:r}){const a=nU[e]??{label:"Plan",Icon:oT},s=a.Icon,o=r;if(typeof o=="string"&&o.trim())return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-purple-400/60"}),m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:a.label})]}),m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:o.trim()})]});let c=[],d=null,f;if(o&&typeof o=="object"){if(d=o.error??null,o.success){const p=o.todos;c=Array.isArray(p)?p:[]}f=o.id??t.todo_id??void 0}const h=e!=="list_todos"?f:void 0;return c.length===0&&!d?m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-purple-400/60"}),m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:a.label})]}):m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-purple-400/60"}),m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:a.label})]}),d&&m.jsx("div",{className:"text-red-400/70 text-[13px] mb-2",children:d}),c.length>0&&m.jsx("div",{className:"rounded-lg border border-white/[0.06] bg-white/[0.015] px-3 py-2",children:m.jsx(iU,{todos:c,highlightId:h})})]})}function g_(e){if(e==null)return null;if(typeof e=="string")return e.trim()?e:null;if(typeof e=="object"){const t=e;if(typeof t.__raw=="string")return t.__raw;if(Object.keys(t).length===0)return null;try{return JSON.stringify(e,null,2)}catch{return String(e)}}return String(e)}function h2({toolName:e,args:t,result:r}){const a=g_(t),s=g_(r);return m.jsxs("div",{children:[m.jsx("span",{className:"text-[#888] font-semibold text-sm",children:e.replace(/_/g," ")}),a&&m.jsx(wi,{className:"text-[#777]",children:a}),s&&m.jsx(wi,{className:"text-[#666]",children:s})]})}function sU({args:e}){const t=e.skills,r=(Array.isArray(t)?t:String(t??"").split(",")).map(a=>String(a).trim()).filter(Boolean);return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:"Loading skill"}),r.length>0&&m.jsx("span",{className:"text-[#888] text-[13px]",children:r.join(", ")})]})}function lU({args:e}){const t=e.message??"";return t?m.jsxs("div",{children:[m.jsx(ua,{text:t}),m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:"waiting for your reply"})]}):null}const oU={reported:{label:"reported",color:"text-orange-400",Icon:Nu},no_issue_found:{label:"no issue found",color:"text-emerald-400",Icon:Eu},ruled_out:{label:"ruled out",color:"text-emerald-400/70",Icon:Eu},not_applicable:{label:"not applicable",color:"text-[#777]",Icon:bC},needs_follow_up:{label:"needs follow-up",color:"text-yellow-400",Icon:gC}},cU=["reported","needs_follow_up","no_issue_found","ruled_out","not_applicable"];function mo(e){const t=(e??"").trim().toLowerCase();return oU[t]??{label:t?t.replace(/_/g," "):"unrecorded",color:"text-[#777]",Icon:$_}}const uU={record_coverage:"Coverage recorded",update_coverage:"Coverage updated",list_coverage:"Coverage"};function gu({toolName:e}){return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(q_,{className:"w-3.5 h-3.5 text-cyan-400/60"}),m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:uU[e]??"Coverage"})]})}function dU({entry:e}){const{label:t,color:r,Icon:a}=mo(e.outcome),s=(e.previous_outcomes??[]).map(o=>mo(o).label).filter(Boolean);return m.jsxs("div",{className:"flex items-start gap-2.5 py-1.5",children:[m.jsx(a,{className:`w-3.5 h-3.5 shrink-0 mt-[2px] ${r}`}),m.jsxs("div",{className:"min-w-0",children:[m.jsxs("div",{className:"text-[13px] leading-snug",children:[m.jsx("span",{className:"text-[#bbb]",children:e.surface??"(unnamed surface)"}),e.risk_area&&m.jsxs("span",{className:"text-[#666]",children:[" · ",e.risk_area]})]}),m.jsxs("div",{className:"text-xs mt-0.5",children:[m.jsx("span",{className:r,children:t}),s.length>0&&m.jsxs("span",{className:"text-[#555]",children:[" (was ",s.join(" → "),")"]}),(e.by_you||e.agent_name)&&m.jsxs("span",{className:"text-[#555]",children:[" · ",e.by_you?"you":e.agent_name]})]}),e.evidence&&m.jsx("div",{className:"text-[#777] text-xs mt-1 leading-snug",children:e.evidence})]})]})}function fU({toolName:e,args:t,result:r}){const a=r;if(typeof a=="string"&&a.trim())return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:a.trim()})]});const s=a&&typeof a=="object"?a:null,o=t.surface??"",c=t.risk_area??"",d=t.evidence??"";if(s&&!s.success)return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),(o||c)&&m.jsxs("div",{className:"mt-1.5 text-[13px] text-[#bbb]",children:[o,c&&m.jsxs("span",{className:"text-[#666]",children:[" · ",c]})]}),m.jsx("div",{className:"mt-1 text-red-400/70 text-[13px]",children:s.error??"Coverage call failed"})]});if(e==="list_coverage"){const b=s==null?void 0:s.entries,_=Array.isArray(b)?b:[],E=(s==null?void 0:s.outcome_counts)??{},S=(s==null?void 0:s.total_count)??0;return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),Object.keys(E).length>0&&m.jsx("div",{className:"mt-2 flex items-center gap-3 flex-wrap",children:cU.filter(w=>E[w]).map(w=>{const{label:k,color:N}=mo(w);return m.jsxs("span",{className:`text-xs ${N}`,children:[k,": ",E[w]]},w)})}),_.length>0?m.jsx("div",{className:"mt-2 rounded-lg border border-white/[0.06] bg-white/[0.015] px-3 py-1 divide-y divide-white/[0.04]",children:_.map((w,k)=>m.jsx(dU,{entry:w},w.entry_id??k))}):m.jsx("div",{className:"mt-1.5 text-[#555] text-xs",children:S===0?"No surfaces recorded yet":"No surfaces match this filter"})]})}const f=(s==null?void 0:s.outcome)??"",h=(s==null?void 0:s.previous_outcome)??"",{label:p,color:g,Icon:y}=mo(f);return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),m.jsxs("div",{className:"mt-2 flex items-start gap-2.5",children:[m.jsx(y,{className:`w-3.5 h-3.5 shrink-0 mt-[2px] ${g}`}),m.jsxs("div",{className:"min-w-0",children:[m.jsxs("div",{className:"text-[13px] leading-snug text-[#bbb]",children:[o||(s!=null&&s.entry_id?`entry ${s.entry_id}`:"(unnamed surface)"),c&&m.jsxs("span",{className:"text-[#666]",children:[" · ",c]})]}),m.jsxs("div",{className:"text-xs mt-0.5",children:[h&&m.jsxs("span",{className:"text-[#666]",children:[mo(h).label," → "]}),m.jsx("span",{className:g,children:p})]}),d&&m.jsx("div",{className:"text-[#777] text-xs mt-1 leading-snug",children:d})]})]})]})}const hU={get_threat_model:{label:"Threat model",Icon:Vu},save_threat_model:{label:"Threat model saved",Icon:mT},amend_threat_model:{label:"Threat model amended",Icon:Y_}};function x_(e){const t=typeof e=="string"?e.trim():"";return!t||t==="unversioned"?"":t.slice(0,8)}function mU({toolName:e,args:t,result:r}){const a=hU[e]??{label:"Threat model",Icon:Vu},s=a.Icon,o=t.target??"",c=r,d=m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-blue-400/60"}),m.jsx("span",{className:"text-blue-400/80 font-semibold text-sm",children:a.label}),o&&m.jsx("span",{className:"text-[#666] font-mono text-xs",children:o})]});if(typeof c=="string"&&c.trim())return m.jsxs("div",{children:[d,m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:c.trim()})]});const f=c&&typeof c=="object"?c:null;if(f&&!f.success)return m.jsxs("div",{children:[d,m.jsx("div",{className:"mt-1.5 text-red-400/70 text-[13px]",children:f.error??"Threat model call failed"})]});if(e==="get_threat_model"){if(f&&!f.found)return m.jsxs("div",{children:[d,m.jsx("div",{className:"mt-1.5 text-[#555] text-xs",children:"No model cached for this target yet"})]});const y=f==null?void 0:f.amendments,b=Array.isArray(y)?y:[],_=x_(f==null?void 0:f.cached_revision);return m.jsxs("div",{children:[d,(f==null?void 0:f.stale)===!0&&m.jsxs("div",{className:"mt-1.5 flex items-center gap-1.5 text-yellow-400/80 text-xs",children:[m.jsx(Nu,{className:"w-3 h-3 shrink-0"}),m.jsxs("span",{children:["stale",_?` — written at ${_}`:""]})]}),b.length>0&&m.jsxs("div",{className:"mt-2",children:[m.jsxs("span",{className:"text-amber-400/70 text-xs font-semibold",children:[b.length," amendment",b.length===1?"":"s"]}),m.jsx("span",{className:"text-[#555] text-xs",children:" — later statements win"}),m.jsx("div",{className:"mt-1 space-y-1",children:b.map((E,S)=>m.jsxs("div",{className:"text-xs leading-snug",children:[m.jsx("span",{className:"text-[#666]",children:E.agent_name??"unknown agent"}),E.content&&m.jsxs("span",{className:"text-[#999]",children:[": ",E.content]})]},S))})]}),typeof(f==null?void 0:f.content)=="string"&&f.content.trim()&&m.jsx("div",{className:"mt-2",children:m.jsx(Yt,{text:f.content,maxLines:14})})]})}if(e==="amend_threat_model"){const y=t.addendum??"",b=f==null?void 0:f.amendment_count;return m.jsxs("div",{children:[d,b!=null&&m.jsxs("div",{className:"mt-1.5 text-[#666] text-xs",children:[b," amendment",b===1?"":"s"," on this model"]}),y&&m.jsx("div",{className:"mt-1.5",children:m.jsx(Yt,{text:y,maxLines:10})})]})}const h=(f==null?void 0:f.amendments_cleared)??0,p=x_(f==null?void 0:f.revision),g=t.content??"";return m.jsxs("div",{children:[d,p&&m.jsxs("div",{className:"mt-1.5 text-[#666] font-mono text-xs",children:["at ",p]}),h>0&&m.jsxs("div",{className:"mt-1.5 flex items-center gap-1.5 text-yellow-400/80 text-xs",children:[m.jsx(Nu,{className:"w-3 h-3 shrink-0"}),m.jsxs("span",{children:["cleared ",h," amendment",h===1?"":"s"]})]}),g&&m.jsx("div",{className:"mt-2",children:m.jsx(Yt,{text:g,maxLines:14})})]})}function pU(e){return!e||typeof e!="object"||Array.isArray(e)?[]:Object.entries(e).map(([t,r])=>{const a=typeof r=="string"?r:JSON.stringify(r);return`${t}: ${a??String(r)}`})}const b_=600;function gU(e){if(typeof e=="string"){const t=e.trim();return t?t.length>b_?`${t.slice(0,b_)}…`:t:null}return null}function xU({toolName:e,mcpTool:t,mcpConnection:r,args:a,result:s,status:o}){const c=pU(a),d=o==="failed"||o==="error",f=d?gU(s):null;return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx("span",{className:"font-mono text-teal-300 font-semibold text-sm",children:t||e}),m.jsx("span",{className:"text-[13px] text-[#555]",children:"via MCP server"}),r&&m.jsx("span",{className:"text-[13px] text-teal-400/80",children:r})]}),c.length>0&&m.jsx("div",{className:"mt-1 font-mono text-[13px] leading-relaxed",children:c.map(h=>m.jsx("div",{className:"text-[#777] break-all",children:h},h))}),m.jsxs("div",{className:"mt-1 text-[13px]",children:[o==="running"&&m.jsx("span",{className:"text-[#666]",children:"Running"}),o==="completed"&&m.jsx("span",{className:"text-emerald-400/80",children:"✓ Done"}),d&&m.jsx("span",{className:"text-red-400/80",children:"✗ Failed"})]}),f&&m.jsx("pre",{className:"mt-1 font-mono text-[13px] leading-relaxed whitespace-pre-wrap break-words text-red-400/70",children:f})]})}const Ga={terminal:{renderer:h7,icon:K_,color:"text-emerald-400"},python:{renderer:Z7,icon:EC,color:"text-yellow-400"},browser:{renderer:p7,icon:G_,color:"text-blue-400"},filesystem:{renderer:g7,icon:MC,color:"text-sky-400"},proxy:{renderer:H7,icon:z_,color:"text-purple-400",match:/request|sitemap|scope/},reporting:{renderer:A7,icon:bT,color:"text-red-400"},thinking:{renderer:$7,icon:B_,color:"text-purple-400"},agents:{renderer:q7,icon:Oo,color:"text-cyan-400",match:/agent/},search:{renderer:P7,icon:gT,color:"text-amber-400"},lifecycle:{renderer:J7,icon:F_,color:"text-emerald-400"},notes:{renderer:tU,icon:NT,color:"text-amber-400",match:/note/},skills:{renderer:sU,icon:Gm,color:"text-emerald-400"},todos:{renderer:aU,icon:YC,color:"text-purple-400",match:/todo/},coverage:{renderer:fU,icon:q_,color:"text-cyan-400",match:/coverage/},threatModel:{renderer:mU,icon:Vu,color:"text-blue-400",match:/threat_model/},telemetry:{renderer:h2,icon:Gm,color:"text-[#555]"},mcp:{renderer:xU,icon:V_,color:"text-teal-400"}},bU={terminal:["exec_command","write_stdin","terminal_execute"],python:["python_action"],browser:["browser_action"],filesystem:["apply_patch","view_image","str_replace_editor","list_files","search_files"],proxy:["list_requests","view_request","repeat_request","list_sitemap","view_sitemap_entry","scope_rules","send_request"],reporting:["create_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"],lifecycle:["scan_start_info","subagent_start_info","finish_scan","respond_to_user"],notes:["create_note","delete_note","update_note","list_notes","get_note"],skills:["load_skill"],todos:["create_todo","list_todos","update_todo","mark_todo_done","mark_todo_pending","delete_todo"],coverage:["record_coverage","update_coverage","list_coverage"],threatModel:["get_threat_model","save_threat_model","amend_threat_model"],telemetry:["sandbox_error_details","llm_error_details"],mcp:[]},yU=Object.fromEntries(Object.entries(bU).flatMap(([e,t])=>t.map(r=>[r,e]))),vU={finish_scan:eU,respond_to_user:lU,apply_patch:E7,view_image:k7,list_reports:f_,get_report:f_},_U={agent_finish:{icon:F_,color:"text-cyan-400"},send_message_to_agent:{icon:yh,color:"text-cyan-400"},wait_for_agents:{icon:yh,color:"text-cyan-400"},respond_to_user:{icon:yh,color:"text-emerald-400"},view_agent_graph:{icon:TC,color:"text-cyan-400"},stop_agent:{icon:I_,color:"text-red-400"},scan_start_info:{icon:Vu,color:"text-emerald-400"},subagent_start_info:{icon:Oo,color:"text-purple-400"},view_image:{icon:PC,color:"text-sky-400"}},wU=Ga.telemetry;function m2(e){var r;const t=yU[e];if(t)return t;for(const[a,s]of Object.entries(Ga))if((r=s.match)!=null&&r.test(e))return a;return null}function EU(e,t){if(t)return Ga.mcp.renderer;const r=vU[e];if(r)return r;const a=m2(e);return a?Ga[a].renderer:h2}function NU(e,t){if(t)return{icon:Ga.mcp.icon,color:Ga.mcp.color};const r=_U[e];if(r)return r;const a=m2(e),s=a?Ga[a]:wU;return{icon:s.icon,color:s.color}}const SU=30;function kU({role:e,content:t}){const r=e==="user"||e==="human";return m.jsxs("div",{children:[m.jsx("span",{className:`font-semibold text-sm ${r?"text-blue-400/80":"text-purple-400/80"}`,children:r?"User":"Thinking"}),m.jsx("div",{className:"mt-1.5 italic text-[#888]",children:m.jsx(Yt,{text:t,maxLines:SU})})]})}class CU extends ee.Component{constructor(t){super(t),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}render(){return this.state.hasError?m.jsx("span",{className:"text-[#555] font-semibold text-sm",children:this.props.toolName.replace(/_/g," ")}):this.props.children}}function TU(e){const t=EU(e.toolName,e.mcpConnection);return m.jsx(CU,{toolName:e.toolName,children:m.jsx(t,{...e})})}function p2(e){if(e==null||typeof e!="string")return e;const t=e.trim();if(!t)return e;try{return JSON.parse(t)}catch{}try{const r=t.replace(/\bNone\b/g,"null").replace(/\bTrue\b/g,"true").replace(/\bFalse\b/g,"false").replace(/'/g,'"');return JSON.parse(r)}catch{return{__raw:e}}}function y_(e){return typeof e=="string"&&e?e:null}function g2(e){const t=p2(e);return t&&typeof t=="object"&&!Array.isArray(t)?t:t==null?{}:{__raw:typeof t=="string"?t:JSON.stringify(t)}}function v_(e){const t=/(\d+)$/.exec(e);return t?parseInt(t[1],10):0}function yg(e){var r;const t=(r=e.data)==null?void 0:r.role;return e.type==="chat"&&(t==="user"||t==="human")}function AU(e){var t;return yg(e)&&String(((t=e.data)==null?void 0:t.content)??"").startsWith("[Message from ")}function MU(e){const t=new Set;let r=!1;for(const a of e)if(yg(a)){if(AU(a)){t.add(a.id);continue}r||(r=!0,t.add(a.id))}return t}const OU={completed:"text-emerald-400 border-emerald-500/30 bg-emerald-500/10",running:"text-blue-400 border-blue-500/30 bg-blue-500/10",waiting:"text-yellow-400 border-yellow-500/30 bg-yellow-500/10",stopped:"text-[#aaa] border-[#333] bg-[#1a1a1a]",crashed:"text-red-400 border-red-500/30 bg-red-500/10",failed:"text-red-400 border-red-500/30 bg-red-500/10"};function RU(e){return e==="completed"?"completed":e==="running"?"running":e==="failed"||e==="crashed"?"failed":e}function jU(e,t){var d;const r=new Map;for(const f of e)if(f.parent_id){const h=r.get(f.parent_id)??[];h.push(f.id),r.set(f.parent_id,h)}const a=new Map,s=new Map,o=new Map;for(const f of t)if(f.type==="tool"){if(a.set(f.agent_id,(a.get(f.agent_id)??0)+1),((d=f.data)==null?void 0:d.tool_name)==="create_agent"){const h=g2(f.data.args),p=h.name??h.agent_name??"",g=h.task??"";p&&g&&o.set(p,g)}}else yg(f)||s.set(f.agent_id,(s.get(f.agent_id)??0)+1);const c=new Map;for(const f of e)c.set(f.id,{id:f.id,name:f.name,task:o.get(f.name)??"",status:RU(f.status),parentId:f.parent_id,children:r.get(f.id)??[],createdAt:f.created_at,toolCount:a.get(f.id)??0,messageCount:s.get(f.id)??0});return c}function DU({agent:e,events:t,showHeader:r=!0}){const a=ee.useMemo(()=>{const c=t.filter(f=>f.agent_id===e.id).sort((f,h)=>v_(f.id)-v_(h.id)),d=MU(c);return c.filter(f=>!d.has(f.id))},[t,e.id]),s=a.filter(c=>c.type==="tool").length,o=a.length-s;return m.jsxs("div",{children:[r&&m.jsxs(m.Fragment,{children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap mb-1",children:[m.jsx("span",{className:"text-base font-semibold text-white truncate",children:e.name}),m.jsx("span",{className:`flex-shrink-0 text-xs font-medium capitalize px-2 py-0.5 rounded-full border ${OU[e.status]??"text-[#aaa] border-[#333] bg-[#1a1a1a]"}`,children:e.status}),m.jsx("span",{className:"font-mono text-xs text-[#555]",children:e.id})]}),m.jsxs("p",{className:"text-xs text-[#666] mb-4",children:[o," message",o===1?"":"s"," · ",s," tool call",s===1?"":"s"]})]}),a.length===0?m.jsx("p",{className:"text-sm text-[#666]",children:"No recorded activity for this agent."}):m.jsx("div",{className:"py-1",children:a.map((c,d)=>{var w,k,N,M,B,R,U,I;const f=d===a.length-1,h=c.type==="tool",p=h?String(((w=c.data)==null?void 0:w.tool_name)??"tool"):"",g=h?"":String(((k=c.data)==null?void 0:k.role)??"assistant"),y=y_((N=c.data)==null?void 0:N.mcp_connection),b=y_((M=c.data)==null?void 0:M.mcp_tool);let _,E;if(h){const X=NU(p,y);_=X.icon,E=X.color}else{const X=g==="user"||g==="human";_=X?Oo:B_,E=X?"text-blue-400":"text-purple-400"}const S=h?String(((B=c.data)==null?void 0:B.status)??"completed"):"completed";return m.jsxs("div",{className:"flex gap-3",children:[m.jsxs("div",{className:"flex flex-col items-center shrink-0",children:[m.jsx("div",{className:`w-[30px] h-[30px] rounded-full bg-black border flex items-center justify-center shrink-0 ${h&&S==="running"?"border-blue-500/40 animate-pulse":h&&S==="failed"?"border-red-500/30":"border-[#222]"}`,children:m.jsx(_,{className:`w-3.5 h-3.5 ${E}`})}),!f&&m.jsx("div",{className:"w-px flex-1 bg-[#1a1a1a] mt-1"})]}),m.jsx("div",{className:"flex-1 min-w-0 pt-[5px] pb-6",children:h?m.jsx(TU,{toolName:p,mcpConnection:y,mcpTool:b,args:g2((R=c.data)==null?void 0:R.args),result:p2((U=c.data)==null?void 0:U.result)??null,status:S}):m.jsx(kU,{role:g,content:String(((I=c.data)==null?void 0:I.content)??"")})})]},c.id)})})]})}class qu extends Error{constructor(t){super(t),this.name="RunParseError"}}const LU=["critical","high","medium","low"];function zU(e){const t=String(e??"").toLowerCase().trim();return LU.includes(t)?t:"low"}function IU(e){if(typeof e=="string"&&e.trim()){const t=e.trim().replace(" UTC","Z").replace(" ","T"),r=new Date(t);if(!Number.isNaN(r.getTime()))return r.toISOString();const a=new Date(e);if(!Number.isNaN(a.getTime()))return a.toISOString()}return new Date().toISOString()}function Ot(e){return typeof e=="string"&&e.length>0?e:null}function BU(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function x2(e,t){try{return JSON.parse(e)}catch{throw new qu(`${t} isn't valid JSON. Make sure you selected a Strix run directory.`)}}function UU(e){const t=x2(e,"run.json");if(!t||typeof t!="object"||Array.isArray(t))throw new qu("run.json is not an object.");const r=t,a=[],s=r.targets_info;if(Array.isArray(s)){for(const b of s)if(b&&typeof b=="object"){const _=b.original;typeof _=="string"&&_&&a.push(_)}}const o=Ot(r.start_time),c=Ot(r.end_time);let d=null;if(o&&c){const b=new Date(o).getTime(),_=new Date(c).getTime();!Number.isNaN(b)&&!Number.isNaN(_)&&_>=b&&(d=Math.round((_-b)/1e3))}let f=null,h=null,p=null,g=null;const y=r.scan_results;if(y&&typeof y=="object"){const b=y;f=Ot(b.executive_summary),h=Ot(b.technical_analysis),p=Ot(b.methodology),g=Ot(b.recommendations)}return{runId:Ot(r.run_id),runName:Ot(r.run_name),targets:a,scanMode:Ot(r.scan_mode),status:Ot(r.status),startTime:o,endTime:c,durationSeconds:d,executiveSummary:f,technicalAnalysis:h,methodology:p,recommendations:g}}function HU(){return{pr_review_id:null,cve:null,cvss:null,potential_risk_saving:null,risk_saving_description:null,impact:null,endpoint:null,method:null,target:null,technical_analysis:null,poc_description:null,poc_script_code:null,code_diff:null,code_file:null,code_before:null,code_after:null,cwe:null,code_locations:null,remediation_steps:null,fix_pr_body:null,evidence:null,assumptions:null,fix_effort:null,cvss_breakdown:null,status_changed_at:null,status_changed_by:null,status_note:null,snoozed_until:null,reopened_at:null,reopened_by:null,original_severity:null,severity_changed_at:null,severity_changed_by:null,severity_override_reason:null,retest_of_vulnerability_id:null}}function $U(e,t,r){const a=e.cwe,s=typeof a=="string"&&a.trim()?[a.trim()]:Array.isArray(a)?a.filter(c=>typeof c=="string"&&c):null;return{...HU(),id:Ot(e.id)??`vuln-${t+1}`,scan_id:r,title:Ot(e.title)??"Untitled finding",description:Ot(e.description)??"",severity:zU(e.severity),status:"open",created_at:IU(e.timestamp),cve:Ot(e.cve),cvss:BU(e.cvss),impact:Ot(e.impact),endpoint:Ot(e.endpoint),method:Ot(e.method),target:Ot(e.target),technical_analysis:Ot(e.technical_analysis),poc_description:Ot(e.poc_description),poc_script_code:Ot(e.poc_script_code),cwe:s,code_locations:Array.isArray(e.code_locations)?e.code_locations:null,remediation_steps:Ot(e.remediation_steps),fix_pr_body:Ot(e.fix_pr_body),evidence:Ot(e.evidence),assumptions:Ot(e.assumptions),fix_effort:Ot(e.fix_effort)??null,cvss_breakdown:e.cvss_breakdown??null}}function qU(e,t=null){const r=x2(e,"vulnerabilities.json");if(!Array.isArray(r))throw new qu("vulnerabilities.json is not a JSON array.");return r.map((a,s)=>{if(!a||typeof a!="object")throw new qu(`vulnerabilities.json entry #${s+1} is not an object.`);return $U(a,s,t)})}function PU(e){const t={critical:0,high:0,medium:0,low:0};for(const r of e)t[r.severity]+=1;return t}async function es(e){const t=await fetch(e,{cache:"no-store"});if(!t.ok)throw new Error(`${e} responded ${t.status}`);return t.json()}function dd(e){return e?`?run=${encodeURIComponent(e)}`:""}async function b2(e){const t=await es("/api/run"+dd(e)),r=UU(JSON.stringify(t)),a=t.finished===!0;return{summary:r,raw:t,finished:a}}async function y2(e,t){const r=await es("/api/vulnerabilities"+dd(t));return qU(JSON.stringify(r),e)}async function FU(e){const t=await es("/api/report"+dd(e));return(t==null?void 0:t.markdown)??null}async function v2(e){const t=await es("/api/transcript"+dd(e));return{agents:Array.isArray(t==null?void 0:t.agents)?t.agents:[],events:Array.isArray(t==null?void 0:t.events)?t.events:[]}}async function __(e){const{summary:t,raw:r,finished:a}=await b2(e),[s,o,c]=await Promise.all([y2(t.runId,e).catch(()=>[]),FU(e).catch(()=>null),v2(e).catch(()=>({agents:[],events:[]}))]);return{summary:t,raw:r,finished:a,vulnerabilities:s,reportMarkdown:o,transcript:c}}async function il(e,t){const r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),cache:"no-store"});let a={};try{const s=await r.json();s&&typeof s=="object"&&(a=s)}catch{}return{ok:r.ok,status:r.status,data:a}}async function GU(){const e=await es("/api/runs");return{locked:(e==null?void 0:e.locked)??!0,count:typeof(e==null?void 0:e.count)=="number"?e.count:0,runs:Array.isArray(e==null?void 0:e.runs)?e.runs:[]}}async function VU(){const e=await es("/api/capabilities");return{can_steer:(e==null?void 0:e.can_steer)===!0}}async function YU(e,t){const{ok:r,data:a}=await il("/api/agents/steer",{agent_id:e,message:t});return r&&a.ok===!0?{ok:!0}:{ok:!1,error:String(a.error??"unavailable")}}async function XU(e,t){const{ok:r,data:a}=await il("/api/feedback",{message:e,email:t});return r&&a.ok===!0?{ok:!0}:{ok:!1,error:String(a.error??"unavailable")}}async function KU(){const e=await es("/api/auth/status");return{verified:(e==null?void 0:e.verified)===!0,email:(e==null?void 0:e.email)??null}}async function _2(e){const{ok:t,data:r}=await il("/api/auth/otp/start",{email:e});return t&&r.ok===!0?{ok:!0}:{ok:!1,error:String(r.error??"unavailable")}}async function w2(e,t){const{ok:r,data:a}=await il("/api/auth/otp/verify",{email:e,code:t});return r&&a.verified===!0?{verified:!0,email:String(a.email??e)}:{verified:!1,error:String(a.error??"invalid_code")}}async function ZU(){await il("/api/auth/forget",{})}async function QU(e){const{ok:t,data:r}=await il("/api/report/send",e?{run:e}:{});return t&&r.ok===!0?{ok:!0,password:String(r.password??""),filename:String(r.filename??"strix-report.pdf")}:{ok:!1,error:String(r.error??"unavailable")}}const Us="__root__";function E2({agents:e,fixedAgentId:t,className:r}){const a=ee.useRef(null),[s,o]=ee.useState(!1),[c,d]=ee.useState(!1),[f,h]=ee.useState(""),[p,g]=ee.useState(!1),[y,b]=ee.useState(null),_=t!=null,E=ee.useMemo(()=>e.find(z=>!z.parent_id)??e[0]??null,[e]),S=ee.useMemo(()=>e.filter(z=>z.parent_id&&z.status==="running"),[e]),[w,k]=ee.useState(Us),[N,M]=ee.useState(!1);ee.useEffect(()=>{w!==Us&&!S.some(z=>z.id===w)&&k(Us)},[w,S]);const{targetId:B,targetName:R}=ee.useMemo(()=>{if(_){const V=e.find(P=>P.id===t)??null;return{targetId:t??null,targetName:(V==null?void 0:V.name)??"this agent"}}if(w===Us)return{targetId:(E==null?void 0:E.id)??null,targetName:"Root agent"};const z=e.find(V=>V.id===w)??null;return{targetId:(z==null?void 0:z.id)??(E==null?void 0:E.id)??null,targetName:(z==null?void 0:z.name)??"Root agent"}},[e,t,_,E,w]),U=f.trim().length===0;ee.useLayoutEffect(()=>{const z=a.current;z&&(z.style.height="auto",z.style.height=`${z.scrollHeight}px`)},[f]);const I=ee.useCallback(()=>{o(!0),requestAnimationFrame(()=>{var z;return(z=a.current)==null?void 0:z.focus()})},[]),X=ee.useCallback(()=>{o(!1),d(!1),M(!1)},[]),j=ee.useCallback(async()=>{if(p)return;const z=f.trim();if(!z||!B)return;g(!0),b(null);const V=R,P=await YU(B,z);g(!1),P.ok?(h(""),b(`Sent to ${V}`),Tr("agent_steered")):P.error==="not_delivered"?b("Could not reach that agent (it may have finished)."):b("Could not send that message. Try again.")},[p,f,B,R]);return s?m.jsxs("div",{className:Mr("mt-4 rounded-2xl border border-white/[0.08] bg-[#050505] overflow-hidden transition-colors duration-300",c?"border-white/[0.18]":"hover:border-white/[0.12]",r),children:[m.jsxs("div",{className:"flex items-center justify-between gap-3 border-b border-white/[0.06] px-5 py-3",children:[m.jsxs("div",{className:"min-w-0",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Fm,{className:"h-4 w-4 text-[#666]"}),m.jsx("p",{className:"text-sm font-medium text-white",children:"Live prompt"})]}),m.jsx("p",{className:"mt-0.5 text-xs text-[#777]",children:"Connected"})]}),m.jsxs("div",{className:"flex shrink-0 items-center gap-2",children:[_?m.jsxs("div",{className:"rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-xs text-[#aaa]",children:["Target: ",m.jsx("span",{className:"text-white",children:R})]}):m.jsxs("div",{className:"flex items-center gap-1.5",children:[m.jsx("span",{className:"text-xs text-[#aaa]",children:"Target:"}),m.jsxs("div",{className:"relative",children:[m.jsxs("button",{type:"button",onClick:()=>M(z=>!z),onBlur:()=>requestAnimationFrame(()=>M(!1)),className:"inline-flex h-7 items-center gap-1 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 text-xs text-white transition-colors hover:border-white/[0.16] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20","aria-haspopup":"listbox","aria-expanded":N,children:[m.jsx("span",{className:"max-w-[140px] truncate",children:R}),m.jsx(po,{className:"h-3.5 w-3.5 text-[#999]"})]}),N&&m.jsxs("div",{className:"absolute right-0 z-10 mt-1 min-w-[160px] overflow-hidden rounded-lg border border-[#333] bg-[#0a0a0a] py-1 shadow-xl",role:"listbox",children:[m.jsx(w_,{label:"Root agent",active:w===Us,onSelect:()=>{k(Us),M(!1)}}),S.map(z=>m.jsx(w_,{label:z.name,active:w===z.id,onSelect:()=>{k(z.id),M(!1)}},z.id))]})]})]}),m.jsx("button",{type:"button",onClick:X,className:"inline-flex h-7 w-7 items-center justify-center rounded-full text-[#777] transition-colors hover:bg-white/[0.06] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20","aria-label":"Collapse live prompt composer",children:m.jsx(po,{className:"h-4 w-4"})})]})]}),m.jsx("div",{className:"px-5 pt-4 pb-3",children:m.jsx("textarea",{ref:a,rows:1,value:f,onChange:z=>h(z.target.value),onFocus:()=>d(!0),onBlur:()=>d(!1),onKeyDown:z=>{z.key==="Enter"&&!z.shiftKey&&(z.preventDefault(),j())},placeholder:"Send a live prompt to the running pentest…",maxLength:4e3,disabled:p,className:"block w-full resize-none border-0 bg-transparent p-0 text-[15px] leading-6 text-white placeholder:text-[#444] focus:outline-none disabled:opacity-60 max-h-[160px] overflow-y-auto"})}),m.jsxs("div",{className:"flex items-center justify-between gap-3 px-4 pb-4",children:[m.jsx("div",{className:"text-xs text-[#666]",children:y??"Press Enter to send."}),m.jsxs("button",{type:"button",onClick:z=>{z.stopPropagation(),j()},disabled:p||U,className:Mr("inline-flex h-10 min-w-[112px] items-center justify-center gap-2 rounded-full px-4 text-sm font-medium transition-colors",p||U?"bg-white/[0.08] text-[#666]":"bg-white text-black hover:bg-neutral-200"),children:[p?m.jsx(Ps,{className:"h-4 w-4 animate-spin"}):m.jsx(Yk,{className:"h-4 w-4",strokeWidth:2.5}),m.jsx("span",{children:"Send prompt"})]})]})]}):m.jsxs("button",{type:"button",onClick:I,className:Mr("mt-4 flex w-full items-center justify-between gap-3 rounded-2xl border border-white/[0.08] bg-[#050505] px-5 py-3 text-left transition-colors duration-300 hover:border-white/[0.12] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20",r),"aria-expanded":!1,"aria-label":"Expand live prompt composer",children:[m.jsxs("div",{className:"flex min-w-0 items-center gap-2",children:[m.jsx(Fm,{className:"h-4 w-4 shrink-0 text-[#666]"}),m.jsx("span",{className:"truncate text-sm font-medium text-white",children:"Guide the agent"})]}),m.jsx(U_,{className:"h-4 w-4 shrink-0 text-[#777]"})]})}function w_({label:e,active:t,onSelect:r}){return m.jsx("button",{type:"button",role:"option","aria-selected":t,onMouseDown:a=>{a.preventDefault(),r()},className:Mr("block w-full truncate px-3 py-1.5 text-left text-xs transition-colors hover:bg-white/[0.06]",t?"text-white":"text-[#aaa]"),children:e})}const WU={completed:"bg-emerald-400",running:"bg-blue-400",waiting:"bg-yellow-400",stopped:"bg-[#888]",crashed:"bg-red-400",failed:"bg-red-400"},JU=80;function eH({open:e,agent:t,events:r,steerable:a,onClose:s}){const o=ee.useRef(null),c=ee.useRef(!1),[d,f]=ee.useState(e),[h,p]=ee.useState(e?"open":"closed"),[g,y]=ee.useState(!1),b=ee.useRef(t);ee.useEffect(()=>{t&&(b.current=t)},[t]);const _=t??b.current;ee.useEffect(()=>{if(e){f(!0),p("open");return}p("closed");const S=setTimeout(()=>f(!1),140);return()=>clearTimeout(S)},[e]),ee.useEffect(()=>{if(!d){y(!1);return}const S=requestAnimationFrame(()=>y(!0));return()=>cancelAnimationFrame(S)},[d]);const E=ee.useCallback(()=>{const S=o.current;S&&(c.current=S.scrollHeight-S.scrollTop-S.clientHeight{const S=o.current;!S||!c.current||requestAnimationFrame(()=>{S.scrollTo({top:S.scrollHeight,behavior:"smooth"})})},[r]),ee.useEffect(()=>{if(!d)return;const S=k=>{k.key==="Escape"&&s()};document.addEventListener("keydown",S);const w=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{document.removeEventListener("keydown",S),document.body.style.overflow=w}},[d,s]),!d||!_?null:m.jsx("div",{"data-state":h,className:"agent-modal fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4 sm:p-8",onClick:s,role:"dialog","aria-modal":"true","aria-label":`Agent ${_.name}`,children:m.jsxs("div",{className:"relative flex h-[60vh] w-[calc(100vw-4rem)] max-w-6xl flex-col overflow-hidden rounded-xl border border-[#222] bg-[#0a0a0a] shadow-2xl",onClick:S=>S.stopPropagation(),children:[m.jsxs("div",{className:"flex items-center justify-between gap-3 border-b border-[#222] px-5 py-3.5",children:[m.jsxs("div",{className:"flex min-w-0 items-center gap-2",children:[m.jsx("span",{className:`h-2 w-2 flex-shrink-0 rounded-full ${WU[_.status]??"bg-[#888]"}`}),m.jsx("span",{className:"truncate text-sm font-semibold text-white",children:_.name}),m.jsx("span",{className:"flex-shrink-0 font-mono text-xs text-[#555]",children:_.id})]}),m.jsx("button",{type:"button",onClick:s,"aria-label":"Close",className:"flex-shrink-0 rounded-md p-1 text-[#888] transition-colors hover:bg-[#1a1a1a] hover:text-white",children:m.jsx(Sp,{className:"h-4 w-4"})})]}),m.jsx("div",{ref:o,onScroll:E,className:"flex-1 overflow-y-auto p-5",children:g&&m.jsx(DU,{agent:_,events:r,showHeader:!1})}),a&&m.jsx("div",{className:"border-t border-[#222] px-5 py-3",children:m.jsx(E2,{agents:[_],fixedAgentId:_.id,className:"mt-0"})})]})})}var N2={color:void 0,size:void 0,className:void 0,style:void 0,attr:void 0},E_=da.createContext&&da.createContext(N2),tH=["attr","size","title"];function nH(e,t){if(e==null)return{};var r,a,s=rH(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;ada.createElement(t.tag,Fu({key:r},t.attr),S2(t.child)))}function vg(e){return t=>da.createElement(lH,Pu({attr:Fu({},e.attr)},t),S2(e.child))}function lH(e){var t=r=>{var a=e.attr,s=e.size,o=e.title,c=nH(e,tH),d=s||r.size||"1em",f;return r.className&&(f=r.className),e.className&&(f=(f?f+" ":"")+e.className),da.createElement("svg",Pu({stroke:"currentColor",fill:"currentColor",strokeWidth:"0"},r.attr,a,c,{className:f,style:Fu(Fu({color:e.color||r.color},r.style),e.style),height:d,width:d,xmlns:"http://www.w3.org/2000/svg"}),o&&da.createElement("title",null,o),e.children)};return E_!==void 0?da.createElement(E_.Consumer,null,r=>t(r)):t(N2)}function oH(e){return vg({attr:{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"},child:[{tag:"circle",attr:{cx:"5",cy:"6",r:"3"},child:[]},{tag:"path",attr:{d:"M5 9v12"},child:[]},{tag:"circle",attr:{cx:"19",cy:"18",r:"3"},child:[]},{tag:"path",attr:{d:"m15 9-3-3 3-3"},child:[]},{tag:"path",attr:{d:"M12 6h5a2 2 0 0 1 2 2v7"},child:[]}]})(e)}function cH(e){return vg({attr:{viewBox:"0 0 16 16",fill:"currentColor"},child:[{tag:"path",attr:{d:"M15 4.95703C15 4.58711 14.8563 4.24054 14.5949 3.97992L12.0096 1.39234C11.4879 0.86922 10.5788 0.86922 10.0571 1.39234L8 3.45119V3.32321C8 2.55068 7.37187 1.922 6.6 1.922H2.4C1.62813 1.922 1 2.55068 1 3.32321V13.5988C1 14.3713 1.62813 15 2.4 15H12.6667C13.4385 15 14.0667 14.3713 14.0667 13.5988V9.39514C14.0667 8.62261 13.4385 7.99393 12.6667 7.99393H12.5379L14.5949 5.93508C14.8553 5.67445 15 5.32602 15 4.95703ZM2.4 2.85521H6.6C6.85667 2.85521 7.06667 3.06446 7.06667 3.32228V7.99299H1.93333V3.32228C1.93333 3.06446 2.14333 2.85521 2.4 2.85521ZM1.93333 13.5979V8.92714H7.06667V14.0649H2.4C2.14333 14.0649 1.93333 13.8547 1.93333 13.5979ZM13.1333 9.39421V13.5979C13.1333 13.8547 12.9233 14.0649 12.6667 14.0649H8V8.92714H12.6667C12.9233 8.92714 13.1333 9.13638 13.1333 9.39421ZM8 7.99299V6.46287L9.5288 7.99299H8ZM13.9351 5.2737L11.3488 7.86221C11.1789 8.03223 10.8859 8.03223 10.716 7.86221L8.12973 5.2737C8.0448 5.18963 7.99813 5.07753 7.99813 4.95796C7.99813 4.83839 8.0448 4.7263 8.12973 4.64129L10.716 2.05278C10.8009 1.96777 10.9129 1.92106 11.0324 1.92106C11.1519 1.92106 11.2639 1.96777 11.3488 2.05278L13.9351 4.64129C14.02 4.72536 14.0667 4.83746 14.0667 4.95703C14.0667 5.0766 14.02 5.1887 13.9351 5.2737Z"},child:[]}]})(e)}function k2(e){return vg({attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{fill:"none",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"32",d:"M431 320.6c-1-3.6 1.2-8.6 3.3-12.2a34 34 0 0 1 2.1-3.1A162 162 0 0 0 464 215c.3-92.2-77.5-167-173.7-167-83.9 0-153.9 57.1-170.3 132.9a160.7 160.7 0 0 0-3.7 34.2c0 92.3 74.8 169.1 171 169.1 15.3 0 35.9-4.6 47.2-7.7s22.5-7.2 25.4-8.3a26.4 26.4 0 0 1 9.3-1.7 26 26 0 0 1 10.1 2l56.7 20.1a13.5 13.5 0 0 0 3.9 1 8 8 0 0 0 8-8 13 13 0 0 0-.5-2.7z"},child:[]},{tag:"path",attr:{fill:"none",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"32",d:"M66.46 232a146.23 146.23 0 0 0 6.39 152.67c2.31 3.49 3.61 6.19 3.21 8s-11.93 61.87-11.93 61.87a8 8 0 0 0 2.71 7.68A8.17 8.17 0 0 0 72 464a7.3 7.3 0 0 0 2.91-.6l56.21-22a15.7 15.7 0 0 1 12 .2c18.94 7.38 39.88 12 60.83 12A159.2 159.2 0 0 0 284 432.11"},child:[]}]})(e)}const uH=[{icon:LC,label:"PR security reviews"},{icon:_T,label:"Attack surface monitoring"},{icon:zT,label:"Real-time threat intelligence"},{icon:eC,label:"Scheduled pentesting"},{icon:RT,label:"One-click autofix"},{icon:V_,label:"Jira, Linear & Slack integrations"}];function dH({open:e,onClose:t,description:r,source:a="sidebar"}){const[s,o]=ee.useState(e),[c,d]=ee.useState(e?"open":"closed");return ee.useEffect(()=>{if(e){o(!0),d("open");return}d("closed");const f=setTimeout(()=>o(!1),200);return()=>clearTimeout(f)},[e]),ee.useEffect(()=>{if(!s)return;const f=p=>{p.key==="Escape"&&t()};document.addEventListener("keydown",f);const h=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{document.removeEventListener("keydown",f),document.body.style.overflow=h}},[s,t]),s?m.jsx("div",{"data-state":c,className:"dialog-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4",onClick:t,role:"dialog","aria-modal":"true","aria-label":"Upgrade your plan",children:m.jsxs("div",{"data-state":c,className:"dialog-panel relative w-full max-w-md rounded-2xl border border-[#222] bg-black p-6 shadow-lg sm:rounded-lg",onClick:f=>f.stopPropagation(),children:[m.jsx("button",{type:"button",onClick:t,"aria-label":"Close",className:"absolute right-4 top-4 rounded-md p-1 text-[#888] transition-colors hover:bg-[#1a1a1a] hover:text-white",children:m.jsx(Sp,{className:"h-4 w-4"})}),m.jsxs("div",{children:[m.jsx("h2",{className:"text-lg text-white",children:"Available in Strix Cloud"}),r&&m.jsx("p",{className:"mt-2 text-base leading-relaxed text-[#e5e5e5]",children:r})]}),m.jsxs("div",{className:"space-y-4 pt-4",children:[m.jsxs("div",{className:"rounded-xl border border-[#333] bg-[#0a0a0a] p-4 sm:rounded-lg",children:[m.jsxs("div",{className:"mb-3 flex items-center gap-2",children:[m.jsx(Fm,{className:"h-4 w-4 text-blue-400"}),m.jsx("span",{className:"text-sm font-medium text-white",children:"Strix Cloud also includes"})]}),m.jsx("ul",{className:"space-y-2 text-sm text-[#888]",children:uH.map(f=>m.jsxs("li",{className:"flex items-center gap-2",children:[m.jsx(f.icon,{className:"h-3.5 w-3.5 text-[#555]"}),f.label]},f.label))})]}),m.jsxs("div",{className:"flex flex-col gap-2",children:[m.jsxs("a",{href:ha(Yu,"upgrade_try_free"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("upgrade_try_free",a),className:"flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition-colors hover:bg-neutral-200",children:["Open Strix Cloud",m.jsx(oy,{className:"h-3.5 w-3.5"})]}),m.jsxs("a",{href:ha($T,"upgrade_view_plans"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("upgrade_view_plans",a),className:"flex h-9 w-full items-center justify-center gap-1.5 rounded-lg border border-[#333] px-4 text-sm font-medium text-[#888] transition-colors hover:border-[#555] hover:text-white",children:["Learn more",m.jsx(oy,{className:"h-3 w-3"})]})]})]})]})}):null}const Hm=160,$m=260,io=400,fH=140,S_="strix_viewer_sidebar_width",k_="strix_viewer_sidebar_collapsed";function hH(e,t){try{const r=localStorage.getItem(e),a=r?parseInt(r,10):NaN;return Number.isFinite(a)?a:t}catch{return t}}function mH({view:e,onSelectView:t,issuesCount:r,agentCount:a,runCount:s,finished:o,verified:c,email:d,onOpenEmail:f,onOpenHistory:h,onForget:p}){var z;const[g,y]=ee.useState(()=>{const V=hH(S_,$m);return Math.min(io,Math.max(Hm,V))}),[b,_]=ee.useState(()=>{try{return localStorage.getItem(k_)==="1"}catch{return!1}}),[E,S]=ee.useState(!1),[w,k]=ee.useState(!1),[N,M]=ee.useState(null),B=ee.useRef(null),R=(V,P)=>{jr(V,"sidebar"),M(P)},U=ee.useCallback(V=>{y(V);try{localStorage.setItem(S_,String(V))}catch{}},[]),I=ee.useCallback(V=>{_(V);try{localStorage.setItem(k_,V?"1":"0")}catch{}},[]),X=ee.useCallback(()=>{I(!1),U($m)},[I,U]),j=ee.useCallback(V=>{V.preventDefault(),S(!0)},[]);return ee.useEffect(()=>{if(!E||b)return;const V=T=>{const $=T.clientX;$>=Hm&&$<=io?y($):$>io&&y(io)},P=T=>{const $=T.clientX;${window.removeEventListener("mousemove",V),window.removeEventListener("mouseup",P)}},[E,b,I,U]),ee.useEffect(()=>{if(!w)return;const V=P=>{B.current&&!B.current.contains(P.target)&&k(!1)};return document.addEventListener("mousedown",V),()=>document.removeEventListener("mousedown",V)},[w]),m.jsxs(m.Fragment,{children:[b&&m.jsx("div",{className:"fixed left-0 top-0 z-40 hidden h-full w-4 cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.08)] lg:block",onClick:X,title:"Expand sidebar"}),m.jsxs("aside",{className:Mr("sticky top-0 z-20 hidden h-screen flex-shrink-0 flex-col overflow-hidden border-r border-[rgba(255,255,255,0.08)] bg-black lg:flex",!E&&"transition-[width] duration-200 ease-out"),style:{width:b?0:g},children:[m.jsx("header",{className:"relative flex flex-col gap-1 pt-1 min-w-[160px]",children:m.jsx("div",{className:"flex flex-row py-1 px-2",children:m.jsxs("div",{className:"flex h-10 w-full flex-row items-center",children:[m.jsxs("a",{href:ha("https://app.strix.ai","logo"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("logo","sidebar"),className:"flex flex-1 flex-row items-center gap-2 rounded-md py-2 pl-2.5 pr-1 min-w-0 transition-colors hover:bg-[rgba(255,255,255,0.06)]",title:"Open Strix Cloud",children:[m.jsx("span",{className:"flex flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500",style:{width:20,height:20},children:m.jsx("span",{className:"text-[10px] font-semibold text-white",children:"S"})}),m.jsxs("span",{className:"flex flex-1 flex-row items-center gap-2 min-w-0",children:[m.jsx("span",{className:"truncate min-w-0 text-[14px] font-medium text-[#ededed]",children:"Strix"}),m.jsx("span",{className:"flex h-5 flex-shrink-0 items-center rounded px-2 text-[11px] font-medium text-[#888] bg-[rgba(255,255,255,0.08)]",children:"Local"})]})]}),m.jsx("a",{href:ha("https://app.strix.ai","logo"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("logo","sidebar"),className:"flex flex-none items-center rounded-md px-1.5 py-2 transition-colors hover:bg-[rgba(255,255,255,0.06)]","aria-label":"Open Strix Cloud",children:m.jsx(cC,{className:"h-4 w-4 text-[#666]"})})]})})}),m.jsx("nav",{className:"relative min-w-[160px] flex-1 overflow-y-auto overflow-x-clip scrollbar-thin pb-10 pt-2",children:m.jsxs("div",{className:"relative flex flex-col gap-px px-2",children:[m.jsx(yi,{icon:m.jsx(pH,{}),label:"Pentest Overview",active:e==="overview",onClick:()=>t("overview")}),m.jsx(yi,{icon:m.jsx(Nu,{className:"h-4 w-4"}),label:"Issues",count:r>0?r:void 0,active:e==="issues",onClick:()=>t("issues")}),a>0&&m.jsx(yi,{icon:m.jsx(Oo,{className:"h-4 w-4"}),label:"Agents",count:a,active:e==="agents",onClick:()=>t("agents")}),m.jsx(yi,{icon:m.jsx(Ys,{className:"h-4 w-4"}),label:"Past runs",count:s>0?s:void 0,active:e==="history",onClick:h}),o&&m.jsx(yi,{icon:m.jsx(Np,{className:"h-4 w-4"}),label:"Export report",active:e==="email",onClick:f}),m.jsx(yi,{icon:m.jsx(k2,{className:"h-4 w-4"}),label:"Feedback & support",active:e==="feedback",onClick:()=>t("feedback")}),m.jsx("hr",{className:"mx-0 my-1 h-px w-full border-0 bg-[rgba(255,255,255,0.08)]"}),m.jsx(yi,{icon:m.jsx(oH,{className:"h-4 w-4"}),label:"PR Security Reviews",active:!1,onClick:()=>R("pr_reviews","Strix reviews every pull request and flags exploitable changes before they merge.")}),m.jsx(yi,{icon:m.jsx(cH,{className:"h-4 w-4"}),label:"Integrations",active:!1,onClick:()=>R("integrations","Sync findings to Jira, Linear, and Slack so fixes happen where your team already works.")}),m.jsx(yi,{icon:m.jsx(MT,{className:"h-4 w-4"}),label:"Members",active:!1,onClick:()=>R("members","Invite your team, set roles, and share findings and run history across your org.")})]})}),m.jsx("section",{className:"flex min-w-[160px] flex-col gap-0.5",ref:B,children:m.jsxs("div",{className:"relative p-2",children:[c&&d?m.jsxs("button",{onClick:()=>k(V=>!V),className:"relative flex w-full cursor-pointer items-center gap-2 rounded-md bg-transparent px-2.5 py-2 transition-colors hover:bg-[rgba(255,255,255,0.06)]",children:[m.jsx("span",{className:"flex flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500",style:{width:20,height:20},children:m.jsx("span",{className:"text-[9px] font-semibold text-white",children:((z=d[0])==null?void 0:z.toUpperCase())||"U"})}),m.jsxs("span",{className:"flex min-w-0 flex-1 flex-col text-left",children:[m.jsx("span",{className:"truncate text-[13px] font-medium text-[#ededed]",children:d}),m.jsx("span",{className:"truncate text-[11px] text-[#555]",children:"Linked to this machine"})]})]}):m.jsxs("div",{className:"flex items-center gap-2 rounded-md px-2.5 py-2",children:[m.jsx("span",{className:"flex flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500",style:{width:20,height:20},children:m.jsx("span",{className:"text-[9px] font-semibold text-white",children:"S"})}),m.jsx("span",{className:"flex min-w-0 flex-1 flex-col text-left",children:m.jsx("span",{className:"truncate text-[13px] font-medium text-[#ededed]",children:"Local viewer"})})]}),w&&c&&d&&m.jsxs("div",{className:"absolute bottom-full left-2 right-2 z-50 mb-1 overflow-hidden rounded-lg border border-[#333] bg-black shadow-xl",children:[m.jsxs("div",{className:"border-b border-[#333] px-3 py-2",children:[m.jsx("p",{className:"truncate text-[13px] font-medium text-white",children:"Linked email"}),m.jsx("p",{className:"truncate text-[11px] text-[#666]",children:d})]}),m.jsxs("button",{onClick:()=>{k(!1),p()},className:"flex w-full items-center gap-2 px-3 py-2 text-[13px] text-[#888] transition-colors hover:bg-[rgba(255,255,255,0.06)] hover:text-red-400",children:[m.jsx(WC,{className:"h-4 w-4"}),"Forget this email"]})]})]})}),m.jsx("div",{className:"group absolute right-0 top-0 z-30 h-full w-1 cursor-col-resize",onMouseDown:j,children:m.jsx("div",{className:Mr("absolute right-0 top-0 h-full w-px bg-[rgba(255,255,255,0.08)] transition-all duration-100",E?"w-0.5 bg-[rgba(255,255,255,0.3)]":"group-hover:bg-[rgba(255,255,255,0.2)]")})})]}),E&&m.jsx("div",{className:"fixed inset-0 z-10 cursor-col-resize"}),m.jsx(dH,{open:N!==null,description:N??"",source:"sidebar",onClose:()=>M(null)})]})}function yi({icon:e,label:t,active:r,onClick:a,count:s}){return m.jsxs("button",{onClick:a,className:Mr("group flex h-9 w-full origin-left flex-row items-center rounded-md transition-colors",r?"bg-[rgba(255,255,255,0.12)] text-white":"text-[#888] hover:bg-[rgba(255,255,255,0.06)] hover:text-[#ededed]"),children:[m.jsx("div",{className:"grid flex-none place-content-center",style:{width:36,height:36},children:e}),m.jsx("span",{className:"min-w-0 flex-1 truncate text-left text-[14px] font-medium",children:t}),s!=null&&m.jsx("span",{className:"mr-2 flex-none rounded-full border border-white/10 px-2 py-0.5 text-[10px] tabular-nums leading-none text-[#777]",children:s})]})}function pH(){return m.jsx("svg",{style:{width:16,height:16,color:"currentcolor"},viewBox:"0 0 16 16",fill:"currentColor",children:m.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2.5 5.5V2.5H5.5V5.5H2.5ZM1 2C1 1.44772 1.44772 1 2 1H6C6.55228 1 7 1.44772 7 2V6C7 6.55228 6.55228 7 6 7H2C1.44772 7 1 6.55228 1 6V2ZM2.5 13.5V10.5H5.5V13.5H2.5ZM1 10C1 9.44772 1.44772 9 2 9H6C6.55228 9 7 9.44772 7 10V14C7 14.5523 6.55228 15 6 15H2C1.44772 15 1 14.5523 1 14V10ZM10.5 2.5V5.5H13.5V2.5H10.5ZM10 1C9.44772 1 9 1.44772 9 2V6C9 6.55228 9.44772 7 10 7H14C14.5523 7 15 6.55228 15 6V2C15 1.44772 14.5523 1 14 1H10ZM10.5 13.5V10.5H13.5V13.5H10.5ZM9 10C9 9.44772 9.44772 9 10 9H14C14.5523 9 15 9.44772 15 10V14C15 14.5523 14.5523 15 14 15H10C9.44772 15 9 14.5523 9 14V10Z"})})}const C_={work_email_required:"Please use your work email, not a personal one.",rate_limited:"Too many requests. Wait a minute and try again.",invalid_email:"That email does not look right. Check it and try again.",unavailable:"The email service is unavailable right now. Try again shortly."},gH=new Set(["gmail.com","googlemail.com","yahoo.com","ymail.com","outlook.com","hotmail.com","live.com","icloud.com","me.com","aol.com","proton.me","protonmail.com","gmx.com","mail.com"]);function xH({onVerified:e}){const[t,r]=ee.useState("email"),[a,s]=ee.useState(""),[o,c]=ee.useState(""),[d,f]=ee.useState(!1),[h,p]=ee.useState(null),[g,y]=ee.useState(null),b=async()=>{const E=a.trim();if(!E){p("Enter your email to continue.");return}const S=E.slice(E.lastIndexOf("@")+1).toLowerCase();if(gH.has(S)){Tr("work_email_required"),p(C_.work_email_required);return}f(!0),p(null);const w=await _2(E);f(!1),w.ok?(Tr("email_submitted",{purpose:"verify"}),y(`We sent a 6-digit code to ${E}.`),r("code")):(w.error==="work_email_required"&&Tr("work_email_required"),p(C_[w.error]??"Could not send a code. Try again."))},_=async()=>{const E=o.trim();if(E.length<4){p("Enter the 6-digit code from your email.");return}f(!0),p(null);const S=await w2(a.trim(),E);if(f(!1),!S.verified){p("That code did not match. Check it and try again.");return}Tr("email_verified",{purpose:"verify"}),e()};return m.jsxs("div",{className:"mx-auto mt-5 max-w-sm text-left",children:[h&&m.jsxs("div",{className:"mb-3 flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2",children:[m.jsx(Gu,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs text-red-300",children:h})]}),g&&!h&&m.jsx("p",{className:"mb-3 text-xs text-[#888]",children:g}),t==="email"?m.jsxs("form",{className:"space-y-3",onSubmit:E=>{E.preventDefault(),b()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your work email"}),m.jsx("input",{type:"email",autoFocus:!0,value:a,onChange:E=>s(E.target.value),placeholder:"you@company.com",className:"w-full rounded-lg bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}}),m.jsx("span",{className:"mt-1.5 block text-[11px] text-[#666]",children:"Use your work email."})]}),m.jsxs("button",{type:"submit",disabled:d,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[d&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),"Send me a code"]})]}):m.jsxs("form",{className:"space-y-3",onSubmit:E=>{E.preventDefault(),_()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"6-digit code"}),m.jsx("input",{inputMode:"numeric",autoFocus:!0,value:o,onChange:E=>c(E.target.value.replace(/\D/g,"").slice(0,6)),placeholder:"123456",className:"w-full rounded-lg bg-black px-3 py-2.5 text-center text-lg font-mono tracking-[0.4em] text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}})]}),m.jsxs("button",{type:"submit",disabled:d,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[d&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),"Verify"]}),m.jsx("button",{type:"button",onClick:()=>{r("email"),p(null),y(null)},className:"w-full cursor-pointer text-center text-xs text-[#666] transition-colors hover:text-[#aaa]",children:"Use a different email"})]})]})}const bH=[{key:"critical",dot:"bg-red-500",text:"text-red-500"},{key:"high",dot:"bg-orange-500",text:"text-orange-500"},{key:"medium",dot:"bg-yellow-500",text:"text-yellow-500"},{key:"low",dot:"bg-blue-500",text:"text-blue-500"}];function yH({counts:e}){const t=bH.filter(r=>e[r.key]>0);return t.length===0?m.jsx("span",{className:"text-xs text-[#555]",children:"No findings"}):m.jsx("div",{className:"flex items-center gap-3",children:t.map(r=>m.jsxs("div",{className:"flex items-center gap-1.5",children:[m.jsx("span",{className:`h-2 w-2 rounded-full ${r.dot}`,"aria-hidden":"true"}),m.jsx("span",{className:`text-xs tabular-nums ${r.text}`,children:e[r.key]})]},r.key))})}function vH(e){if(!e)return null;const t=e.trim().replace(" UTC","Z").replace(" ","T"),r=new Date(t);return Number.isNaN(r.getTime())?null:r.toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function T_(e){if(!e)return null;const t=e.trim().replace(" UTC","Z").replace(" ","T"),r=new Date(t);if(Number.isNaN(r.getTime()))return null;const a=Date.now()-r.getTime(),s=Math.floor(a/6e4);if(s<1)return"just now";if(s<60)return`${s}m ago`;const o=Math.floor(s/60);if(o<24)return`${o}h ago`;const c=Math.floor(o/24);return c<7?`${c}d ago`:vH(e)}function _H({runs:e,activeRun:t,onSelectRun:r,onVerified:a}){const s=(e==null?void 0:e.count)??0,[o,c]=ee.useState(!1);return!e||e.locked?m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center",children:[m.jsx("div",{className:"mx-auto mb-4 flex h-11 w-11 items-center justify-center rounded-xl",style:{border:"1px solid #2a2a2a",background:"rgba(255,255,255,0.04)"},children:m.jsx(Ys,{className:"h-5 w-5 text-[#888]","aria-hidden":"true"})}),m.jsx("h2",{className:"text-base font-semibold text-white",children:"Browse every run on this machine"}),m.jsxs("p",{className:"mx-auto mt-1.5 max-w-md text-sm text-[#888]",children:["You have ",s," past ",s===1?"run":"runs"," on this machine."]}),o?m.jsxs(m.Fragment,{children:[m.jsx("p",{className:"mx-auto mt-3 max-w-sm text-xs text-[#666]",children:"Verify your email with a one-time code to unlock the full history."}),m.jsx(xH,{onVerified:a})]}):m.jsx("button",{onClick:()=>{jr("history_unlock","past_runs"),c(!0)},className:"mt-4 cursor-pointer rounded-lg bg-white px-4 py-2 text-sm font-semibold text-black transition-opacity hover:opacity-90",children:"View runs"}),m.jsxs("p",{className:"mt-4 flex items-center justify-center gap-1.5 text-xs text-[#555]",children:[m.jsx(K_,{className:"h-3.5 w-3.5","aria-hidden":"true"}),"Or open one from the CLI with"," ",m.jsx("code",{className:"font-mono text-[#888]",children:"strix view "})]})]}):e.runs.length===0?m.jsx("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center text-sm text-[#888]",children:"No past runs found on this machine yet."}):m.jsx("div",{className:"space-y-2",children:e.runs.map(d=>{const f=d.name===t,h=T_(d.start_time)??T_(d.end_time),p=yo(d.target,d.name);return m.jsxs("button",{onClick:()=>r(d.name),className:`animate-card-in group flex w-full cursor-pointer items-center gap-4 rounded-lg border px-4 py-3 text-left transition-colors ${f?"border-[#444] bg-[rgba(255,255,255,0.04)]":"border-[#222] bg-[rgba(255,255,255,0.02)] hover:border-[#444]"}`,children:[m.jsxs("div",{className:"min-w-0 flex-1",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"truncate text-sm font-medium text-white",children:p}),f&&m.jsx("span",{className:"rounded-full px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-400",style:{border:"1px solid rgba(16,185,129,0.3)"},children:"Active"})]}),m.jsxs("div",{className:"mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-[#666]",children:[d.scan_mode&&m.jsx("span",{className:"capitalize",children:d.scan_mode}),d.scan_mode&&(h||d.status)&&m.jsx("span",{className:"text-[#333]",children:"·"}),h&&m.jsx("span",{children:h}),h&&d.status&&m.jsx("span",{className:"text-[#333]",children:"·"}),d.status&&m.jsx("span",{className:"capitalize",children:d.status})]})]}),m.jsx(yH,{counts:d.severity_counts}),m.jsx(sC,{className:"h-4 w-4 flex-shrink-0 text-[#555] transition-colors group-hover:text-[#aaa]","aria-hidden":"true"})]},d.name)})})}const A_={work_email_required:"Please use your work email, not a personal one.",rate_limited:"Too many requests. Wait a minute and try again.",invalid_email:"That email does not look right. Check it and try again.",unavailable:"The email service is unavailable right now. Try again shortly."},wH={forbidden:"This email was unsubscribed from Strix, so we cannot send to it.",too_large:"This report is too large to email. Try a smaller run.",unavailable:"The email service is unavailable right now. Try again shortly."},EH=new Set(["gmail.com","googlemail.com","yahoo.com","ymail.com","outlook.com","hotmail.com","live.com","icloud.com","me.com","aol.com","proton.me","protonmail.com","gmx.com","mail.com"]);function NH({activeRun:e,auth:t,purpose:r,skipDisclosure:a=!1,onAuthChanged:s,onExit:o}){const c=(t==null?void 0:t.verified)===!0,d=r==="verify",[f,h]=ee.useState(()=>d?"email":a?c?"sending":"email":"disclosure"),[p,g]=ee.useState((t==null?void 0:t.email)??""),[y,b]=ee.useState(""),[_,E]=ee.useState(!1),[S,w]=ee.useState(null),[k,N]=ee.useState(null),[M,B]=ee.useState(""),[R,U]=ee.useState(""),[I,X]=ee.useState(!1),[j,z]=ee.useState(""),V=ee.useRef(!1),P=async()=>{h("sending"),w(null);const Z=await QU(e);if(Z.ok){Tr("report_sent"),B(Z.password),U(Z.filename),h("password");return}if(Z.error==="reverify"||Z.error==="unverified"){N("Your verification expired. Enter your email to verify again."),h("email");return}w(wH[Z.error]??"Could not send the report. Try again."),h("disclosure")},T=()=>{w(null),N(null),c?P():h("email")};ee.useEffect(()=>{!d&&a&&c&&!V.current&&(V.current=!0,P())},[]);const $=async()=>{const Z=p.trim();if(!Z){w("Enter your email to continue.");return}const C=Z.slice(Z.lastIndexOf("@")+1).toLowerCase();if(EH.has(C)){Tr("work_email_required"),w(A_.work_email_required);return}E(!0),w(null);const D=await _2(Z);E(!1),D.ok?(Tr("email_submitted",{purpose:r}),N(`We sent a 6-digit code to ${Z}.`),h("code")):(D.error==="work_email_required"&&Tr("work_email_required"),w(A_[D.error]??"Could not send a code. Try again."))},O=async()=>{const Z=y.trim();if(Z.length<4){w("Enter the 6-digit code from your email.");return}E(!0),w(null);const C=await w2(p.trim(),Z);if(E(!1),!C.verified){w("That code did not match. Check it and try again.");return}Tr("email_verified",{purpose:r}),z(C.email),s(),d?o("history"):P()},H=async()=>{try{await navigator.clipboard.writeText(M),X(!0),setTimeout(()=>X(!1),1500)}catch{}},K=j||(t==null?void 0:t.email)||p.trim();return m.jsxs("div",{className:"mx-auto max-w-xl space-y-4",children:[m.jsxs("button",{onClick:()=>o(d?"history":"overview"),className:"cursor-pointer inline-flex items-center gap-1.5 text-sm text-[#888] transition-colors hover:text-white",children:[m.jsx(Ep,{className:"h-4 w-4"}),d?"Back to past runs":"Back to results"]}),m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Np,{className:"h-5 w-5 text-[#888]","aria-hidden":"true"}),m.jsx("h1",{className:"text-2xl font-semibold text-white",children:d?"Verify your email":"Export report to PDF"})]}),m.jsxs("div",{className:"w-full rounded-2xl bg-[rgba(255,255,255,0.02)] p-6",style:{border:"1px solid #2a2a2a"},children:[m.jsx("p",{className:"mb-4 text-xs text-[#666]",children:d?"We send a one-time code to confirm it is you.":"Verified by a one-time code sent to your email"}),S&&m.jsxs("div",{className:"mb-4 flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2",children:[m.jsx(Gu,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs text-red-300",children:S})]}),k&&!S&&f!=="password"&&m.jsx("p",{className:"mb-4 text-xs text-[#888]",children:k}),f==="disclosure"&&m.jsxs("div",{className:"space-y-4",children:[m.jsxs("div",{className:"space-y-2.5 rounded-lg p-3.5",style:{border:"1px solid #222",background:"rgba(255,255,255,0.02)"},children:[m.jsxs("div",{className:"flex items-start gap-2.5",children:[m.jsx(X_,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsxs("p",{className:"text-xs leading-relaxed text-[#aaa]",children:["We email an ",m.jsx("span",{className:"text-white",children:"encrypted PDF"}),". Nothing else leaves your machine."]})]}),m.jsxs("div",{className:"flex items-start gap-2.5",children:[m.jsx(ZC,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-[#888]","aria-hidden":"true"}),m.jsx("p",{className:"text-xs leading-relaxed text-[#aaa]",children:"Only you hold the password; Strix can't read it."})]})]}),m.jsx("button",{onClick:T,className:"w-full cursor-pointer rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90",children:"Export report"}),c&&(t==null?void 0:t.email)&&m.jsxs("p",{className:"text-center text-xs text-[#666]",children:["Sending to ",t.email]})]}),f==="email"&&m.jsxs("form",{className:"space-y-4",onSubmit:Z=>{Z.preventDefault(),$()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your work email"}),m.jsx("input",{type:"email",autoFocus:!0,value:p,onChange:Z=>g(Z.target.value),placeholder:"you@company.com",className:"w-full rounded-lg bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}})]}),m.jsxs("button",{type:"submit",disabled:_,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[_&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),"Send me a code"]})]}),f==="code"&&m.jsxs("form",{className:"space-y-4",onSubmit:Z=>{Z.preventDefault(),O()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"6-digit code"}),m.jsx("input",{inputMode:"numeric",autoFocus:!0,value:y,onChange:Z=>b(Z.target.value.replace(/\D/g,"").slice(0,6)),placeholder:"123456",className:"w-full rounded-lg bg-black px-3 py-2.5 text-center text-lg font-mono tracking-[0.4em] text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}})]}),m.jsxs("button",{type:"submit",disabled:_,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[_&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),d?"Verify":"Verify and send"]}),m.jsx("button",{type:"button",onClick:()=>{h("email"),w(null),N(null)},className:"w-full cursor-pointer text-center text-xs text-[#666] transition-colors hover:text-[#aaa]",children:"Use a different email"})]}),f==="sending"&&m.jsxs("div",{className:"flex flex-col items-center gap-3 py-8",children:[m.jsx(Ps,{className:"h-6 w-6 animate-spin text-white","aria-hidden":"true"}),m.jsx("p",{className:"text-sm text-[#aaa]",children:"Generating and encrypting locally..."})]}),f==="password"&&m.jsxs("div",{className:"space-y-4",children:[m.jsxs("div",{className:"flex items-start gap-2.5 rounded-lg border border-emerald-500/30 bg-emerald-500/5 px-3 py-2.5",children:[m.jsx(Vs,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsxs("p",{className:"text-xs text-emerald-200",children:["Sent to ",K,". Open the attached PDF with this password."]})]}),m.jsxs("div",{children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your one-time password"}),m.jsxs("div",{className:"flex items-center gap-2 rounded-lg bg-black p-3",style:{border:"1px solid #2a2a2a"},children:[m.jsx("code",{className:"flex-1 break-all font-mono text-base text-white",children:M}),m.jsxs("button",{onClick:H,className:"flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs text-[#aaa] transition-colors hover:bg-[rgba(255,255,255,0.06)] hover:text-white",style:{border:"1px solid #2a2a2a"},children:[I?m.jsx(Vs,{className:"h-3.5 w-3.5"}):m.jsx(go,{className:"h-3.5 w-3.5"}),I?"Copied":"Copy"]})]}),m.jsxs("p",{className:"mt-2 text-xs text-[#666]",children:["Save this now. Strix never stores it, so we cannot show it again. File:"," ",m.jsx("span",{className:"font-mono text-[#888]",children:R})]})]}),m.jsx("button",{onClick:()=>o("overview"),className:"w-full cursor-pointer rounded-lg px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-[rgba(255,255,255,0.06)]",style:{border:"1px solid #2a2a2a"},children:"Done"})]})]})]})}function la(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}function ao(e){return Array.isArray(e)?e:[]}function nr(e){return typeof e=="string"&&e.trim()?e:null}function Ba(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function SH(e){return e.replace(/_/g," ")}function M_(e){return e&&e.charAt(0).toUpperCase()+e.slice(1)}function kH(e){if(e==null||e<0)return"n/a";const t=Math.floor(e/3600),r=Math.floor(e%3600/60),a=e%60;return t?`${t}h ${r}m ${a}s`:r?`${r}m ${a}s`:`${a}s`}function En({label:e,children:t}){return m.jsxs("div",{className:"grid grid-cols-[7rem_1fr] gap-3 items-baseline",children:[m.jsx("dt",{className:"text-[11px] uppercase tracking-wide text-[#666]",children:e}),m.jsx("dd",{className:"min-w-0 break-words text-sm text-[#ddd]",children:t})]})}function CH({raw:e,durationSeconds:t}){const[r,a]=ee.useState(!0),s=ao(e.targets_info).map(P=>{const T=la(P),$=nr(T.original)??nr(la(T.details).target_url)??"unknown target",O=nr(T.type);return{display:$,type:O?SH(O):null}}),o=nr(e.instruction),c=M_(nr(e.scan_mode)),d=nr(e.scope_mode),f=la(e.diff_scope),h=f.active===!0,p=nr(f.mode),g=nr(e.diff_base),y=e.non_interactive===!0,b=ao(e.local_sources).map(P=>{if(typeof P=="string")return P;const T=la(P);return nr(T.source_path)??nr(T.target_path)??""}).filter(Boolean),_=M_(nr(e.status));let E=d??"auto";h&&(E+=` (diff${p?`: ${p}`:""}${g?` vs ${g}`:""})`);const S=la(e.llm_usage),w=Object.keys(S).length>0,k=ao(S.agents).map(la),N=Array.from(new Set(k.map(P=>nr(P.model)).filter(P=>!!P))),M=Ba(S.requests),B=Ba(S.input_tokens),R=Ba(la(ao(S.input_tokens_details)[0]).cached_tokens),U=Ba(S.output_tokens),I=Ba(la(ao(S.output_tokens_details)[0]).reasoning_tokens),X=Ba(S.total_tokens),j=Ba(S.cost),z=nr(e.auth_mode)==="subscription",V=(P,T)=>m.jsxs("span",{className:"text-[#666]",children:[" (",Ls(P)," ",T,")"]});return m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsxs("button",{type:"button",onClick:()=>a(P=>!P),"aria-expanded":r,className:"flex w-full cursor-pointer items-center gap-2 text-left",children:[m.jsx(GC,{className:"h-4 w-4 text-[#888]","aria-hidden":"true"}),m.jsx("h2",{className:"text-sm font-semibold text-white",children:"Run details"}),r?m.jsx(U_,{className:"ml-auto h-4 w-4 text-[#666]","aria-hidden":"true"}):m.jsx(po,{className:"ml-auto h-4 w-4 text-[#666]","aria-hidden":"true"})]}),r&&m.jsxs("div",{className:"mt-4 grid grid-cols-1 gap-x-8 gap-y-6 md:grid-cols-2",children:[m.jsxs("section",{children:[m.jsx("h3",{className:"mb-3 text-[11px] font-semibold uppercase tracking-wide text-[#555]",children:"Configuration"}),m.jsxs("dl",{className:"space-y-2.5",children:[s.length>0&&m.jsx(En,{label:"Targets",children:m.jsx("div",{className:"space-y-1",children:s.map((P,T)=>m.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[m.jsx("span",{className:"font-mono text-[#ddd]",children:P.display}),P.type&&m.jsx("span",{className:"rounded-full border border-[#2a2a2a] px-1.5 py-0.5 text-[10px] text-[#888]",children:P.type})]},T))})}),m.jsx(En,{label:"Instruction",children:o?m.jsx("span",{className:"whitespace-pre-wrap",children:o}):m.jsx("span",{className:"text-[#666]",children:"None"})}),c&&m.jsx(En,{label:"Pentest mode",children:c}),m.jsx(En,{label:"Scope",children:E}),m.jsx(En,{label:"Mode",children:y?"Non-interactive":"Interactive"}),b.length>0&&m.jsx(En,{label:"Local sources",children:m.jsx("div",{className:"space-y-0.5 font-mono text-[#ddd]",children:b.map((P,T)=>m.jsx("div",{children:P},T))})}),_&&m.jsx(En,{label:"Status",children:_})]})]}),m.jsxs("section",{children:[m.jsx("h3",{className:"mb-3 text-[11px] font-semibold uppercase tracking-wide text-[#555]",children:"Usage & cost"}),w?m.jsxs("dl",{className:"space-y-2.5 tabular-nums",children:[m.jsx(En,{label:"Model",children:N.length?N.join(", "):"n/a"}),z&&m.jsx(En,{label:"Provider",children:m.jsx("span",{className:"inline-flex items-center gap-1.5",children:m.jsx("span",{className:"rounded-full border border-[#22c55e]/40 bg-[#22c55e]/10 px-2 py-0.5 text-[11px] text-[#22c55e]",children:"ChatGPT subscription"})})}),m.jsx(En,{label:"Run time",children:kH(t)}),M!=null&&m.jsx(En,{label:"Requests",children:Ls(M)}),B!=null&&m.jsxs(En,{label:"Input tokens",children:[Ls(B),R!=null&&V(R,"cached")]}),U!=null&&m.jsxs(En,{label:"Output tokens",children:[Ls(U),I!=null&&V(I,"reasoning")]}),X!=null&&m.jsx(En,{label:"Total tokens",children:Ls(X)}),z?m.jsxs(En,{label:"Cost",children:[m.jsx("span",{className:"text-[#22c55e]",children:"$0.00"}),m.jsx("span",{className:"text-[#666]",children:" (subscription)"})]}):j!=null&&m.jsxs(En,{label:"Cost",children:["$",j.toFixed(2)]}),k.length>0&&m.jsx(En,{label:"Agents",children:Ls(k.length)})]}):m.jsx("p",{className:"text-sm text-[#666]",children:"Not available yet."})]})]})]})}const O_="strix_viewer_trust_dismissed";function TH({message:e}){const[t,r]=ee.useState(()=>{try{return localStorage.getItem(O_)==="1"}catch{return!1}});if(t)return null;const a=()=>{try{localStorage.setItem(O_,"1")}catch{}r(!0)};return m.jsx("div",{className:"fixed bottom-3 left-3 z-[60] max-w-xs rounded-lg bg-[#0a0a0a] p-3 shadow-2xl",style:{border:"1px solid #2a2a2a"},role:"status",children:m.jsxs("div",{className:"flex gap-2.5",children:[m.jsx(X_,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs leading-relaxed text-[#aaa]",children:e}),m.jsx("button",{onClick:a,"aria-label":"Dismiss",className:"-mr-0.5 -mt-0.5 flex-shrink-0 cursor-pointer rounded p-0.5 text-[#666] transition-colors hover:text-white",children:m.jsx(Sp,{className:"h-3.5 w-3.5"})})]})})}const AH=5e3,R_={invalid_email:"That email doesn't look right.",invalid_message:"Please write a little more.",unavailable:"Couldn't send that just now. Try again."};function MH({defaultEmail:e,onExit:t}){const[r,a]=ee.useState(""),[s,o]=ee.useState(e??""),[c,d]=ee.useState("form"),[f,h]=ee.useState(null),p=r.trim().length>0&&s.trim().length>0&&c!=="sending",g=async()=>{if(!p)return;d("sending"),h(null);const y=await XU(r.trim(),s.trim());if(y.ok){d("sent");return}d("form"),h(R_[y.error]??R_.unavailable)};return m.jsxs("div",{className:"mx-auto max-w-xl space-y-4",children:[m.jsxs("button",{onClick:()=>t("overview"),className:"cursor-pointer inline-flex items-center gap-1.5 text-sm text-[#888] transition-colors hover:text-white",children:[m.jsx(Ep,{className:"h-4 w-4"}),"Back to results"]}),m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(k2,{className:"h-5 w-5 text-[#888]","aria-hidden":"true"}),m.jsx("h1",{className:"text-2xl font-semibold text-white",children:"Feedback & support"})]}),m.jsx("div",{className:"w-full rounded-2xl bg-[rgba(255,255,255,0.02)] p-6",style:{border:"1px solid #2a2a2a"},children:c==="sent"?m.jsxs("div",{className:"flex items-start gap-3",children:[m.jsx(Eu,{className:"mt-0.5 h-5 w-5 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsxs("div",{className:"min-w-0",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Thanks, we got it."}),m.jsx("p",{className:"mt-1 text-xs text-[#888]",children:"We read every message. If it needs a reply, we'll reach out to the email you gave."}),m.jsx("button",{onClick:()=>{a(""),d("form")},className:"mt-4 cursor-pointer text-xs text-[#888] transition-colors hover:text-white",children:"Send more feedback"})]})]}):m.jsxs(m.Fragment,{children:[m.jsx("p",{className:"mb-4 text-xs text-[#666]",children:"Bugs, feature requests, or anything else. Tell us what's on your mind."}),f&&m.jsxs("div",{className:"mb-4 flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2",children:[m.jsx(Gu,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs text-red-300",children:f})]}),m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your feedback"}),m.jsx("textarea",{autoFocus:!0,value:r,maxLength:AH,onChange:y=>a(y.target.value),rows:5,placeholder:"What's working, what's not, what you'd love to see…",className:"w-full resize-y rounded-lg border border-[#2a2a2a] bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-white/50 focus:ring-2 focus:ring-white/10"})]}),m.jsxs("label",{className:"mt-4 block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your work email"}),m.jsx("input",{type:"email",value:s,onChange:y=>o(y.target.value),placeholder:"you@company.com",className:"w-full rounded-lg border border-[#2a2a2a] bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-white/50 focus:ring-2 focus:ring-white/10"})]}),m.jsx("button",{onClick:()=>void g(),disabled:!p,className:"mt-4 flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:c==="sending"?"Sending…":"Send feedback"})]})})]})}function OH({text:e,children:t,className:r=""}){const[a,s]=ee.useState(!1);return m.jsxs("span",{className:`relative inline-flex ${r}`,onMouseEnter:()=>s(!0),onMouseLeave:()=>s(!1),onFocus:()=>s(!0),onBlur:()=>s(!1),children:[t,a&&m.jsx("span",{role:"tooltip",className:"pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 w-max max-w-[240px] -translate-x-1/2 rounded-md px-2.5 py-1.5 text-xs text-[#ddd] shadow-lg",style:{border:"1px solid #2a2a2a",background:"#0a0a0a"},children:e})]})}function C2({label:e,desc:t,slug:r,icon:a,surface:s}){return m.jsx(OH,{text:t,children:m.jsxs("a",{href:ha(Yu,r),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr(r,s),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",children:[m.jsx(a,{className:"h-4 w-4 text-[#888] transition-colors group-hover:text-white","aria-hidden":"true"}),m.jsx("span",{children:e})]})})}const RH="Your findings stay on your machine. They're rendered here locally in your browser and never uploaded or stored by Strix.",j_=["critical","high","medium","low"],jH=500;function DH(){const[e,t]=ee.useState(null),[r,a]=ee.useState(null),[s,o]=ee.useState(null),[c,d]=ee.useState(null),[f,h]=ee.useState("overview"),[p,g]=ee.useState(null),[y,b]=ee.useState(null),[_,E]=ee.useState("report"),[S,w]=ee.useState(!1),[k,N]=ee.useState(!1),M=ee.useCallback(async()=>{try{g(await KU())}catch{}},[]),B=ee.useCallback(async()=>{try{b(await GU())}catch{}},[]);ee.useEffect(()=>{M(),B(),VU().then(C=>N(C.can_steer)).catch(()=>{})},[M,B]);const R=ee.useRef(!1);ee.useEffect(()=>{let C=!1,D;R.current=!1;const Y=()=>{D=setTimeout(L,jH)},L=async()=>{if(!C)try{const{summary:G,raw:q,finished:Q}=await b2(e);if(C)return;if(Q&&!R.current){R.current=!0;const te=await __(e);C||a(te);return}const[J,W]=await Promise.all([v2(e).catch(()=>({agents:[],events:[]})),y2(G.runId,e).catch(()=>[])]);if(C)return;a(te=>({summary:G,raw:q,finished:Q,transcript:J,vulnerabilities:W,reportMarkdown:(te==null?void 0:te.reportMarkdown)??null})),Y()}catch(G){if(C)return;o(G instanceof Error?G.message:"Could not load run data."),Y()}};return(async()=>{try{const G=await __(e);if(C)return;a(G),G.finished?R.current=!0:Y()}catch(G){if(C)return;o(G instanceof Error?G.message:"Could not load run data."),Y()}})(),()=>{C=!0,D&&clearTimeout(D)}},[e]);const U=ee.useMemo(()=>r?PU(r.vulnerabilities):null,[r]),I=(r==null?void 0:r.vulnerabilities.find(C=>C.id===c))??null,X=(r==null?void 0:r.transcript.agents.length)??0,j=(p==null?void 0:p.verified)===!0,z=ee.useRef(!1);ee.useEffect(()=>{z.current=!1},[e]),ee.useEffect(()=>{z.current||!r||(r.finished?(z.current=!0,h("overview")):X>0&&(z.current=!0,h("agents")))},[r,X]);const V=ee.useCallback(C=>{z.current=!0,h(C)},[]),P=ee.useCallback(C=>{t(C),d(null),a(null),o(null),z.current=!1},[]),T=ee.useCallback((C,D)=>{jr("email_report",D),E("report"),w(C),V("email")},[V]),$=ee.useCallback(()=>T(!1,"sidebar"),[T]),O=ee.useCallback(()=>T(!0,"overview"),[T]),H=ee.useCallback(()=>{B(),V("history")},[B,V]),K=ee.useCallback(async()=>{await M(),await B()},[M,B]),Z=ee.useCallback(async()=>{await ZU(),await M(),await B()},[M,B]);return m.jsxs("div",{className:"min-h-screen bg-black text-white flex",children:[m.jsx(mH,{view:f,onSelectView:C=>{d(null),C==="history"?H():V(C)},issuesCount:(r==null?void 0:r.vulnerabilities.length)??0,agentCount:X,runCount:(y==null?void 0:y.count)??0,finished:(r==null?void 0:r.finished)??!1,verified:j,email:(p==null?void 0:p.email)??null,onOpenEmail:$,onOpenHistory:H,onForget:()=>void Z()}),m.jsxs("div",{className:"flex-1 min-w-0",children:[m.jsx("div",{className:"border-b border-[#222]",children:m.jsxs("div",{className:"max-w-[88rem] mx-auto px-3 sm:px-6 py-4 flex items-center gap-1.5",children:[m.jsxs("a",{href:ha("https://app.strix.ai","logo"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("logo","topbar"),className:"flex items-center gap-1.5 opacity-90 transition-opacity hover:opacity-100 lg:hidden",title:"Open Strix Cloud",children:[m.jsx("img",{src:"./logo.png",alt:"Strix",className:"w-10 h-8 object-cover"}),m.jsx("div",{className:"text-base text-white font-medium tracking-tight",children:"Strix"})]}),r&&m.jsx(zH,{finished:r.finished}),m.jsxs("div",{className:"ml-auto flex items-center gap-3",children:[j&&y&&!y.locked&&y.runs.length>0&&m.jsx(LH,{runs:y,activeRun:e,launchedName:yo((r==null?void 0:r.summary.targets[0])??null,(r==null?void 0:r.summary.runName)??(r==null?void 0:r.summary.runId)??"Current run"),onSelect:P}),m.jsxs("a",{href:ha(Yu,"run_in_cloud"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("run_in_cloud","topbar"),className:"inline-flex items-center gap-1 rounded-lg bg-white px-3 py-1.5 text-xs font-semibold text-black transition-opacity hover:opacity-90",children:["Run in the cloud",m.jsx(z_,{className:"w-3 h-3","aria-hidden":"true"})]})]})]})}),m.jsxs("div",{className:"max-w-[88rem] mx-auto px-3 sm:px-6 py-8 sm:py-12 space-y-6",children:[s&&!r&&f!=="history"&&f!=="email"&&m.jsxs("div",{className:"rounded-lg px-4 py-3 flex gap-3 items-start border border-red-500/30 bg-red-500/5",children:[m.jsx(Gu,{className:"w-5 h-5 flex-shrink-0 mt-0.5 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-sm text-red-300",children:s})]}),m.jsx("div",{className:"animate-page-in space-y-6",children:f==="email"?m.jsx(NH,{activeRun:e,auth:p,purpose:_,skipDisclosure:S,onAuthChanged:()=>{M(),B()},onExit:C=>h(C==="history"?"history":"overview")}):f==="feedback"?m.jsx(MH,{defaultEmail:(p==null?void 0:p.email)??null,onExit:C=>h(C)}):f==="history"?m.jsxs("div",{className:"space-y-4",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Ys,{className:"w-5 h-5 text-[#888]","aria-hidden":"true"}),m.jsx("h1",{className:"text-2xl font-semibold text-white",children:"Past runs"})]}),m.jsx(_H,{runs:y,activeRun:e,onSelectRun:P,onVerified:()=>void K()})]}):!r&&!s?m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-10 text-center",children:[m.jsx("div",{className:"w-6 h-6 mx-auto mb-3 rounded-full border-2 border-[#333] border-t-white animate-spin"}),m.jsx("p",{className:"text-sm text-[#888]",children:"Loading run data…"})]}):r&&U?m.jsxs(m.Fragment,{children:[m.jsx(BH,{summary:r.summary}),m.jsxs("div",{className:"flex gap-5 border-b border-[#2a2a2a] lg:hidden",children:[m.jsx(Pm,{active:f==="overview",onClick:()=>V("overview"),children:"Pentest Overview"}),m.jsxs(Pm,{active:f==="issues",onClick:()=>V("issues"),children:["Issues",r.vulnerabilities.length>0?` (${r.vulnerabilities.length})`:""]}),X>0&&m.jsxs(Pm,{active:f==="agents",onClick:()=>V("agents"),children:["Agents (",X,")"]})]}),f==="overview"?m.jsx(PH,{summary:r.summary,counts:U,total:r.vulnerabilities.length,reportMarkdown:r.reportMarkdown,raw:r.raw,finished:r.finished,onOpenEmail:O}):f==="agents"&&X>0?m.jsx(FH,{run:r,canSteer:k}):I?m.jsxs("div",{className:"space-y-4",children:[m.jsxs("button",{onClick:()=>d(null),className:"cursor-pointer inline-flex items-center gap-1.5 text-sm text-[#888] hover:text-white transition-colors",children:[m.jsx(Ep,{className:"w-4 h-4"})," Back to all findings"]}),m.jsx(mD,{vulnerability:I})]}):m.jsx(UH,{vulnerabilities:r.vulnerabilities,finished:r.finished,onSelect:C=>d(C)})]}):null},`${e??"launched"}:${f}:${c??""}`)]})]}),m.jsx(TH,{message:RH})]})}function LH({runs:e,activeRun:t,launchedName:r,onSelect:a}){const[s,o]=ee.useState(!1),c=e.runs.find(f=>f.name===t),d=c?yo(c.target,c.name):r;return m.jsxs("div",{className:"relative",children:[m.jsxs("button",{onClick:()=>o(f=>!f),onBlur:()=>setTimeout(()=>o(!1),150),"aria-label":"Switch pentest",className:"flex items-center gap-2 rounded-lg border border-[#3a3a3a] bg-[rgba(255,255,255,0.05)] px-3 py-2 text-sm text-white transition-colors hover:border-[#555] hover:bg-[rgba(255,255,255,0.09)]",children:[m.jsx(Ys,{className:"h-4 w-4 flex-shrink-0 text-[#888]","aria-hidden":"true"}),m.jsx("span",{className:"flex-shrink-0 text-[#888]",children:"Pentest"}),m.jsx("span",{className:"max-w-[260px] truncate font-medium",children:d}),m.jsx(po,{className:"h-4 w-4 flex-shrink-0 text-[#aaa]","aria-hidden":"true"})]}),s&&m.jsxs("div",{className:"absolute right-0 z-50 mt-2 max-h-96 w-96 overflow-y-auto rounded-xl py-1.5 shadow-2xl",style:{border:"1px solid #3a3a3a",background:"#0a0a0a"},children:[m.jsx("div",{className:"border-b border-[#222] px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[#666]",children:"Switch pentest"}),e.runs.map(f=>{const h=f.name===t;return m.jsxs("button",{onMouseDown:()=>a(f.name),className:`flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm transition-colors hover:bg-[rgba(255,255,255,0.06)] ${h?"bg-[rgba(255,255,255,0.04)] text-white":"text-[#aaa]"}`,children:[m.jsxs("span",{className:"min-w-0 flex-1",children:[m.jsx("span",{className:"block truncate font-medium",children:yo(f.target,f.name)}),f.target&&m.jsx("span",{className:"block truncate font-mono text-xs text-[#666]",children:f.target})]}),h&&m.jsx("span",{className:"h-2 w-2 flex-shrink-0 rounded-full bg-emerald-400"})]},f.name)})]})]})}function zH({finished:e}){return e?m.jsxs("span",{className:"ml-3 inline-flex items-center gap-1.5 text-xs text-[#888]",children:[m.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-[#555]"}),"Complete"]}):m.jsxs("span",{className:"ml-3 inline-flex items-center gap-1.5 text-xs text-emerald-400",children:[m.jsxs("span",{className:"relative flex h-1.5 w-1.5",children:[m.jsx("span",{className:"absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75 animate-ping"}),m.jsx("span",{className:"relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-400"})]}),"Live"]})}function IH(e){if(e==null)return null;if(e<60)return`${e}s`;const t=Math.floor(e/60);return t<60?`${t}m`:`${Math.floor(t/60)}h ${t%60}m`}function BH({summary:e}){const t=IH(e.durationSeconds);return m.jsxs("div",{children:[m.jsx("h1",{className:"text-2xl font-semibold text-white",children:yo(e.targets[0]??null,e.runName??e.runId??"Pentest results")}),m.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[#888]",children:[e.targets.length>0&&m.jsx("span",{className:"font-mono text-[#aaa]",children:e.targets.join(", ")}),e.scanMode&&m.jsx(qm,{label:e.scanMode}),t&&m.jsx(qm,{label:t}),e.status&&m.jsx(qm,{label:e.status})]})]})}function qm({label:e}){return m.jsxs(m.Fragment,{children:[m.jsx("span",{className:"text-[#333]",children:"·"}),m.jsx("span",{className:"capitalize",children:e})]})}function UH({vulnerabilities:e,finished:t,onSelect:r}){const a=[...e].sort((s,o)=>j_.indexOf(s.severity)-j_.indexOf(o.severity));return a.length===0?m.jsxs("div",{className:"space-y-4",children:[m.jsx("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center text-sm text-[#888]",children:t?"No findings in this run.":"No findings yet. The pentest is still running…"}),t&&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-medium text-white",children:"Stay ahead of new exposures"}),m.jsx("p",{className:"mt-0.5 mb-3 text-xs text-[#666]",children:"Attack surface monitoring catches new exposures for your org over time."}),m.jsx(C2,{label:"Attack surface monitoring",desc:"Continuous coverage for your whole org.",slug:"asm",surface:"empty_state",icon:sT})]})]}):m.jsx("div",{className:"space-y-2",children:a.map(s=>m.jsxs("button",{onClick:()=>r(s.id),className:"animate-card-in cursor-pointer w-full text-left rounded-lg border border-[#222] hover:border-[#444] bg-[rgba(255,255,255,0.02)] px-4 py-3 transition-colors flex items-center gap-3",children:[m.jsx("span",{className:`w-2.5 h-2.5 rounded-full flex-shrink-0 ${kp(s.severity)}`,"aria-hidden":"true"}),m.jsxs("span",{className:"flex-1 min-w-0",children:[m.jsx("span",{className:"block text-sm font-medium text-white truncate",children:s.title}),s.target&&m.jsx("span",{className:"block text-xs text-[#666] font-mono truncate",children:s.target})]}),m.jsx("span",{className:`text-xs font-semibold px-2 py-0.5 rounded-full border capitalize ${Z_[s.severity]}`,children:s.severity})]},s.id))})}function HH(e){return e.replace(/^\s*#{1,6}[ \t]+.*(?:\r?\n)+/,"").trimStart()}function $H(e){const t=[];let r=null;for(const a of e.split(` +`)}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]",children:o.title??"(untitled)"}),m.jsxs("span",{className:"text-[#555] ml-1",children:["(",o.category??"general",")"]}),(o.by_you||o.agent_name)&&m.jsxs("span",{className:"text-[#666] ml-1 text-xs",children:["by ",o.by_you?"you":o.agent_name]}),o.content&&m.jsx("div",{className:"ml-3",children:m.jsx(ua,{text:o.content})})]},c))}):m.jsx("div",{className:"mt-1 text-[#555] text-xs",children:"No notes"})]})}return m.jsx("span",{className:"text-amber-400/80 font-semibold text-sm",children:"note"})}const nU={create_todo:{label:"Task added",Icon:Y_},list_todos:{label:"Plan",Icon:nC},update_todo:{label:"Task updated",Icon:nT},mark_todo_done:{label:"Task completed",Icon:H_},mark_todo_pending:{label:"Task reopened",Icon:fT},delete_todo:{label:"Task removed",Icon:CT}};function rU({status:e}){return e==="done"?m.jsx(H_,{className:"w-3.5 h-3.5 text-emerald-400/70 shrink-0"}):e==="in_progress"?m.jsx(mC,{className:"w-3.5 h-3.5 text-purple-400/70 shrink-0 animate-pulse"}):m.jsx($_,{className:"w-3.5 h-3.5 text-[#444] shrink-0"})}function iU({todos:e,highlightId:t}){return m.jsx("div",{className:"space-y-0",children:e.map((r,a)=>{const s=r.status??"pending",o=t&&r.id===t;return m.jsxs("div",{className:`flex items-start gap-2.5 py-1.5 px-2 -mx-2 rounded-md transition-colors ${o?"bg-purple-500/[0.08]":""}`,children:[m.jsx("div",{className:"mt-[1px]",children:m.jsx(rU,{status:s})}),m.jsx("span",{className:`text-[13px] leading-snug ${s==="done"?"text-[#555] line-through":s==="in_progress"?"text-[#bbb]":"text-[#999]"}`,children:r.title??"(untitled)"})]},r.id??a)})})}function aU({toolName:e,args:t,result:r}){const a=nU[e]??{label:"Plan",Icon:oT},s=a.Icon,o=r;if(typeof o=="string"&&o.trim())return m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-purple-400/60"}),m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:a.label})]}),m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:o.trim()})]});let c=[],d=null,f;if(o&&typeof o=="object"){if(d=o.error??null,o.success){const p=o.todos;c=Array.isArray(p)?p:[]}f=o.id??t.todo_id??void 0}const h=e!=="list_todos"?f:void 0;return c.length===0&&!d?m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-purple-400/60"}),m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:a.label})]}):m.jsxs("div",{children:[m.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-purple-400/60"}),m.jsx("span",{className:"text-purple-400/80 font-semibold text-sm",children:a.label})]}),d&&m.jsx("div",{className:"text-red-400/70 text-[13px] mb-2",children:d}),c.length>0&&m.jsx("div",{className:"rounded-lg border border-white/[0.06] bg-white/[0.015] px-3 py-2",children:m.jsx(iU,{todos:c,highlightId:h})})]})}function g_(e){if(e==null)return null;if(typeof e=="string")return e.trim()?e:null;if(typeof e=="object"){const t=e;if(typeof t.__raw=="string")return t.__raw;if(Object.keys(t).length===0)return null;try{return JSON.stringify(e,null,2)}catch{return String(e)}}return String(e)}function h2({toolName:e,args:t,result:r}){const a=g_(t),s=g_(r);return m.jsxs("div",{children:[m.jsx("span",{className:"text-[#888] font-semibold text-sm",children:e.replace(/_/g," ")}),a&&m.jsx(wi,{className:"text-[#777]",children:a}),s&&m.jsx(wi,{className:"text-[#666]",children:s})]})}function sU({args:e}){const t=e.skills,r=(Array.isArray(t)?t:String(t??"").split(",")).map(a=>String(a).trim()).filter(Boolean);return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"text-emerald-400/80 font-semibold text-sm",children:"Loading skill"}),r.length>0&&m.jsx("span",{className:"text-[#888] text-[13px]",children:r.join(", ")})]})}function lU({args:e}){const t=e.message??"";return t?m.jsxs("div",{children:[m.jsx(ua,{text:t}),m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:"waiting for your reply"})]}):null}const oU={reported:{label:"reported",color:"text-orange-400",Icon:Nu},no_issue_found:{label:"no issue found",color:"text-emerald-400",Icon:Eu},ruled_out:{label:"ruled out",color:"text-emerald-400/70",Icon:Eu},not_applicable:{label:"not applicable",color:"text-[#777]",Icon:bC},needs_follow_up:{label:"needs follow-up",color:"text-yellow-400",Icon:gC}},cU=["reported","needs_follow_up","no_issue_found","ruled_out","not_applicable"];function mo(e){const t=(e??"").trim().toLowerCase();return oU[t]??{label:t?t.replace(/_/g," "):"unrecorded",color:"text-[#777]",Icon:$_}}const uU={record_coverage:"Coverage recorded",update_coverage:"Coverage updated",list_coverage:"Coverage"};function gu({toolName:e}){return m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(q_,{className:"w-3.5 h-3.5 text-cyan-400/60"}),m.jsx("span",{className:"text-cyan-400/80 font-semibold text-sm",children:uU[e]??"Coverage"})]})}function dU({entry:e}){const{label:t,color:r,Icon:a}=mo(e.outcome),s=(e.previous_outcomes??[]).map(o=>mo(o).label).filter(Boolean);return m.jsxs("div",{className:"flex items-start gap-2.5 py-1.5",children:[m.jsx(a,{className:`w-3.5 h-3.5 shrink-0 mt-[2px] ${r}`}),m.jsxs("div",{className:"min-w-0",children:[m.jsxs("div",{className:"text-[13px] leading-snug",children:[m.jsx("span",{className:"text-[#bbb]",children:e.surface??"(unnamed surface)"}),e.risk_area&&m.jsxs("span",{className:"text-[#666]",children:[" · ",e.risk_area]})]}),m.jsxs("div",{className:"text-xs mt-0.5",children:[m.jsx("span",{className:r,children:t}),s.length>0&&m.jsxs("span",{className:"text-[#555]",children:[" (was ",s.join(" → "),")"]}),(e.by_you||e.agent_name)&&m.jsxs("span",{className:"text-[#555]",children:[" · ",e.by_you?"you":e.agent_name]})]}),e.evidence&&m.jsx("div",{className:"text-[#777] text-xs mt-1 leading-snug",children:e.evidence})]})]})}function fU({toolName:e,args:t,result:r}){const a=r;if(typeof a=="string"&&a.trim())return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:a.trim()})]});const s=a&&typeof a=="object"?a:null,o=t.surface??"",c=t.risk_area??"",d=t.evidence??"";if(s&&!s.success)return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),(o||c)&&m.jsxs("div",{className:"mt-1.5 text-[13px] text-[#bbb]",children:[o,c&&m.jsxs("span",{className:"text-[#666]",children:[" · ",c]})]}),m.jsx("div",{className:"mt-1 text-red-400/70 text-[13px]",children:s.error??"Coverage call failed"})]});if(e==="list_coverage"){const b=s==null?void 0:s.entries,_=Array.isArray(b)?b:[],E=(s==null?void 0:s.outcome_counts)??{},S=(s==null?void 0:s.total_count)??0;return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),Object.keys(E).length>0&&m.jsx("div",{className:"mt-2 flex items-center gap-3 flex-wrap",children:cU.filter(w=>E[w]).map(w=>{const{label:k,color:N}=mo(w);return m.jsxs("span",{className:`text-xs ${N}`,children:[k,": ",E[w]]},w)})}),_.length>0?m.jsx("div",{className:"mt-2 rounded-lg border border-white/[0.06] bg-white/[0.015] px-3 py-1 divide-y divide-white/[0.04]",children:_.map((w,k)=>m.jsx(dU,{entry:w},w.entry_id??k))}):m.jsx("div",{className:"mt-1.5 text-[#555] text-xs",children:S===0?"No surfaces recorded yet":"No surfaces match this filter"})]})}const f=(s==null?void 0:s.outcome)??"",h=(s==null?void 0:s.previous_outcome)??"",{label:p,color:g,Icon:y}=mo(f);return m.jsxs("div",{children:[m.jsx(gu,{toolName:e}),m.jsxs("div",{className:"mt-2 flex items-start gap-2.5",children:[m.jsx(y,{className:`w-3.5 h-3.5 shrink-0 mt-[2px] ${g}`}),m.jsxs("div",{className:"min-w-0",children:[m.jsxs("div",{className:"text-[13px] leading-snug text-[#bbb]",children:[o||(s!=null&&s.entry_id?`entry ${s.entry_id}`:"(unnamed surface)"),c&&m.jsxs("span",{className:"text-[#666]",children:[" · ",c]})]}),m.jsxs("div",{className:"text-xs mt-0.5",children:[h&&m.jsxs("span",{className:"text-[#666]",children:[mo(h).label," → "]}),m.jsx("span",{className:g,children:p})]}),d&&m.jsx("div",{className:"text-[#777] text-xs mt-1 leading-snug",children:d})]})]})]})}const hU={get_threat_model:{label:"Threat model",Icon:Vu},save_threat_model:{label:"Threat model saved",Icon:mT},amend_threat_model:{label:"Threat model amended",Icon:Y_}};function x_(e){const t=typeof e=="string"?e.trim():"";return!t||t==="unversioned"?"":t.slice(0,8)}function mU({toolName:e,args:t,result:r}){const a=hU[e]??{label:"Threat model",Icon:Vu},s=a.Icon,o=t.target??"",c=r,d=m.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[m.jsx(s,{className:"w-3.5 h-3.5 text-blue-400/60"}),m.jsx("span",{className:"text-blue-400/80 font-semibold text-sm",children:a.label}),o&&m.jsx("span",{className:"text-[#666] font-mono text-xs",children:o})]});if(typeof c=="string"&&c.trim())return m.jsxs("div",{children:[d,m.jsx("div",{className:"mt-1.5 text-[#888] text-[13px]",children:c.trim()})]});const f=c&&typeof c=="object"?c:null;if(f&&!f.success)return m.jsxs("div",{children:[d,m.jsx("div",{className:"mt-1.5 text-red-400/70 text-[13px]",children:f.error??"Threat model call failed"})]});if(e==="get_threat_model"){if(f&&!f.found)return m.jsxs("div",{children:[d,m.jsx("div",{className:"mt-1.5 text-[#555] text-xs",children:"No model cached for this target yet"})]});const y=f==null?void 0:f.amendments,b=Array.isArray(y)?y:[],_=x_(f==null?void 0:f.cached_revision);return m.jsxs("div",{children:[d,(f==null?void 0:f.stale)===!0&&m.jsxs("div",{className:"mt-1.5 flex items-center gap-1.5 text-yellow-400/80 text-xs",children:[m.jsx(Nu,{className:"w-3 h-3 shrink-0"}),m.jsxs("span",{children:["stale",_?` — written at ${_}`:""]})]}),b.length>0&&m.jsxs("div",{className:"mt-2",children:[m.jsxs("span",{className:"text-amber-400/70 text-xs font-semibold",children:[b.length," amendment",b.length===1?"":"s"]}),m.jsx("span",{className:"text-[#555] text-xs",children:" — later statements win"}),m.jsx("div",{className:"mt-1 space-y-1",children:b.map((E,S)=>m.jsxs("div",{className:"text-xs leading-snug",children:[m.jsx("span",{className:"text-[#666]",children:E.agent_name??"unknown agent"}),E.content&&m.jsxs("span",{className:"text-[#999]",children:[": ",E.content]})]},S))})]}),typeof(f==null?void 0:f.content)=="string"&&f.content.trim()&&m.jsx("div",{className:"mt-2",children:m.jsx(Yt,{text:f.content,maxLines:14})})]})}if(e==="amend_threat_model"){const y=t.addendum??"",b=f==null?void 0:f.amendment_count;return m.jsxs("div",{children:[d,b!=null&&m.jsxs("div",{className:"mt-1.5 text-[#666] text-xs",children:[b," amendment",b===1?"":"s"," on this model"]}),y&&m.jsx("div",{className:"mt-1.5",children:m.jsx(Yt,{text:y,maxLines:10})})]})}const h=(f==null?void 0:f.amendments_cleared)??0,p=x_(f==null?void 0:f.revision),g=t.content??"";return m.jsxs("div",{children:[d,p&&m.jsxs("div",{className:"mt-1.5 text-[#666] font-mono text-xs",children:["at ",p]}),h>0&&m.jsxs("div",{className:"mt-1.5 flex items-center gap-1.5 text-yellow-400/80 text-xs",children:[m.jsx(Nu,{className:"w-3 h-3 shrink-0"}),m.jsxs("span",{children:["cleared ",h," amendment",h===1?"":"s"]})]}),g&&m.jsx("div",{className:"mt-2",children:m.jsx(Yt,{text:g,maxLines:14})})]})}function pU(e){return!e||typeof e!="object"||Array.isArray(e)?[]:Object.entries(e).map(([t,r])=>{const a=typeof r=="string"?r:JSON.stringify(r);return`${t}: ${a??String(r)}`})}const b_=600;function gU(e){if(typeof e=="string"){const t=e.trim();return t?t.length>b_?`${t.slice(0,b_)}…`:t:null}return null}function xU({toolName:e,mcpTool:t,mcpConnection:r,args:a,result:s,status:o}){const c=pU(a),d=o==="failed"||o==="error",f=d?gU(s):null,h=e==="describe_mcp";return m.jsxs("div",{children:[m.jsx("div",{className:"flex items-center gap-2 flex-wrap",children:h?m.jsxs(m.Fragment,{children:[m.jsx("span",{className:"text-[13px] text-[#555]",children:"Inspecting MCP server"}),r&&m.jsx("span",{className:"font-mono text-teal-300 font-semibold text-sm",children:r})]}):m.jsxs(m.Fragment,{children:[m.jsx("span",{className:"font-mono text-teal-300 font-semibold text-sm",children:t||e}),m.jsx("span",{className:"text-[13px] text-[#555]",children:"via MCP server"}),r&&m.jsx("span",{className:"text-[13px] text-teal-400/80",children:r})]})}),c.length>0&&m.jsx("div",{className:"mt-1 font-mono text-[13px] leading-relaxed",children:c.map(p=>m.jsx("div",{className:"text-[#777] break-all",children:p},p))}),m.jsxs("div",{className:"mt-1 text-[13px]",children:[o==="running"&&m.jsx("span",{className:"text-[#666]",children:"Running"}),o==="completed"&&m.jsx("span",{className:"text-emerald-400/80",children:"✓ Done"}),d&&m.jsx("span",{className:"text-red-400/80",children:"✗ Failed"})]}),f&&m.jsx("pre",{className:"mt-1 font-mono text-[13px] leading-relaxed whitespace-pre-wrap break-words text-red-400/70",children:f})]})}const Ga={terminal:{renderer:h7,icon:K_,color:"text-emerald-400"},python:{renderer:Z7,icon:EC,color:"text-yellow-400"},browser:{renderer:p7,icon:G_,color:"text-blue-400"},filesystem:{renderer:g7,icon:MC,color:"text-sky-400"},proxy:{renderer:H7,icon:z_,color:"text-purple-400",match:/request|sitemap|scope/},reporting:{renderer:A7,icon:bT,color:"text-red-400"},thinking:{renderer:$7,icon:B_,color:"text-purple-400"},agents:{renderer:q7,icon:Oo,color:"text-cyan-400",match:/agent/},search:{renderer:P7,icon:gT,color:"text-amber-400"},lifecycle:{renderer:J7,icon:F_,color:"text-emerald-400"},notes:{renderer:tU,icon:NT,color:"text-amber-400",match:/note/},skills:{renderer:sU,icon:Gm,color:"text-emerald-400"},todos:{renderer:aU,icon:YC,color:"text-purple-400",match:/todo/},coverage:{renderer:fU,icon:q_,color:"text-cyan-400",match:/coverage/},threatModel:{renderer:mU,icon:Vu,color:"text-blue-400",match:/threat_model/},telemetry:{renderer:h2,icon:Gm,color:"text-[#555]"},mcp:{renderer:xU,icon:V_,color:"text-teal-400"}},bU={terminal:["exec_command","write_stdin","terminal_execute"],python:["python_action"],browser:["browser_action"],filesystem:["apply_patch","view_image","str_replace_editor","list_files","search_files"],proxy:["list_requests","view_request","repeat_request","list_sitemap","view_sitemap_entry","scope_rules","send_request"],reporting:["create_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"],lifecycle:["scan_start_info","subagent_start_info","finish_scan","respond_to_user"],notes:["create_note","delete_note","update_note","list_notes","get_note"],skills:["load_skill"],todos:["create_todo","list_todos","update_todo","mark_todo_done","mark_todo_pending","delete_todo"],coverage:["record_coverage","update_coverage","list_coverage"],threatModel:["get_threat_model","save_threat_model","amend_threat_model"],telemetry:["sandbox_error_details","llm_error_details"],mcp:[]},yU=Object.fromEntries(Object.entries(bU).flatMap(([e,t])=>t.map(r=>[r,e]))),vU={finish_scan:eU,respond_to_user:lU,apply_patch:E7,view_image:k7,list_reports:f_,get_report:f_},_U={agent_finish:{icon:F_,color:"text-cyan-400"},send_message_to_agent:{icon:yh,color:"text-cyan-400"},wait_for_agents:{icon:yh,color:"text-cyan-400"},respond_to_user:{icon:yh,color:"text-emerald-400"},view_agent_graph:{icon:TC,color:"text-cyan-400"},stop_agent:{icon:I_,color:"text-red-400"},scan_start_info:{icon:Vu,color:"text-emerald-400"},subagent_start_info:{icon:Oo,color:"text-purple-400"},view_image:{icon:PC,color:"text-sky-400"}},wU=Ga.telemetry;function m2(e){var r;const t=yU[e];if(t)return t;for(const[a,s]of Object.entries(Ga))if((r=s.match)!=null&&r.test(e))return a;return null}function EU(e,t){if(t)return Ga.mcp.renderer;const r=vU[e];if(r)return r;const a=m2(e);return a?Ga[a].renderer:h2}function NU(e,t){if(t)return{icon:Ga.mcp.icon,color:Ga.mcp.color};const r=_U[e];if(r)return r;const a=m2(e),s=a?Ga[a]:wU;return{icon:s.icon,color:s.color}}const SU=30;function kU({role:e,content:t}){const r=e==="user"||e==="human";return m.jsxs("div",{children:[m.jsx("span",{className:`font-semibold text-sm ${r?"text-blue-400/80":"text-purple-400/80"}`,children:r?"User":"Thinking"}),m.jsx("div",{className:"mt-1.5 italic text-[#888]",children:m.jsx(Yt,{text:t,maxLines:SU})})]})}class CU extends ee.Component{constructor(t){super(t),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}render(){return this.state.hasError?m.jsx("span",{className:"text-[#555] font-semibold text-sm",children:this.props.toolName.replace(/_/g," ")}):this.props.children}}function TU(e){const t=EU(e.toolName,e.mcpConnection);return m.jsx(CU,{toolName:e.toolName,children:m.jsx(t,{...e})})}function p2(e){if(e==null||typeof e!="string")return e;const t=e.trim();if(!t)return e;try{return JSON.parse(t)}catch{}try{const r=t.replace(/\bNone\b/g,"null").replace(/\bTrue\b/g,"true").replace(/\bFalse\b/g,"false").replace(/'/g,'"');return JSON.parse(r)}catch{return{__raw:e}}}function y_(e){return typeof e=="string"&&e?e:null}function g2(e){const t=p2(e);return t&&typeof t=="object"&&!Array.isArray(t)?t:t==null?{}:{__raw:typeof t=="string"?t:JSON.stringify(t)}}function v_(e){const t=/(\d+)$/.exec(e);return t?parseInt(t[1],10):0}function yg(e){var r;const t=(r=e.data)==null?void 0:r.role;return e.type==="chat"&&(t==="user"||t==="human")}function AU(e){var t;return yg(e)&&String(((t=e.data)==null?void 0:t.content)??"").startsWith("[Message from ")}function MU(e){const t=new Set;let r=!1;for(const a of e)if(yg(a)){if(AU(a)){t.add(a.id);continue}r||(r=!0,t.add(a.id))}return t}const OU={completed:"text-emerald-400 border-emerald-500/30 bg-emerald-500/10",running:"text-blue-400 border-blue-500/30 bg-blue-500/10",waiting:"text-yellow-400 border-yellow-500/30 bg-yellow-500/10",stopped:"text-[#aaa] border-[#333] bg-[#1a1a1a]",crashed:"text-red-400 border-red-500/30 bg-red-500/10",failed:"text-red-400 border-red-500/30 bg-red-500/10"};function RU(e){return e==="completed"?"completed":e==="running"?"running":e==="failed"||e==="crashed"?"failed":e}function jU(e,t){var d;const r=new Map;for(const f of e)if(f.parent_id){const h=r.get(f.parent_id)??[];h.push(f.id),r.set(f.parent_id,h)}const a=new Map,s=new Map,o=new Map;for(const f of t)if(f.type==="tool"){if(a.set(f.agent_id,(a.get(f.agent_id)??0)+1),((d=f.data)==null?void 0:d.tool_name)==="create_agent"){const h=g2(f.data.args),p=h.name??h.agent_name??"",g=h.task??"";p&&g&&o.set(p,g)}}else yg(f)||s.set(f.agent_id,(s.get(f.agent_id)??0)+1);const c=new Map;for(const f of e)c.set(f.id,{id:f.id,name:f.name,task:o.get(f.name)??"",status:RU(f.status),parentId:f.parent_id,children:r.get(f.id)??[],createdAt:f.created_at,toolCount:a.get(f.id)??0,messageCount:s.get(f.id)??0});return c}function DU({agent:e,events:t,showHeader:r=!0}){const a=ee.useMemo(()=>{const c=t.filter(f=>f.agent_id===e.id).sort((f,h)=>v_(f.id)-v_(h.id)),d=MU(c);return c.filter(f=>!d.has(f.id))},[t,e.id]),s=a.filter(c=>c.type==="tool").length,o=a.length-s;return m.jsxs("div",{children:[r&&m.jsxs(m.Fragment,{children:[m.jsxs("div",{className:"flex items-center gap-2 flex-wrap mb-1",children:[m.jsx("span",{className:"text-base font-semibold text-white truncate",children:e.name}),m.jsx("span",{className:`flex-shrink-0 text-xs font-medium capitalize px-2 py-0.5 rounded-full border ${OU[e.status]??"text-[#aaa] border-[#333] bg-[#1a1a1a]"}`,children:e.status}),m.jsx("span",{className:"font-mono text-xs text-[#555]",children:e.id})]}),m.jsxs("p",{className:"text-xs text-[#666] mb-4",children:[o," message",o===1?"":"s"," · ",s," tool call",s===1?"":"s"]})]}),a.length===0?m.jsx("p",{className:"text-sm text-[#666]",children:"No recorded activity for this agent."}):m.jsx("div",{className:"py-1",children:a.map((c,d)=>{var w,k,N,M,B,R,U,I;const f=d===a.length-1,h=c.type==="tool",p=h?String(((w=c.data)==null?void 0:w.tool_name)??"tool"):"",g=h?"":String(((k=c.data)==null?void 0:k.role)??"assistant"),y=y_((N=c.data)==null?void 0:N.mcp_connection),b=y_((M=c.data)==null?void 0:M.mcp_tool);let _,E;if(h){const X=NU(p,y);_=X.icon,E=X.color}else{const X=g==="user"||g==="human";_=X?Oo:B_,E=X?"text-blue-400":"text-purple-400"}const S=h?String(((B=c.data)==null?void 0:B.status)??"completed"):"completed";return m.jsxs("div",{className:"flex gap-3",children:[m.jsxs("div",{className:"flex flex-col items-center shrink-0",children:[m.jsx("div",{className:`w-[30px] h-[30px] rounded-full bg-black border flex items-center justify-center shrink-0 ${h&&S==="running"?"border-blue-500/40 animate-pulse":h&&S==="failed"?"border-red-500/30":"border-[#222]"}`,children:m.jsx(_,{className:`w-3.5 h-3.5 ${E}`})}),!f&&m.jsx("div",{className:"w-px flex-1 bg-[#1a1a1a] mt-1"})]}),m.jsx("div",{className:"flex-1 min-w-0 pt-[5px] pb-6",children:h?m.jsx(TU,{toolName:p,mcpConnection:y,mcpTool:b,args:g2((R=c.data)==null?void 0:R.args),result:p2((U=c.data)==null?void 0:U.result)??null,status:S}):m.jsx(kU,{role:g,content:String(((I=c.data)==null?void 0:I.content)??"")})})]},c.id)})})]})}class qu extends Error{constructor(t){super(t),this.name="RunParseError"}}const LU=["critical","high","medium","low"];function zU(e){const t=String(e??"").toLowerCase().trim();return LU.includes(t)?t:"low"}function IU(e){if(typeof e=="string"&&e.trim()){const t=e.trim().replace(" UTC","Z").replace(" ","T"),r=new Date(t);if(!Number.isNaN(r.getTime()))return r.toISOString();const a=new Date(e);if(!Number.isNaN(a.getTime()))return a.toISOString()}return new Date().toISOString()}function Ot(e){return typeof e=="string"&&e.length>0?e:null}function BU(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function x2(e,t){try{return JSON.parse(e)}catch{throw new qu(`${t} isn't valid JSON. Make sure you selected a Strix run directory.`)}}function UU(e){const t=x2(e,"run.json");if(!t||typeof t!="object"||Array.isArray(t))throw new qu("run.json is not an object.");const r=t,a=[],s=r.targets_info;if(Array.isArray(s)){for(const b of s)if(b&&typeof b=="object"){const _=b.original;typeof _=="string"&&_&&a.push(_)}}const o=Ot(r.start_time),c=Ot(r.end_time);let d=null;if(o&&c){const b=new Date(o).getTime(),_=new Date(c).getTime();!Number.isNaN(b)&&!Number.isNaN(_)&&_>=b&&(d=Math.round((_-b)/1e3))}let f=null,h=null,p=null,g=null;const y=r.scan_results;if(y&&typeof y=="object"){const b=y;f=Ot(b.executive_summary),h=Ot(b.technical_analysis),p=Ot(b.methodology),g=Ot(b.recommendations)}return{runId:Ot(r.run_id),runName:Ot(r.run_name),targets:a,scanMode:Ot(r.scan_mode),status:Ot(r.status),startTime:o,endTime:c,durationSeconds:d,executiveSummary:f,technicalAnalysis:h,methodology:p,recommendations:g}}function HU(){return{pr_review_id:null,cve:null,cvss:null,potential_risk_saving:null,risk_saving_description:null,impact:null,endpoint:null,method:null,target:null,technical_analysis:null,poc_description:null,poc_script_code:null,code_diff:null,code_file:null,code_before:null,code_after:null,cwe:null,code_locations:null,remediation_steps:null,fix_pr_body:null,evidence:null,assumptions:null,fix_effort:null,cvss_breakdown:null,status_changed_at:null,status_changed_by:null,status_note:null,snoozed_until:null,reopened_at:null,reopened_by:null,original_severity:null,severity_changed_at:null,severity_changed_by:null,severity_override_reason:null,retest_of_vulnerability_id:null}}function $U(e,t,r){const a=e.cwe,s=typeof a=="string"&&a.trim()?[a.trim()]:Array.isArray(a)?a.filter(c=>typeof c=="string"&&c):null;return{...HU(),id:Ot(e.id)??`vuln-${t+1}`,scan_id:r,title:Ot(e.title)??"Untitled finding",description:Ot(e.description)??"",severity:zU(e.severity),status:"open",created_at:IU(e.timestamp),cve:Ot(e.cve),cvss:BU(e.cvss),impact:Ot(e.impact),endpoint:Ot(e.endpoint),method:Ot(e.method),target:Ot(e.target),technical_analysis:Ot(e.technical_analysis),poc_description:Ot(e.poc_description),poc_script_code:Ot(e.poc_script_code),cwe:s,code_locations:Array.isArray(e.code_locations)?e.code_locations:null,remediation_steps:Ot(e.remediation_steps),fix_pr_body:Ot(e.fix_pr_body),evidence:Ot(e.evidence),assumptions:Ot(e.assumptions),fix_effort:Ot(e.fix_effort)??null,cvss_breakdown:e.cvss_breakdown??null}}function qU(e,t=null){const r=x2(e,"vulnerabilities.json");if(!Array.isArray(r))throw new qu("vulnerabilities.json is not a JSON array.");return r.map((a,s)=>{if(!a||typeof a!="object")throw new qu(`vulnerabilities.json entry #${s+1} is not an object.`);return $U(a,s,t)})}function PU(e){const t={critical:0,high:0,medium:0,low:0};for(const r of e)t[r.severity]+=1;return t}async function es(e){const t=await fetch(e,{cache:"no-store"});if(!t.ok)throw new Error(`${e} responded ${t.status}`);return t.json()}function dd(e){return e?`?run=${encodeURIComponent(e)}`:""}async function b2(e){const t=await es("/api/run"+dd(e)),r=UU(JSON.stringify(t)),a=t.finished===!0;return{summary:r,raw:t,finished:a}}async function y2(e,t){const r=await es("/api/vulnerabilities"+dd(t));return qU(JSON.stringify(r),e)}async function FU(e){const t=await es("/api/report"+dd(e));return(t==null?void 0:t.markdown)??null}async function v2(e){const t=await es("/api/transcript"+dd(e));return{agents:Array.isArray(t==null?void 0:t.agents)?t.agents:[],events:Array.isArray(t==null?void 0:t.events)?t.events:[]}}async function __(e){const{summary:t,raw:r,finished:a}=await b2(e),[s,o,c]=await Promise.all([y2(t.runId,e).catch(()=>[]),FU(e).catch(()=>null),v2(e).catch(()=>({agents:[],events:[]}))]);return{summary:t,raw:r,finished:a,vulnerabilities:s,reportMarkdown:o,transcript:c}}async function il(e,t){const r=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),cache:"no-store"});let a={};try{const s=await r.json();s&&typeof s=="object"&&(a=s)}catch{}return{ok:r.ok,status:r.status,data:a}}async function GU(){const e=await es("/api/runs");return{locked:(e==null?void 0:e.locked)??!0,count:typeof(e==null?void 0:e.count)=="number"?e.count:0,runs:Array.isArray(e==null?void 0:e.runs)?e.runs:[]}}async function VU(){const e=await es("/api/capabilities");return{can_steer:(e==null?void 0:e.can_steer)===!0}}async function YU(e,t){const{ok:r,data:a}=await il("/api/agents/steer",{agent_id:e,message:t});return r&&a.ok===!0?{ok:!0}:{ok:!1,error:String(a.error??"unavailable")}}async function XU(e,t){const{ok:r,data:a}=await il("/api/feedback",{message:e,email:t});return r&&a.ok===!0?{ok:!0}:{ok:!1,error:String(a.error??"unavailable")}}async function KU(){const e=await es("/api/auth/status");return{verified:(e==null?void 0:e.verified)===!0,email:(e==null?void 0:e.email)??null}}async function _2(e){const{ok:t,data:r}=await il("/api/auth/otp/start",{email:e});return t&&r.ok===!0?{ok:!0}:{ok:!1,error:String(r.error??"unavailable")}}async function w2(e,t){const{ok:r,data:a}=await il("/api/auth/otp/verify",{email:e,code:t});return r&&a.verified===!0?{verified:!0,email:String(a.email??e)}:{verified:!1,error:String(a.error??"invalid_code")}}async function ZU(){await il("/api/auth/forget",{})}async function QU(e){const{ok:t,data:r}=await il("/api/report/send",e?{run:e}:{});return t&&r.ok===!0?{ok:!0,password:String(r.password??""),filename:String(r.filename??"strix-report.pdf")}:{ok:!1,error:String(r.error??"unavailable")}}const Us="__root__";function E2({agents:e,fixedAgentId:t,className:r}){const a=ee.useRef(null),[s,o]=ee.useState(!1),[c,d]=ee.useState(!1),[f,h]=ee.useState(""),[p,g]=ee.useState(!1),[y,b]=ee.useState(null),_=t!=null,E=ee.useMemo(()=>e.find(z=>!z.parent_id)??e[0]??null,[e]),S=ee.useMemo(()=>e.filter(z=>z.parent_id&&z.status==="running"),[e]),[w,k]=ee.useState(Us),[N,M]=ee.useState(!1);ee.useEffect(()=>{w!==Us&&!S.some(z=>z.id===w)&&k(Us)},[w,S]);const{targetId:B,targetName:R}=ee.useMemo(()=>{if(_){const V=e.find(P=>P.id===t)??null;return{targetId:t??null,targetName:(V==null?void 0:V.name)??"this agent"}}if(w===Us)return{targetId:(E==null?void 0:E.id)??null,targetName:"Root agent"};const z=e.find(V=>V.id===w)??null;return{targetId:(z==null?void 0:z.id)??(E==null?void 0:E.id)??null,targetName:(z==null?void 0:z.name)??"Root agent"}},[e,t,_,E,w]),U=f.trim().length===0;ee.useLayoutEffect(()=>{const z=a.current;z&&(z.style.height="auto",z.style.height=`${z.scrollHeight}px`)},[f]);const I=ee.useCallback(()=>{o(!0),requestAnimationFrame(()=>{var z;return(z=a.current)==null?void 0:z.focus()})},[]),X=ee.useCallback(()=>{o(!1),d(!1),M(!1)},[]),j=ee.useCallback(async()=>{if(p)return;const z=f.trim();if(!z||!B)return;g(!0),b(null);const V=R,P=await YU(B,z);g(!1),P.ok?(h(""),b(`Sent to ${V}`),Tr("agent_steered")):P.error==="not_delivered"?b("Could not reach that agent (it may have finished)."):b("Could not send that message. Try again.")},[p,f,B,R]);return s?m.jsxs("div",{className:Mr("mt-4 rounded-2xl border border-white/[0.08] bg-[#050505] overflow-hidden transition-colors duration-300",c?"border-white/[0.18]":"hover:border-white/[0.12]",r),children:[m.jsxs("div",{className:"flex items-center justify-between gap-3 border-b border-white/[0.06] px-5 py-3",children:[m.jsxs("div",{className:"min-w-0",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Fm,{className:"h-4 w-4 text-[#666]"}),m.jsx("p",{className:"text-sm font-medium text-white",children:"Live prompt"})]}),m.jsx("p",{className:"mt-0.5 text-xs text-[#777]",children:"Connected"})]}),m.jsxs("div",{className:"flex shrink-0 items-center gap-2",children:[_?m.jsxs("div",{className:"rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-xs text-[#aaa]",children:["Target: ",m.jsx("span",{className:"text-white",children:R})]}):m.jsxs("div",{className:"flex items-center gap-1.5",children:[m.jsx("span",{className:"text-xs text-[#aaa]",children:"Target:"}),m.jsxs("div",{className:"relative",children:[m.jsxs("button",{type:"button",onClick:()=>M(z=>!z),onBlur:()=>requestAnimationFrame(()=>M(!1)),className:"inline-flex h-7 items-center gap-1 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 text-xs text-white transition-colors hover:border-white/[0.16] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20","aria-haspopup":"listbox","aria-expanded":N,children:[m.jsx("span",{className:"max-w-[140px] truncate",children:R}),m.jsx(po,{className:"h-3.5 w-3.5 text-[#999]"})]}),N&&m.jsxs("div",{className:"absolute right-0 z-10 mt-1 min-w-[160px] overflow-hidden rounded-lg border border-[#333] bg-[#0a0a0a] py-1 shadow-xl",role:"listbox",children:[m.jsx(w_,{label:"Root agent",active:w===Us,onSelect:()=>{k(Us),M(!1)}}),S.map(z=>m.jsx(w_,{label:z.name,active:w===z.id,onSelect:()=>{k(z.id),M(!1)}},z.id))]})]})]}),m.jsx("button",{type:"button",onClick:X,className:"inline-flex h-7 w-7 items-center justify-center rounded-full text-[#777] transition-colors hover:bg-white/[0.06] hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20","aria-label":"Collapse live prompt composer",children:m.jsx(po,{className:"h-4 w-4"})})]})]}),m.jsx("div",{className:"px-5 pt-4 pb-3",children:m.jsx("textarea",{ref:a,rows:1,value:f,onChange:z=>h(z.target.value),onFocus:()=>d(!0),onBlur:()=>d(!1),onKeyDown:z=>{z.key==="Enter"&&!z.shiftKey&&(z.preventDefault(),j())},placeholder:"Send a live prompt to the running pentest…",maxLength:4e3,disabled:p,className:"block w-full resize-none border-0 bg-transparent p-0 text-[15px] leading-6 text-white placeholder:text-[#444] focus:outline-none disabled:opacity-60 max-h-[160px] overflow-y-auto"})}),m.jsxs("div",{className:"flex items-center justify-between gap-3 px-4 pb-4",children:[m.jsx("div",{className:"text-xs text-[#666]",children:y??"Press Enter to send."}),m.jsxs("button",{type:"button",onClick:z=>{z.stopPropagation(),j()},disabled:p||U,className:Mr("inline-flex h-10 min-w-[112px] items-center justify-center gap-2 rounded-full px-4 text-sm font-medium transition-colors",p||U?"bg-white/[0.08] text-[#666]":"bg-white text-black hover:bg-neutral-200"),children:[p?m.jsx(Ps,{className:"h-4 w-4 animate-spin"}):m.jsx(Yk,{className:"h-4 w-4",strokeWidth:2.5}),m.jsx("span",{children:"Send prompt"})]})]})]}):m.jsxs("button",{type:"button",onClick:I,className:Mr("mt-4 flex w-full items-center justify-between gap-3 rounded-2xl border border-white/[0.08] bg-[#050505] px-5 py-3 text-left transition-colors duration-300 hover:border-white/[0.12] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/20",r),"aria-expanded":!1,"aria-label":"Expand live prompt composer",children:[m.jsxs("div",{className:"flex min-w-0 items-center gap-2",children:[m.jsx(Fm,{className:"h-4 w-4 shrink-0 text-[#666]"}),m.jsx("span",{className:"truncate text-sm font-medium text-white",children:"Guide the agent"})]}),m.jsx(U_,{className:"h-4 w-4 shrink-0 text-[#777]"})]})}function w_({label:e,active:t,onSelect:r}){return m.jsx("button",{type:"button",role:"option","aria-selected":t,onMouseDown:a=>{a.preventDefault(),r()},className:Mr("block w-full truncate px-3 py-1.5 text-left text-xs transition-colors hover:bg-white/[0.06]",t?"text-white":"text-[#aaa]"),children:e})}const WU={completed:"bg-emerald-400",running:"bg-blue-400",waiting:"bg-yellow-400",stopped:"bg-[#888]",crashed:"bg-red-400",failed:"bg-red-400"},JU=80;function eH({open:e,agent:t,events:r,steerable:a,onClose:s}){const o=ee.useRef(null),c=ee.useRef(!1),[d,f]=ee.useState(e),[h,p]=ee.useState(e?"open":"closed"),[g,y]=ee.useState(!1),b=ee.useRef(t);ee.useEffect(()=>{t&&(b.current=t)},[t]);const _=t??b.current;ee.useEffect(()=>{if(e){f(!0),p("open");return}p("closed");const S=setTimeout(()=>f(!1),140);return()=>clearTimeout(S)},[e]),ee.useEffect(()=>{if(!d){y(!1);return}const S=requestAnimationFrame(()=>y(!0));return()=>cancelAnimationFrame(S)},[d]);const E=ee.useCallback(()=>{const S=o.current;S&&(c.current=S.scrollHeight-S.scrollTop-S.clientHeight{const S=o.current;!S||!c.current||requestAnimationFrame(()=>{S.scrollTo({top:S.scrollHeight,behavior:"smooth"})})},[r]),ee.useEffect(()=>{if(!d)return;const S=k=>{k.key==="Escape"&&s()};document.addEventListener("keydown",S);const w=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{document.removeEventListener("keydown",S),document.body.style.overflow=w}},[d,s]),!d||!_?null:m.jsx("div",{"data-state":h,className:"agent-modal fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4 sm:p-8",onClick:s,role:"dialog","aria-modal":"true","aria-label":`Agent ${_.name}`,children:m.jsxs("div",{className:"relative flex h-[60vh] w-[calc(100vw-4rem)] max-w-6xl flex-col overflow-hidden rounded-xl border border-[#222] bg-[#0a0a0a] shadow-2xl",onClick:S=>S.stopPropagation(),children:[m.jsxs("div",{className:"flex items-center justify-between gap-3 border-b border-[#222] px-5 py-3.5",children:[m.jsxs("div",{className:"flex min-w-0 items-center gap-2",children:[m.jsx("span",{className:`h-2 w-2 flex-shrink-0 rounded-full ${WU[_.status]??"bg-[#888]"}`}),m.jsx("span",{className:"truncate text-sm font-semibold text-white",children:_.name}),m.jsx("span",{className:"flex-shrink-0 font-mono text-xs text-[#555]",children:_.id})]}),m.jsx("button",{type:"button",onClick:s,"aria-label":"Close",className:"flex-shrink-0 rounded-md p-1 text-[#888] transition-colors hover:bg-[#1a1a1a] hover:text-white",children:m.jsx(Sp,{className:"h-4 w-4"})})]}),m.jsx("div",{ref:o,onScroll:E,className:"flex-1 overflow-y-auto p-5",children:g&&m.jsx(DU,{agent:_,events:r,showHeader:!1})}),a&&m.jsx("div",{className:"border-t border-[#222] px-5 py-3",children:m.jsx(E2,{agents:[_],fixedAgentId:_.id,className:"mt-0"})})]})})}var N2={color:void 0,size:void 0,className:void 0,style:void 0,attr:void 0},E_=da.createContext&&da.createContext(N2),tH=["attr","size","title"];function nH(e,t){if(e==null)return{};var r,a,s=rH(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;ada.createElement(t.tag,Fu({key:r},t.attr),S2(t.child)))}function vg(e){return t=>da.createElement(lH,Pu({attr:Fu({},e.attr)},t),S2(e.child))}function lH(e){var t=r=>{var a=e.attr,s=e.size,o=e.title,c=nH(e,tH),d=s||r.size||"1em",f;return r.className&&(f=r.className),e.className&&(f=(f?f+" ":"")+e.className),da.createElement("svg",Pu({stroke:"currentColor",fill:"currentColor",strokeWidth:"0"},r.attr,a,c,{className:f,style:Fu(Fu({color:e.color||r.color},r.style),e.style),height:d,width:d,xmlns:"http://www.w3.org/2000/svg"}),o&&da.createElement("title",null,o),e.children)};return E_!==void 0?da.createElement(E_.Consumer,null,r=>t(r)):t(N2)}function oH(e){return vg({attr:{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"},child:[{tag:"circle",attr:{cx:"5",cy:"6",r:"3"},child:[]},{tag:"path",attr:{d:"M5 9v12"},child:[]},{tag:"circle",attr:{cx:"19",cy:"18",r:"3"},child:[]},{tag:"path",attr:{d:"m15 9-3-3 3-3"},child:[]},{tag:"path",attr:{d:"M12 6h5a2 2 0 0 1 2 2v7"},child:[]}]})(e)}function cH(e){return vg({attr:{viewBox:"0 0 16 16",fill:"currentColor"},child:[{tag:"path",attr:{d:"M15 4.95703C15 4.58711 14.8563 4.24054 14.5949 3.97992L12.0096 1.39234C11.4879 0.86922 10.5788 0.86922 10.0571 1.39234L8 3.45119V3.32321C8 2.55068 7.37187 1.922 6.6 1.922H2.4C1.62813 1.922 1 2.55068 1 3.32321V13.5988C1 14.3713 1.62813 15 2.4 15H12.6667C13.4385 15 14.0667 14.3713 14.0667 13.5988V9.39514C14.0667 8.62261 13.4385 7.99393 12.6667 7.99393H12.5379L14.5949 5.93508C14.8553 5.67445 15 5.32602 15 4.95703ZM2.4 2.85521H6.6C6.85667 2.85521 7.06667 3.06446 7.06667 3.32228V7.99299H1.93333V3.32228C1.93333 3.06446 2.14333 2.85521 2.4 2.85521ZM1.93333 13.5979V8.92714H7.06667V14.0649H2.4C2.14333 14.0649 1.93333 13.8547 1.93333 13.5979ZM13.1333 9.39421V13.5979C13.1333 13.8547 12.9233 14.0649 12.6667 14.0649H8V8.92714H12.6667C12.9233 8.92714 13.1333 9.13638 13.1333 9.39421ZM8 7.99299V6.46287L9.5288 7.99299H8ZM13.9351 5.2737L11.3488 7.86221C11.1789 8.03223 10.8859 8.03223 10.716 7.86221L8.12973 5.2737C8.0448 5.18963 7.99813 5.07753 7.99813 4.95796C7.99813 4.83839 8.0448 4.7263 8.12973 4.64129L10.716 2.05278C10.8009 1.96777 10.9129 1.92106 11.0324 1.92106C11.1519 1.92106 11.2639 1.96777 11.3488 2.05278L13.9351 4.64129C14.02 4.72536 14.0667 4.83746 14.0667 4.95703C14.0667 5.0766 14.02 5.1887 13.9351 5.2737Z"},child:[]}]})(e)}function k2(e){return vg({attr:{viewBox:"0 0 512 512"},child:[{tag:"path",attr:{fill:"none",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"32",d:"M431 320.6c-1-3.6 1.2-8.6 3.3-12.2a34 34 0 0 1 2.1-3.1A162 162 0 0 0 464 215c.3-92.2-77.5-167-173.7-167-83.9 0-153.9 57.1-170.3 132.9a160.7 160.7 0 0 0-3.7 34.2c0 92.3 74.8 169.1 171 169.1 15.3 0 35.9-4.6 47.2-7.7s22.5-7.2 25.4-8.3a26.4 26.4 0 0 1 9.3-1.7 26 26 0 0 1 10.1 2l56.7 20.1a13.5 13.5 0 0 0 3.9 1 8 8 0 0 0 8-8 13 13 0 0 0-.5-2.7z"},child:[]},{tag:"path",attr:{fill:"none",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"32",d:"M66.46 232a146.23 146.23 0 0 0 6.39 152.67c2.31 3.49 3.61 6.19 3.21 8s-11.93 61.87-11.93 61.87a8 8 0 0 0 2.71 7.68A8.17 8.17 0 0 0 72 464a7.3 7.3 0 0 0 2.91-.6l56.21-22a15.7 15.7 0 0 1 12 .2c18.94 7.38 39.88 12 60.83 12A159.2 159.2 0 0 0 284 432.11"},child:[]}]})(e)}const uH=[{icon:LC,label:"PR security reviews"},{icon:_T,label:"Attack surface monitoring"},{icon:zT,label:"Real-time threat intelligence"},{icon:eC,label:"Scheduled pentesting"},{icon:RT,label:"One-click autofix"},{icon:V_,label:"Jira, Linear & Slack integrations"}];function dH({open:e,onClose:t,description:r,source:a="sidebar"}){const[s,o]=ee.useState(e),[c,d]=ee.useState(e?"open":"closed");return ee.useEffect(()=>{if(e){o(!0),d("open");return}d("closed");const f=setTimeout(()=>o(!1),200);return()=>clearTimeout(f)},[e]),ee.useEffect(()=>{if(!s)return;const f=p=>{p.key==="Escape"&&t()};document.addEventListener("keydown",f);const h=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{document.removeEventListener("keydown",f),document.body.style.overflow=h}},[s,t]),s?m.jsx("div",{"data-state":c,className:"dialog-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4",onClick:t,role:"dialog","aria-modal":"true","aria-label":"Upgrade your plan",children:m.jsxs("div",{"data-state":c,className:"dialog-panel relative w-full max-w-md rounded-2xl border border-[#222] bg-black p-6 shadow-lg sm:rounded-lg",onClick:f=>f.stopPropagation(),children:[m.jsx("button",{type:"button",onClick:t,"aria-label":"Close",className:"absolute right-4 top-4 rounded-md p-1 text-[#888] transition-colors hover:bg-[#1a1a1a] hover:text-white",children:m.jsx(Sp,{className:"h-4 w-4"})}),m.jsxs("div",{children:[m.jsx("h2",{className:"text-lg text-white",children:"Available in Strix Cloud"}),r&&m.jsx("p",{className:"mt-2 text-base leading-relaxed text-[#e5e5e5]",children:r})]}),m.jsxs("div",{className:"space-y-4 pt-4",children:[m.jsxs("div",{className:"rounded-xl border border-[#333] bg-[#0a0a0a] p-4 sm:rounded-lg",children:[m.jsxs("div",{className:"mb-3 flex items-center gap-2",children:[m.jsx(Fm,{className:"h-4 w-4 text-blue-400"}),m.jsx("span",{className:"text-sm font-medium text-white",children:"Strix Cloud also includes"})]}),m.jsx("ul",{className:"space-y-2 text-sm text-[#888]",children:uH.map(f=>m.jsxs("li",{className:"flex items-center gap-2",children:[m.jsx(f.icon,{className:"h-3.5 w-3.5 text-[#555]"}),f.label]},f.label))})]}),m.jsxs("div",{className:"flex flex-col gap-2",children:[m.jsxs("a",{href:ha(Yu,"upgrade_try_free"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("upgrade_try_free",a),className:"flex h-10 w-full items-center justify-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition-colors hover:bg-neutral-200",children:["Open Strix Cloud",m.jsx(oy,{className:"h-3.5 w-3.5"})]}),m.jsxs("a",{href:ha($T,"upgrade_view_plans"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("upgrade_view_plans",a),className:"flex h-9 w-full items-center justify-center gap-1.5 rounded-lg border border-[#333] px-4 text-sm font-medium text-[#888] transition-colors hover:border-[#555] hover:text-white",children:["Learn more",m.jsx(oy,{className:"h-3 w-3"})]})]})]})]})}):null}const Hm=160,$m=260,io=400,fH=140,S_="strix_viewer_sidebar_width",k_="strix_viewer_sidebar_collapsed";function hH(e,t){try{const r=localStorage.getItem(e),a=r?parseInt(r,10):NaN;return Number.isFinite(a)?a:t}catch{return t}}function mH({view:e,onSelectView:t,issuesCount:r,agentCount:a,runCount:s,finished:o,verified:c,email:d,onOpenEmail:f,onOpenHistory:h,onForget:p}){var z;const[g,y]=ee.useState(()=>{const V=hH(S_,$m);return Math.min(io,Math.max(Hm,V))}),[b,_]=ee.useState(()=>{try{return localStorage.getItem(k_)==="1"}catch{return!1}}),[E,S]=ee.useState(!1),[w,k]=ee.useState(!1),[N,M]=ee.useState(null),B=ee.useRef(null),R=(V,P)=>{jr(V,"sidebar"),M(P)},U=ee.useCallback(V=>{y(V);try{localStorage.setItem(S_,String(V))}catch{}},[]),I=ee.useCallback(V=>{_(V);try{localStorage.setItem(k_,V?"1":"0")}catch{}},[]),X=ee.useCallback(()=>{I(!1),U($m)},[I,U]),j=ee.useCallback(V=>{V.preventDefault(),S(!0)},[]);return ee.useEffect(()=>{if(!E||b)return;const V=T=>{const $=T.clientX;$>=Hm&&$<=io?y($):$>io&&y(io)},P=T=>{const $=T.clientX;${window.removeEventListener("mousemove",V),window.removeEventListener("mouseup",P)}},[E,b,I,U]),ee.useEffect(()=>{if(!w)return;const V=P=>{B.current&&!B.current.contains(P.target)&&k(!1)};return document.addEventListener("mousedown",V),()=>document.removeEventListener("mousedown",V)},[w]),m.jsxs(m.Fragment,{children:[b&&m.jsx("div",{className:"fixed left-0 top-0 z-40 hidden h-full w-4 cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.08)] lg:block",onClick:X,title:"Expand sidebar"}),m.jsxs("aside",{className:Mr("sticky top-0 z-20 hidden h-screen flex-shrink-0 flex-col overflow-hidden border-r border-[rgba(255,255,255,0.08)] bg-black lg:flex",!E&&"transition-[width] duration-200 ease-out"),style:{width:b?0:g},children:[m.jsx("header",{className:"relative flex flex-col gap-1 pt-1 min-w-[160px]",children:m.jsx("div",{className:"flex flex-row py-1 px-2",children:m.jsxs("div",{className:"flex h-10 w-full flex-row items-center",children:[m.jsxs("a",{href:ha("https://app.strix.ai","logo"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("logo","sidebar"),className:"flex flex-1 flex-row items-center gap-2 rounded-md py-2 pl-2.5 pr-1 min-w-0 transition-colors hover:bg-[rgba(255,255,255,0.06)]",title:"Open Strix Cloud",children:[m.jsx("span",{className:"flex flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500",style:{width:20,height:20},children:m.jsx("span",{className:"text-[10px] font-semibold text-white",children:"S"})}),m.jsxs("span",{className:"flex flex-1 flex-row items-center gap-2 min-w-0",children:[m.jsx("span",{className:"truncate min-w-0 text-[14px] font-medium text-[#ededed]",children:"Strix"}),m.jsx("span",{className:"flex h-5 flex-shrink-0 items-center rounded px-2 text-[11px] font-medium text-[#888] bg-[rgba(255,255,255,0.08)]",children:"Local"})]})]}),m.jsx("a",{href:ha("https://app.strix.ai","logo"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("logo","sidebar"),className:"flex flex-none items-center rounded-md px-1.5 py-2 transition-colors hover:bg-[rgba(255,255,255,0.06)]","aria-label":"Open Strix Cloud",children:m.jsx(cC,{className:"h-4 w-4 text-[#666]"})})]})})}),m.jsx("nav",{className:"relative min-w-[160px] flex-1 overflow-y-auto overflow-x-clip scrollbar-thin pb-10 pt-2",children:m.jsxs("div",{className:"relative flex flex-col gap-px px-2",children:[m.jsx(yi,{icon:m.jsx(pH,{}),label:"Pentest Overview",active:e==="overview",onClick:()=>t("overview")}),m.jsx(yi,{icon:m.jsx(Nu,{className:"h-4 w-4"}),label:"Issues",count:r>0?r:void 0,active:e==="issues",onClick:()=>t("issues")}),a>0&&m.jsx(yi,{icon:m.jsx(Oo,{className:"h-4 w-4"}),label:"Agents",count:a,active:e==="agents",onClick:()=>t("agents")}),m.jsx(yi,{icon:m.jsx(Ys,{className:"h-4 w-4"}),label:"Past runs",count:s>0?s:void 0,active:e==="history",onClick:h}),o&&m.jsx(yi,{icon:m.jsx(Np,{className:"h-4 w-4"}),label:"Export report",active:e==="email",onClick:f}),m.jsx(yi,{icon:m.jsx(k2,{className:"h-4 w-4"}),label:"Feedback & support",active:e==="feedback",onClick:()=>t("feedback")}),m.jsx("hr",{className:"mx-0 my-1 h-px w-full border-0 bg-[rgba(255,255,255,0.08)]"}),m.jsx(yi,{icon:m.jsx(oH,{className:"h-4 w-4"}),label:"PR Security Reviews",active:!1,onClick:()=>R("pr_reviews","Strix reviews every pull request and flags exploitable changes before they merge.")}),m.jsx(yi,{icon:m.jsx(cH,{className:"h-4 w-4"}),label:"Integrations",active:!1,onClick:()=>R("integrations","Sync findings to Jira, Linear, and Slack so fixes happen where your team already works.")}),m.jsx(yi,{icon:m.jsx(MT,{className:"h-4 w-4"}),label:"Members",active:!1,onClick:()=>R("members","Invite your team, set roles, and share findings and run history across your org.")})]})}),m.jsx("section",{className:"flex min-w-[160px] flex-col gap-0.5",ref:B,children:m.jsxs("div",{className:"relative p-2",children:[c&&d?m.jsxs("button",{onClick:()=>k(V=>!V),className:"relative flex w-full cursor-pointer items-center gap-2 rounded-md bg-transparent px-2.5 py-2 transition-colors hover:bg-[rgba(255,255,255,0.06)]",children:[m.jsx("span",{className:"flex flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500",style:{width:20,height:20},children:m.jsx("span",{className:"text-[9px] font-semibold text-white",children:((z=d[0])==null?void 0:z.toUpperCase())||"U"})}),m.jsxs("span",{className:"flex min-w-0 flex-1 flex-col text-left",children:[m.jsx("span",{className:"truncate text-[13px] font-medium text-[#ededed]",children:d}),m.jsx("span",{className:"truncate text-[11px] text-[#555]",children:"Linked to this machine"})]})]}):m.jsxs("div",{className:"flex items-center gap-2 rounded-md px-2.5 py-2",children:[m.jsx("span",{className:"flex flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-cyan-500",style:{width:20,height:20},children:m.jsx("span",{className:"text-[9px] font-semibold text-white",children:"S"})}),m.jsx("span",{className:"flex min-w-0 flex-1 flex-col text-left",children:m.jsx("span",{className:"truncate text-[13px] font-medium text-[#ededed]",children:"Local viewer"})})]}),w&&c&&d&&m.jsxs("div",{className:"absolute bottom-full left-2 right-2 z-50 mb-1 overflow-hidden rounded-lg border border-[#333] bg-black shadow-xl",children:[m.jsxs("div",{className:"border-b border-[#333] px-3 py-2",children:[m.jsx("p",{className:"truncate text-[13px] font-medium text-white",children:"Linked email"}),m.jsx("p",{className:"truncate text-[11px] text-[#666]",children:d})]}),m.jsxs("button",{onClick:()=>{k(!1),p()},className:"flex w-full items-center gap-2 px-3 py-2 text-[13px] text-[#888] transition-colors hover:bg-[rgba(255,255,255,0.06)] hover:text-red-400",children:[m.jsx(WC,{className:"h-4 w-4"}),"Forget this email"]})]})]})}),m.jsx("div",{className:"group absolute right-0 top-0 z-30 h-full w-1 cursor-col-resize",onMouseDown:j,children:m.jsx("div",{className:Mr("absolute right-0 top-0 h-full w-px bg-[rgba(255,255,255,0.08)] transition-all duration-100",E?"w-0.5 bg-[rgba(255,255,255,0.3)]":"group-hover:bg-[rgba(255,255,255,0.2)]")})})]}),E&&m.jsx("div",{className:"fixed inset-0 z-10 cursor-col-resize"}),m.jsx(dH,{open:N!==null,description:N??"",source:"sidebar",onClose:()=>M(null)})]})}function yi({icon:e,label:t,active:r,onClick:a,count:s}){return m.jsxs("button",{onClick:a,className:Mr("group flex h-9 w-full origin-left flex-row items-center rounded-md transition-colors",r?"bg-[rgba(255,255,255,0.12)] text-white":"text-[#888] hover:bg-[rgba(255,255,255,0.06)] hover:text-[#ededed]"),children:[m.jsx("div",{className:"grid flex-none place-content-center",style:{width:36,height:36},children:e}),m.jsx("span",{className:"min-w-0 flex-1 truncate text-left text-[14px] font-medium",children:t}),s!=null&&m.jsx("span",{className:"mr-2 flex-none rounded-full border border-white/10 px-2 py-0.5 text-[10px] tabular-nums leading-none text-[#777]",children:s})]})}function pH(){return m.jsx("svg",{style:{width:16,height:16,color:"currentcolor"},viewBox:"0 0 16 16",fill:"currentColor",children:m.jsx("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M2.5 5.5V2.5H5.5V5.5H2.5ZM1 2C1 1.44772 1.44772 1 2 1H6C6.55228 1 7 1.44772 7 2V6C7 6.55228 6.55228 7 6 7H2C1.44772 7 1 6.55228 1 6V2ZM2.5 13.5V10.5H5.5V13.5H2.5ZM1 10C1 9.44772 1.44772 9 2 9H6C6.55228 9 7 9.44772 7 10V14C7 14.5523 6.55228 15 6 15H2C1.44772 15 1 14.5523 1 14V10ZM10.5 2.5V5.5H13.5V2.5H10.5ZM10 1C9.44772 1 9 1.44772 9 2V6C9 6.55228 9.44772 7 10 7H14C14.5523 7 15 6.55228 15 6V2C15 1.44772 14.5523 1 14 1H10ZM10.5 13.5V10.5H13.5V13.5H10.5ZM9 10C9 9.44772 9.44772 9 10 9H14C14.5523 9 15 9.44772 15 10V14C15 14.5523 14.5523 15 14 15H10C9.44772 15 9 14.5523 9 14V10Z"})})}const C_={work_email_required:"Please use your work email, not a personal one.",rate_limited:"Too many requests. Wait a minute and try again.",invalid_email:"That email does not look right. Check it and try again.",unavailable:"The email service is unavailable right now. Try again shortly."},gH=new Set(["gmail.com","googlemail.com","yahoo.com","ymail.com","outlook.com","hotmail.com","live.com","icloud.com","me.com","aol.com","proton.me","protonmail.com","gmx.com","mail.com"]);function xH({onVerified:e}){const[t,r]=ee.useState("email"),[a,s]=ee.useState(""),[o,c]=ee.useState(""),[d,f]=ee.useState(!1),[h,p]=ee.useState(null),[g,y]=ee.useState(null),b=async()=>{const E=a.trim();if(!E){p("Enter your email to continue.");return}const S=E.slice(E.lastIndexOf("@")+1).toLowerCase();if(gH.has(S)){Tr("work_email_required"),p(C_.work_email_required);return}f(!0),p(null);const w=await _2(E);f(!1),w.ok?(Tr("email_submitted",{purpose:"verify"}),y(`We sent a 6-digit code to ${E}.`),r("code")):(w.error==="work_email_required"&&Tr("work_email_required"),p(C_[w.error]??"Could not send a code. Try again."))},_=async()=>{const E=o.trim();if(E.length<4){p("Enter the 6-digit code from your email.");return}f(!0),p(null);const S=await w2(a.trim(),E);if(f(!1),!S.verified){p("That code did not match. Check it and try again.");return}Tr("email_verified",{purpose:"verify"}),e()};return m.jsxs("div",{className:"mx-auto mt-5 max-w-sm text-left",children:[h&&m.jsxs("div",{className:"mb-3 flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2",children:[m.jsx(Gu,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs text-red-300",children:h})]}),g&&!h&&m.jsx("p",{className:"mb-3 text-xs text-[#888]",children:g}),t==="email"?m.jsxs("form",{className:"space-y-3",onSubmit:E=>{E.preventDefault(),b()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your work email"}),m.jsx("input",{type:"email",autoFocus:!0,value:a,onChange:E=>s(E.target.value),placeholder:"you@company.com",className:"w-full rounded-lg bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}}),m.jsx("span",{className:"mt-1.5 block text-[11px] text-[#666]",children:"Use your work email."})]}),m.jsxs("button",{type:"submit",disabled:d,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[d&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),"Send me a code"]})]}):m.jsxs("form",{className:"space-y-3",onSubmit:E=>{E.preventDefault(),_()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"6-digit code"}),m.jsx("input",{inputMode:"numeric",autoFocus:!0,value:o,onChange:E=>c(E.target.value.replace(/\D/g,"").slice(0,6)),placeholder:"123456",className:"w-full rounded-lg bg-black px-3 py-2.5 text-center text-lg font-mono tracking-[0.4em] text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}})]}),m.jsxs("button",{type:"submit",disabled:d,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[d&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),"Verify"]}),m.jsx("button",{type:"button",onClick:()=>{r("email"),p(null),y(null)},className:"w-full cursor-pointer text-center text-xs text-[#666] transition-colors hover:text-[#aaa]",children:"Use a different email"})]})]})}const bH=[{key:"critical",dot:"bg-red-500",text:"text-red-500"},{key:"high",dot:"bg-orange-500",text:"text-orange-500"},{key:"medium",dot:"bg-yellow-500",text:"text-yellow-500"},{key:"low",dot:"bg-blue-500",text:"text-blue-500"}];function yH({counts:e}){const t=bH.filter(r=>e[r.key]>0);return t.length===0?m.jsx("span",{className:"text-xs text-[#555]",children:"No findings"}):m.jsx("div",{className:"flex items-center gap-3",children:t.map(r=>m.jsxs("div",{className:"flex items-center gap-1.5",children:[m.jsx("span",{className:`h-2 w-2 rounded-full ${r.dot}`,"aria-hidden":"true"}),m.jsx("span",{className:`text-xs tabular-nums ${r.text}`,children:e[r.key]})]},r.key))})}function vH(e){if(!e)return null;const t=e.trim().replace(" UTC","Z").replace(" ","T"),r=new Date(t);return Number.isNaN(r.getTime())?null:r.toLocaleString(void 0,{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}function T_(e){if(!e)return null;const t=e.trim().replace(" UTC","Z").replace(" ","T"),r=new Date(t);if(Number.isNaN(r.getTime()))return null;const a=Date.now()-r.getTime(),s=Math.floor(a/6e4);if(s<1)return"just now";if(s<60)return`${s}m ago`;const o=Math.floor(s/60);if(o<24)return`${o}h ago`;const c=Math.floor(o/24);return c<7?`${c}d ago`:vH(e)}function _H({runs:e,activeRun:t,onSelectRun:r,onVerified:a}){const s=(e==null?void 0:e.count)??0,[o,c]=ee.useState(!1);return!e||e.locked?m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center",children:[m.jsx("div",{className:"mx-auto mb-4 flex h-11 w-11 items-center justify-center rounded-xl",style:{border:"1px solid #2a2a2a",background:"rgba(255,255,255,0.04)"},children:m.jsx(Ys,{className:"h-5 w-5 text-[#888]","aria-hidden":"true"})}),m.jsx("h2",{className:"text-base font-semibold text-white",children:"Browse every run on this machine"}),m.jsxs("p",{className:"mx-auto mt-1.5 max-w-md text-sm text-[#888]",children:["You have ",s," past ",s===1?"run":"runs"," on this machine."]}),o?m.jsxs(m.Fragment,{children:[m.jsx("p",{className:"mx-auto mt-3 max-w-sm text-xs text-[#666]",children:"Verify your email with a one-time code to unlock the full history."}),m.jsx(xH,{onVerified:a})]}):m.jsx("button",{onClick:()=>{jr("history_unlock","past_runs"),c(!0)},className:"mt-4 cursor-pointer rounded-lg bg-white px-4 py-2 text-sm font-semibold text-black transition-opacity hover:opacity-90",children:"View runs"}),m.jsxs("p",{className:"mt-4 flex items-center justify-center gap-1.5 text-xs text-[#555]",children:[m.jsx(K_,{className:"h-3.5 w-3.5","aria-hidden":"true"}),"Or open one from the CLI with"," ",m.jsx("code",{className:"font-mono text-[#888]",children:"strix view "})]})]}):e.runs.length===0?m.jsx("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center text-sm text-[#888]",children:"No past runs found on this machine yet."}):m.jsx("div",{className:"space-y-2",children:e.runs.map(d=>{const f=d.name===t,h=T_(d.start_time)??T_(d.end_time),p=yo(d.target,d.name);return m.jsxs("button",{onClick:()=>r(d.name),className:`animate-card-in group flex w-full cursor-pointer items-center gap-4 rounded-lg border px-4 py-3 text-left transition-colors ${f?"border-[#444] bg-[rgba(255,255,255,0.04)]":"border-[#222] bg-[rgba(255,255,255,0.02)] hover:border-[#444]"}`,children:[m.jsxs("div",{className:"min-w-0 flex-1",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx("span",{className:"truncate text-sm font-medium text-white",children:p}),f&&m.jsx("span",{className:"rounded-full px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-400",style:{border:"1px solid rgba(16,185,129,0.3)"},children:"Active"})]}),m.jsxs("div",{className:"mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-[#666]",children:[d.scan_mode&&m.jsx("span",{className:"capitalize",children:d.scan_mode}),d.scan_mode&&(h||d.status)&&m.jsx("span",{className:"text-[#333]",children:"·"}),h&&m.jsx("span",{children:h}),h&&d.status&&m.jsx("span",{className:"text-[#333]",children:"·"}),d.status&&m.jsx("span",{className:"capitalize",children:d.status})]})]}),m.jsx(yH,{counts:d.severity_counts}),m.jsx(sC,{className:"h-4 w-4 flex-shrink-0 text-[#555] transition-colors group-hover:text-[#aaa]","aria-hidden":"true"})]},d.name)})})}const A_={work_email_required:"Please use your work email, not a personal one.",rate_limited:"Too many requests. Wait a minute and try again.",invalid_email:"That email does not look right. Check it and try again.",unavailable:"The email service is unavailable right now. Try again shortly."},wH={forbidden:"This email was unsubscribed from Strix, so we cannot send to it.",too_large:"This report is too large to email. Try a smaller run.",unavailable:"The email service is unavailable right now. Try again shortly."},EH=new Set(["gmail.com","googlemail.com","yahoo.com","ymail.com","outlook.com","hotmail.com","live.com","icloud.com","me.com","aol.com","proton.me","protonmail.com","gmx.com","mail.com"]);function NH({activeRun:e,auth:t,purpose:r,skipDisclosure:a=!1,onAuthChanged:s,onExit:o}){const c=(t==null?void 0:t.verified)===!0,d=r==="verify",[f,h]=ee.useState(()=>d?"email":a?c?"sending":"email":"disclosure"),[p,g]=ee.useState((t==null?void 0:t.email)??""),[y,b]=ee.useState(""),[_,E]=ee.useState(!1),[S,w]=ee.useState(null),[k,N]=ee.useState(null),[M,B]=ee.useState(""),[R,U]=ee.useState(""),[I,X]=ee.useState(!1),[j,z]=ee.useState(""),V=ee.useRef(!1),P=async()=>{h("sending"),w(null);const Z=await QU(e);if(Z.ok){Tr("report_sent"),B(Z.password),U(Z.filename),h("password");return}if(Z.error==="reverify"||Z.error==="unverified"){N("Your verification expired. Enter your email to verify again."),h("email");return}w(wH[Z.error]??"Could not send the report. Try again."),h("disclosure")},T=()=>{w(null),N(null),c?P():h("email")};ee.useEffect(()=>{!d&&a&&c&&!V.current&&(V.current=!0,P())},[]);const $=async()=>{const Z=p.trim();if(!Z){w("Enter your email to continue.");return}const C=Z.slice(Z.lastIndexOf("@")+1).toLowerCase();if(EH.has(C)){Tr("work_email_required"),w(A_.work_email_required);return}E(!0),w(null);const D=await _2(Z);E(!1),D.ok?(Tr("email_submitted",{purpose:r}),N(`We sent a 6-digit code to ${Z}.`),h("code")):(D.error==="work_email_required"&&Tr("work_email_required"),w(A_[D.error]??"Could not send a code. Try again."))},O=async()=>{const Z=y.trim();if(Z.length<4){w("Enter the 6-digit code from your email.");return}E(!0),w(null);const C=await w2(p.trim(),Z);if(E(!1),!C.verified){w("That code did not match. Check it and try again.");return}Tr("email_verified",{purpose:r}),z(C.email),s(),d?o("history"):P()},H=async()=>{try{await navigator.clipboard.writeText(M),X(!0),setTimeout(()=>X(!1),1500)}catch{}},K=j||(t==null?void 0:t.email)||p.trim();return m.jsxs("div",{className:"mx-auto max-w-xl space-y-4",children:[m.jsxs("button",{onClick:()=>o(d?"history":"overview"),className:"cursor-pointer inline-flex items-center gap-1.5 text-sm text-[#888] transition-colors hover:text-white",children:[m.jsx(Ep,{className:"h-4 w-4"}),d?"Back to past runs":"Back to results"]}),m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Np,{className:"h-5 w-5 text-[#888]","aria-hidden":"true"}),m.jsx("h1",{className:"text-2xl font-semibold text-white",children:d?"Verify your email":"Export report to PDF"})]}),m.jsxs("div",{className:"w-full rounded-2xl bg-[rgba(255,255,255,0.02)] p-6",style:{border:"1px solid #2a2a2a"},children:[m.jsx("p",{className:"mb-4 text-xs text-[#666]",children:d?"We send a one-time code to confirm it is you.":"Verified by a one-time code sent to your email"}),S&&m.jsxs("div",{className:"mb-4 flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2",children:[m.jsx(Gu,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs text-red-300",children:S})]}),k&&!S&&f!=="password"&&m.jsx("p",{className:"mb-4 text-xs text-[#888]",children:k}),f==="disclosure"&&m.jsxs("div",{className:"space-y-4",children:[m.jsxs("div",{className:"space-y-2.5 rounded-lg p-3.5",style:{border:"1px solid #222",background:"rgba(255,255,255,0.02)"},children:[m.jsxs("div",{className:"flex items-start gap-2.5",children:[m.jsx(X_,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsxs("p",{className:"text-xs leading-relaxed text-[#aaa]",children:["We email an ",m.jsx("span",{className:"text-white",children:"encrypted PDF"}),". Nothing else leaves your machine."]})]}),m.jsxs("div",{className:"flex items-start gap-2.5",children:[m.jsx(ZC,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-[#888]","aria-hidden":"true"}),m.jsx("p",{className:"text-xs leading-relaxed text-[#aaa]",children:"Only you hold the password; Strix can't read it."})]})]}),m.jsx("button",{onClick:T,className:"w-full cursor-pointer rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90",children:"Export report"}),c&&(t==null?void 0:t.email)&&m.jsxs("p",{className:"text-center text-xs text-[#666]",children:["Sending to ",t.email]})]}),f==="email"&&m.jsxs("form",{className:"space-y-4",onSubmit:Z=>{Z.preventDefault(),$()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your work email"}),m.jsx("input",{type:"email",autoFocus:!0,value:p,onChange:Z=>g(Z.target.value),placeholder:"you@company.com",className:"w-full rounded-lg bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}})]}),m.jsxs("button",{type:"submit",disabled:_,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[_&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),"Send me a code"]})]}),f==="code"&&m.jsxs("form",{className:"space-y-4",onSubmit:Z=>{Z.preventDefault(),O()},children:[m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"6-digit code"}),m.jsx("input",{inputMode:"numeric",autoFocus:!0,value:y,onChange:Z=>b(Z.target.value.replace(/\D/g,"").slice(0,6)),placeholder:"123456",className:"w-full rounded-lg bg-black px-3 py-2.5 text-center text-lg font-mono tracking-[0.4em] text-white outline-none transition-colors focus:border-[#444]",style:{border:"1px solid #2a2a2a"}})]}),m.jsxs("button",{type:"submit",disabled:_,className:"flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:[_&&m.jsx(Ps,{className:"h-4 w-4 animate-spin","aria-hidden":"true"}),d?"Verify":"Verify and send"]}),m.jsx("button",{type:"button",onClick:()=>{h("email"),w(null),N(null)},className:"w-full cursor-pointer text-center text-xs text-[#666] transition-colors hover:text-[#aaa]",children:"Use a different email"})]}),f==="sending"&&m.jsxs("div",{className:"flex flex-col items-center gap-3 py-8",children:[m.jsx(Ps,{className:"h-6 w-6 animate-spin text-white","aria-hidden":"true"}),m.jsx("p",{className:"text-sm text-[#aaa]",children:"Generating and encrypting locally..."})]}),f==="password"&&m.jsxs("div",{className:"space-y-4",children:[m.jsxs("div",{className:"flex items-start gap-2.5 rounded-lg border border-emerald-500/30 bg-emerald-500/5 px-3 py-2.5",children:[m.jsx(Vs,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsxs("p",{className:"text-xs text-emerald-200",children:["Sent to ",K,". Open the attached PDF with this password."]})]}),m.jsxs("div",{children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your one-time password"}),m.jsxs("div",{className:"flex items-center gap-2 rounded-lg bg-black p-3",style:{border:"1px solid #2a2a2a"},children:[m.jsx("code",{className:"flex-1 break-all font-mono text-base text-white",children:M}),m.jsxs("button",{onClick:H,className:"flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs text-[#aaa] transition-colors hover:bg-[rgba(255,255,255,0.06)] hover:text-white",style:{border:"1px solid #2a2a2a"},children:[I?m.jsx(Vs,{className:"h-3.5 w-3.5"}):m.jsx(go,{className:"h-3.5 w-3.5"}),I?"Copied":"Copy"]})]}),m.jsxs("p",{className:"mt-2 text-xs text-[#666]",children:["Save this now. Strix never stores it, so we cannot show it again. File:"," ",m.jsx("span",{className:"font-mono text-[#888]",children:R})]})]}),m.jsx("button",{onClick:()=>o("overview"),className:"w-full cursor-pointer rounded-lg px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-[rgba(255,255,255,0.06)]",style:{border:"1px solid #2a2a2a"},children:"Done"})]})]})]})}function la(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}function ao(e){return Array.isArray(e)?e:[]}function nr(e){return typeof e=="string"&&e.trim()?e:null}function Ba(e){return typeof e=="number"&&Number.isFinite(e)?e:null}function SH(e){return e.replace(/_/g," ")}function M_(e){return e&&e.charAt(0).toUpperCase()+e.slice(1)}function kH(e){if(e==null||e<0)return"n/a";const t=Math.floor(e/3600),r=Math.floor(e%3600/60),a=e%60;return t?`${t}h ${r}m ${a}s`:r?`${r}m ${a}s`:`${a}s`}function En({label:e,children:t}){return m.jsxs("div",{className:"grid grid-cols-[7rem_1fr] gap-3 items-baseline",children:[m.jsx("dt",{className:"text-[11px] uppercase tracking-wide text-[#666]",children:e}),m.jsx("dd",{className:"min-w-0 break-words text-sm text-[#ddd]",children:t})]})}function CH({raw:e,durationSeconds:t}){const[r,a]=ee.useState(!0),s=ao(e.targets_info).map(P=>{const T=la(P),$=nr(T.original)??nr(la(T.details).target_url)??"unknown target",O=nr(T.type);return{display:$,type:O?SH(O):null}}),o=nr(e.instruction),c=M_(nr(e.scan_mode)),d=nr(e.scope_mode),f=la(e.diff_scope),h=f.active===!0,p=nr(f.mode),g=nr(e.diff_base),y=e.non_interactive===!0,b=ao(e.local_sources).map(P=>{if(typeof P=="string")return P;const T=la(P);return nr(T.source_path)??nr(T.target_path)??""}).filter(Boolean),_=M_(nr(e.status));let E=d??"auto";h&&(E+=` (diff${p?`: ${p}`:""}${g?` vs ${g}`:""})`);const S=la(e.llm_usage),w=Object.keys(S).length>0,k=ao(S.agents).map(la),N=Array.from(new Set(k.map(P=>nr(P.model)).filter(P=>!!P))),M=Ba(S.requests),B=Ba(S.input_tokens),R=Ba(la(ao(S.input_tokens_details)[0]).cached_tokens),U=Ba(S.output_tokens),I=Ba(la(ao(S.output_tokens_details)[0]).reasoning_tokens),X=Ba(S.total_tokens),j=Ba(S.cost),z=nr(e.auth_mode)==="subscription",V=(P,T)=>m.jsxs("span",{className:"text-[#666]",children:[" (",Ls(P)," ",T,")"]});return m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-5",children:[m.jsxs("button",{type:"button",onClick:()=>a(P=>!P),"aria-expanded":r,className:"flex w-full cursor-pointer items-center gap-2 text-left",children:[m.jsx(GC,{className:"h-4 w-4 text-[#888]","aria-hidden":"true"}),m.jsx("h2",{className:"text-sm font-semibold text-white",children:"Run details"}),r?m.jsx(U_,{className:"ml-auto h-4 w-4 text-[#666]","aria-hidden":"true"}):m.jsx(po,{className:"ml-auto h-4 w-4 text-[#666]","aria-hidden":"true"})]}),r&&m.jsxs("div",{className:"mt-4 grid grid-cols-1 gap-x-8 gap-y-6 md:grid-cols-2",children:[m.jsxs("section",{children:[m.jsx("h3",{className:"mb-3 text-[11px] font-semibold uppercase tracking-wide text-[#555]",children:"Configuration"}),m.jsxs("dl",{className:"space-y-2.5",children:[s.length>0&&m.jsx(En,{label:"Targets",children:m.jsx("div",{className:"space-y-1",children:s.map((P,T)=>m.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[m.jsx("span",{className:"font-mono text-[#ddd]",children:P.display}),P.type&&m.jsx("span",{className:"rounded-full border border-[#2a2a2a] px-1.5 py-0.5 text-[10px] text-[#888]",children:P.type})]},T))})}),m.jsx(En,{label:"Instruction",children:o?m.jsx("span",{className:"whitespace-pre-wrap",children:o}):m.jsx("span",{className:"text-[#666]",children:"None"})}),c&&m.jsx(En,{label:"Pentest mode",children:c}),m.jsx(En,{label:"Scope",children:E}),m.jsx(En,{label:"Mode",children:y?"Non-interactive":"Interactive"}),b.length>0&&m.jsx(En,{label:"Local sources",children:m.jsx("div",{className:"space-y-0.5 font-mono text-[#ddd]",children:b.map((P,T)=>m.jsx("div",{children:P},T))})}),_&&m.jsx(En,{label:"Status",children:_})]})]}),m.jsxs("section",{children:[m.jsx("h3",{className:"mb-3 text-[11px] font-semibold uppercase tracking-wide text-[#555]",children:"Usage & cost"}),w?m.jsxs("dl",{className:"space-y-2.5 tabular-nums",children:[m.jsx(En,{label:"Model",children:N.length?N.join(", "):"n/a"}),z&&m.jsx(En,{label:"Provider",children:m.jsx("span",{className:"inline-flex items-center gap-1.5",children:m.jsx("span",{className:"rounded-full border border-[#22c55e]/40 bg-[#22c55e]/10 px-2 py-0.5 text-[11px] text-[#22c55e]",children:"ChatGPT subscription"})})}),m.jsx(En,{label:"Run time",children:kH(t)}),M!=null&&m.jsx(En,{label:"Requests",children:Ls(M)}),B!=null&&m.jsxs(En,{label:"Input tokens",children:[Ls(B),R!=null&&V(R,"cached")]}),U!=null&&m.jsxs(En,{label:"Output tokens",children:[Ls(U),I!=null&&V(I,"reasoning")]}),X!=null&&m.jsx(En,{label:"Total tokens",children:Ls(X)}),z?m.jsxs(En,{label:"Cost",children:[m.jsx("span",{className:"text-[#22c55e]",children:"$0.00"}),m.jsx("span",{className:"text-[#666]",children:" (subscription)"})]}):j!=null&&m.jsxs(En,{label:"Cost",children:["$",j.toFixed(2)]}),k.length>0&&m.jsx(En,{label:"Agents",children:Ls(k.length)})]}):m.jsx("p",{className:"text-sm text-[#666]",children:"Not available yet."})]})]})]})}const O_="strix_viewer_trust_dismissed";function TH({message:e}){const[t,r]=ee.useState(()=>{try{return localStorage.getItem(O_)==="1"}catch{return!1}});if(t)return null;const a=()=>{try{localStorage.setItem(O_,"1")}catch{}r(!0)};return m.jsx("div",{className:"fixed bottom-3 left-3 z-[60] max-w-xs rounded-lg bg-[#0a0a0a] p-3 shadow-2xl",style:{border:"1px solid #2a2a2a"},role:"status",children:m.jsxs("div",{className:"flex gap-2.5",children:[m.jsx(X_,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs leading-relaxed text-[#aaa]",children:e}),m.jsx("button",{onClick:a,"aria-label":"Dismiss",className:"-mr-0.5 -mt-0.5 flex-shrink-0 cursor-pointer rounded p-0.5 text-[#666] transition-colors hover:text-white",children:m.jsx(Sp,{className:"h-3.5 w-3.5"})})]})})}const AH=5e3,R_={invalid_email:"That email doesn't look right.",invalid_message:"Please write a little more.",unavailable:"Couldn't send that just now. Try again."};function MH({defaultEmail:e,onExit:t}){const[r,a]=ee.useState(""),[s,o]=ee.useState(e??""),[c,d]=ee.useState("form"),[f,h]=ee.useState(null),p=r.trim().length>0&&s.trim().length>0&&c!=="sending",g=async()=>{if(!p)return;d("sending"),h(null);const y=await XU(r.trim(),s.trim());if(y.ok){d("sent");return}d("form"),h(R_[y.error]??R_.unavailable)};return m.jsxs("div",{className:"mx-auto max-w-xl space-y-4",children:[m.jsxs("button",{onClick:()=>t("overview"),className:"cursor-pointer inline-flex items-center gap-1.5 text-sm text-[#888] transition-colors hover:text-white",children:[m.jsx(Ep,{className:"h-4 w-4"}),"Back to results"]}),m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(k2,{className:"h-5 w-5 text-[#888]","aria-hidden":"true"}),m.jsx("h1",{className:"text-2xl font-semibold text-white",children:"Feedback & support"})]}),m.jsx("div",{className:"w-full rounded-2xl bg-[rgba(255,255,255,0.02)] p-6",style:{border:"1px solid #2a2a2a"},children:c==="sent"?m.jsxs("div",{className:"flex items-start gap-3",children:[m.jsx(Eu,{className:"mt-0.5 h-5 w-5 flex-shrink-0 text-emerald-400","aria-hidden":"true"}),m.jsxs("div",{className:"min-w-0",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Thanks, we got it."}),m.jsx("p",{className:"mt-1 text-xs text-[#888]",children:"We read every message. If it needs a reply, we'll reach out to the email you gave."}),m.jsx("button",{onClick:()=>{a(""),d("form")},className:"mt-4 cursor-pointer text-xs text-[#888] transition-colors hover:text-white",children:"Send more feedback"})]})]}):m.jsxs(m.Fragment,{children:[m.jsx("p",{className:"mb-4 text-xs text-[#666]",children:"Bugs, feature requests, or anything else. Tell us what's on your mind."}),f&&m.jsxs("div",{className:"mb-4 flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/5 px-3 py-2",children:[m.jsx(Gu,{className:"mt-0.5 h-4 w-4 flex-shrink-0 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-xs text-red-300",children:f})]}),m.jsxs("label",{className:"block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your feedback"}),m.jsx("textarea",{autoFocus:!0,value:r,maxLength:AH,onChange:y=>a(y.target.value),rows:5,placeholder:"What's working, what's not, what you'd love to see…",className:"w-full resize-y rounded-lg border border-[#2a2a2a] bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-white/50 focus:ring-2 focus:ring-white/10"})]}),m.jsxs("label",{className:"mt-4 block",children:[m.jsx("span",{className:"mb-1.5 block text-xs text-[#888]",children:"Your work email"}),m.jsx("input",{type:"email",value:s,onChange:y=>o(y.target.value),placeholder:"you@company.com",className:"w-full rounded-lg border border-[#2a2a2a] bg-black px-3 py-2.5 text-sm text-white outline-none transition-colors focus:border-white/50 focus:ring-2 focus:ring-white/10"})]}),m.jsx("button",{onClick:()=>void g(),disabled:!p,className:"mt-4 flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90 disabled:opacity-60",children:c==="sending"?"Sending…":"Send feedback"})]})})]})}function OH({text:e,children:t,className:r=""}){const[a,s]=ee.useState(!1);return m.jsxs("span",{className:`relative inline-flex ${r}`,onMouseEnter:()=>s(!0),onMouseLeave:()=>s(!1),onFocus:()=>s(!0),onBlur:()=>s(!1),children:[t,a&&m.jsx("span",{role:"tooltip",className:"pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 w-max max-w-[240px] -translate-x-1/2 rounded-md px-2.5 py-1.5 text-xs text-[#ddd] shadow-lg",style:{border:"1px solid #2a2a2a",background:"#0a0a0a"},children:e})]})}function C2({label:e,desc:t,slug:r,icon:a,surface:s}){return m.jsx(OH,{text:t,children:m.jsxs("a",{href:ha(Yu,r),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr(r,s),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",children:[m.jsx(a,{className:"h-4 w-4 text-[#888] transition-colors group-hover:text-white","aria-hidden":"true"}),m.jsx("span",{children:e})]})})}const RH="Your findings stay on your machine. They're rendered here locally in your browser and never uploaded or stored by Strix.",j_=["critical","high","medium","low"],jH=500;function DH(){const[e,t]=ee.useState(null),[r,a]=ee.useState(null),[s,o]=ee.useState(null),[c,d]=ee.useState(null),[f,h]=ee.useState("overview"),[p,g]=ee.useState(null),[y,b]=ee.useState(null),[_,E]=ee.useState("report"),[S,w]=ee.useState(!1),[k,N]=ee.useState(!1),M=ee.useCallback(async()=>{try{g(await KU())}catch{}},[]),B=ee.useCallback(async()=>{try{b(await GU())}catch{}},[]);ee.useEffect(()=>{M(),B(),VU().then(C=>N(C.can_steer)).catch(()=>{})},[M,B]);const R=ee.useRef(!1);ee.useEffect(()=>{let C=!1,D;R.current=!1;const Y=()=>{D=setTimeout(L,jH)},L=async()=>{if(!C)try{const{summary:G,raw:q,finished:Q}=await b2(e);if(C)return;if(Q&&!R.current){R.current=!0;const te=await __(e);C||a(te);return}const[J,W]=await Promise.all([v2(e).catch(()=>({agents:[],events:[]})),y2(G.runId,e).catch(()=>[])]);if(C)return;a(te=>({summary:G,raw:q,finished:Q,transcript:J,vulnerabilities:W,reportMarkdown:(te==null?void 0:te.reportMarkdown)??null})),Y()}catch(G){if(C)return;o(G instanceof Error?G.message:"Could not load run data."),Y()}};return(async()=>{try{const G=await __(e);if(C)return;a(G),G.finished?R.current=!0:Y()}catch(G){if(C)return;o(G instanceof Error?G.message:"Could not load run data."),Y()}})(),()=>{C=!0,D&&clearTimeout(D)}},[e]);const U=ee.useMemo(()=>r?PU(r.vulnerabilities):null,[r]),I=(r==null?void 0:r.vulnerabilities.find(C=>C.id===c))??null,X=(r==null?void 0:r.transcript.agents.length)??0,j=(p==null?void 0:p.verified)===!0,z=ee.useRef(!1);ee.useEffect(()=>{z.current=!1},[e]),ee.useEffect(()=>{z.current||!r||(r.finished?(z.current=!0,h("overview")):X>0&&(z.current=!0,h("agents")))},[r,X]);const V=ee.useCallback(C=>{z.current=!0,h(C)},[]),P=ee.useCallback(C=>{t(C),d(null),a(null),o(null),z.current=!1},[]),T=ee.useCallback((C,D)=>{jr("email_report",D),E("report"),w(C),V("email")},[V]),$=ee.useCallback(()=>T(!1,"sidebar"),[T]),O=ee.useCallback(()=>T(!0,"overview"),[T]),H=ee.useCallback(()=>{B(),V("history")},[B,V]),K=ee.useCallback(async()=>{await M(),await B()},[M,B]),Z=ee.useCallback(async()=>{await ZU(),await M(),await B()},[M,B]);return m.jsxs("div",{className:"min-h-screen bg-black text-white flex",children:[m.jsx(mH,{view:f,onSelectView:C=>{d(null),C==="history"?H():V(C)},issuesCount:(r==null?void 0:r.vulnerabilities.length)??0,agentCount:X,runCount:(y==null?void 0:y.count)??0,finished:(r==null?void 0:r.finished)??!1,verified:j,email:(p==null?void 0:p.email)??null,onOpenEmail:$,onOpenHistory:H,onForget:()=>void Z()}),m.jsxs("div",{className:"flex-1 min-w-0",children:[m.jsx("div",{className:"border-b border-[#222]",children:m.jsxs("div",{className:"max-w-[88rem] mx-auto px-3 sm:px-6 py-4 flex items-center gap-1.5",children:[m.jsxs("a",{href:ha("https://app.strix.ai","logo"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("logo","topbar"),className:"flex items-center gap-1.5 opacity-90 transition-opacity hover:opacity-100 lg:hidden",title:"Open Strix Cloud",children:[m.jsx("img",{src:"./logo.png",alt:"Strix",className:"w-10 h-8 object-cover"}),m.jsx("div",{className:"text-base text-white font-medium tracking-tight",children:"Strix"})]}),r&&m.jsx(zH,{finished:r.finished}),m.jsxs("div",{className:"ml-auto flex items-center gap-3",children:[j&&y&&!y.locked&&y.runs.length>0&&m.jsx(LH,{runs:y,activeRun:e,launchedName:yo((r==null?void 0:r.summary.targets[0])??null,(r==null?void 0:r.summary.runName)??(r==null?void 0:r.summary.runId)??"Current run"),onSelect:P}),m.jsxs("a",{href:ha(Yu,"run_in_cloud"),target:"_blank",rel:"noopener noreferrer",onClick:()=>jr("run_in_cloud","topbar"),className:"inline-flex items-center gap-1 rounded-lg bg-white px-3 py-1.5 text-xs font-semibold text-black transition-opacity hover:opacity-90",children:["Run in the cloud",m.jsx(z_,{className:"w-3 h-3","aria-hidden":"true"})]})]})]})}),m.jsxs("div",{className:"max-w-[88rem] mx-auto px-3 sm:px-6 py-8 sm:py-12 space-y-6",children:[s&&!r&&f!=="history"&&f!=="email"&&m.jsxs("div",{className:"rounded-lg px-4 py-3 flex gap-3 items-start border border-red-500/30 bg-red-500/5",children:[m.jsx(Gu,{className:"w-5 h-5 flex-shrink-0 mt-0.5 text-red-400","aria-hidden":"true"}),m.jsx("p",{className:"text-sm text-red-300",children:s})]}),m.jsx("div",{className:"animate-page-in space-y-6",children:f==="email"?m.jsx(NH,{activeRun:e,auth:p,purpose:_,skipDisclosure:S,onAuthChanged:()=>{M(),B()},onExit:C=>h(C==="history"?"history":"overview")}):f==="feedback"?m.jsx(MH,{defaultEmail:(p==null?void 0:p.email)??null,onExit:C=>h(C)}):f==="history"?m.jsxs("div",{className:"space-y-4",children:[m.jsxs("div",{className:"flex items-center gap-2",children:[m.jsx(Ys,{className:"w-5 h-5 text-[#888]","aria-hidden":"true"}),m.jsx("h1",{className:"text-2xl font-semibold text-white",children:"Past runs"})]}),m.jsx(_H,{runs:y,activeRun:e,onSelectRun:P,onVerified:()=>void K()})]}):!r&&!s?m.jsxs("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-10 text-center",children:[m.jsx("div",{className:"w-6 h-6 mx-auto mb-3 rounded-full border-2 border-[#333] border-t-white animate-spin"}),m.jsx("p",{className:"text-sm text-[#888]",children:"Loading run data…"})]}):r&&U?m.jsxs(m.Fragment,{children:[m.jsx(BH,{summary:r.summary}),m.jsxs("div",{className:"flex gap-5 border-b border-[#2a2a2a] lg:hidden",children:[m.jsx(Pm,{active:f==="overview",onClick:()=>V("overview"),children:"Pentest Overview"}),m.jsxs(Pm,{active:f==="issues",onClick:()=>V("issues"),children:["Issues",r.vulnerabilities.length>0?` (${r.vulnerabilities.length})`:""]}),X>0&&m.jsxs(Pm,{active:f==="agents",onClick:()=>V("agents"),children:["Agents (",X,")"]})]}),f==="overview"?m.jsx(PH,{summary:r.summary,counts:U,total:r.vulnerabilities.length,reportMarkdown:r.reportMarkdown,raw:r.raw,finished:r.finished,onOpenEmail:O}):f==="agents"&&X>0?m.jsx(FH,{run:r,canSteer:k}):I?m.jsxs("div",{className:"space-y-4",children:[m.jsxs("button",{onClick:()=>d(null),className:"cursor-pointer inline-flex items-center gap-1.5 text-sm text-[#888] hover:text-white transition-colors",children:[m.jsx(Ep,{className:"w-4 h-4"})," Back to all findings"]}),m.jsx(mD,{vulnerability:I})]}):m.jsx(UH,{vulnerabilities:r.vulnerabilities,finished:r.finished,onSelect:C=>d(C)})]}):null},`${e??"launched"}:${f}:${c??""}`)]})]}),m.jsx(TH,{message:RH})]})}function LH({runs:e,activeRun:t,launchedName:r,onSelect:a}){const[s,o]=ee.useState(!1),c=e.runs.find(f=>f.name===t),d=c?yo(c.target,c.name):r;return m.jsxs("div",{className:"relative",children:[m.jsxs("button",{onClick:()=>o(f=>!f),onBlur:()=>setTimeout(()=>o(!1),150),"aria-label":"Switch pentest",className:"flex items-center gap-2 rounded-lg border border-[#3a3a3a] bg-[rgba(255,255,255,0.05)] px-3 py-2 text-sm text-white transition-colors hover:border-[#555] hover:bg-[rgba(255,255,255,0.09)]",children:[m.jsx(Ys,{className:"h-4 w-4 flex-shrink-0 text-[#888]","aria-hidden":"true"}),m.jsx("span",{className:"flex-shrink-0 text-[#888]",children:"Pentest"}),m.jsx("span",{className:"max-w-[260px] truncate font-medium",children:d}),m.jsx(po,{className:"h-4 w-4 flex-shrink-0 text-[#aaa]","aria-hidden":"true"})]}),s&&m.jsxs("div",{className:"absolute right-0 z-50 mt-2 max-h-96 w-96 overflow-y-auto rounded-xl py-1.5 shadow-2xl",style:{border:"1px solid #3a3a3a",background:"#0a0a0a"},children:[m.jsx("div",{className:"border-b border-[#222] px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[#666]",children:"Switch pentest"}),e.runs.map(f=>{const h=f.name===t;return m.jsxs("button",{onMouseDown:()=>a(f.name),className:`flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm transition-colors hover:bg-[rgba(255,255,255,0.06)] ${h?"bg-[rgba(255,255,255,0.04)] text-white":"text-[#aaa]"}`,children:[m.jsxs("span",{className:"min-w-0 flex-1",children:[m.jsx("span",{className:"block truncate font-medium",children:yo(f.target,f.name)}),f.target&&m.jsx("span",{className:"block truncate font-mono text-xs text-[#666]",children:f.target})]}),h&&m.jsx("span",{className:"h-2 w-2 flex-shrink-0 rounded-full bg-emerald-400"})]},f.name)})]})]})}function zH({finished:e}){return e?m.jsxs("span",{className:"ml-3 inline-flex items-center gap-1.5 text-xs text-[#888]",children:[m.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-[#555]"}),"Complete"]}):m.jsxs("span",{className:"ml-3 inline-flex items-center gap-1.5 text-xs text-emerald-400",children:[m.jsxs("span",{className:"relative flex h-1.5 w-1.5",children:[m.jsx("span",{className:"absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75 animate-ping"}),m.jsx("span",{className:"relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-400"})]}),"Live"]})}function IH(e){if(e==null)return null;if(e<60)return`${e}s`;const t=Math.floor(e/60);return t<60?`${t}m`:`${Math.floor(t/60)}h ${t%60}m`}function BH({summary:e}){const t=IH(e.durationSeconds);return m.jsxs("div",{children:[m.jsx("h1",{className:"text-2xl font-semibold text-white",children:yo(e.targets[0]??null,e.runName??e.runId??"Pentest results")}),m.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[#888]",children:[e.targets.length>0&&m.jsx("span",{className:"font-mono text-[#aaa]",children:e.targets.join(", ")}),e.scanMode&&m.jsx(qm,{label:e.scanMode}),t&&m.jsx(qm,{label:t}),e.status&&m.jsx(qm,{label:e.status})]})]})}function qm({label:e}){return m.jsxs(m.Fragment,{children:[m.jsx("span",{className:"text-[#333]",children:"·"}),m.jsx("span",{className:"capitalize",children:e})]})}function UH({vulnerabilities:e,finished:t,onSelect:r}){const a=[...e].sort((s,o)=>j_.indexOf(s.severity)-j_.indexOf(o.severity));return a.length===0?m.jsxs("div",{className:"space-y-4",children:[m.jsx("div",{className:"rounded-xl border border-[#222] bg-[rgba(255,255,255,0.02)] p-8 text-center text-sm text-[#888]",children:t?"No findings in this run.":"No findings yet. The pentest is still running…"}),t&&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-medium text-white",children:"Stay ahead of new exposures"}),m.jsx("p",{className:"mt-0.5 mb-3 text-xs text-[#666]",children:"Attack surface monitoring catches new exposures for your org over time."}),m.jsx(C2,{label:"Attack surface monitoring",desc:"Continuous coverage for your whole org.",slug:"asm",surface:"empty_state",icon:sT})]})]}):m.jsx("div",{className:"space-y-2",children:a.map(s=>m.jsxs("button",{onClick:()=>r(s.id),className:"animate-card-in cursor-pointer w-full text-left rounded-lg border border-[#222] hover:border-[#444] bg-[rgba(255,255,255,0.02)] px-4 py-3 transition-colors flex items-center gap-3",children:[m.jsx("span",{className:`w-2.5 h-2.5 rounded-full flex-shrink-0 ${kp(s.severity)}`,"aria-hidden":"true"}),m.jsxs("span",{className:"flex-1 min-w-0",children:[m.jsx("span",{className:"block text-sm font-medium text-white truncate",children:s.title}),s.target&&m.jsx("span",{className:"block text-xs text-[#666] font-mono truncate",children:s.target})]}),m.jsx("span",{className:`text-xs font-semibold px-2 py-0.5 rounded-full border capitalize ${Z_[s.severity]}`,children:s.severity})]},s.id))})}function HH(e){return e.replace(/^\s*#{1,6}[ \t]+.*(?:\r?\n)+/,"").trimStart()}function $H(e){const t=[];let r=null;for(const a of e.split(` `)){const s=a.match(/^#{1,6}\s+(.*)$/);if(s){const o=s[1].trim().toLowerCase();if(o===r)continue;r=o}else a.trim()!==""&&(r=null);t.push(a)}return t.join(` `)}function qH({onOpenEmail:e}){return m.jsx("button",{onClick:e,className:"group w-full cursor-pointer rounded-xl border border-emerald-500/25 bg-emerald-500/[0.06] p-4 text-left transition-colors hover:border-emerald-500/40",children:m.jsxs("div",{className:"flex items-center gap-3",children:[m.jsx("div",{className:"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg",style:{border:"1px solid rgba(16,185,129,0.3)",background:"rgba(16,185,129,0.08)"},children:m.jsx(Np,{className:"h-4 w-4 text-emerald-400","aria-hidden":"true"})}),m.jsxs("div",{className:"min-w-0 flex-1",children:[m.jsx("p",{className:"text-sm font-semibold text-white",children:"Email an encrypted PDF report of this run"}),m.jsx("p",{className:"mt-0.5 text-xs text-[#888]",children:"Encrypted with a key only you can see, email verified with a one-time code before sending."})]}),m.jsx("span",{className:"flex-shrink-0 rounded-lg bg-white px-3 py-1.5 text-xs font-semibold text-black transition-opacity group-hover:opacity-90",children:"Export report to PDF"})]})})}function PH({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(([,f])=>!!f).map(([f,h])=>({title:f,content:HH(h)}));return m.jsxs("div",{className:"space-y-6",children:[m.jsx("div",{className:"animate-card-in",children:m.jsx(CH,{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(qH,{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(f=>m.jsx(oa,{title:f.title,content:f.content},f.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:$H(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 FH({run:e,canSteer:t}){const{agents:r,events:a}=e.transcript,s=ee.useMemo(()=>jU(r,a),[r,a]),[o,c]=ee.useState(null),d=o?r.find(h=>h.id===o)??null:null,f=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:h=>c(h),eventsLoaded:!0,eventsEmpty:s.size===0,scanCompleted:e.finished})})]}),f&&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(eH,{open:d!==null,agent:d,events:a,steerable:f,onClose:()=>c(null)})]})}Bk.createRoot(document.getElementById("root")).render(m.jsx(ee.StrictMode,{children:m.jsx(DH,{})})); diff --git a/strix/interface/viewer/static/index.html b/strix/interface/viewer/static/index.html index 22fad9fc..73057a7e 100644 --- a/strix/interface/viewer/static/index.html +++ b/strix/interface/viewer/static/index.html @@ -6,7 +6,7 @@ Strix Results - + diff --git a/strix/tools/mcp/__init__.py b/strix/tools/mcp/__init__.py index 4b0160b1..47276c24 100644 --- a/strix/tools/mcp/__init__.py +++ b/strix/tools/mcp/__init__.py @@ -1,25 +1,54 @@ -"""Generic MCP client: connect MCP servers and expose their tools.""" +"""Generic MCP client: connect MCP servers and reach their tools on demand.""" from __future__ import annotations -from strix.tools.mcp.client import ConnectedMcpServer, connect_mcp_servers +from strix.tools.mcp.agent_tools import call_mcp, describe_mcp, list_mcps +from strix.tools.mcp.client import ( + ConnectedMcpServer, + attach_mcp_requests, + connect_mcp_servers, +) from strix.tools.mcp.config import ( BearerAuth, McpAuth, McpConnectionConfig, ) from strix.tools.mcp.loader import load_user_mcp_configs -from strix.tools.mcp.naming import McpToolOrigin, namespaced_tool_name, resolve_mcp_tool +from strix.tools.mcp.naming import namespaced_tool_name +from strix.tools.mcp.registry import ( + CALL_MCP_TOOL, + DESCRIBE_MCP_TOOL, + MCP_DISPATCH_TOOLS, + MCP_REGISTRY_CONTEXT_KEY, + McpCallInfo, + McpConnectionEntry, + McpConnectionRequest, + McpConnectionSummary, + McpRegistry, + resolve_mcp_call, +) __all__ = [ + "CALL_MCP_TOOL", + "DESCRIBE_MCP_TOOL", + "MCP_DISPATCH_TOOLS", + "MCP_REGISTRY_CONTEXT_KEY", "BearerAuth", "ConnectedMcpServer", "McpAuth", + "McpCallInfo", "McpConnectionConfig", - "McpToolOrigin", + "McpConnectionEntry", + "McpConnectionRequest", + "McpConnectionSummary", + "McpRegistry", + "attach_mcp_requests", + "call_mcp", "connect_mcp_servers", + "describe_mcp", + "list_mcps", "load_user_mcp_configs", "namespaced_tool_name", - "resolve_mcp_tool", + "resolve_mcp_call", ] diff --git a/strix/tools/mcp/agent_tools.py b/strix/tools/mcp/agent_tools.py new file mode 100644 index 00000000..9aa5ae27 --- /dev/null +++ b/strix/tools/mcp/agent_tools.py @@ -0,0 +1,173 @@ +"""The three generic MCP dispatch tools every agent carries. + +Under the generic-dispatch model an agent does not get one tool per MCP tool. +It gets exactly these three and discovers connections on demand: + +- ``list_mcps()`` returns the connections available this run — each connection's + id, name, description, and tool count, with no tool schemas — so the model can + discover what it can reach without any inventory in the system prompt. +- ``describe_mcp(connection)`` returns, as text, one connection's tools with + their names, descriptions, and JSON input schemas — the schemas the model + needs, fetched on demand instead of loaded onto every request up front. +- ``call_mcp(connection, tool, arguments)`` dispatches one call to a + connection's tool and returns its result. + +All three read the per-run :class:`~strix.tools.mcp.registry.McpRegistry` from the +run context under :data:`~strix.tools.mcp.registry.MCP_REGISTRY_CONTEXT_KEY`. They +are ordinary ``FunctionTool`` objects placed in the agent factory's base tool set, +so the factory's output-bounding and disk-spill wrapping apply to their results +automatically. +""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +from agents import RunContextWrapper, function_tool + +from strix.tools.mcp.client import dispatch_mcp_call +from strix.tools.mcp.naming import namespaced_tool_name +from strix.tools.mcp.registry import MCP_REGISTRY_CONTEXT_KEY, McpRegistry + + +if TYPE_CHECKING: + from mcp.types import Tool as MCPTool + + +def _registry_from_ctx(ctx: RunContextWrapper) -> McpRegistry | None: + context = ctx.context if isinstance(ctx.context, dict) else {} + registry = context.get(MCP_REGISTRY_CONTEXT_KEY) + return registry if isinstance(registry, McpRegistry) else None + + +_NO_CONNECTIONS = "No MCP connections are configured for this run." + + +def _unknown_connection(connection: str, registry: McpRegistry) -> str: + available = ", ".join(registry.names()) or "(none)" + return f"Unknown MCP connection {connection!r}. Available connections: {available}." + + +def _format_tool(tool: MCPTool) -> str: + schema = json.dumps(tool.inputSchema or {"type": "object"}, indent=2, ensure_ascii=False) + description = (tool.description or "").strip() or "(no description)" + return f"- {tool.name}: {description}\n input schema:\n{schema}" + + +@function_tool(timeout=60) +async def list_mcps(ctx: RunContextWrapper) -> dict[str, Any]: + """List the MCP connections available this run, so you can discover them. + + Read-only. Returns one entry per connection with its ``id`` (the exact name + you pass to ``describe_mcp`` and ``call_mcp``), ``name``, ``description``, and + ``tool_count`` — no tool schemas. The three MCP tools work in order: call + ``list_mcps`` to discover the available connections, then ``describe_mcp`` on + one connection to inspect its tools and their input schemas, then ``call_mcp`` + to run one of its tools. Returns an empty ``connections`` list when the run has + no MCP connections. + """ + registry = _registry_from_ctx(ctx) + if registry is None or not registry: + return {"connections": []} + return { + "connections": [ + { + "id": summary.name, + "name": summary.name, + "description": summary.purpose, + "tool_count": summary.tool_count, + } + for summary in registry.summaries() + ] + } + + +@function_tool(timeout=60) +async def describe_mcp(ctx: RunContextWrapper, connection: str) -> str: + """List the tools one MCP connection offers, with their input schemas. + + Read-only. Look up a connection by the id ``list_mcps`` reported for it; this + returns each of its tools with the tool's name, description, and JSON input + schema — the argument shape you pass to ``call_mcp``. Call this before + ``call_mcp`` on any connection you have not used yet. Nothing is fetched from + or run against the connection's data. + + Args: + connection: The connection name exactly as reported by ``list_mcps``. + """ + registry = _registry_from_ctx(ctx) + if registry is None or not registry: + return _NO_CONNECTIONS + entry = registry.get(connection) + if entry is None: + return _unknown_connection(connection, registry) + tools = await entry.server.list_tools() + if not tools: + return f"MCP connection {connection!r} offers no tools." + header = f"MCP connection {connection!r} offers {len(tools)} tool(s):" + body = "\n".join(_format_tool(tool) for tool in tools) + return f"{header}\n{body}" + + +@function_tool(timeout=120, strict_mode=False) +async def call_mcp( + ctx: RunContextWrapper, + connection: str, + tool: str, + arguments: Any = None, +) -> Any: + """Call one tool on one MCP connection and return its result. + + Address the tool by the connection id from ``list_mcps`` and the tool name + from ``describe_mcp`` on that connection. Pass the tool's arguments as an + object matching the input schema ``describe_mcp`` showed for it (omit it, or + pass an empty object, for a tool that takes no arguments). + + Args: + connection: The connection name exactly as reported by ``list_mcps``. + tool: The tool name, exactly as reported by ``describe_mcp``. + arguments: The tool's arguments as a JSON object of names to values (for + example ``{"path": "app.py"}``), or omitted/empty for a tool that + takes none. Pass an object, not a stringified one. Its shape is + whatever ``describe_mcp`` showed for the tool rather than a shape this + tool fixes in advance. + """ + registry = _registry_from_ctx(ctx) + if registry is None or not registry: + return _NO_CONNECTIONS + entry = registry.get(connection) + if entry is None: + return _unknown_connection(connection, registry) + invalid_arguments = ( + f"Invalid arguments for {connection!r}.{tool}: expected a JSON object of " + "argument names to values, or none. Call describe_mcp for the input schema." + ) + if isinstance(arguments, str): + # The ``arguments`` parameter is schema-less (an open object is not + # expressible as a strict tool schema), so some models serialize it as a + # JSON string instead of a bare object. Accept a string that decodes to an + # object so a correct call is not rejected over its encoding. + stripped = arguments.strip() + try: + arguments = json.loads(stripped) if stripped else {} + except json.JSONDecodeError: + return invalid_arguments + if arguments is not None and not isinstance(arguments, dict): + return invalid_arguments + available = await entry.server.list_tools() + valid_names = {mcp_tool.name for mcp_tool in available} + if tool not in valid_names: + offered = ", ".join(sorted(valid_names)) or "(none)" + return ( + f"Unknown tool {tool!r} on MCP connection {connection!r}. " + f"Tools this connection offers: {offered}. " + "Call describe_mcp for their input schemas." + ) + return await dispatch_mcp_call( + entry.server, + tool, + arguments or {}, + label=namespaced_tool_name(connection, tool), + result_transform=entry.result_transform, + ) diff --git a/strix/tools/mcp/client.py b/strix/tools/mcp/client.py index f1ee9bb0..1c49ff26 100644 --- a/strix/tools/mcp/client.py +++ b/strix/tools/mcp/client.py @@ -1,14 +1,15 @@ -"""Connect to MCP servers and expose their tools to the agent. +"""Connect to MCP servers so a run can reach their tools on demand. Given one :class:`McpConnectionConfig` per server, :func:`connect_mcp_servers` -lists each server's tools, keeps the ones on the connection's allowlist (or all -of them when none is set), prefixes each with the connection name so servers do -not collide, and registers them through the agent factory. The factory applies -output bounding, per-call timeouts, and structured errors to every registered -tool, so this layer does not reimplement them. +connects each server, counts the tools it offers (honoring the connection's +allowlist), and returns the live sessions. It does NOT register anything as an +agent tool: under the generic-dispatch model the run holds these sessions in a +per-run :class:`~strix.tools.mcp.registry.McpRegistry`, and the agent reaches +them through the two dispatch tools (``describe_mcp`` / ``call_mcp``), which call +:func:`dispatch_mcp_call` here to run one tool and serialize its result. -A server that cannot connect, or a tool set that cannot be registered, is logged -and skipped, so one bad connection never fails the run. +A server that cannot connect is logged and skipped, so one bad connection never +fails the run. """ from __future__ import annotations @@ -16,36 +17,34 @@ from __future__ import annotations import contextlib import json import logging +import os +from pathlib import Path from typing import TYPE_CHECKING, Any, NamedTuple, cast -from agents.exceptions import ModelBehaviorError from agents.mcp import ( MCPServer, MCPServerStdio, MCPServerStdioParams, MCPServerStreamableHttp, MCPServerStreamableHttpParams, - MCPUtil, create_static_tool_filter, ) - -from strix.agents.factory import register_agent_tools -from strix.tools.mcp.naming import namespaced_tool_name +from mcp.client.stdio import stdio_client if TYPE_CHECKING: from collections.abc import Callable - from agents.tool import FunctionTool, Tool - from mcp.types import Tool as MCPTool - from strix.tools.mcp.config import McpConnectionConfig + from strix.tools.mcp.registry import McpConnectionRequest, McpRegistry - # Runs on each tool's structured result before it reaches the agent. Called - # ``result_transform(namespaced_tool_name, structured_result)`` and its return - # value becomes the tool's output. ``structured_result`` is the parsed - # ``CallToolResult`` as a dict (not a serialized string), so the transform can - # project or drop individual fields. + # Runs on one tool call's structured result before it reaches the agent. + # Called ``result_transform(label, structured_result)`` and its return value + # becomes the tool's output. ``label`` is the model-facing + # ``_`` name so a transform keyed on names still resolves + # the same way it did under per-tool registration; ``structured_result`` is + # the parsed ``CallToolResult`` as a dict (not a serialized string), so the + # transform can project or drop individual fields. ResultTransform = Callable[[str, Any], Any] @@ -53,12 +52,14 @@ logger = logging.getLogger(__name__) class ConnectedMcpServer(NamedTuple): - """One successfully connected MCP server and how many tools it registered. + """One successfully connected MCP server and how many tools it offers. - ``server`` is kept so the caller can clean it up when the run ends; - ``name`` and ``tool_count`` let the caller show the user a startup summary; + ``server`` is kept so the caller can clean it up when the run ends, and so + the caller can hand the live session to the run's + :class:`~strix.tools.mcp.registry.McpRegistry`; ``name`` and ``tool_count`` + let the caller show the user a startup summary and fill the prompt inventory; ``notes`` carries the connection's optional free-text description so the - caller can surface it to the agent as context about the connection. + caller can surface it as the connection's purpose in the inventory. """ server: MCPServer @@ -75,13 +76,42 @@ def _auth_headers(config: McpConnectionConfig) -> dict[str, str]: return {"Authorization": f"Bearer {auth.token}"} +@contextlib.asynccontextmanager +async def _quiet_stdio_streams(params: Any) -> Any: + """Run a stdio MCP server with its stderr sent to the void. + + A stdio MCP server chats on stderr as it boots (the filesystem server, for + one, prints ``Allowed directories: [ ... ]``). The mcp library forwards that + stderr to the parent's ``sys.stderr`` by default, which is the terminal the + TUI is drawing on, so the banner corrupts the display. Pointing ``errlog`` at + ``os.devnull`` drops that chatter. Connection failures are unaffected: they + still raise from ``connect`` and are logged by :func:`connect_mcp_servers`. + """ + with Path(os.devnull).open("w", encoding="utf-8") as errlog: + async with stdio_client(params, errlog=errlog) as streams: + yield streams + + +class _QuietMCPServerStdio(MCPServerStdio): + """``MCPServerStdio`` whose subprocess stderr is kept off the terminal. + + The SDK's ``create_streams`` calls ``stdio_client(self.params)`` with no + ``errlog``, so the subprocess stderr defaults to ``sys.stderr`` and paints + server banners over the running TUI. Overriding it lets us redirect that + stream; everything else about the stdio transport is unchanged. + """ + + def create_streams(self) -> Any: + return _quiet_stdio_streams(self.params) + + def _build_server(config: McpConnectionConfig) -> MCPServer: """Construct (but do not connect) the SDK server for one connection. When ``allowed_tools`` is a list the static filter means the server will not - even list tools outside it; :func:`_register_server_tools` re-applies the - same allowlist as the authoritative gate on what gets registered. When it is - ``None`` no filter is applied and every listed tool is registered. + even list tools outside it, so it is the authoritative gate on what + ``describe_mcp`` and ``call_mcp`` can see. When it is ``None`` no filter is + applied and every listed tool is reachable. """ tool_filter = ( create_static_tool_filter(allowed_tool_names=config.allowed_tools) @@ -95,7 +125,7 @@ def _build_server(config: McpConnectionConfig) -> MCPServer: "args": config.args, "env": config.env, } - return MCPServerStdio( + return _QuietMCPServerStdio( params=stdio_params, name=config.name, tool_filter=tool_filter, @@ -114,105 +144,14 @@ def _build_server(config: McpConnectionConfig) -> MCPServer: ) -def _build_tool( - config: McpConnectionConfig, - server: MCPServer, - mcp_tool: MCPTool, - result_transform: ResultTransform | None, -) -> FunctionTool: - """Build one namespaced FunctionTool from a listed MCP tool. - - The SDK builds the tool (so name override, input schema, approval policy, - error-as-result handling, and tool-origin metadata are unchanged). With a - ``result_transform`` we route the underlying MCP call through - :func:`_install_result_transform` so the transform sees the structured result - and decides the tool's output. Without one (the stock path), we still route - the call, through :func:`_install_error_status_capture`, so an errored result - reads as failed in the TUI while the agent's content is unchanged. - """ - namespaced_name = namespaced_tool_name(config.name, mcp_tool.name) - tool = MCPUtil.to_function_tool( - mcp_tool, - server, - convert_schemas_to_strict=False, - tool_name_override=namespaced_name, - ) - if result_transform is not None: - _install_result_transform(tool, server, mcp_tool.name, namespaced_name, result_transform) - else: - _install_error_status_capture(tool, server, mcp_tool.name, namespaced_name) - return tool - - -def _install_result_transform( - tool: FunctionTool, - server: MCPServer, - base_tool_name: str, - namespaced_name: str, - result_transform: ResultTransform, -) -> None: - """Route a tool's MCP call through ``result_transform``, innermost. - - ``MCPUtil.to_function_tool`` serializes the result inside its own invoke, so - the structured result cannot be intercepted through it. Instead we call - ``server.call_tool`` ourselves, hand the parsed :class:`CallToolResult` to the - transform, and return the transform's output as the tool result. - - This runs INSIDE the tool's invoke. The agent factory wraps a registered - tool's ``on_invoke_tool`` with output bounding, disk spill, and tracing at - agent-build time, which is OUTSIDE this invoke, so the transform is genuinely - the innermost step: nothing sees the raw result before the transform does. - - ``to_function_tool`` wraps the real invoke in the SDK's failure-handling - invoker, which stores the inner coroutine on ``_invoke_tool_impl`` and calls - it inside its try/except. Swapping that inner impl keeps the SDK's - error-as-result handling and all tool metadata while inserting the transform. - If the SDK ever renames that attribute we fail loudly rather than silently - skip the transform. - """ - - async def _invoke(_ctx: Any, input_json: str) -> Any: - parsed: Any = json.loads(input_json) if input_json else {} - if not isinstance(parsed, dict): - raise ModelBehaviorError( - f"Invalid JSON input for tool {namespaced_name}: expected a JSON object" - ) - args = cast("dict[str, Any]", parsed) - result = await server.call_tool(base_tool_name, args) - structured_result = result.model_dump(mode="json") - return result_transform(namespaced_name, structured_result) - - _replace_tool_invoke(tool, _invoke) - - -def _replace_tool_invoke(tool: FunctionTool, invoke: Callable[[Any, str], Any]) -> None: - """Swap a FunctionTool's inner invoke, failing loudly if the SDK shape changed. - - ``to_function_tool`` wraps the real invoke in the SDK's failure-handling - invoker, which stores the inner coroutine on ``_invoke_tool_impl`` and calls - it inside its own try/except. Swapping that inner impl keeps the SDK's - error-as-result handling and every piece of tool metadata intact. It is a - plain object with the coroutine as an attribute, not a function, so we treat - it as untyped to swap it. If the SDK ever renames that attribute we raise - rather than silently leave the swap un-applied. - """ - invoker = cast("Any", tool.on_invoke_tool) - if not hasattr(invoker, "_invoke_tool_impl"): - raise RuntimeError( - "agents SDK FunctionTool invoker shape changed: cannot swap the tool " - "invoke without risking it being silently skipped." - ) - invoker._invoke_tool_impl = invoke - - def _mcp_result_to_tool_output(server: MCPServer, result: Any) -> Any: """Serialize a ``CallToolResult`` to a tool output, mirroring the agents SDK. This reproduces the serialization in ``agents.mcp.util.MCPUtil.invoke_mcp_tool`` (structured-content JSON when the server asks for it, otherwise text/image - content blocks, unwrapping a single block). Because the stock path now routes + content blocks, unwrapping a single block). Because the dispatch tool routes its own call, this is what makes the agent see byte-identical content to what - the SDK would have produced on its own. + the SDK would have produced building the tool itself. """ if getattr(server, "use_structured_content", False) and result.structuredContent: return json.dumps(result.structuredContent) @@ -232,85 +171,88 @@ def _mcp_result_to_tool_output(server: MCPServer, result: Any) -> Any: return outputs -def _install_error_status_capture( - tool: FunctionTool, - server: MCPServer, - base_tool_name: str, - namespaced_name: str, -) -> None: - """Make an errored MCP result read as failed in the TUI, agent content unchanged. - - The stock SDK invoke returns only the text/image tool output and drops the - ``CallToolResult.isError`` flag, so the TUI cannot tell an errored MCP call - (which it renders as a green "done") from a successful one. We route the call - the same way :func:`_install_result_transform` does, read ``isError`` off the - full result, and on an error tag the returned output dict with - ``success: False``. - - That tag reaches the human-facing status but not the agent. The SDK stores the - raw return value on the run item's ``output`` (which the TUI reads to derive a - tool's status), but hands the agent the value re-projected through its - ToolOutput schema, which keeps only the known ``type``/``text`` fields and - drops the extra ``success`` key. So the status flips to failed while the agent - still receives exactly the same error content it does today. Non-error calls - return the stock output unchanged and keep rendering as done. - """ - - async def _invoke(_ctx: Any, input_json: str) -> Any: - parsed: Any = json.loads(input_json) if input_json else {} - if not isinstance(parsed, dict): - raise ModelBehaviorError( - f"Invalid JSON input for tool {namespaced_name}: expected a JSON object" - ) - args = cast("dict[str, Any]", parsed) - result = await server.call_tool(base_tool_name, args) - tool_output = _mcp_result_to_tool_output(server, result) - if getattr(result, "isError", False) and isinstance(tool_output, dict): - return {**tool_output, "success": False} - return tool_output - - _replace_tool_invoke(tool, _invoke) - - -async def _register_server_tools( - config: McpConnectionConfig, +async def dispatch_mcp_call( server: MCPServer, + tool_name: str, + arguments: dict[str, Any], + *, + label: str, result_transform: ResultTransform | None = None, -) -> list[Tool]: - """List a connected server's tools, prefix + filter them, and register them. +) -> Any: + """Run one MCP tool call and convert its result to a tool output. - ``allowed_tools`` of ``None`` registers every listed tool; a list restricts - to exactly those names. + Shared single dispatch point for the generic ``call_mcp`` tool. Calls + ``server.call_tool`` with the tool's unprefixed name, then: + + - with a ``result_transform`` (strix-pro's sanitizer), hands the parsed + :class:`CallToolResult` to it as ``result_transform(label, structured)`` and + returns whatever the transform returns; or + - without one, serializes the result the way the agents SDK does (see + :func:`_mcp_result_to_tool_output`) and, when the result is an MCP error, + normalizes it through :func:`_errored_tool_output` so the failure reaches + the interfaces (see that function for the representation and why it does not + corrupt the content the agent receives). + """ + result = await server.call_tool(tool_name, arguments) + if result_transform is not None: + return result_transform(label, result.model_dump(mode="json")) + tool_output = _mcp_result_to_tool_output(server, result) + if getattr(result, "isError", False): + return _errored_tool_output(tool_output) + return tool_output + + +def _errored_tool_output(tool_output: Any) -> dict[str, Any]: + """Tag a serialized MCP error so the interfaces render it as failed. + + Both the TUI and the run viewer decide a tool call failed by reading a + ``success`` key off a top-level dict in the result (``success is False`` means + failed). :func:`_mcp_result_to_tool_output` returns a dict only for a single + content block; a structured-content result comes back as a string and a + multi-block result as a list, and on those the failure flag had nowhere to + ride, so the interfaces showed a failed call as done. This normalizes every + errored result to a top-level dict carrying ``success: False``: + + - a single content block (already a dict) keeps its ``type``/``text`` and gains + ``success: False`` alongside. The SDK's ToolOutput projection keeps the known + ``type``/``text`` fields and drops ``success`` before the agent sees it, so + the agent still receives exactly the error content; + - a list (multiple blocks) or a string (structured content) is placed under a + stable ``content`` key so the flag has a top-level dict to ride on. The agent + still receives the full error content, under ``content``, rather than losing + it. + """ + if isinstance(tool_output, dict): + return {**tool_output, "success": False} + return {"success": False, "content": tool_output} + + +async def _count_server_tools(config: McpConnectionConfig, server: MCPServer) -> int: + """Count a connected server's reachable tools for the startup summary. + + ``allowed_tools`` of ``None`` counts every listed tool; a list counts only + those names. The count matches what ``describe_mcp`` will show, because the + static tool filter built in :func:`_build_server` restricts the server's own + ``list_tools`` to the same allowlist. """ allowed = config.allowed_tools mcp_tools = await server.list_tools() - - tools: list[Tool] = [ - _build_tool(config, server, mcp_tool, result_transform) - for mcp_tool in mcp_tools - if allowed is None or mcp_tool.name in allowed - ] - - register_agent_tools(*tools) - return tools + return sum(1 for mcp_tool in mcp_tools if allowed is None or mcp_tool.name in allowed) async def connect_mcp_servers( configs: list[McpConnectionConfig], - result_transform: ResultTransform | None = None, ) -> list[ConnectedMcpServer]: - """Connect to each MCP server and register its tools. - - When ``result_transform`` is given, every registered tool routes its result - through it before the result reaches the agent (see - :func:`_install_result_transform`). When it is ``None`` the tools behave - exactly as the SDK builds them. + """Connect to each MCP server and return its live session. Returns one :class:`ConnectedMcpServer` per server that connected, carrying - the SDK server (so the caller can clean it up when the run ends) plus the - server name and how many tools it registered (so the caller can show the - user a startup summary). Connections that fail are skipped rather than - raised. + the SDK server (so the caller can clean it up when the run ends and hand it to + the run's registry) plus the server name, how many tools it offers, and the + connection's notes. Connections that fail are skipped rather than raised. + + Nothing is registered as an agent tool: the caller builds a per-run + :class:`~strix.tools.mcp.registry.McpRegistry` from these sessions, and the + agent reaches each tool on demand through ``describe_mcp`` / ``call_mcp``. """ connected: list[ConnectedMcpServer] = [] for config in configs: @@ -318,7 +260,7 @@ async def connect_mcp_servers( try: server = _build_server(config) await server.connect() # type: ignore[no-untyped-call] - tools = await _register_server_tools(config, server, result_transform) + tool_count = await _count_server_tools(config, server) except Exception: logger.exception("Skipping MCP connection %r", config.name) if server is not None: @@ -339,11 +281,47 @@ async def connect_mcp_servers( await established.server.cleanup() # type: ignore[no-untyped-call] raise - logger.info("Connected MCP server %r (%d tools)", config.name, len(tools)) + logger.info("Connected MCP server %r (%d tools)", config.name, tool_count) connected.append( ConnectedMcpServer( - server=server, name=config.name, tool_count=len(tools), notes=config.notes + server=server, name=config.name, tool_count=tool_count, notes=config.notes ) ) return connected + + +async def attach_mcp_requests( + requests: list[McpConnectionRequest], + registry: McpRegistry, +) -> list[ConnectedMcpServer]: + """Connect a caller's MCP requests and populate the run's registry. + + The one shared attach-and-populate path both the command-line and the + SaaS/pro product go through, so all connecting and cleanup lives in one owner. + The caller supplies inert :class:`McpConnectionRequest` objects (a config plus + a provider label, an optional per-connection ``result_transform``, and an + optional ``purpose``) and never a live session: the engine connects each + config here, reusing :func:`connect_mcp_servers` so the fail-open behavior (a + connection that will not connect is logged and skipped) and the cancellation + cleanup are preserved unchanged. + + For each connection that came up, this registers it under its config name with + its tool count, its ``provider`` label, its ``result_transform``, and a purpose + of ``request.purpose`` when set else the connection's notes. Returns the + connected servers (the runner records them and cleans them up when the run + ends). + """ + request_by_name = {request.config.name: request for request in requests} + connections = await connect_mcp_servers([request.config for request in requests]) + for connection in connections: + request = request_by_name[connection.name] + registry.add( + name=connection.name, + server=connection.server, + tool_count=connection.tool_count, + purpose=request.purpose or connection.notes, + provider=request.provider, + result_transform=request.result_transform, + ) + return connections diff --git a/strix/tools/mcp/config.py b/strix/tools/mcp/config.py index a57121e7..b0cfffd1 100644 --- a/strix/tools/mcp/config.py +++ b/strix/tools/mcp/config.py @@ -61,9 +61,9 @@ class McpConnectionConfig(BaseModel): notes: str | None = None """Free-text notes for the agent describing what this connection is and how - to use it. When set, the runner collects the notes of every connection into - a single block on the root task, so a note describes its connection once - rather than being repeated onto each of its tools.""" + to use it. When set, the note becomes the connection's purpose line in the + MCP inventory every agent renders in its prompt, so it describes the + connection once rather than being repeated onto each of its tools.""" @model_validator(mode="after") def _check_transport_fields(self) -> McpConnectionConfig: diff --git a/strix/tools/mcp/loader.py b/strix/tools/mcp/loader.py index c179981d..168ec8fb 100644 --- a/strix/tools/mcp/loader.py +++ b/strix/tools/mcp/loader.py @@ -1,9 +1,10 @@ """Read the open-source user's MCP servers from ``~/.strix/mcp-servers.json``. An open-source user lists the MCP servers they want the agent to reach in a -small JSON file. Strix reads it at the start of a run, connects to each server, -and registers its tools. The file is optional; without it the run simply gets -no MCP tools. +small JSON file. Strix reads it at the start of a run and connects to each +server, holding the live sessions in the run's registry for the agent to reach +on demand. The file is optional; without it the run simply gets no MCP +connections. Parsing is fail-open. A single malformed entry is logged and skipped rather than raising, so one bad row never blocks the servers that are valid, and a missing @@ -46,9 +47,9 @@ def _resolve_path(path: Path | None) -> Path: def _dedupe_by_name(configs: list[McpConnectionConfig]) -> list[McpConnectionConfig]: """Keep the first connection of each name, dropping later duplicates. - Names namespace a server's tools (``.``), so two connections - sharing a name would collide and the second's tools would be silently - rejected at registration. Drop the duplicate here, with a warning, instead. + A connection's name is its key in the run's registry, so two connections + sharing a name would collide and the second would overwrite the first. Drop + the duplicate here, with a warning, instead. """ seen: set[str] = set() unique: list[McpConnectionConfig] = [] diff --git a/strix/tools/mcp/naming.py b/strix/tools/mcp/naming.py index 818c34b2..dff2726c 100644 --- a/strix/tools/mcp/naming.py +++ b/strix/tools/mcp/naming.py @@ -1,18 +1,13 @@ -"""How an MCP server's tools are named for the model, and how to read that back. +"""How an MCP server's tools are named for the model. -Kept apart from the client, and stdlib-only, so the interfaces can resolve which -connection a tool call went to without importing the MCP client (and through it -the agents SDK and every registered tool). +Kept apart from the client, and stdlib-only, so a caller can build the +model-facing name for a connection's tool without importing the MCP client (and +through it the agents SDK and every registered tool). """ from __future__ import annotations import re -from typing import TYPE_CHECKING, NamedTuple - - -if TYPE_CHECKING: - from collections.abc import Iterable # A tool name offered to a model has to be letters, digits, underscores or @@ -32,49 +27,3 @@ def namespaced_tool_name(connection: str, tool: str) -> str: which tool is invoked. """ return _INVALID_TOOL_NAME_CHARS.sub("_", f"{connection}_{tool}") - - -class McpToolOrigin(NamedTuple): - """Where a model-facing tool name came from, for showing the user. - - ``connection`` is the name the user gave the connection in their config, so - it reads the way they wrote it. ``tool`` is what is left of the model-facing - name once the connection prefix is removed, which is the server's own name - for the tool and the part a reader cares about. - """ - - connection: str - tool: str - - -def resolve_mcp_tool(tool_name: str, connections: Iterable[str]) -> McpToolOrigin | None: - """Split a model-facing tool name against the run's connections, or ``None``. - - Matched against the connections the run actually made rather than by - splitting the name on the separator: the connection name and the server's own - tool name can both contain underscores, so a split is ambiguous and would - attribute calls to a connection that does not exist. Each connection name is - sanitized the same way :func:`namespaced_tool_name` sanitizes it before - comparing, so a connection whose name has characters a model-facing name - cannot carry still matches. - - The longest match wins, so one connection whose name is a prefix of another's - still resolves to the right one. The character after the prefix has to be a - separator rather than more of a name, which any non-alphanumeric satisfies, - so this holds whichever separator :func:`namespaced_tool_name` uses. - """ - best: McpToolOrigin | None = None - best_length = 0 - for connection in connections: - prefix = _INVALID_TOOL_NAME_CHARS.sub("_", connection) - if not prefix or len(tool_name) <= len(prefix) or not tool_name.startswith(prefix): - continue - if tool_name[len(prefix)].isalnum(): - continue - if len(prefix) > best_length: - # Past the prefix and its single separator character is the tool's - # own name; if a server named a tool nothing but separators, fall - # back to the whole name so the row still says something. - tool = tool_name[len(prefix) + 1 :] or tool_name - best, best_length = McpToolOrigin(connection, tool), len(prefix) - return best diff --git a/strix/tools/mcp/registry.py b/strix/tools/mcp/registry.py new file mode 100644 index 00000000..d90bf02c --- /dev/null +++ b/strix/tools/mcp/registry.py @@ -0,0 +1,216 @@ +"""Per-run registry of the MCP connections a scan may reach. + +Replaces per-tool registration. The old model turned every tool of every +connected MCP server into its own agent tool, so a run with a handful of +connections put dozens of provider tool schemas on the root agent's first LLM +request. Instead, a run holds its live connections here, keyed by the name the +user gave each connection, and every agent reaches them through three generic +dispatch tools: ``list_mcps`` to discover the available connections, ``describe_mcp`` +to learn one connection's tool schemas on demand, and ``call_mcp`` to run one of +its tools. + +One :class:`McpRegistry` is built per run in :mod:`strix.core.runner`, stored in +the run context under :data:`MCP_REGISTRY_CONTEXT_KEY`, and shared by the root +agent and every child (the child context is a copy of the parent's, so it +carries the same registry object). + +strix-pro imports :class:`McpRegistry` to add its cloud connections into the +same registry and to attach a per-connection ``result_transform`` (its +sanitizer), which :func:`strix.tools.mcp.client.dispatch_mcp_call` applies at the +single dispatch point. +""" + +from __future__ import annotations + +import dataclasses +from typing import TYPE_CHECKING, Any, NamedTuple + + +if TYPE_CHECKING: + from agents.mcp import MCPServer + + from strix.tools.mcp.client import ResultTransform + from strix.tools.mcp.config import McpConnectionConfig + + +# The run-context key under which the runner stores the per-run registry, and +# the two dispatch tools read it back. Kept here so the tools, the runner, and +# strix-pro all agree on one name. +MCP_REGISTRY_CONTEXT_KEY = "mcp_registry" + + +# The two connection-scoped dispatch tools an interface attributes to a specific +# MCP connection. ``call_mcp`` runs one tool on a connection; ``describe_mcp`` +# lists a connection's tool schemas. (``list_mcps`` is deliberately not here: it +# names no single connection, so it renders as an ordinary tool call.) Kept here +# (not in the interface layer) so the engine, the OSS viewer, and strix-pro's +# tracer all recognise a connection-scoped dispatch call by the same names. +CALL_MCP_TOOL = "call_mcp" +DESCRIBE_MCP_TOOL = "describe_mcp" +MCP_DISPATCH_TOOLS = frozenset({CALL_MCP_TOOL, DESCRIBE_MCP_TOOL}) + + +@dataclasses.dataclass(frozen=True) +class McpConnectionEntry: + """One live MCP connection a scan may reach, keyed by ``name``. + + ``server`` is the connected SDK session the dispatch tools list tools on and + call tools through. ``purpose`` is the human label ``list_mcps`` reports as the + connection's description (the user's connection notes, or whatever the caller + supplies). ``tool_count`` is how many tools the connection offers, also + reported by ``list_mcps``. ``result_transform``, when set, runs on each call's structured result + at the single dispatch point (strix-pro's sanitizer uses it). ``provider`` is + an optional source label (e.g. ``"supabase"``) the caller tags the connection + with; the command-line path leaves it ``None``, and event tagging surfaces it + when set. + """ + + server: MCPServer + name: str + purpose: str | None = None + tool_count: int = 0 + result_transform: ResultTransform | None = None + provider: str | None = None + + +@dataclasses.dataclass(frozen=True) +class McpConnectionSummary: + """One connection summary ``list_mcps`` returns: what an agent needs to decide + whether to ``describe_mcp`` a connection, with no tool schemas.""" + + name: str + purpose: str | None + tool_count: int + provider: str | None = None + + +@dataclasses.dataclass(frozen=True) +class McpConnectionRequest: + """A source-agnostic request to attach one MCP connection to a run. + + The caller hands the engine an inert ``config`` (how to reach the server, its + name, and any auth token) plus metadata, and never a live session: the engine + owns connecting and cleaning up. ``provider`` is an optional source label + (e.g. ``"supabase"``; empty for the command-line path). ``result_transform`` + is an optional per-connection transform run on each call's structured result + at the single dispatch point (strix-pro's sanitizer; empty for the + command-line path). ``purpose`` is the human label ``list_mcps`` reports as the + connection's description; when unset it falls back to ``config.notes``. + """ + + config: McpConnectionConfig + provider: str | None = None + result_transform: ResultTransform | None = None + purpose: str | None = None + + +class McpCallInfo(NamedTuple): + """What one MCP dispatch call resolved to: the connection name, the + underlying tool (empty for ``describe_mcp``), and the connection's provider + label (``None`` when unknown or untagged).""" + + connection: str + tool: str + provider: str | None + + +class McpRegistry: + """Connection name -> live MCP connection, built per run and shared by every + agent in the run. + + Public API (strix-pro builds against it): the constructor, :meth:`add`, + :meth:`get`, and :meth:`summaries`. + """ + + def __init__(self) -> None: + self._entries: dict[str, McpConnectionEntry] = {} + + def add( + self, + *, + name: str, + server: MCPServer, + purpose: str | None = None, + tool_count: int = 0, + result_transform: ResultTransform | None = None, + provider: str | None = None, + ) -> McpConnectionEntry: + """Register one connection under ``name`` (last write wins).""" + entry = McpConnectionEntry( + server=server, + name=name, + purpose=purpose, + tool_count=tool_count, + result_transform=result_transform, + provider=provider, + ) + self._entries[name] = entry + return entry + + def get(self, name: str) -> McpConnectionEntry | None: + """The connection registered under ``name``, or ``None``.""" + return self._entries.get(name) + + def names(self) -> list[str]: + """The registered connection names, in insertion order.""" + return list(self._entries) + + def summaries(self) -> list[McpConnectionSummary]: + """One inventory summary per connection, in insertion order.""" + return [ + McpConnectionSummary( + name=entry.name, + purpose=entry.purpose, + tool_count=entry.tool_count, + provider=entry.provider, + ) + for entry in self._entries.values() + ] + + def clear(self) -> None: + """Drop every connection (the sessions themselves are closed by the + runner).""" + self._entries.clear() + + def __len__(self) -> int: + return len(self._entries) + + def __bool__(self) -> bool: + return bool(self._entries) + + +def resolve_mcp_call( + tool_name: str, + args: dict[str, Any], + registry: McpRegistry | None = None, +) -> McpCallInfo | None: + """Resolve one tool call to the MCP connection/tool/provider it went out to. + + The single resolver both the OSS viewer and strix-pro's tracer read a + dispatch call through, so a call is attributed the same way everywhere. Every + MCP call an agent makes goes through ``call_mcp`` or ``describe_mcp``, and the + connection (and, for ``call_mcp``, the server's own tool name) ride in the + call's ``args`` rather than the tool name, so they are read from there. + + Returns ``None`` when ``tool_name`` is not one of the two dispatch tools, when + the call carries no connection name, or when a ``registry`` is supplied and + has no connection under that name. ``tool`` is the underlying tool for + ``call_mcp`` and empty for ``describe_mcp`` (which inspects the connection + itself). ``provider`` comes from the registry entry; it is ``None`` when no + ``registry`` is supplied (the viewer projects calls without one) or when the + connection carries no provider label. + """ + if tool_name not in MCP_DISPATCH_TOOLS: + return None + connection = args.get("connection") + if not isinstance(connection, str) or not connection: + return None + provider: str | None = None + if registry is not None: + entry = registry.get(connection) + if entry is None: + return None + provider = entry.provider + raw_tool = args.get("tool") if tool_name == CALL_MCP_TOOL else "" + tool = raw_tool if isinstance(raw_tool, str) else "" + return McpCallInfo(connection=connection, tool=tool, provider=provider) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..a840a37b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,26 @@ +"""Shared test fixtures.""" + +from __future__ import annotations + +import pytest + + +@pytest.fixture(autouse=True) +def _isolate_mcp_config( + monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory +) -> None: + """Keep the whole suite from reading the developer's real MCP config. + + ``run_strix_scan`` connects the MCP servers listed in + ``~/.strix/mcp-servers.json`` and threads an inventory of them into the + prompt context. Without isolation, any test that drives the runner on a + machine that has a real config would do real network I/O and see MCP + connections it never asked for. Point the loader at a path that does not + exist so it resolves to "no connections", and clear the per-run selection + env vars. Tests that exercise the loader itself set their own + ``STRIX_MCP_CONFIG`` after this runs and so override it. + """ + missing = tmp_path_factory.mktemp("mcp-isolation") / "no-servers.json" + monkeypatch.setenv("STRIX_MCP_CONFIG", str(missing)) + monkeypatch.delenv("STRIX_MCP_ONLY", raising=False) + monkeypatch.delenv("STRIX_MCP_EXCLUDE", raising=False) diff --git a/tests/test_mcp_client.py b/tests/test_mcp_client.py index ec70882b..069b9604 100644 --- a/tests/test_mcp_client.py +++ b/tests/test_mcp_client.py @@ -1,4 +1,9 @@ -"""Tests for the generic MCP client: config contract, namespacing, and filtering.""" +"""Tests for the generic MCP dispatch model. + +Connections are connected without being registered as agent tools; their live +sessions go into a per-run registry; and every agent reaches them through the +two dispatch tools ``describe_mcp`` and ``call_mcp``. +""" from __future__ import annotations @@ -9,30 +14,35 @@ from typing import TYPE_CHECKING, Any import pytest from agents.mcp import MCPServer, MCPServerStdio, MCPServerStreamableHttp +from agents.tool_context import ToolContext from mcp.types import CallToolResult, TextContent from mcp.types import Tool as MCPTool from pydantic import ValidationError from strix.agents import factory -from strix.core.runner import _mcp_connection_notes -from strix.interface.tui.live_view import TuiLiveView, _tool_status_from_result +from strix.agents.prompt import render_system_prompt +from strix.interface.tui.live_view import TuiLiveView from strix.tools.mcp import ( + MCP_REGISTRY_CONTEXT_KEY, BearerAuth, - ConnectedMcpServer, + McpCallInfo, McpConnectionConfig, + McpConnectionRequest, + McpRegistry, + attach_mcp_requests, + call_mcp, + describe_mcp, + list_mcps, load_user_mcp_configs, namespaced_tool_name, - resolve_mcp_tool, + resolve_mcp_call, ) from strix.tools.mcp import client as mcp_client -from strix.tools.mcp.client import _auth_headers, _build_server, _register_server_tools if TYPE_CHECKING: from pathlib import Path - from agents.tool import Tool - class FakeMCPServer(MCPServer): """A connected MCP server stand-in, so tests never touch the network.""" @@ -76,15 +86,73 @@ class FakeMCPServer(MCPServer): raise NotImplementedError -def _mcp_tool(name: str) -> MCPTool: +class ErroringMCPServer(FakeMCPServer): + """A connected server whose calls come back as MCP errors (isError=True).""" + + async def call_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + self.calls.append((tool_name, arguments)) + return CallToolResult( + content=[TextContent(type="text", text=f"boom:{tool_name}")], + isError=True, + ) + + +class MultiBlockErrorServer(FakeMCPServer): + """An errored call whose serialized output is a list (multiple content blocks).""" + + async def call_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + self.calls.append((tool_name, arguments)) + return CallToolResult( + content=[ + TextContent(type="text", text="first"), + TextContent(type="text", text="second"), + ], + isError=True, + ) + + +class StructuredErrorServer(FakeMCPServer): + """An errored call whose serialized output is a string (structured content).""" + + def __init__(self, name: str, tools: list[MCPTool]) -> None: + super().__init__(name, tools) + # The base server sets this in __init__, so flip it on the instance to + # take the structured-content serialization branch. + self.use_structured_content = True + + async def call_tool( + self, + tool_name: str, + arguments: dict[str, Any] | None, + meta: dict[str, Any] | None = None, + ) -> CallToolResult: + self.calls.append((tool_name, arguments)) + return CallToolResult( + content=[TextContent(type="text", text="ignored")], + structuredContent={"error": "boom"}, + isError=True, + ) + + +def _mcp_tool(name: str, *, description: str | None = None) -> MCPTool: return MCPTool( name=name, - description=f"remote tool {name}", - inputSchema={"type": "object", "properties": {}}, + description=description if description is not None else f"remote tool {name}", + inputSchema={"type": "object", "properties": {"path": {"type": "string"}}}, ) -def _config(name: str, allowed_tools: list[str]) -> McpConnectionConfig: +def _config(name: str, allowed_tools: list[str] | None) -> McpConnectionConfig: return McpConnectionConfig( name=name, url="https://mcp.example.com", @@ -93,28 +161,23 @@ def _config(name: str, allowed_tools: list[str]) -> McpConnectionConfig: ) +def _ctx(registry: McpRegistry | None) -> ToolContext[dict[str, Any]]: + context: dict[str, Any] = {} if registry is None else {MCP_REGISTRY_CONTEXT_KEY: registry} + return ToolContext( + context=context, + tool_name="mcp", + tool_call_id="call-1", + tool_arguments="{}", + ) + + @pytest.fixture(autouse=True) def _clear_mcp_env(monkeypatch: pytest.MonkeyPatch) -> None: - """Hide any MCP settings the developer has exported in their own shell. - - The loader reads these to resolve the config path and the per-run - include/exclude selection, so a shell that has them set (from using - --mcp-config or --mcp-server) would otherwise filter what these tests see. - """ + """Hide any MCP settings the developer has exported in their own shell.""" for name in ("STRIX_MCP_CONFIG", "STRIX_MCP_ONLY", "STRIX_MCP_EXCLUDE"): monkeypatch.delenv(name, raising=False) -@pytest.fixture(autouse=True) -def _reset_registry() -> Any: - saved = list(factory._EXTRA_TOOLS) - factory._EXTRA_TOOLS.clear() - try: - yield - finally: - factory._EXTRA_TOOLS[:] = saved - - # --- config contract --------------------------------------------------------- @@ -160,7 +223,6 @@ def test_stdio_config_parses_from_dict() -> None: assert config.command == "npx" assert config.args == ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"] assert config.env == {"FOO": "bar"} - # A local stdio server needs no auth, and omitting allowed_tools means "all". assert config.auth is None assert config.allowed_tools is None @@ -178,12 +240,7 @@ def test_http_config_without_url_is_rejected() -> None: def test_stdio_config_without_command_is_rejected() -> None: with pytest.raises(ValidationError): - McpConnectionConfig.model_validate( - { - "name": "x", - "transport": "stdio", - } - ) + McpConnectionConfig.model_validate({"name": "x", "transport": "stdio"}) def test_empty_name_is_rejected() -> None: @@ -213,221 +270,61 @@ def test_unknown_field_is_rejected() -> None: def test_bearer_auth_builds_authorization_header() -> None: - headers = _auth_headers(_config("files_main", [])) + headers = mcp_client._auth_headers(_config("files_main", [])) assert headers == {"Authorization": "Bearer run-token"} -# --- namespacing and filtering ----------------------------------------------- - - -def _registered_names() -> list[str]: - return [tool.name for tool in factory.registered_agent_tools()] +# --- connect without global registration ------------------------------------- @pytest.mark.asyncio -async def test_tools_are_namespaced_per_connection() -> None: - server_a = FakeMCPServer("conn_a", [_mcp_tool("describe")]) - server_b = FakeMCPServer("conn_b", [_mcp_tool("describe")]) +async def test_connect_returns_sessions_without_registering_agent_tools( + monkeypatch: pytest.MonkeyPatch, +) -> None: + before = list(factory.registered_agent_tools()) + servers = { + "fs": FakeMCPServer("fs", [_mcp_tool("read_file"), _mcp_tool("write_file")]), + "db": FakeMCPServer("db", [_mcp_tool("query")]), + } + monkeypatch.setattr(mcp_client, "_build_server", lambda config: servers[config.name]) - await _register_server_tools(_config("conn_a", ["describe"]), server_a) - await _register_server_tools(_config("conn_b", ["describe"]), server_b) - - # Same remote tool name on two connections does not collide. - assert _registered_names() == ["conn_a_describe", "conn_b_describe"] - - -@pytest.mark.asyncio -async def test_registered_names_are_valid_tool_names() -> None: - # Model APIs reject a tool name containing anything but letters, digits, - # underscores and hyphens, and reject the whole request rather than the one - # tool. A server naming its own tools with dots, or a connection named with - # a space in the user's config, must not be able to break a run. - server = FakeMCPServer("my server", [_mcp_tool("db.query"), _mcp_tool("ok_tool")]) - - await _register_server_tools(_config("my server", None), server) - - names = _registered_names() - assert names == ["my_server_db_query", "my_server_ok_tool"] - assert all(re.fullmatch(r"[a-zA-Z0-9_-]{1,128}", name) for name in names) - - -@pytest.mark.asyncio -async def test_a_rename_does_not_change_which_tool_is_called() -> None: - # Only the model-facing name is sanitized; the server is always asked for the - # tool name it reported. - server = FakeMCPServer("my server", [_mcp_tool("db.query")]) - - tools = await _register_server_tools(_config("my server", None), server) - - assert tools[0].name == "my_server_db_query" - await tools[0].on_invoke_tool(None, "{}") - assert server.calls == [("db.query", {})] - - -@pytest.mark.asyncio -async def test_disallowed_tool_is_not_registered() -> None: - server = FakeMCPServer( - "files_main", - [_mcp_tool("list_files"), _mcp_tool("search")], + connections = await mcp_client.connect_mcp_servers( + [_config("fs", None), _config("db", ["query"])] ) - await _register_server_tools(_config("files_main", ["list_files"]), server) - - names = _registered_names() - assert "files_main_list_files" in names - assert "files_main_search" not in names + # The live sessions come back with their tool counts, and nothing was added + # to the global agent-tool registry that pro shares. + assert [(c.name, c.tool_count) for c in connections] == [("fs", 2), ("db", 1)] + assert list(factory.registered_agent_tools()) == before @pytest.mark.asyncio -async def test_allowed_tools_none_registers_every_listed_tool() -> None: - server = FakeMCPServer( - "local_fs", - [_mcp_tool("read_file"), _mcp_tool("write_file")], - ) - config = McpConnectionConfig(name="local_fs", url="https://mcp.example.com", allowed_tools=None) +async def test_tool_count_honors_the_allowlist(monkeypatch: pytest.MonkeyPatch) -> None: + server = FakeMCPServer("fs", [_mcp_tool("read_file"), _mcp_tool("write_file")]) + monkeypatch.setattr(mcp_client, "_build_server", lambda _config: server) - await _register_server_tools(config, server) + connections = await mcp_client.connect_mcp_servers([_config("fs", ["read_file"])]) - names = _registered_names() - assert "local_fs_read_file" in names - assert "local_fs_write_file" in names + assert connections[0].tool_count == 1 @pytest.mark.asyncio -async def test_allowed_tools_list_restricts_registration() -> None: - server = FakeMCPServer( - "local_fs", - [_mcp_tool("read_file"), _mcp_tool("write_file")], +async def test_connection_notes_ride_on_the_connection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + server = FakeMCPServer("db", [_mcp_tool("query")]) + monkeypatch.setattr(mcp_client, "_build_server", lambda _config: server) + config = McpConnectionConfig( + name="db", + url="https://mcp.example.com", + notes="Staging analytics DB; read-only.", + allowed_tools=["query"], ) - await _register_server_tools(_config("local_fs", ["read_file"]), server) + connections = await mcp_client.connect_mcp_servers([config]) - names = _registered_names() - assert names == ["local_fs_read_file"] - - -@pytest.mark.asyncio -async def test_registered_tool_routes_to_its_server_with_the_original_name() -> None: - server = FakeMCPServer("files_main", [_mcp_tool("list_files")]) - - tools: list[Tool] = await _register_server_tools(_config("files_main", ["list_files"]), server) - tool = tools[0] - - output = await tool.on_invoke_tool(None, "{}") # type: ignore[union-attr] - - # The call reaches the right server, addressed by the unprefixed remote name. - assert server.calls == [("list_files", {})] - assert output == {"type": "text", "text": "routed:list_files"} - - -# --- result transform -------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_result_transform_receives_namespaced_name_and_structured_result() -> None: - server = FakeMCPServer("files_main", [_mcp_tool("list_files")]) - seen: list[tuple[str, Any]] = [] - - def transform(name: str, structured: Any) -> Any: - seen.append((name, structured)) - return {"kept": structured["content"][0]["text"]} - - tools: list[Tool] = await _register_server_tools( - _config("files_main", ["list_files"]), server, result_transform=transform - ) - - output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr] - - # The underlying MCP call still routes by the unprefixed remote name. - assert server.calls == [("list_files", {})] - - # The transform is called with the namespaced name and the parsed result. - assert len(seen) == 1 - name, structured = seen[0] - assert name == "files_main_list_files" - # A parsed CallToolResult (dict/list), not a pre-serialized string. - assert structured["content"][0]["text"] == "routed:list_files" - assert structured["isError"] is False - - # The transform's return value is exactly what the tool yields. - assert output == {"kept": "routed:list_files"} - - -@pytest.mark.asyncio -async def test_result_transform_can_rewrite_the_tool_output() -> None: - server = FakeMCPServer("files_main", [_mcp_tool("list_files")]) - - def transform(_name: str, structured: Any) -> Any: - # Keep only a truncated view of the text field. - return structured["content"][0]["text"][:6] - - tools: list[Tool] = await _register_server_tools( - _config("files_main", ["list_files"]), server, result_transform=transform - ) - - output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr] - - assert output == "routed" - - -@pytest.mark.asyncio -async def test_without_result_transform_output_is_unchanged() -> None: - server = FakeMCPServer("files_main", [_mcp_tool("list_files")]) - - tools: list[Tool] = await _register_server_tools( - _config("files_main", ["list_files"]), server, result_transform=None - ) - - output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr] - - # Same shape the SDK produces today: no transform in the path. - assert server.calls == [("list_files", {})] - assert output == {"type": "text", "text": "routed:list_files"} - - -# --- error status capture ---------------------------------------------------- - - -class ErroringMCPServer(FakeMCPServer): - """A connected server whose calls come back as MCP errors (isError=True).""" - - async def call_tool( - self, - tool_name: str, - arguments: dict[str, Any] | None, - meta: dict[str, Any] | None = None, - ) -> CallToolResult: - self.calls.append((tool_name, arguments)) - return CallToolResult( - content=[TextContent(type="text", text=f"boom:{tool_name}")], - isError=True, - ) - - -@pytest.mark.asyncio -async def test_errored_mcp_result_is_flagged_failed_for_the_tui() -> None: - server = ErroringMCPServer("files_main", [_mcp_tool("list_files")]) - - tools: list[Tool] = await _register_server_tools(_config("files_main", ["list_files"]), server) - output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr] - - # The error text stays exactly what the agent gets today; a success:False tag - # rides alongside it purely so the TUI can tell the call apart from a success. - assert output == {"type": "text", "text": "boom:list_files", "success": False} - assert _tool_status_from_result(output) == "failed" - - -@pytest.mark.asyncio -async def test_successful_mcp_result_stays_completed_for_the_tui() -> None: - server = FakeMCPServer("files_main", [_mcp_tool("list_files")]) - - tools: list[Tool] = await _register_server_tools(_config("files_main", ["list_files"]), server) - output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr] - - # A non-error result is untouched and keeps rendering as done. - assert output == {"type": "text", "text": "routed:list_files"} - assert _tool_status_from_result(output) == "completed" + assert connections[0].notes == "Staging analytics DB; read-only." # --- server build branch ----------------------------------------------------- @@ -442,9 +339,8 @@ def test_build_server_stdio_branch() -> None: env={"TOKEN": "x"}, ) - server = _build_server(config) + server = mcp_client._build_server(config) - # Built, not connected: no subprocess is launched here. assert isinstance(server, MCPServerStdio) assert server.name == "local_fs" assert server.params.command == "my-server" @@ -453,12 +349,337 @@ def test_build_server_stdio_branch() -> None: def test_build_server_http_branch() -> None: - server = _build_server(_config("files_main", ["list_files"])) + server = mcp_client._build_server(_config("files_main", ["list_files"])) assert isinstance(server, MCPServerStreamableHttp) assert server.name == "files_main" +# --- registry ---------------------------------------------------------------- + + +def test_registry_add_get_and_names() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + + registry.add(name="fs", server=server, purpose="local files", tool_count=1) + + entry = registry.get("fs") + assert entry is not None + assert entry.server is server + assert entry.purpose == "local files" + assert entry.tool_count == 1 + assert registry.get("missing") is None + assert registry.names() == ["fs"] + assert bool(registry) is True + assert len(registry) == 1 + + +def test_registry_summaries() -> None: + registry = McpRegistry() + registry.add(name="fs", server=FakeMCPServer("fs", []), purpose="local files", tool_count=2) + registry.add(name="db", server=FakeMCPServer("db", []), purpose=None, tool_count=1) + + summaries = registry.summaries() + assert [(s.name, s.purpose, s.tool_count) for s in summaries] == [ + ("fs", "local files", 2), + ("db", None, 1), + ] + + +# --- list_mcps --------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_list_mcps_returns_connections_with_ids_and_descriptions() -> None: + registry = McpRegistry() + registry.add(name="fs", server=FakeMCPServer("fs", []), purpose="local files", tool_count=2) + registry.add(name="db", server=FakeMCPServer("db", []), purpose=None, tool_count=1) + + out = await list_mcps.on_invoke_tool(_ctx(registry), "{}") + + # ``id`` is the exact connection name describe_mcp/call_mcp accept; + # ``description`` is the summary's purpose; no tool schemas are included. + assert out == { + "connections": [ + {"id": "fs", "name": "fs", "description": "local files", "tool_count": 2}, + {"id": "db", "name": "db", "description": None, "tool_count": 1}, + ] + } + + +@pytest.mark.asyncio +async def test_list_mcps_empty_without_a_registry() -> None: + assert await list_mcps.on_invoke_tool(_ctx(None), "{}") == {"connections": []} + + +@pytest.mark.asyncio +async def test_list_mcps_empty_when_registry_has_no_connections() -> None: + assert await list_mcps.on_invoke_tool(_ctx(McpRegistry()), "{}") == {"connections": []} + + +# --- describe_mcp ------------------------------------------------------------ + + +@pytest.mark.asyncio +async def test_describe_mcp_returns_tool_names_and_schemas() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file", description="Read a file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await describe_mcp.on_invoke_tool(_ctx(registry), json.dumps({"connection": "fs"})) + + assert "read_file" in out + assert "Read a file" in out + # The tool's JSON input schema is shown so the model can build call arguments. + assert '"path"' in out + + +@pytest.mark.asyncio +async def test_describe_mcp_errors_clearly_on_unknown_connection() -> None: + registry = McpRegistry() + registry.add(name="fs", server=FakeMCPServer("fs", []), purpose=None, tool_count=0) + + out = await describe_mcp.on_invoke_tool(_ctx(registry), json.dumps({"connection": "nope"})) + + assert "Unknown MCP connection 'nope'" in out + assert "fs" in out + + +@pytest.mark.asyncio +async def test_describe_mcp_without_any_connections() -> None: + out = await describe_mcp.on_invoke_tool(_ctx(None), json.dumps({"connection": "fs"})) + + assert out == "No MCP connections are configured for this run." + + +# --- call_mcp ---------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_call_mcp_dispatches_and_returns_converted_output() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), + json.dumps({"connection": "fs", "tool": "read_file", "arguments": {"path": "/etc/hosts"}}), + ) + + # The call reaches the server by the unprefixed tool name with its arguments. + assert server.calls == [("read_file", {"path": "/etc/hosts"})] + assert out == {"type": "text", "text": "routed:read_file"} + + +@pytest.mark.asyncio +async def test_call_mcp_defaults_missing_arguments_to_empty_object() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("ping")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + await call_mcp.on_invoke_tool(_ctx(registry), json.dumps({"connection": "fs", "tool": "ping"})) + + assert server.calls == [("ping", {})] + + +@pytest.mark.asyncio +async def test_call_mcp_coerces_json_string_arguments() -> None: + # Some models serialize the schema-less ``arguments`` object as a JSON string; + # a correct call must not be rejected over that encoding. + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), + json.dumps( + {"connection": "fs", "tool": "read_file", "arguments": '{"path": "/etc/hosts"}'} + ), + ) + + assert server.calls == [("read_file", {"path": "/etc/hosts"})] + assert out == {"type": "text", "text": "routed:read_file"} + + +@pytest.mark.asyncio +async def test_call_mcp_errors_on_unparseable_string_arguments() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), + json.dumps({"connection": "fs", "tool": "read_file", "arguments": "not json"}), + ) + + assert "expected a JSON object" in out + assert server.calls == [] + + +@pytest.mark.asyncio +async def test_call_mcp_errors_on_unknown_connection() -> None: + registry = McpRegistry() + registry.add(name="fs", server=FakeMCPServer("fs", []), purpose=None, tool_count=0) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "nope", "tool": "x"}) + ) + + assert "Unknown MCP connection 'nope'" in out + assert "fs" in out + + +@pytest.mark.asyncio +async def test_call_mcp_errors_on_unknown_tool() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "fs", "tool": "delete_everything"}) + ) + + assert "Unknown tool 'delete_everything'" in out + assert "read_file" in out + # A rejected tool name never reaches the server. + assert server.calls == [] + + +@pytest.mark.asyncio +async def test_call_mcp_errors_on_non_dict_arguments() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), + json.dumps({"connection": "fs", "tool": "read_file", "arguments": ["not", "a", "dict"]}), + ) + + assert "expected a JSON object" in out + assert server.calls == [] + + +@pytest.mark.asyncio +async def test_call_mcp_applies_a_connection_result_transform() -> None: + registry = McpRegistry() + server = FakeMCPServer("fs", [_mcp_tool("read_file")]) + seen: list[tuple[str, Any]] = [] + + def transform(label: str, structured: Any) -> Any: + seen.append((label, structured)) + return {"kept": structured["content"][0]["text"]} + + registry.add(name="fs", server=server, purpose=None, tool_count=1, result_transform=transform) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "fs", "tool": "read_file"}) + ) + + # The transform sees the model-facing _ label and the + # parsed CallToolResult, and its return becomes the tool output. + assert seen[0][0] == "fs_read_file" + assert seen[0][1]["content"][0]["text"] == "routed:read_file" + assert out == {"kept": "routed:read_file"} + + +@pytest.mark.asyncio +async def test_call_mcp_flags_an_errored_result_failed_for_the_tui() -> None: + registry = McpRegistry() + server = ErroringMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, purpose=None, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "fs", "tool": "read_file"}) + ) + + # The agent content is unchanged; success:False rides alongside so the TUI + # can tell an errored call from a done one. + assert out == {"type": "text", "text": "boom:read_file", "success": False} + + +# --- the two tools are the only MCP surface every agent gets ----------------- + + +def test_agent_carries_exactly_the_dispatch_tools_regardless_of_connections() -> None: + """No matter how many MCP connections a run makes, an agent's tool list gains + exactly list_mcps, describe_mcp, and call_mcp and never a per-connection + provider tool.""" + root = factory.build_strix_agent(is_root=True) + child = factory.build_strix_agent(is_root=False) + + root_names = [t.name for t in root.tools] + child_names = [t.name for t in child.tools] + + assert {"list_mcps", "describe_mcp", "call_mcp"} <= set(root_names) + assert {"list_mcps", "describe_mcp", "call_mcp"} <= set(child_names) + + # Five hypothetical connections would once have added ~all their tools as + # namespaced provider tools; none of those names may appear now. + provider_names = { + namespaced_tool_name(f"conn{i}", tool) + for i in range(5) + for tool in ("read_file", "write_file", "query") + } + assert provider_names.isdisjoint(root_names) + assert provider_names.isdisjoint(child_names) + + # The tool list does not grow with connection count: it is the same set of + # names whether or not any connection exists, because connections never + # contribute tools. + assert root_names == [t.name for t in factory.build_strix_agent(is_root=True).tools] + + +# --- prompt guidance replaces the old per-connection inventory --------------- + + +def test_prompt_renders_static_three_tool_guidance_when_mcp_available() -> None: + prompt = render_system_prompt(system_prompt_context={"mcp_available": True}) + + assert "MCP CONNECTIONS" in prompt + # The three discovery/dispatch tools are named as the way in. + assert "list_mcps" in prompt + assert "describe_mcp" in prompt + assert "call_mcp" in prompt + + +def test_prompt_has_no_mcp_section_without_availability() -> None: + assert "MCP CONNECTIONS" not in render_system_prompt(system_prompt_context={}) + + +def test_prompt_renders_named_connection_inventory() -> None: + """With mcp_available set, the prompt names each connected server (name, tool + count, purpose) so every agent sees what is available at the start, alongside + the three dispatch tools for re-listing and inspecting them at run time.""" + prompt = render_system_prompt( + system_prompt_context={ + "mcp_available": True, + "mcp_connections": [ + {"name": "supabase", "purpose": "read the app's schema", "tool_count": 13} + ], + } + ) + + assert "MCP CONNECTIONS" in prompt + assert "supabase" in prompt + assert "13 tools" in prompt + assert "read the app's schema" in prompt + + +def test_prompt_inventory_is_gated_on_availability() -> None: + """The block is gated on ``mcp_available``; an ``mcp_connections`` payload + without it renders nothing, so a stale or spoofed list cannot leak names.""" + prompt = render_system_prompt( + system_prompt_context={ + "mcp_connections": [{"name": "secret-conn", "purpose": "x", "tool_count": 3}] + } + ) + + assert "MCP CONNECTIONS" not in prompt + assert "secret-conn" not in prompt + + # --- loader ------------------------------------------------------------------ @@ -497,12 +718,8 @@ def test_loader_skips_bad_entry_but_keeps_good_ones(tmp_path: Path) -> None: config_file.write_text( json.dumps( [ - {"name": "broken", "transport": "http"}, # missing url - { - "name": "local_fs", - "transport": "stdio", - "command": "npx", - }, + {"name": "broken", "transport": "http"}, + {"name": "local_fs", "transport": "stdio", "command": "npx"}, ] ), encoding="utf-8", @@ -530,51 +747,54 @@ def test_loader_reads_env_var_override(tmp_path: Path, monkeypatch: pytest.Monke assert [c.name for c in configs] == ["local_fs"] -# --- connection notes -------------------------------------------------------- +def _names_file(tmp_path: Path, *names: str) -> Path: + config_file = tmp_path / "mcp-servers.json" + config_file.write_text( + json.dumps([{"name": n, "transport": "stdio", "command": "npx"} for n in names]), + encoding="utf-8", + ) + return config_file -@pytest.mark.asyncio -async def test_connection_notes_are_carried_on_the_connection( - monkeypatch: pytest.MonkeyPatch, -) -> None: - server = FakeMCPServer("db", [_mcp_tool("query")]) - monkeypatch.setattr(mcp_client, "_build_server", lambda _config: server) - config = McpConnectionConfig( - name="db", - url="https://mcp.example.com", - notes="Staging analytics DB; read-only.", - allowed_tools=["query"], +def test_loader_drops_duplicate_named_connections(tmp_path: Path) -> None: + config_file = tmp_path / "mcp-servers.json" + config_file.write_text( + json.dumps( + [ + {"name": "dup", "transport": "stdio", "command": "first"}, + {"name": "dup", "transport": "stdio", "command": "second"}, + {"name": "other", "transport": "stdio", "command": "npx"}, + ] + ), + encoding="utf-8", ) - connections = await mcp_client.connect_mcp_servers([config]) + configs = load_user_mcp_configs(config_file) - # Notes ride on the connection (surfaced once), not stapled onto each tool. - assert connections[0].notes == "Staging analytics DB; read-only." + assert [c.name for c in configs] == ["dup", "other"] + assert configs[0].command == "first" -def test_connection_notes_block_lists_only_noted_connections() -> None: - connections = [ - ConnectedMcpServer( - server=FakeMCPServer("db", []), name="db", tool_count=2, notes="staging, read-only" - ), - ConnectedMcpServer(server=FakeMCPServer("fs", []), name="fs", tool_count=1, notes=None), - ] +def test_loader_include_selection_keeps_only_named( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + config_file = _names_file(tmp_path, "a", "b", "c") + monkeypatch.setenv("STRIX_MCP_ONLY", "a,c") - block = _mcp_connection_notes(connections) + configs = load_user_mcp_configs(config_file) - assert block is not None - assert "db" in block - assert "staging, read-only" in block - # A connection without notes is not listed. - assert "fs" not in block + assert [c.name for c in configs] == ["a", "c"] -def test_connection_notes_block_is_none_without_notes() -> None: - connections = [ - ConnectedMcpServer(server=FakeMCPServer("db", []), name="db", tool_count=1, notes=None) - ] +def test_loader_exclude_selection_drops_named( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + config_file = _names_file(tmp_path, "a", "b", "c") + monkeypatch.setenv("STRIX_MCP_EXCLUDE", "b") - assert _mcp_connection_notes(connections) is None + configs = load_user_mcp_configs(config_file) + + assert [c.name for c in configs] == ["a", "c"] # --- cancellation cleanup ---------------------------------------------------- @@ -614,95 +834,31 @@ async def test_connect_cleans_up_when_cancelled_mid_connect( assert cleaned == ["bad", "good"] -# --- duplicate names and run selection --------------------------------------- - - -def _names_file(tmp_path: Path, *names: str) -> Path: - config_file = tmp_path / "mcp-servers.json" - config_file.write_text( - json.dumps([{"name": n, "transport": "stdio", "command": "npx"} for n in names]), - encoding="utf-8", - ) - return config_file - - -def test_loader_drops_duplicate_named_connections(tmp_path: Path) -> None: - config_file = tmp_path / "mcp-servers.json" - config_file.write_text( - json.dumps( - [ - {"name": "dup", "transport": "stdio", "command": "first"}, - {"name": "dup", "transport": "stdio", "command": "second"}, - {"name": "other", "transport": "stdio", "command": "npx"}, - ] - ), - encoding="utf-8", - ) - - configs = load_user_mcp_configs(config_file) - - # Duplicate name is dropped; the first entry wins. - assert [c.name for c in configs] == ["dup", "other"] - assert configs[0].command == "first" - - -def test_loader_include_selection_keeps_only_named( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - config_file = _names_file(tmp_path, "a", "b", "c") - monkeypatch.setenv("STRIX_MCP_ONLY", "a,c") - - configs = load_user_mcp_configs(config_file) - - assert [c.name for c in configs] == ["a", "c"] - - -def test_loader_exclude_selection_drops_named( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - config_file = _names_file(tmp_path, "a", "b", "c") - monkeypatch.setenv("STRIX_MCP_EXCLUDE", "b") - - configs = load_user_mcp_configs(config_file) - - assert [c.name for c in configs] == ["a", "c"] - - # --- reading a tool call back to the server it went out to ------------------- +# namespaced_tool_name stays in strix.tools.mcp.naming so call_mcp can build the +# result_transform label. The connection a call went out to is read off the +# call's arguments by the TUI projection, not off the tool name. -def test_resolve_mcp_tool_splits_against_the_run_connections() -> None: - assert resolve_mcp_tool("local_fs_read_file", ["github", "local_fs"]) == ( - "local_fs", - "read_file", - ) +def test_namespaced_name_is_a_valid_tool_name() -> None: + # A connection named with a space and a server tool named with a dot still + # sanitize to a valid model-facing label for the result_transform. + name = namespaced_tool_name("my server", "db.query") + + assert name == "my_server_db_query" + assert re.fullmatch(r"[a-zA-Z0-9_-]{1,128}", name) -def test_resolve_mcp_tool_prefers_the_longest_matching_connection() -> None: - # One connection's name being a prefix of another's must not misattribute. - assert resolve_mcp_tool("files_main_list", ["files", "files_main"]) == ("files_main", "list") - - -def test_resolve_mcp_tool_matches_a_connection_name_it_had_to_sanitize() -> None: - # "my server" reaches the model as "my_server_db_query". - tool_name = namespaced_tool_name("my server", "db.query") - - assert resolve_mcp_tool(tool_name, ["my server"]) == ("my server", "db_query") - - -def test_resolve_mcp_tool_ignores_tools_that_are_not_a_connection_s() -> None: - assert resolve_mcp_tool("exec_command", ["local_fs"]) is None - # A name that merely starts like a connection is not one of its tools. - assert resolve_mcp_tool("local_fsx", ["local_fs"]) is None - - -def test_projected_tool_call_names_the_server_it_went_out_to() -> None: +def test_projected_call_mcp_names_the_server_and_tool_from_its_args() -> None: view = TuiLiveView() - view.set_mcp_connections(["local_fs"]) view._record_tool_call_data( "agent-1", - {"call_id": "c1", "tool_name": "local_fs_read_file", "args": {"path": "/etc/hosts"}}, + { + "call_id": "c1", + "tool_name": "call_mcp", + "args": {"connection": "local_fs", "tool": "read_file", "arguments": {"path": "/x"}}, + }, ) view._record_tool_call_data( "agent-1", @@ -711,5 +867,228 @@ def test_projected_tool_call_names_the_server_it_went_out_to() -> None: mcp_call, built_in = (event["data"] for event in view.events) assert (mcp_call["mcp_connection"], mcp_call["mcp_tool"]) == ("local_fs", "read_file") - # A built-in call carries no connection, which is what keeps it rendering as one. assert "mcp_connection" not in built_in + + +def test_projected_describe_mcp_names_the_connection_with_no_tool() -> None: + view = TuiLiveView() + + view._record_tool_call_data( + "agent-1", + {"call_id": "c1", "tool_name": "describe_mcp", "args": {"connection": "local_fs"}}, + ) + + (describe,) = (event["data"] for event in view.events) + # An empty tool is what tells both renderers to present the row as inspecting + # the connection rather than as a call to a tool on it. + assert describe["mcp_connection"] == "local_fs" + assert describe["mcp_tool"] == "" + + +# --- source-agnostic attach -------------------------------------------------- + + +@pytest.mark.asyncio +async def test_attach_populates_registry_with_provider_and_transform( + monkeypatch: pytest.MonkeyPatch, +) -> None: + server = FakeMCPServer("db", [_mcp_tool("query")]) + monkeypatch.setattr(mcp_client, "_build_server", lambda _config: server) + + def transform(_label: str, structured: Any) -> Any: + return {"kept": structured} + + registry = McpRegistry() + request = McpConnectionRequest( + config=_config("db", ["query"]), + provider="supabase", + result_transform=transform, + purpose="Customer DB", + ) + + connections = await attach_mcp_requests([request], registry) + + assert [(c.name, c.tool_count) for c in connections] == [("db", 1)] + entry = registry.get("db") + assert entry is not None + assert entry.server is server + assert entry.provider == "supabase" + assert entry.purpose == "Customer DB" + assert entry.result_transform is transform + + +@pytest.mark.asyncio +async def test_attach_bare_request_matches_the_command_line_shape( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # The command-line path wraps each config in a bare request (no provider or + # transform); purpose then falls back to the connection's notes. + server = FakeMCPServer("db", [_mcp_tool("query")]) + monkeypatch.setattr(mcp_client, "_build_server", lambda _config: server) + config = McpConnectionConfig( + name="db", + url="https://mcp.example.com", + notes="Staging analytics DB; read-only.", + allowed_tools=["query"], + ) + + registry = McpRegistry() + await attach_mcp_requests([McpConnectionRequest(config=config)], registry) + + entry = registry.get("db") + assert entry is not None + assert entry.provider is None + assert entry.result_transform is None + assert entry.purpose == "Staging analytics DB; read-only." + + +@pytest.mark.asyncio +async def test_attach_is_fail_open_and_skips_a_failed_connection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + good = FakeMCPServer("good", [_mcp_tool("t")]) + + class _Failing(FakeMCPServer): + async def connect(self) -> None: + raise RuntimeError("cannot reach server") + + servers = {"good": good, "bad": _Failing("bad", [_mcp_tool("t")])} + monkeypatch.setattr(mcp_client, "_build_server", lambda config: servers[config.name]) + + registry = McpRegistry() + connections = await attach_mcp_requests( + [ + McpConnectionRequest(config=_config("bad", ["t"]), provider="p"), + McpConnectionRequest(config=_config("good", ["t"]), provider="q"), + ], + registry, + ) + + # The failed connection is skipped without raising; the good one is attached. + assert [c.name for c in connections] == ["good"] + assert registry.names() == ["good"] + assert registry.get("good") is not None + assert registry.get("bad") is None + + +# --- provider on the registry ------------------------------------------------ + + +def test_provider_round_trips_through_registry_and_summaries() -> None: + registry = McpRegistry() + registry.add( + name="db", + server=FakeMCPServer("db", []), + purpose="Customer DB", + tool_count=1, + provider="supabase", + ) + registry.add(name="fs", server=FakeMCPServer("fs", []), purpose=None, tool_count=0) + + assert registry.get("db").provider == "supabase" # type: ignore[union-attr] + # A connection with no provider defaults to None, not an error. + assert registry.get("fs").provider is None # type: ignore[union-attr] + + summaries = {s.name: s.provider for s in registry.summaries()} + assert summaries == {"db": "supabase", "fs": None} + + +# --- resolve_mcp_call -------------------------------------------------------- + + +def test_resolve_call_mcp_reads_connection_tool_and_provider() -> None: + registry = McpRegistry() + registry.add(name="db", server=FakeMCPServer("db", []), tool_count=1, provider="supabase") + + info = resolve_mcp_call( + "call_mcp", {"connection": "db", "tool": "query", "arguments": {}}, registry + ) + + assert info == McpCallInfo(connection="db", tool="query", provider="supabase") + + +def test_resolve_describe_mcp_has_an_empty_tool() -> None: + registry = McpRegistry() + registry.add(name="db", server=FakeMCPServer("db", []), tool_count=1, provider="supabase") + + info = resolve_mcp_call("describe_mcp", {"connection": "db"}, registry) + + assert info == McpCallInfo(connection="db", tool="", provider="supabase") + + +def test_resolve_without_a_registry_omits_the_provider() -> None: + # The OSS viewer projects calls with no live registry: it still reads the + # connection and tool, and simply leaves the provider out. + info = resolve_mcp_call("call_mcp", {"connection": "db", "tool": "query"}) + + assert info == McpCallInfo(connection="db", tool="query", provider=None) + + +def test_resolve_returns_none_for_a_non_dispatch_tool() -> None: + assert resolve_mcp_call("exec_command", {"cmd": "ls"}) is None + + +def test_resolve_returns_none_for_an_unknown_connection_with_a_registry() -> None: + registry = McpRegistry() + registry.add(name="db", server=FakeMCPServer("db", []), tool_count=1) + + assert resolve_mcp_call("call_mcp", {"connection": "nope", "tool": "x"}, registry) is None + + +def test_resolve_returns_none_when_the_connection_is_missing_from_args() -> None: + assert resolve_mcp_call("call_mcp", {"tool": "query"}) is None + + +# --- errored results surface as failed regardless of output shape ------------ + + +@pytest.mark.asyncio +async def test_errored_dict_output_carries_success_false() -> None: + registry = McpRegistry() + server = ErroringMCPServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "fs", "tool": "read_file"}) + ) + + # A single content block is a dict; success:False rides alongside and the + # SDK's ToolOutput projection drops it before the agent, so the agent keeps + # the exact error content. + assert out == {"type": "text", "text": "boom:read_file", "success": False} + + +@pytest.mark.asyncio +async def test_errored_list_output_is_wrapped_with_success_false() -> None: + registry = McpRegistry() + server = MultiBlockErrorServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "fs", "tool": "read_file"}) + ) + + # Multiple content blocks serialize to a list, which has no top-level dict to + # carry the flag, so it is wrapped under ``content`` with success:False. + assert out == { + "success": False, + "content": [ + {"type": "text", "text": "first"}, + {"type": "text", "text": "second"}, + ], + } + + +@pytest.mark.asyncio +async def test_errored_structured_output_is_wrapped_with_success_false() -> None: + registry = McpRegistry() + server = StructuredErrorServer("fs", [_mcp_tool("read_file")]) + registry.add(name="fs", server=server, tool_count=1) + + out = await call_mcp.on_invoke_tool( + _ctx(registry), json.dumps({"connection": "fs", "tool": "read_file"}) + ) + + # Structured content serializes to a JSON string; it too is wrapped under + # ``content`` so the failure flag has a top-level dict to ride on. + assert out == {"success": False, "content": json.dumps({"error": "boom"})} diff --git a/tests/test_runner_mcp.py b/tests/test_runner_mcp.py new file mode 100644 index 00000000..d9ccdd1f --- /dev/null +++ b/tests/test_runner_mcp.py @@ -0,0 +1,142 @@ +"""The runner attaches MCP connections source-agnostically. + +When a caller supplies ``mcp_connection_requests`` the runner attaches those; +when it does not, the runner reads ``~/.strix/mcp-servers.json`` itself and wraps +each config in a bare request. Either way the one shared ``attach_mcp_requests`` +routine does the connecting. +""" + +from __future__ import annotations + +import types +from typing import Any + +import pytest +from agents import ModelSettings + +import strix.tools.mcp as mcp_pkg +import strix.tools.notes.tools as notes_tools +import strix.tools.todo.tools as todo_tools +from strix.core import runner +from strix.core.agents import AgentCoordinator +from strix.runtime import session_manager +from strix.tools.mcp import McpConnectionConfig, McpConnectionRequest + + +def _settings() -> Any: + return types.SimpleNamespace( + llm=types.SimpleNamespace( + model="openai/gpt-4o", + reasoning_effort="high", + force_required_tool_choice=False, + timeout=300, + prompt_cache=True, + extra_headers=None, + ), + runtime=types.SimpleNamespace(max_context_images=3), + ) + + +def _wire_runner(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None: + monkeypatch.setattr(runner, "run_dir_for", lambda _scan_id: tmp_path) + monkeypatch.setattr(runner, "runtime_state_dir", lambda _run_dir: tmp_path) + monkeypatch.setattr(runner, "setup_scan_logging", lambda _run_dir: lambda: None) + monkeypatch.setattr(runner, "set_scan_id", lambda _scan_id: None) + monkeypatch.setattr(runner, "load_settings", _settings) + monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _s: None) + monkeypatch.setattr(runner, "uses_chat_completions_tool_schema", lambda _m, _s: False) + monkeypatch.setattr(todo_tools, "hydrate_todos_from_disk", lambda _d: None) + monkeypatch.setattr(notes_tools, "hydrate_notes_from_disk", lambda _d: None) + + async def _create_or_reuse(*_a: Any, **_k: Any) -> dict[str, Any]: + return {"client": object(), "session": object(), "caido_client": None} + + async def _cleanup(*_a: Any, **_k: Any) -> None: + return None + + monkeypatch.setattr(session_manager, "create_or_reuse", _create_or_reuse) + monkeypatch.setattr(session_manager, "cleanup", _cleanup) + monkeypatch.setattr(runner, "build_root_task", lambda _c: "task") + monkeypatch.setattr(runner, "build_scope_context", lambda _c: {}) + monkeypatch.setattr(runner, "make_model_settings", lambda *_a, **_k: ModelSettings()) + monkeypatch.setattr(runner, "build_strix_agent", lambda **_k: object()) + monkeypatch.setattr(runner, "make_child_factory", lambda **_k: lambda **_kk: object()) + monkeypatch.setattr(runner, "open_agent_session", lambda _root_id, _db: object()) + + async def _run_agent_loop(**_kwargs: Any) -> None: + return None + + monkeypatch.setattr(runner, "run_agent_loop", _run_agent_loop) + + +@pytest.mark.asyncio +async def test_none_default_attaches_from_the_user_config_file( + monkeypatch: pytest.MonkeyPatch, tmp_path: Any +) -> None: + _wire_runner(monkeypatch, tmp_path) + + file_config = McpConnectionConfig( + name="local_fs", transport="stdio", command="npx", notes="local files" + ) + monkeypatch.setattr(mcp_pkg, "load_user_mcp_configs", lambda: [file_config]) + + captured: list[list[McpConnectionRequest]] = [] + + async def _capture(requests: list[McpConnectionRequest], _registry: Any) -> list[Any]: + captured.append(requests) + return [] + + monkeypatch.setattr(mcp_pkg, "attach_mcp_requests", _capture) + + await runner.run_strix_scan( + scan_config={"targets": [], "scan_mode": "deep"}, + scan_id="scan-none", + image="img", + coordinator=AgentCoordinator(), + ) + + # Each config from the file is wrapped in a bare request: no provider, no + # transform, no explicit purpose (purpose falls back to notes at attach time). + (requests,) = captured + assert len(requests) == 1 + assert requests[0].config is file_config + assert requests[0].provider is None + assert requests[0].result_transform is None + assert requests[0].purpose is None + + +@pytest.mark.asyncio +async def test_supplied_requests_are_attached_and_the_user_file_is_not_read( + monkeypatch: pytest.MonkeyPatch, tmp_path: Any +) -> None: + _wire_runner(monkeypatch, tmp_path) + + def _fail_if_read() -> list[Any]: + raise AssertionError("load_user_mcp_configs must not be read when requests are supplied") + + monkeypatch.setattr(mcp_pkg, "load_user_mcp_configs", _fail_if_read) + + captured: list[list[McpConnectionRequest]] = [] + + async def _capture(requests: list[McpConnectionRequest], _registry: Any) -> list[Any]: + captured.append(requests) + return [] + + monkeypatch.setattr(mcp_pkg, "attach_mcp_requests", _capture) + + supplied = [ + McpConnectionRequest( + config=McpConnectionConfig(name="db", url="https://mcp.example.com"), + provider="supabase", + ) + ] + + await runner.run_strix_scan( + scan_config={"targets": [], "scan_mode": "deep"}, + scan_id="scan-supplied", + image="img", + coordinator=AgentCoordinator(), + mcp_connection_requests=supplied, + ) + + assert captured == [supplied] diff --git a/tests/test_runner_root_prompt.py b/tests/test_runner_root_prompt.py index 2c346203..1bea19c2 100644 --- a/tests/test_runner_root_prompt.py +++ b/tests/test_runner_root_prompt.py @@ -14,11 +14,13 @@ import pytest from agents import ModelSettings from openai import RateLimitError +import strix.tools.mcp as mcp_pkg import strix.tools.notes.tools as notes_tools import strix.tools.todo.tools as todo_tools from strix.core import runner from strix.core.agents import AgentCoordinator from strix.runtime import session_manager +from strix.tools.mcp import BearerAuth, McpConnectionConfig, McpConnectionRequest def _make_rate_limit_error() -> RateLimitError: @@ -180,6 +182,72 @@ async def test_root_prompt_options_default_to_none( assert kwargs["system_prompt_context"] == {"scope": "built-in"} +@pytest.mark.asyncio +async def test_mcp_available_flag_set_when_a_connection_attaches( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Any, +) -> None: + """When at least one MCP connection attaches, the runner sets ``mcp_available`` + plus a named ``mcp_connections`` inventory into the scan context that reaches + every agent, so each agent sees which connections exist at the start while + still being able to re-list them at run time via list_mcps.""" + scope_context: dict[str, Any] = {"scope": "built-in"} + captured = _patch_engine_scaffold(monkeypatch, tmp_path, scope_context) + + async def _attach(_requests: Any, registry: Any) -> list[Any]: + registry.add(name="fs", server=object(), purpose="local files", tool_count=2) + return [types.SimpleNamespace(name="fs", tool_count=2, server=object())] + + monkeypatch.setattr(mcp_pkg, "attach_mcp_requests", _attach) + + request = McpConnectionRequest( + config=McpConnectionConfig( + name="fs", + url="https://mcp.example.com", + auth=BearerAuth(token="run-token"), + ) + ) + + await runner.run_strix_scan( + scan_config={"targets": [], "scan_mode": "deep"}, + scan_id="scan-mcp-available", + image="img", + coordinator=AgentCoordinator(), + mcp_connection_requests=[request], + ) + + kwargs = captured["kwargs"] + assert kwargs["system_prompt_context"]["mcp_available"] is True + # The named inventory names each connected server for the prompt. + assert kwargs["system_prompt_context"]["mcp_connections"] == [ + {"name": "fs", "purpose": "local files", "tool_count": 2} + ] + + +@pytest.mark.asyncio +async def test_mcp_available_flag_absent_without_a_connection( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Any, +) -> None: + """With no MCP connection, the scan context carries no MCP key at all, so the + prompt's MCP section stays off.""" + scope_context: dict[str, Any] = {"scope": "built-in"} + captured = _patch_engine_scaffold(monkeypatch, tmp_path, scope_context) + + monkeypatch.setattr(mcp_pkg, "load_user_mcp_configs", list) + + await runner.run_strix_scan( + scan_config={"targets": [], "scan_mode": "deep"}, + scan_id="scan-mcp-absent", + image="img", + coordinator=AgentCoordinator(), + ) + + kwargs = captured["kwargs"] + assert "mcp_available" not in kwargs["system_prompt_context"] + assert "mcp_connections" not in kwargs["system_prompt_context"] + + @pytest.mark.asyncio async def test_unknown_tool_calls_are_returned_to_the_model( monkeypatch: pytest.MonkeyPatch,