mirror of
https://github.com/usestrix/strix.git
synced 2026-09-15 23:31:27 +00:00
streamline into tool-server
This commit is contained in:
parent
3718943134
commit
9836e3fc84
14 changed files with 118 additions and 230 deletions
|
|
@ -216,8 +216,7 @@ RUN echo 'export PATH="/home/pentester/go/bin:/home/pentester/.local/bin:/home/p
|
|||
USER root
|
||||
COPY containers/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY containers/healthcheck.sh /usr/local/bin/healthcheck.sh
|
||||
COPY containers/proxy.py /usr/local/bin/proxy.py
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/healthcheck.sh /usr/local/bin/proxy.py
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/healthcheck.sh
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD healthcheck.sh
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import WSMsgType, web
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TOKEN = os.environ["TOOL_SERVER_TOKEN"]
|
||||
LISTEN_PORT = int(os.environ.get("CDP_PORT", "9222"))
|
||||
UPSTREAM = f"http://127.0.0.1:{os.environ.get('CDP_INTERNAL_PORT', '19222')}"
|
||||
|
||||
|
||||
def _is_authorized(req: web.Request) -> bool:
|
||||
auth_header: str = req.headers.get("Authorization", "")
|
||||
token_param: str | None = req.query.get("token")
|
||||
return auth_header == f"Bearer {TOKEN}" or token_param == TOKEN
|
||||
|
||||
|
||||
def _strip_token(url: str) -> str:
|
||||
return re.sub(r"[?&]token=[^&]*", "", url).replace("?&", "?").rstrip("?")
|
||||
|
||||
|
||||
async def _proxy_ws(req: web.Request, url: str, headers: dict[str, Any]) -> web.WebSocketResponse:
|
||||
client_ws = web.WebSocketResponse()
|
||||
await client_ws.prepare(req)
|
||||
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
session.ws_connect(url.replace("http", "ws", 1), headers=headers) as upstream_ws,
|
||||
):
|
||||
|
||||
async def relay(src: Any, dst: Any) -> None:
|
||||
async for msg in src:
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
await dst.send_str(msg.data)
|
||||
elif msg.type == WSMsgType.BINARY:
|
||||
await dst.send_bytes(msg.data)
|
||||
else:
|
||||
logger.warning("Unexpected WebSocket message type: %s", msg.type)
|
||||
break
|
||||
|
||||
await asyncio.gather(
|
||||
relay(upstream_ws, client_ws),
|
||||
relay(client_ws, upstream_ws),
|
||||
return_exceptions=True,
|
||||
)
|
||||
return client_ws
|
||||
|
||||
|
||||
async def _proxy_http(req: web.Request, url: str, headers: dict[str, Any]) -> web.StreamResponse:
|
||||
async with (
|
||||
aiohttp.ClientSession() as session,
|
||||
session.request(req.method, url, headers=headers, data=await req.read()) as resp,
|
||||
):
|
||||
out = web.StreamResponse(
|
||||
status=resp.status,
|
||||
headers={k: v for k, v in resp.headers.items() if k.lower() != "transfer-encoding"},
|
||||
)
|
||||
await out.prepare(req)
|
||||
async for chunk in resp.content.iter_any():
|
||||
await out.write(chunk)
|
||||
return out
|
||||
|
||||
|
||||
async def _handle(req: web.Request) -> web.StreamResponse:
|
||||
if not _is_authorized(req):
|
||||
return web.Response(status=401, text="Unauthorized")
|
||||
|
||||
url = _strip_token(f"{UPSTREAM}{req.path_qs}")
|
||||
headers = {k: v for k, v in req.headers.items() if k.lower() not in ("host", "authorization")}
|
||||
|
||||
if req.headers.get("Upgrade", "").lower() == "websocket":
|
||||
return await _proxy_ws(req, url, headers)
|
||||
return await _proxy_http(req, url, headers)
|
||||
|
||||
|
||||
app = web.Application()
|
||||
app.router.add_route("*", "/{path:.*}", _handle)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("CDP auth proxy: 0.0.0.0:%s -> %s", LISTEN_PORT, UPSTREAM)
|
||||
web.run_app(app, host="0.0.0.0", port=LISTEN_PORT, print=None)
|
||||
|
|
@ -151,15 +151,14 @@ sudo -u pentester certutil -N -d sql:/home/pentester/.pki/nssdb --empty-password
|
|||
sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb
|
||||
echo "✅ CA added to browser trust store"
|
||||
|
||||
CDP_PORT="${BROWSER_CDP_PORT:-9222}"
|
||||
# Chromium always binds CDP to 127.0.0.1 regardless of --remote-debugging-address.
|
||||
# We launch it on an internal port and use an auth proxy to expose it on 0.0.0.0.
|
||||
# Chromium always binds CDP to 127.0.0.1.
|
||||
# The tool server proxies CDP traffic via /cdp/proxy/.
|
||||
CDP_INTERNAL_PORT=19222
|
||||
CHROMIUM_BIN=""
|
||||
CHROMIUM_RESTART_COUNT=0
|
||||
CHROMIUM_MAX_RESTARTS=10
|
||||
|
||||
echo "Launching Chromium with CDP (internal port $CDP_INTERNAL_PORT, exposed on $CDP_PORT)..."
|
||||
echo "Launching Chromium with CDP on internal port $CDP_INTERNAL_PORT..."
|
||||
CHROMIUM_BIN=$(find /usr/lib/chromium* /usr/bin -name "chromium" -o -name "chromium-browser" -o -name "chrome" 2>/dev/null | head -1)
|
||||
if [ -z "$CHROMIUM_BIN" ]; then
|
||||
# Playwright-installed Chromium
|
||||
|
|
@ -215,34 +214,6 @@ start_chromium() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Kill any leftover CDP auth proxy from a previous run
|
||||
if [ -n "${CDP_PROXY_PID:-}" ] && kill -0 "$CDP_PROXY_PID" 2>/dev/null; then
|
||||
kill "$CDP_PROXY_PID" 2>/dev/null || true
|
||||
wait "$CDP_PROXY_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Expose CDP on 0.0.0.0 via an authenticated proxy (replaces raw socat).
|
||||
python3 /usr/local/bin/proxy.py &
|
||||
CDP_PROXY_PID=$!
|
||||
echo "Started CDP auth proxy (PID $CDP_PROXY_PID): 0.0.0.0:${CDP_PORT} -> 127.0.0.1:${CDP_INTERNAL_PORT}"
|
||||
|
||||
# Verify the proxied CDP port is reachable (with auth)
|
||||
local fwd_ready=false
|
||||
for i in 1 2 3 4 5; do
|
||||
if curl -s -H "Authorization: Bearer ${TOOL_SERVER_TOKEN}" "http://127.0.0.1:${CDP_PORT}/json/version" | grep -q "webSocketDebuggerUrl"; then
|
||||
echo "✅ CDP auth proxy reachable on port ${CDP_PORT} (attempt $i)"
|
||||
fwd_ready=true
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$fwd_ready" = false ]; then
|
||||
echo "WARNING: CDP not reachable via auth proxy on port ${CDP_PORT}"
|
||||
echo " proxy PID $CDP_PROXY_PID alive: $(kill -0 $CDP_PROXY_PID 2>&1 && echo yes || echo no)"
|
||||
echo " ss output: $(ss -tlnp 2>/dev/null | grep ${CDP_PORT} || echo 'port not listening')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -304,8 +275,6 @@ cd /app
|
|||
export PYTHONPATH=/app
|
||||
export STRIX_SANDBOX_MODE=true
|
||||
export TOOL_SERVER_TIMEOUT="${STRIX_SANDBOX_EXECUTION_TIMEOUT:-120}"
|
||||
export CDP_INTERNAL_PORT
|
||||
export CDP_PORT
|
||||
TOOL_SERVER_LOG="/tmp/tool_server.log"
|
||||
|
||||
sudo -E -u pentester \
|
||||
|
|
@ -313,12 +282,13 @@ sudo -E -u pentester \
|
|||
--token="$TOOL_SERVER_TOKEN" \
|
||||
--host=0.0.0.0 \
|
||||
--port="$TOOL_SERVER_PORT" \
|
||||
--timeout="$TOOL_SERVER_TIMEOUT" > "$TOOL_SERVER_LOG" 2>&1 &
|
||||
--timeout="$TOOL_SERVER_TIMEOUT" \
|
||||
--cdp-upstream="http://127.0.0.1:$CDP_INTERNAL_PORT" > "$TOOL_SERVER_LOG" 2>&1 &
|
||||
|
||||
TOOL_SERVER_PID=$!
|
||||
|
||||
for i in {1..10}; do
|
||||
if curl -s "http://127.0.0.1:$TOOL_SERVER_PORT/health" | grep -q '"status":"healthy"'; then
|
||||
if curl -s -H "Authorization: Bearer ${TOOL_SERVER_TOKEN}" "http://127.0.0.1:$TOOL_SERVER_PORT/health" | grep -q '"status":"healthy"'; then
|
||||
echo "✅ Tool server healthy on port $TOOL_SERVER_PORT"
|
||||
break
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ set -e
|
|||
|
||||
TOOL_SERVER_PORT="${TOOL_SERVER_PORT:-48081}"
|
||||
CAIDO_PORT=48080
|
||||
CDP_PORT="${BROWSER_CDP_PORT:-9222}"
|
||||
|
||||
# 1. Tool server must respond healthy
|
||||
if ! curl -sf --max-time 3 "http://127.0.0.1:${TOOL_SERVER_PORT}/health" | grep -q '"status":"healthy"'; then
|
||||
if ! curl -sf --max-time 3 -H "Authorization: Bearer ${TOOL_SERVER_TOKEN}" "http://127.0.0.1:${TOOL_SERVER_PORT}/health" | grep -q '"status":"healthy"'; then
|
||||
echo "UNHEALTHY: tool server not responding on port ${TOOL_SERVER_PORT}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ module = [
|
|||
"browser_use.*",
|
||||
"cdp_use.*",
|
||||
"aiohttp.*",
|
||||
"websockets.*",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class BrowserRenderer(BaseToolRenderer):
|
|||
# Dispatch to action-specific builders
|
||||
builders: dict[str, Callable[[], Text]] = {
|
||||
"run": lambda: cls._build_run(args, status, result),
|
||||
"launch": lambda: cls._build_launch(args, status, result),
|
||||
"launch": lambda: cls._build_launch(status, result),
|
||||
"navigate": lambda: cls._build_navigate(args, status),
|
||||
"search": lambda: cls._build_search(args, status),
|
||||
"click": lambda: cls._build_click(args, status),
|
||||
|
|
@ -159,8 +159,8 @@ class BrowserRenderer(BaseToolRenderer):
|
|||
return text
|
||||
|
||||
@classmethod
|
||||
def _build_launch(cls, args: dict[str, Any], status: str, result: Any) -> Text:
|
||||
mode = "local" if args.get("use_local") else "sandboxed"
|
||||
def _build_launch(cls, status: str, result: Any) -> Text:
|
||||
mode = result.get("mode", "sandboxed") if isinstance(result, dict) else "sandboxed"
|
||||
text = Text("◈ ", style=cls.LIFE)
|
||||
text.append("launching browser", style=f"bold {cls.LIFE}")
|
||||
text.append(f" {mode}", style=cls.DIM)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ HOST_GATEWAY_HOSTNAME = "host.docker.internal"
|
|||
DOCKER_TIMEOUT = 60
|
||||
CONTAINER_TOOL_SERVER_PORT = 48081
|
||||
CONTAINER_CAIDO_PORT = 48080
|
||||
CONTAINER_BROWSER_CDP_PORT = 9222
|
||||
|
||||
|
||||
class DockerRuntime(AbstractRuntime):
|
||||
|
|
@ -40,7 +39,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
self._tool_server_port: int | None = None
|
||||
self._tool_server_token: str | None = None
|
||||
self._caido_port: int | None = None
|
||||
self._browser_cdp_port: int | None = None
|
||||
|
||||
def _find_available_port(self) -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
|
|
@ -86,10 +84,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
if port_bindings.get(caido_port_key):
|
||||
self._caido_port = int(port_bindings[caido_port_key][0]["HostPort"])
|
||||
|
||||
cdp_port_key = f"{CONTAINER_BROWSER_CDP_PORT}/tcp"
|
||||
if port_bindings.get(cdp_port_key):
|
||||
self._browser_cdp_port = int(port_bindings[cdp_port_key][0]["HostPort"])
|
||||
|
||||
def _wait_for_tool_server(self, max_retries: int = 30, timeout: int = 5) -> None:
|
||||
host = self._resolve_docker_host()
|
||||
health_url = f"http://{host}:{self._tool_server_port}/health"
|
||||
|
|
@ -134,7 +128,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
|
||||
self._tool_server_port = self._find_available_port()
|
||||
self._caido_port = self._find_available_port()
|
||||
self._browser_cdp_port = self._find_available_port()
|
||||
self._tool_server_token = secrets.token_urlsafe(32)
|
||||
execution_timeout = Config.get("strix_sandbox_execution_timeout") or "120"
|
||||
|
||||
|
|
@ -147,7 +140,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
ports={
|
||||
f"{CONTAINER_TOOL_SERVER_PORT}/tcp": self._tool_server_port,
|
||||
f"{CONTAINER_CAIDO_PORT}/tcp": self._caido_port,
|
||||
f"{CONTAINER_BROWSER_CDP_PORT}/tcp": self._browser_cdp_port,
|
||||
},
|
||||
cap_add=["NET_ADMIN", "NET_RAW"],
|
||||
shm_size="256m",
|
||||
|
|
@ -158,7 +150,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
"TOOL_SERVER_TOKEN": self._tool_server_token,
|
||||
"STRIX_SANDBOX_EXECUTION_TIMEOUT": str(execution_timeout),
|
||||
"HOST_GATEWAY": HOST_GATEWAY_HOSTNAME,
|
||||
"BROWSER_CDP_PORT": str(CONTAINER_BROWSER_CDP_PORT),
|
||||
},
|
||||
extra_hosts={HOST_GATEWAY_HOSTNAME: "host-gateway"},
|
||||
tty=True,
|
||||
|
|
@ -173,7 +164,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
self._tool_server_port = None
|
||||
self._tool_server_token = None
|
||||
self._caido_port = None
|
||||
self._browser_cdp_port = None
|
||||
time.sleep(2**attempt)
|
||||
else:
|
||||
return container
|
||||
|
|
@ -196,7 +186,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
self._tool_server_port = None
|
||||
self._tool_server_token = None
|
||||
self._caido_port = None
|
||||
self._browser_cdp_port = None
|
||||
|
||||
try:
|
||||
container = self.client.containers.get(container_name)
|
||||
|
|
@ -290,9 +279,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
if self._caido_port is None:
|
||||
raise RuntimeError("Caido port not initialized")
|
||||
|
||||
if self._browser_cdp_port is None:
|
||||
raise RuntimeError("Browser CDP port not initialized")
|
||||
|
||||
host = self._resolve_docker_host()
|
||||
api_url = f"http://{host}:{self._tool_server_port}"
|
||||
|
||||
|
|
@ -304,7 +290,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
"auth_token": token,
|
||||
"tool_server_port": self._tool_server_port,
|
||||
"caido_port": self._caido_port,
|
||||
"browser_cdp_port": self._browser_cdp_port,
|
||||
"agent_id": agent_id,
|
||||
}
|
||||
|
||||
|
|
@ -347,7 +332,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
self._tool_server_port = None
|
||||
self._tool_server_token = None
|
||||
self._caido_port = None
|
||||
self._browser_cdp_port = None
|
||||
except (NotFound, DockerException):
|
||||
pass
|
||||
|
||||
|
|
@ -358,7 +342,6 @@ class DockerRuntime(AbstractRuntime):
|
|||
self._tool_server_port = None
|
||||
self._tool_server_token = None
|
||||
self._caido_port = None
|
||||
self._browser_cdp_port = None
|
||||
|
||||
if container_name is None:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ class SandboxInfo(TypedDict):
|
|||
auth_token: str | None
|
||||
tool_server_port: int
|
||||
caido_port: int
|
||||
browser_cdp_port: int
|
||||
agent_id: str
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ import signal
|
|||
import sys
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
import websockets
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, WebSocket, status
|
||||
from fastapi.responses import Response
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
|
|
@ -27,10 +30,16 @@ parser.add_argument(
|
|||
default=120,
|
||||
help="Hard timeout in seconds for each request execution (default: 120)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cdp-upstream",
|
||||
default="http://127.0.0.1:19222",
|
||||
help="Chromium CDP upstream address (default: http://127.0.0.1:19222)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
EXPECTED_TOKEN = args.token
|
||||
REQUEST_TIMEOUT = args.timeout
|
||||
CDP_UPSTREAM = args.cdp_upstream.rstrip("/")
|
||||
|
||||
app = FastAPI()
|
||||
security = HTTPBearer()
|
||||
|
|
@ -57,6 +66,14 @@ def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
|||
return credentials.credentials
|
||||
|
||||
|
||||
def verify_ws_token(ws: WebSocket) -> None:
|
||||
auth = ws.headers.get("Authorization", "")
|
||||
token_param = ws.query_params.get("token")
|
||||
if auth == f"Bearer {EXPECTED_TOKEN}" or token_param == EXPECTED_TOKEN:
|
||||
return
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class ToolExecutionRequest(BaseModel):
|
||||
agent_id: str
|
||||
tool_name: str
|
||||
|
|
@ -136,19 +153,9 @@ async def register_agent(
|
|||
|
||||
|
||||
async def _check_cdp_health() -> dict[str, Any]:
|
||||
"""Probe the container-local Chromium CDP endpoint.
|
||||
|
||||
Returns only a boolean status — never the ``webSocketDebuggerUrl``
|
||||
or browser version, because the ``/health`` endpoint is
|
||||
unauthenticated and those fields would leak session secrets.
|
||||
"""
|
||||
cdp_port = os.getenv("CDP_INTERNAL_PORT", "19222")
|
||||
cdp_url = f"http://127.0.0.1:{cdp_port}/json/version"
|
||||
try:
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient(timeout=3, trust_env=False) as client:
|
||||
resp = await client.get(cdp_url)
|
||||
resp = await client.get(f"{CDP_UPSTREAM}/json/version")
|
||||
if resp.status_code == 200:
|
||||
return {"status": "healthy"}
|
||||
return {"status": "unhealthy"}
|
||||
|
|
@ -156,27 +163,6 @@ async def _check_cdp_health() -> dict[str, Any]:
|
|||
return {"status": "unhealthy"}
|
||||
|
||||
|
||||
@app.get("/cdp/version")
|
||||
async def cdp_version(
|
||||
credentials: HTTPAuthorizationCredentials = security_dependency, # noqa: ARG001
|
||||
) -> dict[str, Any]:
|
||||
"""Return Chromium's ``/json/version`` data via the authenticated tool server.
|
||||
|
||||
This lets the host discover the WebSocket debugger URL (which contains
|
||||
a random browser GUID) without exposing ``/json/version`` on the
|
||||
unauthenticated CDP port.
|
||||
"""
|
||||
cdp_port = os.getenv("CDP_INTERNAL_PORT", "19222")
|
||||
cdp_url = f"http://127.0.0.1:{cdp_port}/json/version"
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient(timeout=5, trust_env=False) as client:
|
||||
resp = await client.get(cdp_url)
|
||||
resp.raise_for_status()
|
||||
data: dict[str, Any] = resp.json()
|
||||
return data
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> dict[str, Any]:
|
||||
cdp_health = await _check_cdp_health()
|
||||
|
|
@ -191,6 +177,76 @@ async def health_check() -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
# -- CDP auth proxy ----------------------------------------------------------
|
||||
# Proxies HTTP and WebSocket traffic to the container-local Chromium CDP.
|
||||
|
||||
|
||||
def _cdp_upstream_url(path: str) -> str:
|
||||
# Strip the /cdp/proxy prefix to get the upstream path
|
||||
suffix = path.removeprefix("/cdp/proxy")
|
||||
return f"{CDP_UPSTREAM}{suffix}"
|
||||
|
||||
|
||||
@app.websocket("/cdp/proxy/{path:path}")
|
||||
async def cdp_proxy_ws(ws: WebSocket, path: str) -> None: # noqa: ARG001
|
||||
verify_ws_token(ws)
|
||||
url = _cdp_upstream_url(ws.url.path).replace("http", "ws", 1)
|
||||
await ws.accept()
|
||||
|
||||
async with websockets.connect(url) as upstream:
|
||||
|
||||
async def relay_client_to_upstream() -> None:
|
||||
async for msg in ws.iter_text():
|
||||
await upstream.send(msg)
|
||||
|
||||
async def relay_upstream_to_client() -> None:
|
||||
async for msg in upstream:
|
||||
if isinstance(msg, str):
|
||||
await ws.send_text(msg)
|
||||
else:
|
||||
await ws.send_bytes(msg)
|
||||
|
||||
_, pending = await asyncio.wait(
|
||||
[
|
||||
asyncio.create_task(relay_client_to_upstream()),
|
||||
asyncio.create_task(relay_upstream_to_client()),
|
||||
],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for t in pending:
|
||||
t.cancel()
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/cdp/proxy/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
||||
)
|
||||
async def cdp_proxy_http(
|
||||
request: Request,
|
||||
path: str, # noqa: ARG001
|
||||
credentials: HTTPAuthorizationCredentials = security_dependency,
|
||||
) -> Response:
|
||||
verify_token(credentials)
|
||||
url = _cdp_upstream_url(request.url.path)
|
||||
headers = {
|
||||
k: v for k, v in request.headers.items() if k.lower() not in ("host", "authorization")
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.request(
|
||||
request.method,
|
||||
url,
|
||||
headers=headers,
|
||||
content=await request.body(),
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
headers={k: v for k, v in resp.headers.items() if k.lower() != "transfer-encoding"},
|
||||
)
|
||||
|
||||
|
||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||
if hasattr(signal, "SIGPIPE"):
|
||||
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
|
||||
|
|
|
|||
|
|
@ -55,14 +55,13 @@ if not SANDBOX_MODE:
|
|||
if HAS_PERPLEXITY_API:
|
||||
from .web_search import * # noqa: F403
|
||||
else:
|
||||
if not DISABLE_BROWSER:
|
||||
from .browser import * # noqa: F403
|
||||
from .file_edit import * # noqa: F403
|
||||
from .proxy import * # noqa: F403
|
||||
from .python import * # noqa: F403
|
||||
from .terminal import * # noqa: F403
|
||||
|
||||
if not DISABLE_BROWSER:
|
||||
from .browser import * # noqa: F403
|
||||
|
||||
__all__ = [
|
||||
"ImplementedInClientSideOnlyError",
|
||||
"execute_tool",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Literal
|
||||
|
||||
|
|
@ -88,18 +87,11 @@ def _build_llm() -> Any:
|
|||
|
||||
def _resolve_cdp_url(agent_state: Any) -> tuple[str, str]:
|
||||
info = agent_state.sandbox_info
|
||||
cdp_port = info.get("browser_cdp_port")
|
||||
if not cdp_port:
|
||||
raise ValueError("Missing browser_cdp_port in sandbox_info")
|
||||
api_url = info.get("api_url")
|
||||
if not api_url:
|
||||
raise ValueError("Missing api_url in sandbox_info")
|
||||
|
||||
host = "127.0.0.1"
|
||||
if docker_host := os.getenv("DOCKER_HOST"):
|
||||
from urllib.parse import urlparse
|
||||
|
||||
if (parsed := urlparse(docker_host)).hostname:
|
||||
host = parsed.hostname
|
||||
|
||||
return f"http://{host}:{cdp_port}", info.get("auth_token", "")
|
||||
return f"{api_url}/cdp/proxy", info.get("auth_token", "")
|
||||
|
||||
|
||||
async def _execute_task(session: BrowserSession, operation: Any, desc: str) -> dict[str, Any]:
|
||||
|
|
@ -246,7 +238,6 @@ async def _run_browser_tool(
|
|||
async def browser_actions(
|
||||
action: BrowserUseLocalAction,
|
||||
task: str | None = None,
|
||||
use_local: bool = False,
|
||||
profile_directory: str | None = None,
|
||||
return_fields: list[str] | None = None,
|
||||
*,
|
||||
|
|
@ -259,7 +250,8 @@ async def browser_actions(
|
|||
agent_id = get_current_agent_id()
|
||||
|
||||
if action == "launch":
|
||||
if use_local:
|
||||
has_sandbox = agent_state and getattr(agent_state, "sandbox_info", None)
|
||||
if not has_sandbox:
|
||||
session = await _launch_local_browser(agent_id, profile_directory)
|
||||
result = {
|
||||
"message": "Local browser ready",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<tools>
|
||||
<tool name="browser_actions">
|
||||
<description>Control a browser via natural language or granular commands. Supports two modes:
|
||||
<description>Control a browser via natural language or granular commands.
|
||||
|
||||
**Sandboxed** (default): The browser runs inside the sandbox container (with Caido proxy
|
||||
intercepting all traffic) and is controlled remotely via Chrome DevTools Protocol (CDP).
|
||||
**Local** (use_local=true): Uses the system Chrome installation directly. No sandbox
|
||||
required. Preserves existing login sessions, cookies, and extensions. You may need to
|
||||
fully close Chrome before launching. Optionally select a Chrome profile.
|
||||
The browser runs inside the sandbox container (with Caido proxy intercepting all traffic)
|
||||
and is controlled remotely via Chrome DevTools Protocol (CDP).
|
||||
|
||||
The browser is PERSISTENT and remains active until explicitly closed, allowing for
|
||||
multi-step workflows. State (cookies, auth, tabs) carries over between calls.
|
||||
|
|
@ -21,7 +18,6 @@
|
|||
<description>The action to perform:
|
||||
**Lifecycle:**
|
||||
- launch: Start the browser. MUST be called before any other actions.
|
||||
Pass use_local=true for local Chrome. Optionally pass profile_directory.
|
||||
- close_browser: Shut down the browser session.
|
||||
|
||||
**Agent mode:**
|
||||
|
|
@ -75,12 +71,8 @@
|
|||
<description>Required for 'run' action. A natural-language description of what to do in
|
||||
the browser, e.g. "Go to example.com and find the contact email".</description>
|
||||
</parameter>
|
||||
<parameter name="use_local" type="boolean" required="false">
|
||||
<description>Only used with action='launch'. When true, uses the local system Chrome
|
||||
installation instead of connecting to the sandbox container via CDP. Default: false.</description>
|
||||
</parameter>
|
||||
<parameter name="profile_directory" type="string" required="false">
|
||||
<description>Only used with action='launch' and use_local=true. The Chrome profile
|
||||
<description>Only used with action='launch' outside sandbox mode. The Chrome profile
|
||||
directory to use, e.g. "Default", "Profile 1". If not specified, auto-selected.</description>
|
||||
</parameter>
|
||||
<parameter name="query" type="string" required="false">
|
||||
|
|
@ -261,12 +253,6 @@
|
|||
<parameter=action>launch</parameter>
|
||||
</function>
|
||||
|
||||
# Launch the local system Chrome instead
|
||||
<function=browser_actions>
|
||||
<parameter=action>launch</parameter>
|
||||
<parameter=use_local>true</parameter>
|
||||
</function>
|
||||
|
||||
# Run a natural-language browser task
|
||||
<function=browser_actions>
|
||||
<parameter=action>run</parameter>
|
||||
|
|
|
|||
|
|
@ -232,13 +232,13 @@ def _wait_for_cdp(
|
|||
|
||||
# Rewrite the WebSocket URL: Chromium reports the container-
|
||||
# internal address (e.g. ws://127.0.0.1:19222/devtools/...)
|
||||
# but we need it on the Docker-mapped host:port.
|
||||
# but we need it routed through the tool server's CDP proxy.
|
||||
parsed_cdp = urlparse(cdp_url)
|
||||
parsed_ws = urlparse(raw_ws)
|
||||
ws_url = raw_ws.replace(
|
||||
f"{parsed_ws.hostname}:{parsed_ws.port}",
|
||||
f"{parsed_cdp.hostname}:{parsed_cdp.port}",
|
||||
)
|
||||
ws_url = parsed_ws._replace(
|
||||
netloc=parsed_cdp.netloc,
|
||||
path=parsed_cdp.path.rstrip("/") + parsed_ws.path,
|
||||
).geturl()
|
||||
|
||||
# Append auth token so browser-use's WebSocket upgrade
|
||||
# request passes through the CDP auth proxy.
|
||||
|
|
|
|||
|
|
@ -99,18 +99,10 @@ async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: A
|
|||
|
||||
|
||||
async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
|
||||
from strix.tools.context import set_current_agent_id
|
||||
|
||||
tool_func = get_tool_by_name(tool_name)
|
||||
if not tool_func:
|
||||
raise ValueError(f"Tool '{tool_name}' not found")
|
||||
|
||||
# Propagate agent_id so tools can scope per-agent resources (e.g. browser instances).
|
||||
# NOTE: This is needed for browser_use tools to work correctly.
|
||||
agent_id = getattr(agent_state, "agent_id", None) if agent_state else None
|
||||
if agent_id:
|
||||
set_current_agent_id(agent_id)
|
||||
|
||||
converted_kwargs = convert_arguments(tool_func, kwargs)
|
||||
|
||||
if needs_agent_state(tool_name):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue