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:
+1
-5
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``
|
||||
"""
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user