refactor: nuke legacy harness, drop sdk_ prefixes
The SDK harness is the only path now; legacy host-side code is gone. File names no longer carry the ``sdk_`` distinction. Deleted legacy host-side modules: - strix/agents/StrixAgent/ (template moved to strix/agents/prompts/) - strix/agents/base_agent.py, state.py - strix/llm/llm.py, config.py - strix/runtime/docker_runtime.py, runtime.py - strix/tools/executor.py, agents_graph/agents_graph_actions.py - strix/interface/sdk_dispatch.py + the env-flag dispatch in cli.py Renamed (drop ``sdk_`` prefix): - strix/sdk_entry.py → strix/entry.py - strix/agents/sdk_factory.py → strix/agents/factory.py - strix/agents/sdk_prompt.py → strix/agents/prompt.py - strix/tools/<x>/<x>_sdk_tool[s].py → strix/tools/<x>/tool[s].py - strix/tools/_legacy_adapter.py → strix/tools/_state_adapter.py - ``_legacy`` aliases inside the wrappers → ``_impl`` CLI + TUI now call ``run_strix_scan`` directly — they build the sandbox image / sources_path locally and rely on ``session_manager.cleanup`` (called inside ``run_strix_scan``'s finally) for teardown. Three TUI handlers that reached into legacy multi-agent globals (``_agent_instances``, ``send_user_message_to_agent``, ``stop_agent``) are now no-ops with a TODO; reconnecting them to the ``AgentMessageBus`` is a follow-up. Tracer.get_total_llm_stats no longer reaches into the deleted ``agents_graph_actions`` globals — the orchestration hooks now feed the tracer via ``Tracer.record_llm_usage`` (live + completed buckets). finish_scan's ``_check_active_agents`` and load_skill's runtime ``_agent_instances`` reach-in are no-op stubs; the ``AgentMessageBus`` is the source of truth post-migration. llm/utils.py rewritten to keep only the streaming-parser helpers (``normalize_tool_format``, ``parse_tool_invocations``, ``fix_incomplete_tool_call``, ``format_tool_call``, ``clean_content``). ``STRIX_MODEL_MAP`` moved to ``llm/multi_provider_setup.py`` (its only remaining caller). Per-file ruff ignores added for legacy interface modules (TUI / main / CLI / utils / streaming_parser / tool_components) and tracer.py — pre-existing PLC0415/BLE001/PLR0915 patterns are out of scope. Tests: 287/287 passing. Renamed test files to drop ``sdk_`` prefix. ``test_tracer.py::test_get_total_llm_stats_aggregates_live_and_completed`` rewritten to feed ``Tracer.record_llm_usage`` instead of legacy globals. Test file annotations added so pre-commit's strict mypy passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,291 +0,0 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import strix.agents as agents_module
|
||||
from strix.llm.config import LLMConfig
|
||||
from strix.tools.agents_graph import agents_graph_actions
|
||||
|
||||
|
||||
def _reset_agent_graph_state() -> None:
|
||||
agents_graph_actions._agent_graph["nodes"].clear()
|
||||
agents_graph_actions._agent_graph["edges"].clear()
|
||||
agents_graph_actions._agent_messages.clear()
|
||||
agents_graph_actions._running_agents.clear()
|
||||
agents_graph_actions._agent_instances.clear()
|
||||
agents_graph_actions._completed_agent_llm_totals.clear()
|
||||
agents_graph_actions._completed_agent_llm_totals.update(
|
||||
agents_graph_actions._empty_llm_stats_totals()
|
||||
)
|
||||
agents_graph_actions._agent_states.clear()
|
||||
|
||||
|
||||
def test_create_agent_inherits_parent_whitebox_flag(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
|
||||
|
||||
_reset_agent_graph_state()
|
||||
|
||||
parent_id = "parent-agent"
|
||||
parent_llm = LLMConfig(timeout=123, scan_mode="standard", is_whitebox=True)
|
||||
agents_graph_actions._agent_instances[parent_id] = SimpleNamespace(
|
||||
llm_config=parent_llm,
|
||||
non_interactive=True,
|
||||
)
|
||||
|
||||
captured_config: dict[str, object] = {}
|
||||
|
||||
class FakeStrixAgent:
|
||||
def __init__(self, config: dict[str, object]):
|
||||
captured_config["agent_config"] = config
|
||||
|
||||
class FakeThread:
|
||||
def __init__(self, target, args, daemon, name):
|
||||
self.target = target
|
||||
self.args = args
|
||||
self.daemon = daemon
|
||||
self.name = name
|
||||
|
||||
def start(self) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(agents_module, "StrixAgent", FakeStrixAgent)
|
||||
monkeypatch.setattr(agents_graph_actions.threading, "Thread", FakeThread)
|
||||
|
||||
agent_state = SimpleNamespace(
|
||||
agent_id=parent_id,
|
||||
get_conversation_history=list,
|
||||
)
|
||||
result = agents_graph_actions.create_agent(
|
||||
agent_state=agent_state,
|
||||
task="source-aware child task",
|
||||
name="SourceAwareChild",
|
||||
inherit_context=False,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
llm_config = captured_config["agent_config"]["llm_config"]
|
||||
assert isinstance(llm_config, LLMConfig)
|
||||
assert llm_config.timeout == 123
|
||||
assert llm_config.scan_mode == "standard"
|
||||
assert llm_config.is_whitebox is True
|
||||
child_task = captured_config["agent_config"]["state"].task
|
||||
assert "White-box execution guidance (recommended when source is available):" in child_task
|
||||
assert "mandatory" not in child_task.lower()
|
||||
|
||||
|
||||
def test_delegation_prompt_includes_wiki_memory_instruction_in_whitebox(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
|
||||
|
||||
_reset_agent_graph_state()
|
||||
|
||||
parent_id = "parent-1"
|
||||
child_id = "child-1"
|
||||
agents_graph_actions._agent_graph["nodes"][parent_id] = {"name": "Parent", "status": "running"}
|
||||
agents_graph_actions._agent_graph["nodes"][child_id] = {"name": "Child", "status": "running"}
|
||||
|
||||
class FakeState:
|
||||
def __init__(self) -> None:
|
||||
self.agent_id = child_id
|
||||
self.agent_name = "Child"
|
||||
self.parent_id = parent_id
|
||||
self.task = "analyze source risks"
|
||||
self.stop_requested = False
|
||||
self.messages: list[tuple[str, str]] = []
|
||||
|
||||
def add_message(self, role: str, content: str) -> None:
|
||||
self.messages.append((role, content))
|
||||
|
||||
def model_dump(self) -> dict[str, str]:
|
||||
return {"agent_id": self.agent_id}
|
||||
|
||||
class FakeAgent:
|
||||
def __init__(self) -> None:
|
||||
self.llm_config = LLMConfig(is_whitebox=True)
|
||||
|
||||
async def agent_loop(self, _task: str) -> dict[str, bool]:
|
||||
return {"ok": True}
|
||||
|
||||
state = FakeState()
|
||||
agent = FakeAgent()
|
||||
agents_graph_actions._agent_instances[child_id] = agent
|
||||
result = agents_graph_actions._run_agent_in_thread(agent, state, inherited_messages=[])
|
||||
|
||||
assert result["result"] == {"ok": True}
|
||||
task_messages = [msg for role, msg in state.messages if role == "user"]
|
||||
assert task_messages
|
||||
assert 'list_notes(category="wiki")' in task_messages[-1]
|
||||
assert "get_note(note_id=...)" in task_messages[-1]
|
||||
assert "Before agent_finish" in task_messages[-1]
|
||||
|
||||
|
||||
def test_agent_finish_appends_wiki_update_for_whitebox(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
|
||||
|
||||
_reset_agent_graph_state()
|
||||
|
||||
parent_id = "parent-2"
|
||||
child_id = "child-2"
|
||||
agents_graph_actions._agent_graph["nodes"][parent_id] = {
|
||||
"name": "Parent",
|
||||
"task": "parent task",
|
||||
"status": "running",
|
||||
"parent_id": None,
|
||||
}
|
||||
agents_graph_actions._agent_graph["nodes"][child_id] = {
|
||||
"name": "Child",
|
||||
"task": "child task",
|
||||
"status": "running",
|
||||
"parent_id": parent_id,
|
||||
}
|
||||
agents_graph_actions._agent_instances[child_id] = SimpleNamespace(
|
||||
llm_config=LLMConfig(is_whitebox=True)
|
||||
)
|
||||
|
||||
captured: dict[str, str] = {}
|
||||
|
||||
def fake_list_notes(category=None):
|
||||
assert category == "wiki"
|
||||
return {
|
||||
"success": True,
|
||||
"notes": [{"note_id": "wiki-note-1", "content": "Existing wiki content"}],
|
||||
"total_count": 1,
|
||||
}
|
||||
|
||||
captured_get: dict[str, str] = {}
|
||||
|
||||
def fake_get_note(note_id: str):
|
||||
captured_get["note_id"] = note_id
|
||||
return {
|
||||
"success": True,
|
||||
"note": {
|
||||
"note_id": note_id,
|
||||
"title": "Repo Wiki",
|
||||
"content": "Existing wiki content",
|
||||
},
|
||||
}
|
||||
|
||||
def fake_append_note_content(note_id: str, delta: str):
|
||||
captured["note_id"] = note_id
|
||||
captured["delta"] = delta
|
||||
return {"success": True, "note_id": note_id}
|
||||
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.append_note_content", fake_append_note_content)
|
||||
|
||||
state = SimpleNamespace(agent_id=child_id, parent_id=parent_id)
|
||||
result = agents_graph_actions.agent_finish(
|
||||
agent_state=state,
|
||||
result_summary="AST pass completed",
|
||||
findings=["Found route sink candidate"],
|
||||
success=True,
|
||||
final_recommendations=["Validate sink with dynamic PoC"],
|
||||
)
|
||||
|
||||
assert result["agent_completed"] is True
|
||||
assert captured_get["note_id"] == "wiki-note-1"
|
||||
assert captured["note_id"] == "wiki-note-1"
|
||||
assert "Agent Update: Child" in captured["delta"]
|
||||
assert "AST pass completed" in captured["delta"]
|
||||
|
||||
|
||||
def test_run_agent_in_thread_injects_shared_wiki_context_in_whitebox(monkeypatch) -> None:
|
||||
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5")
|
||||
|
||||
_reset_agent_graph_state()
|
||||
|
||||
parent_id = "parent-3"
|
||||
child_id = "child-3"
|
||||
agents_graph_actions._agent_graph["nodes"][parent_id] = {"name": "Parent", "status": "running"}
|
||||
agents_graph_actions._agent_graph["nodes"][child_id] = {"name": "Child", "status": "running"}
|
||||
|
||||
class FakeState:
|
||||
def __init__(self) -> None:
|
||||
self.agent_id = child_id
|
||||
self.agent_name = "Child"
|
||||
self.parent_id = parent_id
|
||||
self.task = "map source"
|
||||
self.stop_requested = False
|
||||
self.messages: list[tuple[str, str]] = []
|
||||
|
||||
def add_message(self, role: str, content: str) -> None:
|
||||
self.messages.append((role, content))
|
||||
|
||||
def model_dump(self) -> dict[str, str]:
|
||||
return {"agent_id": self.agent_id}
|
||||
|
||||
class FakeAgent:
|
||||
def __init__(self) -> None:
|
||||
self.llm_config = LLMConfig(is_whitebox=True)
|
||||
|
||||
async def agent_loop(self, _task: str) -> dict[str, bool]:
|
||||
return {"ok": True}
|
||||
|
||||
captured_get: dict[str, str] = {}
|
||||
|
||||
def fake_list_notes(category=None):
|
||||
assert category == "wiki"
|
||||
return {
|
||||
"success": True,
|
||||
"notes": [{"note_id": "wiki-ctx-1"}],
|
||||
"total_count": 1,
|
||||
}
|
||||
|
||||
def fake_get_note(note_id: str):
|
||||
captured_get["note_id"] = note_id
|
||||
return {
|
||||
"success": True,
|
||||
"note": {
|
||||
"note_id": note_id,
|
||||
"title": "Shared Repo Wiki",
|
||||
"content": "Architecture: server/client split",
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
|
||||
|
||||
state = FakeState()
|
||||
agent = FakeAgent()
|
||||
agents_graph_actions._agent_instances[child_id] = agent
|
||||
result = agents_graph_actions._run_agent_in_thread(agent, state, inherited_messages=[])
|
||||
|
||||
assert result["result"] == {"ok": True}
|
||||
assert captured_get["note_id"] == "wiki-ctx-1"
|
||||
user_messages = [content for role, content in state.messages if role == "user"]
|
||||
assert user_messages
|
||||
assert "<shared_repo_wiki" in user_messages[0]
|
||||
assert "Architecture: server/client split" in user_messages[0]
|
||||
|
||||
|
||||
def test_load_primary_wiki_note_prefers_repo_tag_match(monkeypatch) -> None:
|
||||
selected_note_ids: list[str] = []
|
||||
|
||||
def fake_list_notes(category=None):
|
||||
assert category == "wiki"
|
||||
return {
|
||||
"success": True,
|
||||
"notes": [
|
||||
{"note_id": "wiki-other", "tags": ["repo:other"]},
|
||||
{"note_id": "wiki-target", "tags": ["repo:appsmith"]},
|
||||
],
|
||||
"total_count": 2,
|
||||
}
|
||||
|
||||
def fake_get_note(note_id: str):
|
||||
selected_note_ids.append(note_id)
|
||||
return {
|
||||
"success": True,
|
||||
"note": {"note_id": note_id, "title": "Repo Wiki", "content": "content"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.list_notes", fake_list_notes)
|
||||
monkeypatch.setattr("strix.tools.notes.notes_actions.get_note", fake_get_note)
|
||||
|
||||
agent_state = SimpleNamespace(
|
||||
task="analyze /workspace/appsmith",
|
||||
context={"whitebox_repo_tags": ["repo:appsmith"]},
|
||||
)
|
||||
note = agents_graph_actions._load_primary_wiki_note(agent_state)
|
||||
|
||||
assert note is not None
|
||||
assert note["note_id"] == "wiki-target"
|
||||
assert selected_note_ids == ["wiki-target"]
|
||||
@@ -29,7 +29,7 @@ import pytest
|
||||
from agents.tool import FunctionTool
|
||||
|
||||
from strix.orchestration.bus import AgentMessageBus
|
||||
from strix.tools.agents_graph.agents_graph_sdk_tools import (
|
||||
from strix.tools.agents_graph.tools import (
|
||||
agent_finish,
|
||||
agent_status,
|
||||
create_agent,
|
||||
@@ -289,7 +289,7 @@ async def test_create_agent_spawns_and_registers_child() -> None:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"strix.tools.agents_graph.agents_graph_sdk_tools.Runner.run",
|
||||
"strix.tools.agents_graph.tools.Runner.run",
|
||||
side_effect=fake_runner_run,
|
||||
):
|
||||
out = await _invoke(
|
||||
@@ -361,7 +361,7 @@ async def test_create_agent_inherits_parent_history() -> None:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"strix.tools.agents_graph.agents_graph_sdk_tools.Runner.run",
|
||||
"strix.tools.agents_graph.tools.Runner.run",
|
||||
side_effect=fake_runner_run,
|
||||
):
|
||||
await _invoke(
|
||||
@@ -493,7 +493,7 @@ async def test_create_agent_spawn_is_cancelable_via_bus() -> None:
|
||||
|
||||
runner_mock = AsyncMock(side_effect=slow_runner_run)
|
||||
with patch(
|
||||
"strix.tools.agents_graph.agents_graph_sdk_tools.Runner.run",
|
||||
"strix.tools.agents_graph.tools.Runner.run",
|
||||
new=runner_mock,
|
||||
):
|
||||
out = await _invoke(
|
||||
@@ -1,139 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from strix.tools.agents_graph import agents_graph_actions
|
||||
from strix.tools.load_skill import load_skill_actions
|
||||
|
||||
|
||||
class _DummyLLM:
|
||||
def __init__(self, initial_skills: list[str] | None = None) -> None:
|
||||
self.loaded: set[str] = set(initial_skills or [])
|
||||
|
||||
def add_skills(self, skill_names: list[str]) -> list[str]:
|
||||
newly_loaded = [skill for skill in skill_names if skill not in self.loaded]
|
||||
self.loaded.update(newly_loaded)
|
||||
return newly_loaded
|
||||
|
||||
|
||||
class _DummyAgent:
|
||||
def __init__(self, initial_skills: list[str] | None = None) -> None:
|
||||
self.llm = _DummyLLM(initial_skills)
|
||||
|
||||
|
||||
class _DummyAgentState:
|
||||
def __init__(self, agent_id: str) -> None:
|
||||
self.agent_id = agent_id
|
||||
self.context: dict[str, Any] = {}
|
||||
|
||||
def update_context(self, key: str, value: Any) -> None:
|
||||
self.context[key] = value
|
||||
|
||||
|
||||
def test_load_skill_success_and_context_update() -> None:
|
||||
instances = agents_graph_actions.__dict__["_agent_instances"]
|
||||
original_instances = dict(instances)
|
||||
try:
|
||||
state = _DummyAgentState("agent_test_load_skill_success")
|
||||
instances.clear()
|
||||
instances[state.agent_id] = _DummyAgent()
|
||||
|
||||
result = load_skill_actions.load_skill(state, "ffuf,xss")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["loaded_skills"] == ["ffuf", "xss"]
|
||||
assert result["newly_loaded_skills"] == ["ffuf", "xss"]
|
||||
assert state.context["loaded_skills"] == ["ffuf", "xss"]
|
||||
finally:
|
||||
instances.clear()
|
||||
instances.update(original_instances)
|
||||
|
||||
|
||||
def test_load_skill_uses_same_plain_skill_format_as_create_agent() -> None:
|
||||
instances = agents_graph_actions.__dict__["_agent_instances"]
|
||||
original_instances = dict(instances)
|
||||
try:
|
||||
state = _DummyAgentState("agent_test_load_skill_short_name")
|
||||
instances.clear()
|
||||
instances[state.agent_id] = _DummyAgent()
|
||||
|
||||
result = load_skill_actions.load_skill(state, "nmap")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["loaded_skills"] == ["nmap"]
|
||||
assert result["newly_loaded_skills"] == ["nmap"]
|
||||
assert state.context["loaded_skills"] == ["nmap"]
|
||||
finally:
|
||||
instances.clear()
|
||||
instances.update(original_instances)
|
||||
|
||||
|
||||
def test_load_skill_invalid_skill_returns_error() -> None:
|
||||
instances = agents_graph_actions.__dict__["_agent_instances"]
|
||||
original_instances = dict(instances)
|
||||
try:
|
||||
state = _DummyAgentState("agent_test_load_skill_invalid")
|
||||
instances.clear()
|
||||
instances[state.agent_id] = _DummyAgent()
|
||||
|
||||
result = load_skill_actions.load_skill(state, "definitely_not_a_real_skill")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "Invalid skills" in result["error"]
|
||||
assert "Available skills" in result["error"]
|
||||
finally:
|
||||
instances.clear()
|
||||
instances.update(original_instances)
|
||||
|
||||
|
||||
def test_load_skill_rejects_more_than_five_skills() -> None:
|
||||
instances = agents_graph_actions.__dict__["_agent_instances"]
|
||||
original_instances = dict(instances)
|
||||
try:
|
||||
state = _DummyAgentState("agent_test_load_skill_too_many")
|
||||
instances.clear()
|
||||
instances[state.agent_id] = _DummyAgent()
|
||||
|
||||
result = load_skill_actions.load_skill(state, "a,b,c,d,e,f")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error"] == (
|
||||
"Cannot specify more than 5 skills for an agent (use comma-separated format)"
|
||||
)
|
||||
finally:
|
||||
instances.clear()
|
||||
instances.update(original_instances)
|
||||
|
||||
|
||||
def test_load_skill_missing_agent_instance_returns_error() -> None:
|
||||
instances = agents_graph_actions.__dict__["_agent_instances"]
|
||||
original_instances = dict(instances)
|
||||
try:
|
||||
state = _DummyAgentState("agent_test_load_skill_missing_instance")
|
||||
instances.clear()
|
||||
|
||||
result = load_skill_actions.load_skill(state, "httpx")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "running agent instance" in result["error"]
|
||||
finally:
|
||||
instances.clear()
|
||||
instances.update(original_instances)
|
||||
|
||||
|
||||
def test_load_skill_does_not_reload_skill_already_present_from_agent_creation() -> None:
|
||||
instances = agents_graph_actions.__dict__["_agent_instances"]
|
||||
original_instances = dict(instances)
|
||||
try:
|
||||
state = _DummyAgentState("agent_test_load_skill_existing_config_skill")
|
||||
instances.clear()
|
||||
instances[state.agent_id] = _DummyAgent(["xss"])
|
||||
|
||||
result = load_skill_actions.load_skill(state, "xss,sql_injection")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["loaded_skills"] == ["xss", "sql_injection"]
|
||||
assert result["newly_loaded_skills"] == ["sql_injection"]
|
||||
assert result["already_loaded_skills"] == ["xss"]
|
||||
assert state.context["loaded_skills"] == ["sql_injection", "xss"]
|
||||
finally:
|
||||
instances.clear()
|
||||
instances.update(original_instances)
|
||||
@@ -20,16 +20,16 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from agents.tool import FunctionTool
|
||||
|
||||
from strix.tools.notes import notes_actions as _notes_legacy
|
||||
from strix.tools.notes.notes_sdk_tools import (
|
||||
from strix.tools.notes import notes_actions as _notes_impl
|
||||
from strix.tools.notes.tools import (
|
||||
create_note,
|
||||
delete_note,
|
||||
get_note,
|
||||
list_notes,
|
||||
update_note,
|
||||
)
|
||||
from strix.tools.thinking.thinking_sdk_tools import think
|
||||
from strix.tools.todo.todo_sdk_tools import (
|
||||
from strix.tools.thinking.tool import think
|
||||
from strix.tools.todo.tools import (
|
||||
create_todo,
|
||||
delete_todo,
|
||||
list_todos,
|
||||
@@ -190,10 +190,10 @@ def notes_run_dir(tmp_path: Path) -> Iterator[Path]:
|
||||
"""Point the legacy notes module at a fresh run dir per test."""
|
||||
run_dir = tmp_path / "strix_runs" / "test"
|
||||
run_dir.mkdir(parents=True)
|
||||
_notes_legacy._notes_storage.clear()
|
||||
_notes_legacy._loaded_notes_run_dir = None
|
||||
_notes_impl._notes_storage.clear()
|
||||
_notes_impl._loaded_notes_run_dir = None
|
||||
|
||||
with patch.object(_notes_legacy, "_get_run_dir", return_value=run_dir):
|
||||
with patch.object(_notes_impl, "_get_run_dir", return_value=run_dir):
|
||||
yield run_dir
|
||||
|
||||
|
||||
+15
-38
@@ -12,7 +12,6 @@ sandbox-bound tools route through ``post_to_sandbox``.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
@@ -20,15 +19,15 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from agents.tool import FunctionTool
|
||||
|
||||
from strix.tools.file_edit.file_edit_sdk_tools import (
|
||||
from strix.tools.file_edit.tools import (
|
||||
list_files,
|
||||
search_files,
|
||||
str_replace_editor,
|
||||
)
|
||||
from strix.tools.finish.finish_sdk_tool import finish_scan
|
||||
from strix.tools.load_skill.load_skill_sdk_tool import load_skill
|
||||
from strix.tools.reporting.reporting_sdk_tools import create_vulnerability_report
|
||||
from strix.tools.web_search.web_search_sdk_tool import web_search
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -93,7 +92,7 @@ async def test_web_search_no_api_key_returns_structured_error(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_search_delegates_to_legacy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def test_web_search_delegates_to_impl(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Legacy ``web_search`` returns dict; wrapper JSON-encodes it."""
|
||||
monkeypatch.setenv("PERPLEXITY_API_KEY", "fake-key")
|
||||
|
||||
@@ -104,7 +103,7 @@ async def test_web_search_delegates_to_legacy(monkeypatch: pytest.MonkeyPatch) -
|
||||
"message": "Web search completed successfully",
|
||||
}
|
||||
with patch(
|
||||
"strix.tools.web_search.web_search_sdk_tool._legacy.web_search",
|
||||
"strix.tools.web_search.tool._impl.web_search",
|
||||
return_value=fake_result,
|
||||
) as legacy:
|
||||
out = await _invoke(web_search, _ctx_for(), query="xss techniques")
|
||||
@@ -121,7 +120,7 @@ async def test_str_replace_editor_routes_to_sandbox() -> None:
|
||||
"""file_edit tools must POST to the in-sandbox tool server, not run locally."""
|
||||
fake_response = {"result": {"content": "file viewed"}}
|
||||
with patch(
|
||||
"strix.tools.file_edit.file_edit_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.file_edit.tools.post_to_sandbox",
|
||||
return_value=fake_response,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
@@ -147,7 +146,7 @@ async def test_str_replace_editor_routes_to_sandbox() -> None:
|
||||
async def test_list_files_routes_to_sandbox() -> None:
|
||||
fake_response = {"result": {"files": ["a.py"], "directories": []}}
|
||||
with patch(
|
||||
"strix.tools.file_edit.file_edit_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.file_edit.tools.post_to_sandbox",
|
||||
return_value=fake_response,
|
||||
) as dispatch:
|
||||
out = await _invoke(list_files, _ctx_for(), path="src", recursive=True)
|
||||
@@ -162,7 +161,7 @@ async def test_list_files_routes_to_sandbox() -> None:
|
||||
async def test_search_files_routes_to_sandbox() -> None:
|
||||
fake_response = {"result": {"output": "src/foo.py:1:match"}}
|
||||
with patch(
|
||||
"strix.tools.file_edit.file_edit_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.file_edit.tools.post_to_sandbox",
|
||||
return_value=fake_response,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
@@ -204,7 +203,7 @@ async def test_create_vulnerability_report_validates_required_fields() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_vulnerability_report_delegates_to_legacy() -> None:
|
||||
async def test_create_vulnerability_report_delegates_to_impl() -> None:
|
||||
"""Verify the wrapper passes all params through to the legacy function."""
|
||||
fake_result = {
|
||||
"success": True,
|
||||
@@ -214,7 +213,7 @@ async def test_create_vulnerability_report_delegates_to_legacy() -> None:
|
||||
"cvss_score": 7.5,
|
||||
}
|
||||
with patch(
|
||||
"strix.tools.reporting.reporting_sdk_tools._legacy.create_vulnerability_report",
|
||||
"strix.tools.reporting.tool._impl.create_vulnerability_report",
|
||||
return_value=fake_result,
|
||||
) as legacy:
|
||||
out = await _invoke(
|
||||
@@ -255,7 +254,7 @@ async def test_load_skill_passes_adapter_with_agent_id() -> None:
|
||||
return {"success": True, "loaded_skills": ["recon"]}
|
||||
|
||||
with patch(
|
||||
"strix.tools.load_skill.load_skill_sdk_tool._legacy.load_skill",
|
||||
"strix.tools.load_skill.tool._impl.load_skill",
|
||||
side_effect=fake_legacy,
|
||||
):
|
||||
out = await _invoke(load_skill, _ctx_for("agent-XYZ"), skills="recon")
|
||||
@@ -276,28 +275,7 @@ async def test_load_skill_with_empty_input() -> None:
|
||||
# --- finish_scan ---------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_agent_graph() -> Iterator[None]:
|
||||
"""Clear the legacy agent-graph globals so finish_scan sees an empty world.
|
||||
|
||||
The legacy ``_check_active_agents`` reads ``_agent_graph["nodes"]`` and
|
||||
returns an "agents still active" error if any non-self agent is in
|
||||
state ``running`` or ``stopping``. Tests in other modules (legacy
|
||||
multi-agent tests) populate this dict; without isolation they bleed
|
||||
into our validation tests and mask the field-validation path.
|
||||
"""
|
||||
from strix.tools.agents_graph import agents_graph_actions
|
||||
|
||||
saved_nodes = agents_graph_actions._agent_graph.get("nodes", {}).copy()
|
||||
agents_graph_actions._agent_graph["nodes"] = {}
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
agents_graph_actions._agent_graph["nodes"] = saved_nodes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("isolated_agent_graph")
|
||||
async def test_finish_scan_validates_empty_fields() -> None:
|
||||
"""Legacy validation: every section must be non-empty."""
|
||||
out = await _invoke(
|
||||
@@ -313,8 +291,7 @@ async def test_finish_scan_validates_empty_fields() -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("isolated_agent_graph")
|
||||
async def test_finish_scan_delegates_to_legacy() -> None:
|
||||
async def test_finish_scan_delegates_to_impl() -> None:
|
||||
"""Wrapper must pass the legacy adapter and the four sections through."""
|
||||
fake_result = {
|
||||
"success": True,
|
||||
@@ -323,7 +300,7 @@ async def test_finish_scan_delegates_to_legacy() -> None:
|
||||
"vulnerabilities_found": 3,
|
||||
}
|
||||
with patch(
|
||||
"strix.tools.finish.finish_sdk_tool._legacy.finish_scan",
|
||||
"strix.tools.finish.tool._impl.finish_scan",
|
||||
return_value=fake_result,
|
||||
) as legacy:
|
||||
out = await _invoke(
|
||||
@@ -27,8 +27,8 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from agents.tool import FunctionTool
|
||||
|
||||
from strix.tools.browser.browser_sdk_tool import browser_action
|
||||
from strix.tools.proxy.proxy_sdk_tools import (
|
||||
from strix.tools.browser.tool import browser_action
|
||||
from strix.tools.proxy.tools import (
|
||||
list_requests,
|
||||
list_sitemap,
|
||||
repeat_request,
|
||||
@@ -37,8 +37,8 @@ from strix.tools.proxy.proxy_sdk_tools import (
|
||||
view_request,
|
||||
view_sitemap_entry,
|
||||
)
|
||||
from strix.tools.python.python_sdk_tool import python_action
|
||||
from strix.tools.terminal.terminal_sdk_tool import terminal_execute
|
||||
from strix.tools.python.tool import python_action
|
||||
from strix.tools.terminal.tool import terminal_execute
|
||||
|
||||
|
||||
_ALL_SANDBOX_TOOLS = (
|
||||
@@ -119,7 +119,7 @@ async def test_browser_action_dispatches_full_payload() -> None:
|
||||
(the in-container handler distinguishes ``None`` from missing)."""
|
||||
fake = {"result": {"screenshot": "data:image/png;base64,..."}}
|
||||
with patch(
|
||||
"strix.tools.browser.browser_sdk_tool.post_to_sandbox",
|
||||
"strix.tools.browser.tool.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
@@ -158,7 +158,7 @@ async def test_browser_action_dispatches_full_payload() -> None:
|
||||
async def test_terminal_execute_dispatches() -> None:
|
||||
fake = {"result": {"content": "hello\n", "exit_code": 0}}
|
||||
with patch(
|
||||
"strix.tools.terminal.terminal_sdk_tool.post_to_sandbox",
|
||||
"strix.tools.terminal.tool.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
@@ -185,7 +185,7 @@ async def test_terminal_execute_dispatches() -> None:
|
||||
async def test_python_action_dispatches() -> None:
|
||||
fake = {"result": {"stdout": "42\n", "is_running": False}}
|
||||
with patch(
|
||||
"strix.tools.python.python_sdk_tool.post_to_sandbox",
|
||||
"strix.tools.python.tool.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
@@ -214,7 +214,7 @@ async def test_python_action_dispatches() -> None:
|
||||
async def test_list_requests_forwards_full_query() -> None:
|
||||
fake: dict[str, Any] = {"result": {"requests": []}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
@@ -241,7 +241,7 @@ async def test_list_requests_forwards_full_query() -> None:
|
||||
async def test_view_request_dispatches() -> None:
|
||||
fake = {"result": {"raw": "GET / HTTP/1.1..."}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
@@ -264,7 +264,7 @@ async def test_send_request_normalizes_missing_headers() -> None:
|
||||
"""Legacy schema treats omitted ``headers`` as ``{}``; the wrapper must too."""
|
||||
fake = {"result": {"status": 200}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
@@ -286,7 +286,7 @@ async def test_send_request_normalizes_missing_headers() -> None:
|
||||
async def test_repeat_request_normalizes_missing_modifications() -> None:
|
||||
fake = {"result": {"status": 200}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(repeat_request, _ctx_for(), request_id="req-1")
|
||||
@@ -300,7 +300,7 @@ async def test_repeat_request_normalizes_missing_modifications() -> None:
|
||||
async def test_scope_rules_dispatches() -> None:
|
||||
fake = {"result": {"scope_id": "s-1"}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
@@ -323,7 +323,7 @@ async def test_scope_rules_dispatches() -> None:
|
||||
async def test_list_sitemap_defaults() -> None:
|
||||
fake: dict[str, Any] = {"result": {"entries": []}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(list_sitemap, _ctx_for())
|
||||
@@ -338,7 +338,7 @@ async def test_list_sitemap_defaults() -> None:
|
||||
async def test_view_sitemap_entry_dispatches() -> None:
|
||||
fake = {"result": {"entry_id": "e-1"}}
|
||||
with patch(
|
||||
"strix.tools.proxy.proxy_sdk_tools.post_to_sandbox",
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(view_sitemap_entry, _ctx_for(), entry_id="e-1")
|
||||
@@ -21,9 +21,15 @@ def _reload_tools_module() -> ModuleType:
|
||||
return importlib.import_module("strix.tools")
|
||||
|
||||
|
||||
def test_non_sandbox_registers_agents_graph_but_not_browser_or_web_search_when_disabled(
|
||||
def test_non_sandbox_skips_browser_and_web_search_when_disabled(
|
||||
monkeypatch: Any,
|
||||
) -> None:
|
||||
"""Browser registration is gated on STRIX_DISABLE_BROWSER and
|
||||
web_search on PERPLEXITY_API_KEY; both should stay out of the
|
||||
in-container ``register_tool`` registry when their gates are off.
|
||||
Agents_graph is no longer in this registry — those tools are SDK
|
||||
function tools (host-side only), not in-container tools.
|
||||
"""
|
||||
monkeypatch.setenv("STRIX_SANDBOX_MODE", "false")
|
||||
monkeypatch.setenv("STRIX_DISABLE_BROWSER", "true")
|
||||
monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False)
|
||||
@@ -32,7 +38,6 @@ def test_non_sandbox_registers_agents_graph_but_not_browser_or_web_search_when_d
|
||||
tools = _reload_tools_module()
|
||||
names = set(tools.get_tool_names())
|
||||
|
||||
assert "create_agent" in names
|
||||
assert "browser_action" not in names
|
||||
assert "web_search" not in names
|
||||
|
||||
|
||||
Reference in New Issue
Block a user