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:
@@ -0,0 +1,348 @@
|
||||
"""Phase 2.5 smoke tests for the sandbox-bound SDK tool wrappers.
|
||||
|
||||
Covers: browser_action, terminal_execute, python_action, and the seven
|
||||
Caido proxy tools.
|
||||
|
||||
These wrappers are pure pass-throughs to ``post_to_sandbox`` — there's
|
||||
no per-tool logic to assert, so the tests focus on:
|
||||
|
||||
- ``FunctionTool`` registration succeeds (which proves the SDK could
|
||||
derive a JSON schema from the type hints — a non-trivial check given
|
||||
Literal types, ``dict[str, str]``, and strict-mode opt-outs).
|
||||
- The dispatch payload to ``post_to_sandbox`` mirrors the legacy XML
|
||||
schema verbatim, so the in-container tool server gets the same
|
||||
``kwargs`` shape it always has.
|
||||
- The ``send_request`` / ``repeat_request`` tools opt out of strict
|
||||
schema mode (their ``headers`` / ``modifications`` dicts are
|
||||
free-form and would otherwise fail registration).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from agents.tool import FunctionTool
|
||||
|
||||
from strix.tools.browser.tool import browser_action
|
||||
from strix.tools.proxy.tools import (
|
||||
list_requests,
|
||||
list_sitemap,
|
||||
repeat_request,
|
||||
scope_rules,
|
||||
send_request,
|
||||
view_request,
|
||||
view_sitemap_entry,
|
||||
)
|
||||
from strix.tools.python.tool import python_action
|
||||
from strix.tools.terminal.tool import terminal_execute
|
||||
|
||||
|
||||
_ALL_SANDBOX_TOOLS = (
|
||||
browser_action,
|
||||
terminal_execute,
|
||||
python_action,
|
||||
list_requests,
|
||||
view_request,
|
||||
send_request,
|
||||
repeat_request,
|
||||
scope_rules,
|
||||
list_sitemap,
|
||||
view_sitemap_entry,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Ctx:
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _ctx_for(agent_id: str = "test-agent") -> _Ctx:
|
||||
return _Ctx(
|
||||
context={
|
||||
"agent_id": agent_id,
|
||||
"tool_server_host_port": 12345,
|
||||
"sandbox_token": "test-token",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _invoke(tool: FunctionTool, ctx: _Ctx, **kwargs: Any) -> dict[str, Any]:
|
||||
from agents.tool_context import ToolContext
|
||||
|
||||
tool_ctx = ToolContext(
|
||||
context=ctx.context,
|
||||
usage=None,
|
||||
tool_name=tool.name,
|
||||
tool_call_id="test-call-id",
|
||||
tool_arguments=json.dumps(kwargs),
|
||||
)
|
||||
result = await tool.on_invoke_tool(tool_ctx, json.dumps(kwargs))
|
||||
assert isinstance(result, str)
|
||||
decoded = json.loads(result)
|
||||
assert isinstance(decoded, dict)
|
||||
return decoded
|
||||
|
||||
|
||||
def test_all_sandbox_tools_register() -> None:
|
||||
for tool in _ALL_SANDBOX_TOOLS:
|
||||
assert isinstance(tool, FunctionTool)
|
||||
|
||||
|
||||
def test_send_and_repeat_request_opt_out_of_strict_mode() -> None:
|
||||
"""The two free-form-dict tools must turn off strict JSON schema."""
|
||||
assert send_request.strict_json_schema is False
|
||||
assert repeat_request.strict_json_schema is False
|
||||
# All other tools should keep strict mode on (the SDK default).
|
||||
for tool in (
|
||||
browser_action,
|
||||
terminal_execute,
|
||||
python_action,
|
||||
list_requests,
|
||||
view_request,
|
||||
scope_rules,
|
||||
list_sitemap,
|
||||
view_sitemap_entry,
|
||||
):
|
||||
assert tool.strict_json_schema is True, tool.name
|
||||
|
||||
|
||||
# --- browser ---------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_browser_action_dispatches_full_payload() -> None:
|
||||
"""All optional browser_action params must forward as None when unset
|
||||
(the in-container handler distinguishes ``None`` from missing)."""
|
||||
fake = {"result": {"screenshot": "data:image/png;base64,..."}}
|
||||
with patch(
|
||||
"strix.tools.browser.tool.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
browser_action,
|
||||
_ctx_for(),
|
||||
action="goto",
|
||||
url="https://example.com",
|
||||
)
|
||||
|
||||
assert out == fake
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "browser_action"
|
||||
payload = args[2]
|
||||
assert payload["action"] == "goto"
|
||||
assert payload["url"] == "https://example.com"
|
||||
# All optional params are present in the payload (as None / defaults)
|
||||
# so the legacy in-container handler sees the full kwarg surface.
|
||||
for key in (
|
||||
"coordinate",
|
||||
"text",
|
||||
"tab_id",
|
||||
"js_code",
|
||||
"duration",
|
||||
"key",
|
||||
"file_path",
|
||||
):
|
||||
assert key in payload, key
|
||||
assert payload[key] is None
|
||||
assert payload["clear"] is False
|
||||
|
||||
|
||||
# --- terminal --------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_terminal_execute_dispatches() -> None:
|
||||
fake = {"result": {"content": "hello\n", "exit_code": 0}}
|
||||
with patch(
|
||||
"strix.tools.terminal.tool.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
terminal_execute,
|
||||
_ctx_for(),
|
||||
command="echo hello",
|
||||
terminal_id="term-1",
|
||||
)
|
||||
|
||||
assert out == fake
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "terminal_execute"
|
||||
assert args[2]["command"] == "echo hello"
|
||||
assert args[2]["terminal_id"] == "term-1"
|
||||
assert args[2]["is_input"] is False
|
||||
assert args[2]["no_enter"] is False
|
||||
assert args[2]["timeout"] is None
|
||||
|
||||
|
||||
# --- python ---------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_python_action_dispatches() -> None:
|
||||
fake = {"result": {"stdout": "42\n", "is_running": False}}
|
||||
with patch(
|
||||
"strix.tools.python.tool.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
out = await _invoke(
|
||||
python_action,
|
||||
_ctx_for(),
|
||||
action="execute",
|
||||
code="print(6*7)",
|
||||
session_id="sess-1",
|
||||
)
|
||||
|
||||
assert out == fake
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "python_action"
|
||||
assert args[2] == {
|
||||
"action": "execute",
|
||||
"code": "print(6*7)",
|
||||
"timeout": 30,
|
||||
"session_id": "sess-1",
|
||||
}
|
||||
|
||||
|
||||
# --- proxy / Caido --------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_requests_forwards_full_query() -> None:
|
||||
fake: dict[str, Any] = {"result": {"requests": []}}
|
||||
with patch(
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
list_requests,
|
||||
_ctx_for(),
|
||||
httpql_filter="resp.code:eq:500",
|
||||
page_size=20,
|
||||
sort_by="response_time",
|
||||
sort_order="asc",
|
||||
)
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "list_requests"
|
||||
assert args[2]["httpql_filter"] == "resp.code:eq:500"
|
||||
assert args[2]["page_size"] == 20
|
||||
assert args[2]["sort_by"] == "response_time"
|
||||
assert args[2]["sort_order"] == "asc"
|
||||
# Defaults preserved.
|
||||
assert args[2]["start_page"] == 1
|
||||
assert args[2]["end_page"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_request_dispatches() -> None:
|
||||
fake = {"result": {"raw": "GET / HTTP/1.1..."}}
|
||||
with patch(
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
view_request,
|
||||
_ctx_for(),
|
||||
request_id="req-9",
|
||||
part="response",
|
||||
search_pattern="Set-Cookie",
|
||||
)
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "view_request"
|
||||
assert args[2]["request_id"] == "req-9"
|
||||
assert args[2]["part"] == "response"
|
||||
assert args[2]["search_pattern"] == "Set-Cookie"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
send_request,
|
||||
_ctx_for(),
|
||||
method="POST",
|
||||
url="https://api.example.com/login",
|
||||
body='{"u":"x"}',
|
||||
)
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "send_request"
|
||||
assert args[2]["headers"] == {} # not None
|
||||
assert args[2]["method"] == "POST"
|
||||
assert args[2]["body"] == '{"u":"x"}'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repeat_request_normalizes_missing_modifications() -> None:
|
||||
fake = {"result": {"status": 200}}
|
||||
with patch(
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(repeat_request, _ctx_for(), request_id="req-1")
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "repeat_request"
|
||||
assert args[2]["modifications"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scope_rules_dispatches() -> None:
|
||||
fake = {"result": {"scope_id": "s-1"}}
|
||||
with patch(
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(
|
||||
scope_rules,
|
||||
_ctx_for(),
|
||||
action="create",
|
||||
scope_name="prod",
|
||||
allowlist=["*.example.com"],
|
||||
)
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "scope_rules"
|
||||
assert args[2]["action"] == "create"
|
||||
assert args[2]["scope_name"] == "prod"
|
||||
assert args[2]["allowlist"] == ["*.example.com"]
|
||||
assert args[2]["denylist"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sitemap_defaults() -> None:
|
||||
fake: dict[str, Any] = {"result": {"entries": []}}
|
||||
with patch(
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(list_sitemap, _ctx_for())
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "list_sitemap"
|
||||
assert args[2]["depth"] == "DIRECT"
|
||||
assert args[2]["page"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_sitemap_entry_dispatches() -> None:
|
||||
fake = {"result": {"entry_id": "e-1"}}
|
||||
with patch(
|
||||
"strix.tools.proxy.tools.post_to_sandbox",
|
||||
return_value=fake,
|
||||
) as dispatch:
|
||||
await _invoke(view_sitemap_entry, _ctx_for(), entry_id="e-1")
|
||||
|
||||
args, _ = dispatch.call_args
|
||||
assert args[1] == "view_sitemap_entry"
|
||||
assert args[2] == {"entry_id": "e-1"}
|
||||
Reference in New Issue
Block a user