refactor: SandboxAgent + SDK Shell/Filesystem; agent-browser CLI; nuke FastAPI sidecar
Combined commits 2+3 of the migration plan because the FastAPI sidecar removal in commit 2 broke ``browser_action`` (which lived in the sidecar); they have to land together. Sandbox tool layer (commit 2 piece): - ``build_strix_agent`` now returns a ``SandboxAgent`` with ``capabilities=[Filesystem(), Shell()]``. The SDK runtime binds the capabilities to the live sandbox session per-run; agents get ``exec_command``, ``write_stdin``, ``apply_patch``, ``view_image`` function tools auto-merged into their tool list. Plain ``Agent`` short-circuits capability binding (``agents/sandbox/runtime.py:190``). - Drop ``Compaction`` from the default capability set — it's OpenAI-Responses-API-only and useless for our litellm-routed Anthropic setup. - Delete the entire custom in-container tool layer: - ``strix/tools/terminal/`` (5 files, 748 LoC libtmux) - ``strix/tools/file_edit/`` (3 files, 276 LoC) - ``strix/tools/python/`` (5 files, 459 LoC) - ``strix/runtime/tool_server.py`` (163 LoC FastAPI sidecar) - ``strix/tools/_sandbox_dispatch.py`` (117 LoC) - ``strix/tools/registry.py`` (109 LoC) - ``strix/tools/context.py`` (12 LoC) - Drop the corresponding TUI renderers (``terminal_renderer.py``, ``file_edit_renderer.py``, ``python_renderer.py``) and update ``interface/tool_components/__init__.py``. Browser → agent-browser CLI (commit 3 piece): - Install ``agent-browser@0.26.0`` globally in the Dockerfile right after the existing ``npm install -g`` block. Run ``agent-browser install --with-deps`` (apt, root) and ``agent-browser install`` (Chrome download, pentester) + ``agent-browser doctor --offline --quick`` smoke test. - Drop the explicit Playwright system-deps apt list (replaced by ``--with-deps``) and ``RUN .venv/bin/python -m playwright install chromium``. - Vendor ``agent-browser/skill-data/core/SKILL.md`` → ``strix/skills/tooling/agent_browser.md`` (476 lines). Adapt frontmatter to Strix format; strip the install/Quickstart and the ``agent-browser skills get electron|slack|...`` specialized-skills block; add the "Caido proxy is wired via env vars; do not pass ``--proxy``" note. - ``_resolve_skills`` now eagerly loads ``tooling/agent_browser`` for every agent (matches the previous unconditional ``browser_action`` in ``_BASE_TOOLS``). - Delete ``strix/tools/browser/`` (5 files, 1338 LoC) and the ``browser_renderer.py`` TUI render. Sandbox plumbing: - Drop ``bearer`` token, ``tool_server_host_port`` resolution + bundle keys, ``TOOL_SERVER_TOKEN``/``TOOL_SERVER_PORT``/ ``STRIX_SANDBOX_EXECUTION_TIMEOUT`` from the manifest env in ``session_manager.create_or_reuse``. Caido proxy env vars (``http_proxy``, ``https_proxy``, ``ALL_PROXY``) stay; manifest applies them to every ``docker exec``-spawned process. - Drop ``sandbox_token`` and ``tool_server_host_port`` params from ``make_agent_context`` and the ``create_agent`` graph tool. - Drop the tool-server health-check from ``entry.py`` (only Caido's ``wait_for_tcp_ready`` remains). - ``docker-entrypoint.sh``: delete the ~30 line ``Starting tool server...`` block (sudo + uvicorn launch + curl /health poll). Add ``NO_PROXY=localhost,127.0.0.1`` to ``/etc/profile.d/proxy.sh`` and ``/etc/environment`` so the agent-browser daemon's CDP traffic on localhost isn't routed through Caido. pyproject.toml: - ``[project.optional-dependencies] sandbox = []`` (every member of the previous list — fastapi, uvicorn, ipython, openhands-aci, playwright, libtmux — is gone with the sidecar). - Drop ``numpydoc.*``, ``IPython.*``, ``openhands_aci.*``, ``playwright.*``, ``uvicorn.*``, ``pyte.*``, ``libtmux.*`` from the missing-imports module list. - Drop the per-file ruff ignores for the deleted modules. Net delta: −5512 LoC. ruff drops to 3 errors (was 21 baseline). mypy falls to 69 errors over 3 files (was 84 over 8 — the drop comes from deleting the modules with the worst untyped-import problems). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,163 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
|
||||
SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
||||
if not SANDBOX_MODE:
|
||||
raise RuntimeError("Tool server should only run in sandbox mode (STRIX_SANDBOX_MODE=true)")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Start Strix tool server")
|
||||
parser.add_argument("--token", required=True, help="Authentication token")
|
||||
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") # nosec
|
||||
parser.add_argument("--port", type=int, required=True, help="Port to bind to")
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=120,
|
||||
help="Hard timeout in seconds for each request execution (default: 120)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
EXPECTED_TOKEN = args.token
|
||||
REQUEST_TIMEOUT = args.timeout
|
||||
|
||||
app = FastAPI()
|
||||
security = HTTPBearer()
|
||||
security_dependency = Depends(security)
|
||||
|
||||
agent_tasks: dict[str, asyncio.Task[Any]] = {}
|
||||
|
||||
|
||||
def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
||||
if not credentials or credentials.scheme != "Bearer":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme. Bearer token required.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if credentials.credentials != EXPECTED_TOKEN:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return credentials.credentials
|
||||
|
||||
|
||||
class ToolExecutionRequest(BaseModel):
|
||||
agent_id: str
|
||||
tool_name: str
|
||||
kwargs: dict[str, Any]
|
||||
|
||||
|
||||
class ToolExecutionResponse(BaseModel):
|
||||
result: Any | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
async def _run_tool(agent_id: str, tool_name: str, kwargs: dict[str, Any]) -> Any:
|
||||
from strix.tools.context import set_current_agent_id
|
||||
from strix.tools.registry import get_tool_by_name
|
||||
|
||||
set_current_agent_id(agent_id)
|
||||
|
||||
tool_func = get_tool_by_name(tool_name)
|
||||
if not tool_func:
|
||||
raise ValueError(f"Tool '{tool_name}' not found")
|
||||
|
||||
return await asyncio.to_thread(tool_func, **kwargs)
|
||||
|
||||
|
||||
@app.post("/execute", response_model=ToolExecutionResponse)
|
||||
async def execute_tool(
|
||||
request: ToolExecutionRequest, credentials: HTTPAuthorizationCredentials = security_dependency
|
||||
) -> ToolExecutionResponse:
|
||||
verify_token(credentials)
|
||||
|
||||
agent_id = request.agent_id
|
||||
|
||||
if agent_id in agent_tasks:
|
||||
old_task = agent_tasks[agent_id]
|
||||
if not old_task.done():
|
||||
old_task.cancel()
|
||||
|
||||
task = asyncio.create_task(
|
||||
asyncio.wait_for(
|
||||
_run_tool(agent_id, request.tool_name, request.kwargs), timeout=REQUEST_TIMEOUT
|
||||
)
|
||||
)
|
||||
agent_tasks[agent_id] = task
|
||||
|
||||
try:
|
||||
result = await task
|
||||
return ToolExecutionResponse(result=result)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
return ToolExecutionResponse(error="Cancelled by newer request")
|
||||
|
||||
except TimeoutError:
|
||||
return ToolExecutionResponse(error=f"Tool timed out after {REQUEST_TIMEOUT}s")
|
||||
|
||||
except ValidationError as e:
|
||||
return ToolExecutionResponse(error=f"Invalid arguments: {e}")
|
||||
|
||||
except (ValueError, RuntimeError, ImportError) as e:
|
||||
return ToolExecutionResponse(error=f"Tool execution error: {e}")
|
||||
|
||||
except Exception as e: # noqa: BLE001
|
||||
return ToolExecutionResponse(error=f"Unexpected error: {e}")
|
||||
|
||||
finally:
|
||||
if agent_tasks.get(agent_id) is task:
|
||||
del agent_tasks[agent_id]
|
||||
|
||||
|
||||
@app.post("/register_agent")
|
||||
async def register_agent(
|
||||
agent_id: str, credentials: HTTPAuthorizationCredentials = security_dependency
|
||||
) -> dict[str, str]:
|
||||
verify_token(credentials)
|
||||
return {"status": "registered", "agent_id": agent_id}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> dict[str, Any]:
|
||||
return {
|
||||
"status": "healthy",
|
||||
"sandbox_mode": str(SANDBOX_MODE),
|
||||
"environment": "sandbox" if SANDBOX_MODE else "main",
|
||||
"auth_configured": "true" if EXPECTED_TOKEN else "false",
|
||||
"active_agents": len(agent_tasks),
|
||||
"agents": list(agent_tasks.keys()),
|
||||
}
|
||||
|
||||
|
||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||
if hasattr(signal, "SIGPIPE"):
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||
for task in agent_tasks.values():
|
||||
task.cancel()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if hasattr(signal, "SIGPIPE"):
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||
Reference in New Issue
Block a user