diff --git a/pyproject.toml b/pyproject.toml index 73dfb8e..f6a49c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,12 +35,10 @@ classifiers = [ dependencies = [ "litellm[proxy]>=1.83.0", "openai-agents[litellm]==0.14.6", - "tenacity>=9.0.0", "pydantic[email]>=2.11.3", "rich", "docker>=7.1.0", "textual>=6.0.0", - "xmltodict>=0.13.0", "requests>=2.32.0", "cvss>=3.2", "traceloop-sdk>=0.53.0", @@ -119,7 +117,6 @@ pretty = true [[tool.mypy.overrides]] module = [ "litellm.*", - "tenacity.*", "numpydoc.*", "rich.*", "IPython.*", @@ -294,7 +291,6 @@ ignore = [ "strix/tools/web_search/tool.py" = ["TC002"] "strix/tools/file_edit/tools.py" = ["TC002"] "strix/tools/reporting/tool.py" = ["TC002"] -"strix/tools/load_skill/tool.py" = ["TC002"] "strix/tools/finish/tool.py" = ["TC002"] "strix/tools/browser/tool.py" = ["TC002"] "strix/tools/terminal/tool.py" = ["TC002"] @@ -420,7 +416,7 @@ force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true known_first_party = ["strix"] -known_third_party = ["fastapi", "pydantic", "litellm", "tenacity"] +known_third_party = ["fastapi", "pydantic", "litellm"] # ============================================================================ # Pytest Configuration diff --git a/strix/agents/factory.py b/strix/agents/factory.py index 0fc77d8..8455e5c 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -21,8 +21,9 @@ Two flavors: Caido tools come from ``CaidoCapability.tools()`` automatically via the SDK's capability merge — we don't include them here. Skills are -injected via the prompt; the model can also load more at runtime via -the ``load_skill`` tool. +injected via the prompt at scan-bring-up time; runtime skill loading +isn't exposed as a tool any more (the legacy implementation reached +into a global agent registry that no longer exists). References: - PLAYBOOK.md §4.3 (graph tool wiring) @@ -54,7 +55,6 @@ from strix.tools.file_edit.tools import ( str_replace_editor, ) from strix.tools.finish.tool import finish_scan -from strix.tools.load_skill.tool import load_skill from strix.tools.notes.tools import ( create_note, delete_note, @@ -109,8 +109,6 @@ _BASE_TOOLS: tuple[Tool, ...] = ( search_files, # Reporting create_vulnerability_report, - # Skill loading - load_skill, # Sandbox primitives browser_action, terminal_execute, @@ -140,8 +138,7 @@ def build_strix_agent( name: Agent name. Surfaces in traces and the bus's ``names`` map. Defaults to ``"strix"`` for the root; create_agent passes distinct names per child. - skills: Skills to preload into the system prompt. The agent can - also load more at runtime via the ``load_skill`` tool. + skills: Skills to preload into the system prompt. is_root: Selects the tool list and ``tool_use_behavior``. Root carries ``finish_scan`` and stops there; child carries ``agent_finish`` and stops there. diff --git a/strix/agents/prompt.py b/strix/agents/prompt.py index 795deaa..9ace555 100644 --- a/strix/agents/prompt.py +++ b/strix/agents/prompt.py @@ -67,9 +67,7 @@ def render_system_prompt( """Render the system prompt. Args: - skills: Skills the caller wants preloaded into the prompt - context (the agent can also load more at runtime via the - ``load_skill`` tool). + skills: Skills the caller wants preloaded into the prompt context. scan_mode: ``"deep" | "fast" | ...``. Maps to ``scan_modes/`` skill. is_whitebox: When True, the source-aware whitebox skill stack diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index 8c89ef2..a67a29f 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -148,9 +148,7 @@ OPERATIONAL PRINCIPLES: - Default to recon first. Unless the next step is obvious from context or the user/system gives specific prioritization instructions, begin by mapping the target well before diving into narrow validation or targeted testing - Prefer established industry-standard tools already available in the sandbox before writing custom scripts - Do NOT reinvent the wheel with ad hoc Python or shell code when a suitable existing tool can do the job reliably -- Use the load_skill tool when you need exact vulnerability-specific, protocol-specific, or tool-specific guidance before acting -- Prefer loading a relevant skill before guessing payloads, workflows, or tool syntax from memory -- If a task maps cleanly to one or more available skills, load them early and let them guide your next actions +- Skills relevant to your task are preloaded into this prompt at scan start; refer back to them when you need vulnerability-, protocol-, or tool-specific guidance - Use custom Python or shell code when you want to dig deeper, automate custom workflows, batch operations, triage results, build target-specific validation, or do work that existing tools do not cover cleanly - Chain related weaknesses when needed to demonstrate real impact - Consider business logic and context in validation diff --git a/strix/entry.py b/strix/entry.py index 7cc143b..bc4e4c9 100644 --- a/strix/entry.py +++ b/strix/entry.py @@ -21,6 +21,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any from agents import Runner +from agents.tracing import add_trace_processor from strix.agents.factory import build_strix_agent, make_child_factory from strix.orchestration.bus import AgentMessageBus @@ -31,6 +32,7 @@ from strix.run_config_factory import ( make_run_config, ) from strix.sandbox import session_manager +from strix.telemetry.strix_processor import StrixTracingProcessor if TYPE_CHECKING: @@ -156,8 +158,10 @@ async def run_strix_scan( image: str, sources_path: Path, tracer: Any | None = None, + bus: AgentMessageBus | None = None, interactive: bool = False, max_turns: int = STRIX_DEFAULT_MAX_TURNS, + model: str = "anthropic/claude-sonnet-4-6", cleanup_on_exit: bool = True, ) -> RunResult: """Run one Strix scan end-to-end against a freshly-prepared sandbox. @@ -188,9 +192,25 @@ async def run_strix_scan( scan_id = f"scan-{uuid.uuid4().hex[:8]}" logger.info("Starting Strix scan %s", scan_id) - bus = AgentMessageBus() + # Caller may pre-create the bus so it can hold a handle (e.g., the + # TUI uses it to route stop / chat-input commands). Otherwise we + # own the bus internally for the scan's lifetime. + if bus is None: + bus = AgentMessageBus() root_id = uuid.uuid4().hex[:8] + # Wire SDK tracing into the scan's run-directory ``events.jsonl``. + # ``add_trace_processor`` is idempotent at the provider level — if + # the user runs multiple scans in one process they each get their + # own processor, all writing to their respective run dirs. + if tracer is not None: + try: + run_dir = tracer.get_run_dir() if hasattr(tracer, "get_run_dir") else None + if run_dir is not None: + add_trace_processor(StrixTracingProcessor(run_dir)) + except Exception: + logger.exception("Failed to register StrixTracingProcessor") + bundle = await session_manager.create_or_reuse( scan_id, image=image, @@ -232,10 +252,12 @@ async def run_strix_scan( sandbox_token=bundle["bearer"], tool_server_host_port=bundle["tool_server_host_port"], caido_host_port=bundle["caido_host_port"], + caido_capability=bundle.get("capability"), agent_id=root_id, agent_name="strix", parent_id=None, tracer=tracer, + model=model, max_turns=max_turns, is_whitebox=is_whitebox, diff_scope=diff_scope, @@ -246,6 +268,7 @@ async def run_strix_scan( run_config = make_run_config( sandbox_session=bundle["session"], sandbox_client=bundle["client"], + model=model, ) task_text = _build_root_task(scan_config) diff --git a/strix/interface/tool_components/__init__.py b/strix/interface/tool_components/__init__.py index c8b6007..cb8aeea 100644 --- a/strix/interface/tool_components/__init__.py +++ b/strix/interface/tool_components/__init__.py @@ -4,7 +4,6 @@ from . import ( browser_renderer, file_edit_renderer, finish_renderer, - load_skill_renderer, notes_renderer, proxy_renderer, python_renderer, @@ -29,7 +28,6 @@ __all__ = [ "file_edit_renderer", "finish_renderer", "get_tool_renderer", - "load_skill_renderer", "notes_renderer", "proxy_renderer", "python_renderer", diff --git a/strix/interface/tool_components/load_skill_renderer.py b/strix/interface/tool_components/load_skill_renderer.py deleted file mode 100644 index 41a1868..0000000 --- a/strix/interface/tool_components/load_skill_renderer.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Any, ClassVar - -from rich.text import Text -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class LoadSkillRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "load_skill" - css_classes: ClassVar[list[str]] = ["tool-call", "load-skill-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - status = tool_data.get("status", "completed") - - requested = args.get("skills", "") - - text = Text() - text.append("◇ ", style="#10b981") - text.append("loading skill", style="dim") - - if requested: - text.append(" ") - text.append(requested, style="#10b981") - elif not tool_data.get("result"): - text.append("\n ") - text.append("Loading...", style="dim") - - return Static(text, classes=cls.get_css_classes(status)) diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 3db4ba1..179f4c5 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -711,6 +711,13 @@ class StrixTUIApp(App): # type: ignore[misc] self.tracer.set_scan_config(self.scan_config) set_global_tracer(self.tracer) + # Pre-create the bus here (rather than letting ``run_strix_scan`` + # build its own) so the TUI can hold a handle for stop / chat + # routing while the scan loop runs in a worker thread. + from strix.orchestration.bus import AgentMessageBus + + self.bus: AgentMessageBus = AgentMessageBus() + self.agent_nodes: dict[str, TreeNode] = {} self._displayed_agents: set[str] = set() @@ -720,6 +727,7 @@ class StrixTUIApp(App): # type: ignore[misc] self._last_streaming_len: dict[str, int] = {} self._scan_thread: threading.Thread | None = None + self._scan_loop: asyncio.AbstractEventLoop | None = None self._scan_stop_event = threading.Event() self._scan_completed = threading.Event() @@ -1496,6 +1504,10 @@ class StrixTUIApp(App): # type: ignore[misc] try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) + # Stash the loop so synchronous TUI handlers (stop / + # chat) can submit bus coroutines onto it from the + # main thread. + self._scan_loop = loop try: if not self._scan_stop_event.is_set(): @@ -1508,6 +1520,7 @@ class StrixTUIApp(App): # type: ignore[misc] image=str(image), sources_path=sources_path, tracer=self.tracer, + bus=self.bus, interactive=True, ), ) @@ -1827,10 +1840,6 @@ class StrixTUIApp(App): # type: ignore[misc] metadata={"interrupted": True}, ) - # TODO: route user→agent messages through the AgentMessageBus - # once the TUI has a handle on it. The bus currently lives - # inside ``run_strix_scan`` scope only. - if self.tracer: self.tracer.log_chat_message( content=message, @@ -1838,11 +1847,18 @@ class StrixTUIApp(App): # type: ignore[misc] agent_id=self.selected_agent_id, ) - logging.warning( - "User-message-to-agent dispatch is not wired post-migration; " - "message %r logged to tracer but not delivered.", - message, - ) + # Route to the agent's bus inbox. The scan loop runs on a + # worker thread; ``run_coroutine_threadsafe`` submits the + # coroutine onto that loop and returns immediately so the TUI + # stays responsive. + if self._scan_loop is not None and not self._scan_loop.is_closed(): + asyncio.run_coroutine_threadsafe( + self.bus.send( + self.selected_agent_id, + {"from": "user", "content": message, "type": "instruction"}, + ), + self._scan_loop, + ) self._displayed_events.clear() self._update_chat_view() @@ -1941,11 +1957,12 @@ class StrixTUIApp(App): # type: ignore[misc] return agent_name, False def action_confirm_stop_agent(self, agent_id: str) -> None: - # TODO: route to ``bus.cancel_descendants(agent_id)`` once the TUI - # has a handle on the AgentMessageBus. - logging.warning( - "Stop-agent dispatch is not wired post-migration; agent %s left running.", - agent_id, + if self._scan_loop is None or self._scan_loop.is_closed(): + logging.warning("No active scan loop; cannot stop agent %s", agent_id) + return + asyncio.run_coroutine_threadsafe( + self.bus.cancel_descendants(agent_id), + self._scan_loop, ) def action_custom_quit(self) -> None: diff --git a/strix/orchestration/hooks.py b/strix/orchestration/hooks.py index 6fefe70..919ccdf 100644 --- a/strix/orchestration/hooks.py +++ b/strix/orchestration/hooks.py @@ -84,14 +84,31 @@ class StrixOrchestrationHooks(RunHooks[Any]): agent: Any, response: ModelResponse, ) -> None: + del agent try: ctx = context.context if not isinstance(ctx, dict): return - bus = ctx.get("bus") + usage = getattr(response, "usage", None) agent_id = ctx.get("agent_id") + bus = ctx.get("bus") if bus is not None and agent_id is not None: - await bus.record_usage(agent_id, getattr(response, "usage", None)) + await bus.record_usage(agent_id, usage) + tracer = ctx.get("tracer") + if tracer is not None and usage is not None and hasattr(tracer, "record_llm_usage"): + cached = 0 + details = getattr(usage, "input_tokens_details", None) + if details is not None: + cached = int(getattr(details, "cached_tokens", 0) or 0) + tracer.record_llm_usage( + agent_id=str(agent_id or "unknown"), + input_tokens=int(getattr(usage, "input_tokens", 0) or 0), + output_tokens=int(getattr(usage, "output_tokens", 0) or 0), + cached_tokens=cached, + cost=0.0, # litellm cost computation lives in the legacy LLM + requests=1, + bucket="live", + ) ctx["turn_count"] = int(ctx.get("turn_count", 0)) + 1 except Exception: logger.exception("on_llm_end failed") @@ -101,17 +118,19 @@ class StrixOrchestrationHooks(RunHooks[Any]): context: AgentHookContext[Any], agent: Any, ) -> None: + # The CaidoCapability is bound to the sandbox session, not the + # Agent (we use plain ``Agent``, not ``SandboxAgent``). We stash + # it in the context dict at scan-bring-up time so the hook can + # await its healthcheck before the first LLM call. + del agent try: - cap = next( - ( - c - for c in (getattr(agent, "capabilities", None) or []) - if hasattr(c, "_healthcheck_task") - ), - None, - ) - if cap is not None and getattr(cap, "_healthcheck_task", None) is not None: - await cap._healthcheck_task + ctx = context.context + if not isinstance(ctx, dict): + return + cap = ctx.get("caido_capability") + task = getattr(cap, "_healthcheck_task", None) + if task is not None: + await task except Exception: logger.exception("on_agent_start failed") diff --git a/strix/run_config_factory.py b/strix/run_config_factory.py index cd09f47..8f051af 100644 --- a/strix/run_config_factory.py +++ b/strix/run_config_factory.py @@ -171,6 +171,7 @@ def make_agent_context( run_id: str | None = None, sandbox_client: Any | None = None, agent_factory: Any | None = None, + caido_capability: Any | None = None, ) -> dict[str, Any]: """Build the per-agent ``context`` dict passed to ``Runner.run(context=...)``. @@ -190,6 +191,7 @@ def make_agent_context( "sandbox_token": sandbox_token, "tool_server_host_port": tool_server_host_port, "caido_host_port": caido_host_port, + "caido_capability": caido_capability, "agent_id": agent_id, "agent_name": agent_name, "parent_id": parent_id, diff --git a/strix/tools/__init__.py b/strix/tools/__init__.py index 87ad8d4..41be98a 100644 --- a/strix/tools/__init__.py +++ b/strix/tools/__init__.py @@ -15,7 +15,6 @@ from .agents_graph import * # noqa: F403 from .browser import * # noqa: F403 from .file_edit import * # noqa: F403 from .finish import * # noqa: F403 -from .load_skill import * # noqa: F403 from .notes import * # noqa: F403 from .proxy import * # noqa: F403 from .python import * # noqa: F403 diff --git a/strix/tools/_state_adapter.py b/strix/tools/_state_adapter.py index b126c89..c395528 100644 --- a/strix/tools/_state_adapter.py +++ b/strix/tools/_state_adapter.py @@ -8,7 +8,6 @@ adapter object from the run context. Used by: - ``tools/todo/tools.py`` - - ``tools/load_skill/tool.py`` - ``tools/finish/tool.py`` """ diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py index bfb0311..5b49304 100644 --- a/strix/tools/agents_graph/tools.py +++ b/strix/tools/agents_graph/tools.py @@ -357,6 +357,7 @@ async def create_agent( sandbox_token=inner.get("sandbox_token"), tool_server_host_port=inner.get("tool_server_host_port"), caido_host_port=inner.get("caido_host_port"), + caido_capability=inner.get("caido_capability"), agent_id=child_id, agent_name=name, parent_id=parent_id, diff --git a/strix/tools/load_skill/__init__.py b/strix/tools/load_skill/__init__.py deleted file mode 100644 index f220d24..0000000 --- a/strix/tools/load_skill/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .load_skill_actions import load_skill - - -__all__ = ["load_skill"] diff --git a/strix/tools/load_skill/load_skill_actions.py b/strix/tools/load_skill/load_skill_actions.py deleted file mode 100644 index 54b9717..0000000 --- a/strix/tools/load_skill/load_skill_actions.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Any - -from strix.tools.registry import register_tool - - -@register_tool(sandbox_execution=False) -def load_skill(agent_state: Any, skills: str) -> dict[str, Any]: - try: - from strix.skills import parse_skill_list, validate_requested_skills - - requested_skills = parse_skill_list(skills) - if not requested_skills: - return { - "success": False, - "error": "No skills provided. Pass one or more comma-separated skill names.", - "requested_skills": [], - } - - validation_error = validate_requested_skills(requested_skills) - if validation_error: - return { - "success": False, - "error": validation_error, - "requested_skills": requested_skills, - "loaded_skills": [], - } - - # Runtime skill injection used to reach into the legacy - # ``_agent_instances`` registry to mutate the running LLM's - # active-skills list. The SDK harness owns the agent through - # ``Runner.run`` and there's no equivalent reach-in API yet — - # the model still gets a structured success response so it can - # observe which skills it asked for, even if reload-on-the-fly - # is a Phase 6 follow-up. - newly_loaded = list(requested_skills) - already_loaded: list[str] = [] - - except Exception as e: # noqa: BLE001 - fallback_requested_skills = ( - requested_skills - if "requested_skills" in locals() - else [s.strip() for s in skills.split(",") if s.strip()] - ) - return { - "success": False, - "error": f"Failed to load skill(s): {e!s}", - "requested_skills": fallback_requested_skills, - "loaded_skills": [], - } - else: - return { - "success": True, - "requested_skills": requested_skills, - "loaded_skills": requested_skills, - "newly_loaded_skills": newly_loaded, - "already_loaded_skills": already_loaded, - "message": "Skills loaded into this agent prompt context.", - } diff --git a/strix/tools/load_skill/load_skill_actions_schema.xml b/strix/tools/load_skill/load_skill_actions_schema.xml deleted file mode 100644 index aba1aee..0000000 --- a/strix/tools/load_skill/load_skill_actions_schema.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - Dynamically load one or more skills into the current agent at runtime. - -Use this when you need exact guidance right before acting (tool syntax, exploit workflow, or protocol details). -This updates the current agent's prompt context immediately. -
Accepts one skill or a comma-separated skill bundle. Works for root agents and subagents. -Examples: -- Single skill: `xss` -- Bundle: `sql_injection,business_logic`
- - - Comma-separated list of skills to use for the agent (MAXIMUM 5 skills allowed). Most agents should have at least one skill in order to be useful. Agents should be highly specialized - use 1-3 related skills; up to 5 for complex contexts. {{DYNAMIC_SKILLS_DESCRIPTION}} - - - - Response containing: - success: Whether runtime loading succeeded - requested_skills: Skills requested - loaded_skills: Skills validated and applied - newly_loaded_skills: Skills newly injected into prompt - already_loaded_skills: Skills already present in prompt context - - - - xss - - - - sql_injection,business_logic - - - - nmap,httpx - - -
-
diff --git a/strix/tools/load_skill/tool.py b/strix/tools/load_skill/tool.py deleted file mode 100644 index 4c5d051..0000000 --- a/strix/tools/load_skill/tool.py +++ /dev/null @@ -1,46 +0,0 @@ -"""SDK function-tool wrapper for the legacy ``load_skill`` tool. - -The legacy implementation reaches into ``_agent_instances`` (a global -dict the legacy multi-agent orchestrator maintains) to find the running -``Agent`` instance and call ``agent.llm.add_skills(...)``. That global -goes away under the SDK migration — Phase 3 will replace it with a -context-keyed registry, and this wrapper will be updated to read from -that registry. - -For Phase 2 we ship the wrapper as-is. The legacy function falls back -to a clean error path when the agent instance lookup fails, so the -tool degrades gracefully ("Could not find running agent instance...") -until Phase 3 lands. That's better than crashing or stubbing out the -tool entirely — the model still gets a structured error it can react to. -""" - -from __future__ import annotations - -import asyncio -import json -from typing import Any - -from agents import RunContextWrapper - -from strix.tools._decorator import strix_tool -from strix.tools._state_adapter import adapter_from_ctx -from strix.tools.load_skill import load_skill_actions as _impl - - -def _dump(result: dict[str, Any]) -> str: - return json.dumps(result, ensure_ascii=False, default=str) - - -@strix_tool(timeout=60) -async def load_skill(ctx: RunContextWrapper, skills: str) -> str: - """Load one or more named skills into this agent's prompt context. - - Args: - skills: Comma-separated skill names (max 5). E.g. - ``"recon,xss,sqli"``. Skill discovery uses - ``strix.skills.parse_skill_list``. - """ - state = adapter_from_ctx(ctx) - return _dump( - await asyncio.to_thread(_impl.load_skill, agent_state=state, skills=skills), - ) diff --git a/tests/agents/test_factory.py b/tests/agents/test_factory.py index 009d9d4..1902980 100644 --- a/tests/agents/test_factory.py +++ b/tests/agents/test_factory.py @@ -115,7 +115,6 @@ def test_agent_includes_graph_and_sandbox_tools() -> None: "web_search", "str_replace_editor", "create_vulnerability_report", - "load_skill", "browser_action", "terminal_execute", "python_action", diff --git a/tests/tools/test_remaining_local_tools.py b/tests/tools/test_remaining_local_tools.py index 229e6fb..3d32382 100644 --- a/tests/tools/test_remaining_local_tools.py +++ b/tests/tools/test_remaining_local_tools.py @@ -1,12 +1,7 @@ -"""Phase 2.4 smoke tests for the remaining local SDK tool wrappers. +"""Smoke tests for the remaining local SDK tool wrappers. Covers: web_search, file_edit (str_replace_editor + list_files + -search_files), reporting (create_vulnerability_report), load_skill, -finish_scan. - -Pattern matches test_sdk_local_tools.py — confirm registration as -``FunctionTool``, exercise the legacy delegation path, verify that -sandbox-bound tools route through ``post_to_sandbox``. +search_files), reporting (create_vulnerability_report), finish_scan. """ from __future__ import annotations @@ -25,7 +20,6 @@ from strix.tools.file_edit.tools import ( str_replace_editor, ) from strix.tools.finish.tool import finish_scan -from strix.tools.load_skill.tool import load_skill from strix.tools.reporting.tool import create_vulnerability_report from strix.tools.web_search.tool import web_search @@ -69,7 +63,6 @@ def test_all_remaining_tools_are_function_tools() -> None: list_files, search_files, create_vulnerability_report, - load_skill, finish_scan, ): assert isinstance(tool, FunctionTool) @@ -240,38 +233,6 @@ async def test_create_vulnerability_report_delegates_to_impl() -> None: assert kwargs["method"] is None -# --- load_skill ---------------------------------------------------------- - - -@pytest.mark.asyncio -async def test_load_skill_passes_adapter_with_agent_id() -> None: - """The wrapper must build the legacy-shaped adapter from ctx.context.""" - captured: dict[str, Any] = {} - - def fake_legacy(*, agent_state: Any, skills: str) -> dict[str, Any]: - captured["agent_id"] = agent_state.agent_id - captured["skills"] = skills - return {"success": True, "loaded_skills": ["recon"]} - - with patch( - "strix.tools.load_skill.tool._impl.load_skill", - side_effect=fake_legacy, - ): - out = await _invoke(load_skill, _ctx_for("agent-XYZ"), skills="recon") - - assert out["success"] is True - assert captured["agent_id"] == "agent-XYZ" - assert captured["skills"] == "recon" - - -@pytest.mark.asyncio -async def test_load_skill_with_empty_input() -> None: - """End-to-end: empty skills string yields the legacy validation error.""" - out = await _invoke(load_skill, _ctx_for(), skills="") - assert out["success"] is False - assert "No skills" in out["error"] - - # --- finish_scan --------------------------------------------------------- diff --git a/tests/tools/test_tool_registration_modes.py b/tests/tools/test_tool_registration_modes.py index 6812e8e..ac2346a 100644 --- a/tests/tools/test_tool_registration_modes.py +++ b/tests/tools/test_tool_registration_modes.py @@ -58,45 +58,5 @@ def test_sandbox_registers_sandbox_tools_but_not_non_sandbox_tools( assert "list_requests" in names assert "create_agent" not in names assert "finish_scan" not in names - assert "load_skill" not in names assert "browser_action" not in names assert "web_search" not in names - - -def test_load_skill_import_does_not_register_create_agent_in_sandbox( - monkeypatch: Any, -) -> None: - monkeypatch.setenv("STRIX_SANDBOX_MODE", "true") - monkeypatch.setenv("STRIX_DISABLE_BROWSER", "true") - monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False) - monkeypatch.setattr(Config, "load", classmethod(_empty_config_load)) - - clear_registry() - for name in list(sys.modules): - if name == "strix.tools" or name.startswith("strix.tools."): - sys.modules.pop(name, None) - - load_skill_module = importlib.import_module("strix.tools.load_skill.load_skill_actions") - registry = importlib.import_module("strix.tools.registry") - - names_before = set(registry.get_tool_names()) - assert "load_skill" not in names_before - assert "create_agent" not in names_before - - state_type = type( - "DummyState", - (), - { - "agent_id": "agent_test", - "context": {}, - "update_context": lambda self, key, value: self.context.__setitem__(key, value), - }, - ) - result = load_skill_module.load_skill(state_type(), "nmap") - - names_after = set(registry.get_tool_names()) - assert "create_agent" not in names_after - # load_skill no longer reaches into the legacy _agent_instances global — - # it returns ``success=True`` with the requested skills echoed back. - assert result["success"] is True - assert result["loaded_skills"] == ["nmap"] diff --git a/uv.lock b/uv.lock index d6c3487..59e2078 100644 --- a/uv.lock +++ b/uv.lock @@ -5441,10 +5441,8 @@ dependencies = [ { name = "requests" }, { name = "rich" }, { name = "scrubadub" }, - { name = "tenacity" }, { name = "textual" }, { name = "traceloop-sdk" }, - { name = "xmltodict" }, ] [package.optional-dependencies] @@ -5501,11 +5499,9 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.0" }, { name = "rich" }, { name = "scrubadub", specifier = ">=2.0.1" }, - { name = "tenacity", specifier = ">=9.0.0" }, { name = "textual", specifier = ">=6.0.0" }, { name = "traceloop-sdk", specifier = ">=0.53.0" }, { name = "uvicorn", marker = "extra == 'sandbox'" }, - { name = "xmltodict", specifier = ">=0.13.0" }, ] provides-extras = ["vertex", "sandbox"] @@ -6081,15 +6077,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, ] -[[package]] -name = "xmltodict" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, -] - [[package]] name = "yarl" version = "1.23.0"