mirror of
https://github.com/usestrix/strix.git
synced 2026-08-24 03:42:37 +02:00
feat(migration): phase 2.5 — wrap sandbox-bound SDK tools
Ten tools ported, all pure pass-throughs to post_to_sandbox: - browser_action (1 tool): the 21-action mega-tool dispatcher kept intact rather than fanned out, to preserve the legacy XML shape. - terminal_execute (1 tool): tmux session driver. - python_action (1 tool): IPython session manager. - proxy / Caido (7 tools): list_requests, view_request, send_request, repeat_request, scope_rules, list_sitemap, view_sitemap_entry. strix_tool decorator gains a strict_mode flag (default True, matching the SDK default). send_request and repeat_request opt out of strict mode because their headers / modifications dicts are free-form — the SDK's strict JSON schema rejects dict[str, X] without enumerated keys. Tests: 12 new tests in test_sdk_sandbox_tools.py covering registration, strict-mode opt-out verification for the two free-form tools, and dispatch shape verification (every wrapper is asserted to forward its full kwarg surface to post_to_sandbox so the in-container handler sees the same payload it always has). Per-file ruff TC002 ignores added for the four new wrapper modules. Phase 2 (tools) is now complete: 24 SDK function tools wrapped across think/todo/notes/web_search/file_edit/reporting/load_skill/finish_scan/ browser/terminal/python/proxy. Total: 7 local + 17 sandbox-bound. Phase 3 (multi-agent orchestration) is next. Refs: PLAYBOOK.md §3.6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
57478e5d0d
commit
044e4e82ae
@@ -40,6 +40,7 @@ def strix_tool(
|
||||
timeout_behavior: _ToolBehavior = "error_as_result",
|
||||
name_override: str | None = None,
|
||||
description_override: str | None = None,
|
||||
strict_mode: bool = True,
|
||||
) -> Callable[[_ToolFn], FunctionTool]:
|
||||
"""Wrap ``agents.function_tool`` with Strix defaults.
|
||||
|
||||
@@ -48,6 +49,13 @@ def strix_tool(
|
||||
``async def``; sync libraries (libtmux, IPython) get wrapped in
|
||||
``asyncio.to_thread`` inside the async tool body.
|
||||
|
||||
The SDK enforces ``strict_mode=True`` by default, which forbids
|
||||
free-form ``dict[str, X]`` parameters (the strict JSON schema needs
|
||||
``additionalProperties: false``). A handful of legacy tools
|
||||
(``send_request``, ``repeat_request``) take arbitrary header /
|
||||
modification dicts whose keys can't be enumerated, so they must
|
||||
opt out of strict mode to preserve parity with the XML schema.
|
||||
|
||||
Usage::
|
||||
|
||||
@strix_tool()
|
||||
@@ -55,10 +63,16 @@ def strix_tool(
|
||||
|
||||
@strix_tool(timeout=300, timeout_behavior="raise_exception")
|
||||
async def critical_tool(ctx: RunContextWrapper, ...) -> str: ...
|
||||
|
||||
@strix_tool(strict_mode=False)
|
||||
async def free_form_dict_tool(
|
||||
ctx: RunContextWrapper, headers: dict[str, str],
|
||||
) -> str: ...
|
||||
"""
|
||||
return function_tool(
|
||||
timeout=timeout,
|
||||
timeout_behavior=timeout_behavior,
|
||||
name_override=name_override,
|
||||
description_override=description_override,
|
||||
strict_mode=strict_mode,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""SDK function-tool wrapper for the legacy ``browser_action`` tool.
|
||||
|
||||
The browser is fully sandbox-bound — the legacy implementation runs
|
||||
inside the container against a Playwright instance the tool server
|
||||
manages. We delegate every action verbatim to ``post_to_sandbox``.
|
||||
|
||||
The legacy ``browser_action`` is a single mega-tool dispatching 21
|
||||
discrete actions (launch, goto, click, scroll_*, new_tab, etc.). We
|
||||
preserve that shape for parity rather than fanning out into 21
|
||||
separate tools — that would balloon the system prompt and surprise
|
||||
the model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Literal
|
||||
|
||||
from agents import RunContextWrapper
|
||||
|
||||
from strix.tools._decorator import strix_tool
|
||||
from strix.tools._sandbox_dispatch import post_to_sandbox
|
||||
|
||||
|
||||
def _dump(result: dict[str, Any]) -> str:
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
BrowserAction = Literal[
|
||||
"launch",
|
||||
"goto",
|
||||
"click",
|
||||
"type",
|
||||
"scroll_down",
|
||||
"scroll_up",
|
||||
"back",
|
||||
"forward",
|
||||
"new_tab",
|
||||
"switch_tab",
|
||||
"close_tab",
|
||||
"wait",
|
||||
"execute_js",
|
||||
"double_click",
|
||||
"hover",
|
||||
"press_key",
|
||||
"save_pdf",
|
||||
"get_console_logs",
|
||||
"view_source",
|
||||
"close",
|
||||
"list_tabs",
|
||||
]
|
||||
|
||||
|
||||
# Browser actions can take time (page loads, navigation timeouts), so
|
||||
# match the sandbox dispatch read budget rather than capping shorter.
|
||||
@strix_tool(timeout=180)
|
||||
async def browser_action(
|
||||
ctx: RunContextWrapper,
|
||||
action: BrowserAction,
|
||||
url: str | None = None,
|
||||
coordinate: str | None = None,
|
||||
text: str | None = None,
|
||||
tab_id: str | None = None,
|
||||
js_code: str | None = None,
|
||||
duration: float | None = None,
|
||||
key: str | None = None,
|
||||
file_path: str | None = None,
|
||||
clear: bool = False,
|
||||
) -> str:
|
||||
"""Drive the sandboxed Playwright browser.
|
||||
|
||||
Args:
|
||||
action: The browser action to dispatch — see ``BrowserAction``
|
||||
literal for the full set.
|
||||
url: Required for ``launch`` / ``goto`` / ``new_tab`` (with URL).
|
||||
coordinate: ``"x,y"`` pixel target for click/hover/double_click.
|
||||
text: Required for ``type``.
|
||||
tab_id: Optional explicit tab targeting; defaults to the active tab.
|
||||
js_code: Required for ``execute_js``.
|
||||
duration: Seconds to wait for ``wait`` action.
|
||||
key: Required for ``press_key`` (e.g. ``"Enter"``, ``"Escape"``).
|
||||
file_path: Required for ``save_pdf``.
|
||||
clear: For ``type``, clears the field first.
|
||||
"""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"browser_action",
|
||||
{
|
||||
"action": action,
|
||||
"url": url,
|
||||
"coordinate": coordinate,
|
||||
"text": text,
|
||||
"tab_id": tab_id,
|
||||
"js_code": js_code,
|
||||
"duration": duration,
|
||||
"key": key,
|
||||
"file_path": file_path,
|
||||
"clear": clear,
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
"""SDK function-tool wrappers for the seven Caido proxy tools.
|
||||
|
||||
All seven dispatch to the in-container Caido manager via the sandbox
|
||||
tool server. Same pattern as browser/terminal/python — host wrapper is
|
||||
pure pass-through, no logic of its own.
|
||||
|
||||
Tools: list_requests, view_request, send_request, repeat_request,
|
||||
scope_rules, list_sitemap, view_sitemap_entry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Literal
|
||||
|
||||
from agents import RunContextWrapper
|
||||
|
||||
from strix.tools._decorator import strix_tool
|
||||
from strix.tools._sandbox_dispatch import post_to_sandbox
|
||||
|
||||
|
||||
def _dump(result: dict[str, Any]) -> str:
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
RequestPart = Literal["request", "response"]
|
||||
SortBy = Literal[
|
||||
"timestamp",
|
||||
"host",
|
||||
"method",
|
||||
"path",
|
||||
"status_code",
|
||||
"response_time",
|
||||
"response_size",
|
||||
"source",
|
||||
]
|
||||
SortOrder = Literal["asc", "desc"]
|
||||
SitemapDepth = Literal["DIRECT", "ALL"]
|
||||
ScopeAction = Literal["get", "list", "create", "update", "delete"]
|
||||
|
||||
|
||||
@strix_tool(timeout=120)
|
||||
async def list_requests(
|
||||
ctx: RunContextWrapper,
|
||||
httpql_filter: str | None = None,
|
||||
start_page: int = 1,
|
||||
end_page: int = 1,
|
||||
page_size: int = 50,
|
||||
sort_by: SortBy = "timestamp",
|
||||
sort_order: SortOrder = "desc",
|
||||
scope_id: str | None = None,
|
||||
) -> str:
|
||||
"""List captured HTTP requests from the Caido proxy.
|
||||
|
||||
Args:
|
||||
httpql_filter: Caido HTTPQL query (e.g. ``"resp.code:eq:500"``).
|
||||
start_page / end_page: Inclusive page range to return.
|
||||
page_size: Entries per page; default 50.
|
||||
sort_by: Field to sort by.
|
||||
sort_order: ``"asc"`` or ``"desc"``.
|
||||
scope_id: Restrict to a specific scope.
|
||||
"""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"list_requests",
|
||||
{
|
||||
"httpql_filter": httpql_filter,
|
||||
"start_page": start_page,
|
||||
"end_page": end_page,
|
||||
"page_size": page_size,
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
"scope_id": scope_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@strix_tool(timeout=60)
|
||||
async def view_request(
|
||||
ctx: RunContextWrapper,
|
||||
request_id: str,
|
||||
part: RequestPart = "request",
|
||||
search_pattern: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> str:
|
||||
"""View a single captured request or its response, with optional regex highlight."""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"view_request",
|
||||
{
|
||||
"request_id": request_id,
|
||||
"part": part,
|
||||
"search_pattern": search_pattern,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# strict_mode=False because ``headers`` is a free-form dict — the model
|
||||
# can't enumerate all possible HTTP headers, and the SDK's strict JSON
|
||||
# schema rejects ``additionalProperties: true``.
|
||||
@strix_tool(timeout=120, strict_mode=False)
|
||||
async def send_request(
|
||||
ctx: RunContextWrapper,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
body: str = "",
|
||||
timeout: int = 30,
|
||||
) -> str:
|
||||
"""Send an arbitrary HTTP request through the Caido proxy.
|
||||
|
||||
Args:
|
||||
method: ``"GET"``, ``"POST"``, etc.
|
||||
url: Full URL.
|
||||
headers: Optional header dict.
|
||||
body: Optional body string.
|
||||
timeout: Per-request timeout in seconds.
|
||||
"""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"send_request",
|
||||
{
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": headers or {},
|
||||
"body": body,
|
||||
"timeout": timeout,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# strict_mode=False because ``modifications`` is a free-form patch dict
|
||||
# (header overrides, body replacements, query-string tweaks) the model
|
||||
# composes per-call.
|
||||
@strix_tool(timeout=120, strict_mode=False)
|
||||
async def repeat_request(
|
||||
ctx: RunContextWrapper,
|
||||
request_id: str,
|
||||
modifications: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
"""Repeat a captured request, optionally applying field modifications."""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"repeat_request",
|
||||
{
|
||||
"request_id": request_id,
|
||||
"modifications": modifications or {},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@strix_tool(timeout=60)
|
||||
async def scope_rules(
|
||||
ctx: RunContextWrapper,
|
||||
action: ScopeAction,
|
||||
allowlist: list[str] | None = None,
|
||||
denylist: list[str] | None = None,
|
||||
scope_id: str | None = None,
|
||||
scope_name: str | None = None,
|
||||
) -> str:
|
||||
"""CRUD on Caido scope rules (allow/deny lists)."""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"scope_rules",
|
||||
{
|
||||
"action": action,
|
||||
"allowlist": allowlist,
|
||||
"denylist": denylist,
|
||||
"scope_id": scope_id,
|
||||
"scope_name": scope_name,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@strix_tool(timeout=60)
|
||||
async def list_sitemap(
|
||||
ctx: RunContextWrapper,
|
||||
scope_id: str | None = None,
|
||||
parent_id: str | None = None,
|
||||
depth: SitemapDepth = "DIRECT",
|
||||
page: int = 1,
|
||||
) -> str:
|
||||
"""List Caido sitemap entries (proxied URL tree).
|
||||
|
||||
Args:
|
||||
scope_id: Restrict to a scope.
|
||||
parent_id: Drill into a specific subtree.
|
||||
depth: ``"DIRECT"`` (direct children only) or ``"ALL"`` (recursive).
|
||||
page: 1-indexed page number.
|
||||
"""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"list_sitemap",
|
||||
{
|
||||
"scope_id": scope_id,
|
||||
"parent_id": parent_id,
|
||||
"depth": depth,
|
||||
"page": page,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@strix_tool(timeout=60)
|
||||
async def view_sitemap_entry(ctx: RunContextWrapper, entry_id: str) -> str:
|
||||
"""Fetch a single sitemap entry's metadata + linked requests."""
|
||||
return _dump(
|
||||
await post_to_sandbox(ctx, "view_sitemap_entry", {"entry_id": entry_id}),
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""SDK function-tool wrapper for the legacy ``python_action`` tool.
|
||||
|
||||
Sandbox-bound. The in-container manager keeps long-lived IPython
|
||||
sessions keyed by ``session_id`` so the model can build up state
|
||||
across multiple ``execute`` calls. Pure pass-through wrapper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Literal
|
||||
|
||||
from agents import RunContextWrapper
|
||||
|
||||
from strix.tools._decorator import strix_tool
|
||||
from strix.tools._sandbox_dispatch import post_to_sandbox
|
||||
|
||||
|
||||
def _dump(result: dict[str, Any]) -> str:
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
PythonAction = Literal["new_session", "execute", "close", "list_sessions"]
|
||||
|
||||
|
||||
@strix_tool(timeout=180)
|
||||
async def python_action(
|
||||
ctx: RunContextWrapper,
|
||||
action: PythonAction,
|
||||
code: str | None = None,
|
||||
timeout: int = 30,
|
||||
session_id: str | None = None,
|
||||
) -> str:
|
||||
"""Manage / execute code in a long-lived sandboxed IPython session.
|
||||
|
||||
Args:
|
||||
action: ``"new_session"`` to spin one up, ``"execute"`` to run code,
|
||||
``"close"`` to terminate, ``"list_sessions"`` to inspect.
|
||||
code: Required for ``execute`` (and optional for ``new_session``
|
||||
to run a setup snippet immediately).
|
||||
timeout: Per-call execution budget in seconds. Default 30.
|
||||
session_id: Required for ``execute`` / ``close``. Optional for
|
||||
``new_session`` (auto-generated when omitted).
|
||||
"""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"python_action",
|
||||
{
|
||||
"action": action,
|
||||
"code": code,
|
||||
"timeout": timeout,
|
||||
"session_id": session_id,
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""SDK function-tool wrapper for the legacy ``terminal_execute`` tool.
|
||||
|
||||
The terminal lives in the sandbox container — each persistent tmux
|
||||
session is keyed by ``terminal_id`` on the in-container manager. The
|
||||
host-side wrapper is a thin pass-through.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from agents import RunContextWrapper
|
||||
|
||||
from strix.tools._decorator import strix_tool
|
||||
from strix.tools._sandbox_dispatch import post_to_sandbox
|
||||
|
||||
|
||||
def _dump(result: dict[str, Any]) -> str:
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
@strix_tool(timeout=180)
|
||||
async def terminal_execute(
|
||||
ctx: RunContextWrapper,
|
||||
command: str,
|
||||
is_input: bool = False,
|
||||
timeout: float | None = None,
|
||||
terminal_id: str | None = None,
|
||||
no_enter: bool = False,
|
||||
) -> str:
|
||||
"""Run a shell command in the sandboxed Kali tmux session.
|
||||
|
||||
Args:
|
||||
command: Shell command (or input for an interactive prompt when
|
||||
``is_input=True``).
|
||||
is_input: Treat ``command`` as input to a running foreground process
|
||||
(e.g., feeding y/n to ``apt install``).
|
||||
timeout: Seconds to wait before returning partial output. Defaults
|
||||
to the in-container manager's policy.
|
||||
terminal_id: Persistent session selector. Defaults to ``"default"``.
|
||||
no_enter: When True, sends keystrokes without a trailing return.
|
||||
Useful for sending raw ANSI control sequences.
|
||||
"""
|
||||
return _dump(
|
||||
await post_to_sandbox(
|
||||
ctx,
|
||||
"terminal_execute",
|
||||
{
|
||||
"command": command,
|
||||
"is_input": is_input,
|
||||
"timeout": timeout,
|
||||
"terminal_id": terminal_id,
|
||||
"no_enter": no_enter,
|
||||
},
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user