fix: address audit findings — SDK plumbing, TUI bus, dead code

Critical fixes:

- ``StrixOrchestrationHooks.on_agent_start`` now finds the
  ``CaidoCapability`` via ``ctx.context['caido_capability']`` instead
  of ``agent.capabilities`` (we use plain ``Agent``, not
  ``SandboxAgent``, so the latter never existed). The session
  manager's bundle already exposes the capability; ``run_strix_scan``
  threads it through ``make_agent_context`` and ``create_agent``
  forwards it to children.

- ``run_strix_scan`` registers the ``StrixTracingProcessor`` with the
  SDK's tracing provider via ``add_trace_processor`` so SDK trace
  spans hit ``run_dir/events.jsonl`` (was previously a parallel stream
  the SDK ignored).

- ``on_llm_end`` now writes to ``Tracer.record_llm_usage`` in
  addition to ``bus.record_usage`` so the CLI/TUI stats panel sees
  real numbers instead of zeros.

- ``run_strix_scan`` accepts an externally-built ``AgentMessageBus``
  + an explicit ``model`` arg. The TUI pre-creates the bus so its
  stop and chat-input handlers can submit ``bus.send`` /
  ``bus.cancel_descendants`` coroutines onto the scan thread's loop
  via ``asyncio.run_coroutine_threadsafe`` — replacing the
  TODO-stub no-ops.

- ``model`` config now propagates root → context → child agents in
  ``create_agent`` (was hardcoded fallback).

Dead-code removal:

- Deleted the ``load_skill`` tool entirely (host module, sandbox
  module, TUI renderer, tests). The legacy implementation reached
  into a global ``_agent_instances`` registry that no longer exists;
  the post-migration stub returned ``success=True`` without
  injecting anything — pure theater. Skills are still preloaded via
  the system prompt at scan-bring-up.

- Dropped ``tenacity`` and ``xmltodict`` from
  ``[project.dependencies]`` — neither is imported anywhere
  post-migration.

- Stripped the system prompt's "use the load_skill tool" lines.

Tests: 278/278 passing. Removed two ``load_skill`` test cases and a
``test_tool_registration_modes::test_load_skill_import_...`` assertion
that exercised the deleted module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 10:08:35 -07:00
parent af42499b95
commit 572ef2a2af
21 changed files with 98 additions and 318 deletions
+1 -5
View File
@@ -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
+4 -7
View File
@@ -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.
+1 -3
View File
@@ -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/<mode>``
skill.
is_whitebox: When True, the source-aware whitebox skill stack
+1 -3
View File
@@ -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
+24 -1
View File
@@ -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)
@@ -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",
@@ -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))
+31 -14
View File
@@ -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:
+31 -12
View File
@@ -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")
+2
View File
@@ -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,
-1
View File
@@ -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
-1
View File
@@ -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``
"""
+1
View File
@@ -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,
-4
View File
@@ -1,4 +0,0 @@
from .load_skill_actions import load_skill
__all__ = ["load_skill"]
@@ -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.",
}
@@ -1,33 +0,0 @@
<tools>
<tool name="load_skill">
<description>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.</description>
<details>Accepts one skill or a comma-separated skill bundle. Works for root agents and subagents.
Examples:
- Single skill: `xss`
- Bundle: `sql_injection,business_logic`</details>
<parameters>
<parameter name="skills" type="string" required="true">
<description>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}}</description>
</parameter>
</parameters>
<returns type="Dict[str, Any]">
<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</description>
</returns>
<examples>
<function=load_skill>
<parameter=skills>xss</parameter>
</function>
<function=load_skill>
<parameter=skills>sql_injection,business_logic</parameter>
</function>
<function=load_skill>
<parameter=skills>nmap,httpx</parameter>
</function>
</examples>
</tool>
</tools>
-46
View File
@@ -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),
)
-1
View File
@@ -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",
+2 -41
View File
@@ -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 ---------------------------------------------------------
@@ -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"]
Generated
-13
View File
@@ -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"