crabcode 0.1.6__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
"""BrowserTool — headless browser automation via Playwright."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import base64
|
|
7
|
+
import mimetypes
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
|
|
16
|
+
from crabcode_core.logging_utils import get_logger
|
|
17
|
+
from crabcode_core.types.tool import PermissionBehavior, PermissionResult, Tool, ToolContext, ToolResult
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
_ACTIONS = {
|
|
22
|
+
"create_session",
|
|
23
|
+
"goto",
|
|
24
|
+
"click",
|
|
25
|
+
"fill",
|
|
26
|
+
"press",
|
|
27
|
+
"wait_for",
|
|
28
|
+
"extract",
|
|
29
|
+
"screenshot",
|
|
30
|
+
"evaluate",
|
|
31
|
+
"list_tabs",
|
|
32
|
+
"new_tab",
|
|
33
|
+
"switch_tab",
|
|
34
|
+
"close_tab",
|
|
35
|
+
"close_session",
|
|
36
|
+
}
|
|
37
|
+
_WAIT_UNTIL = {"load", "domcontentloaded", "networkidle"}
|
|
38
|
+
_RETURN_FORMATS = {"text", "html", "links", "json"}
|
|
39
|
+
_SENSITIVE_ACTIONS = {"fill", "press", "evaluate"}
|
|
40
|
+
_SESSION_ACTIONS = {"create_session", "goto"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class _BrowserSession:
|
|
45
|
+
session_id: str
|
|
46
|
+
playwright: Any
|
|
47
|
+
browser: Any
|
|
48
|
+
context: Any
|
|
49
|
+
headless: bool
|
|
50
|
+
tabs: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
active_tab_id: str | None = None
|
|
52
|
+
user_data_dir: str | None = None
|
|
53
|
+
trusted_origins: set[str] = field(default_factory=set)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _origin_from_url(url: str) -> str:
|
|
57
|
+
parsed = urlparse(url)
|
|
58
|
+
if not parsed.scheme or not parsed.netloc:
|
|
59
|
+
return ""
|
|
60
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BrowserTool(Tool):
|
|
64
|
+
name = "Browser"
|
|
65
|
+
description = "Use a Chromium browser to open pages, interact with them, extract content, and take screenshots. Headless is the default."
|
|
66
|
+
is_read_only = False
|
|
67
|
+
is_concurrency_safe = False
|
|
68
|
+
uses_tool_permission_policy = True
|
|
69
|
+
input_schema = {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"action": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"enum": sorted(_ACTIONS),
|
|
75
|
+
"description": "Browser action to perform.",
|
|
76
|
+
},
|
|
77
|
+
"session_id": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Existing browser session ID. Omit for create_session.",
|
|
80
|
+
},
|
|
81
|
+
"headless": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"description": "Optional create_session override. true runs headless, false opens a visible browser window. Defaults to tool_settings.Browser.headless.",
|
|
84
|
+
},
|
|
85
|
+
"url": {"type": "string", "description": "Navigation target URL."},
|
|
86
|
+
"selector": {"type": "string", "description": "CSS selector for the target element."},
|
|
87
|
+
"text": {"type": "string", "description": "Text to input or key to press."},
|
|
88
|
+
"script": {"type": "string", "description": "JavaScript snippet to evaluate in the page."},
|
|
89
|
+
"timeout_seconds": {"type": "number", "description": "Optional timeout override in seconds."},
|
|
90
|
+
"wait_until": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"enum": sorted(_WAIT_UNTIL),
|
|
93
|
+
"description": "Page readiness target for navigation.",
|
|
94
|
+
},
|
|
95
|
+
"return_format": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"enum": sorted(_RETURN_FORMATS),
|
|
98
|
+
"description": "Format for extract/evaluate output.",
|
|
99
|
+
},
|
|
100
|
+
"path": {"type": "string", "description": "Output path for screenshots."},
|
|
101
|
+
"tab_id": {"type": "string", "description": "Target tab ID."},
|
|
102
|
+
"options": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"description": "Additional action-specific options.",
|
|
105
|
+
"additionalProperties": True,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
"required": ["action"],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
self._tool_config: dict[str, Any] = {}
|
|
113
|
+
self._sessions: dict[str, _BrowserSession] = {}
|
|
114
|
+
self._playwright_module: Any | None = None
|
|
115
|
+
self._playwright_available = False
|
|
116
|
+
self._dependency_error: str | None = None
|
|
117
|
+
self._enabled = True
|
|
118
|
+
self._headless = True
|
|
119
|
+
self._default_timeout_ms = 15_000
|
|
120
|
+
self._max_sessions = 3
|
|
121
|
+
self._default_browser = "chromium"
|
|
122
|
+
self._launch_options: dict[str, Any] = {}
|
|
123
|
+
self._context_options: dict[str, Any] = {}
|
|
124
|
+
self._storage_dir: str | None = None
|
|
125
|
+
self._block_downloads = True
|
|
126
|
+
self._allowed_domains: set[str] = set()
|
|
127
|
+
self._blocked_domains: set[str] = set()
|
|
128
|
+
|
|
129
|
+
async def setup(self, context: ToolContext) -> None:
|
|
130
|
+
await super().setup(context)
|
|
131
|
+
self._tool_config = dict(context.tool_config)
|
|
132
|
+
self._enabled = bool(self._tool_config.get("enabled", True))
|
|
133
|
+
self._default_browser = str(self._tool_config.get("default_browser", "chromium")).lower()
|
|
134
|
+
self._headless = bool(self._tool_config.get("headless", True))
|
|
135
|
+
self._default_timeout_ms = max(1, int(float(self._tool_config.get("default_timeout_seconds", 15)) * 1000))
|
|
136
|
+
self._max_sessions = max(1, int(self._tool_config.get("max_sessions", 3)))
|
|
137
|
+
self._launch_options = dict(self._tool_config.get("launch_options", {}))
|
|
138
|
+
self._context_options = dict(self._tool_config.get("context_options", {}))
|
|
139
|
+
self._storage_dir = self._tool_config.get("storage_dir")
|
|
140
|
+
self._block_downloads = bool(self._tool_config.get("block_downloads", True))
|
|
141
|
+
self._allowed_domains = {str(v) for v in self._tool_config.get("allowed_domains", []) if str(v)}
|
|
142
|
+
self._blocked_domains = {str(v) for v in self._tool_config.get("blocked_domains", []) if str(v)}
|
|
143
|
+
|
|
144
|
+
if not self._enabled:
|
|
145
|
+
self.is_enabled = False
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
from playwright.async_api import async_playwright
|
|
150
|
+
except ImportError as exc:
|
|
151
|
+
self._dependency_error = (
|
|
152
|
+
"Browser is unavailable because Playwright is not installed. "
|
|
153
|
+
"Install the optional browser extra and run 'playwright install chromium'."
|
|
154
|
+
)
|
|
155
|
+
self.is_enabled = False
|
|
156
|
+
logger.info("Disabling Browser tool: %s", exc)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
self._playwright_module = async_playwright
|
|
160
|
+
self._playwright_available = True
|
|
161
|
+
|
|
162
|
+
async def close(self) -> None:
|
|
163
|
+
for session_id in list(self._sessions):
|
|
164
|
+
await self._close_session(session_id)
|
|
165
|
+
|
|
166
|
+
async def get_prompt(self, **kwargs: Any) -> str:
|
|
167
|
+
return (
|
|
168
|
+
"Use a persistent Chromium browser session to navigate pages, interact with DOM elements, "
|
|
169
|
+
"extract content, evaluate page-side JavaScript, and take screenshots. "
|
|
170
|
+
"Prefer WebSearch for discovering URLs or searching the public web. "
|
|
171
|
+
"If WebSearch is unavailable, fails, or its snippets are not enough to answer, use Browser as the fallback "
|
|
172
|
+
"to open the relevant pages and extract their content. "
|
|
173
|
+
"Also use Browser when you need to open a specific page, click, fill forms, inspect DOM state, or capture a screenshot. "
|
|
174
|
+
"Browser sessions run headless by default, but create_session can override that with headless=false when needed. "
|
|
175
|
+
"Create a session once, reuse the returned session_id across actions, and close sessions when done."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def validate_input(self, tool_input: dict[str, Any]) -> str | None:
|
|
179
|
+
action = str(tool_input.get("action", "")).strip()
|
|
180
|
+
if action not in _ACTIONS:
|
|
181
|
+
return f"action must be one of: {', '.join(sorted(_ACTIONS))}"
|
|
182
|
+
|
|
183
|
+
if action != "create_session" and not str(tool_input.get("session_id", "")).strip():
|
|
184
|
+
return "session_id is required for this action"
|
|
185
|
+
|
|
186
|
+
if action in {"goto", "create_session"} and tool_input.get("url"):
|
|
187
|
+
url = str(tool_input["url"]).strip()
|
|
188
|
+
if url and urlparse(url).scheme not in {"http", "https"}:
|
|
189
|
+
return "url must be http or https"
|
|
190
|
+
|
|
191
|
+
wait_until = tool_input.get("wait_until")
|
|
192
|
+
if wait_until is not None and str(wait_until) not in _WAIT_UNTIL:
|
|
193
|
+
return f"wait_until must be one of: {', '.join(sorted(_WAIT_UNTIL))}"
|
|
194
|
+
|
|
195
|
+
return_format = tool_input.get("return_format")
|
|
196
|
+
if return_format is not None and str(return_format) not in _RETURN_FORMATS:
|
|
197
|
+
return f"return_format must be one of: {', '.join(sorted(_RETURN_FORMATS))}"
|
|
198
|
+
|
|
199
|
+
if "headless" in tool_input and not isinstance(tool_input["headless"], bool):
|
|
200
|
+
return "headless must be a boolean"
|
|
201
|
+
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
def get_permission_key(self, tool_input: dict[str, Any]) -> str:
|
|
205
|
+
action = str(tool_input.get("action", "")).strip() or "unknown"
|
|
206
|
+
return f"{self.name}:{action}"
|
|
207
|
+
|
|
208
|
+
async def check_permissions(
|
|
209
|
+
self,
|
|
210
|
+
tool_input: dict[str, Any],
|
|
211
|
+
context: ToolContext,
|
|
212
|
+
) -> PermissionResult:
|
|
213
|
+
action = str(tool_input.get("action", "")).strip()
|
|
214
|
+
permission_key = self.get_permission_key(tool_input)
|
|
215
|
+
|
|
216
|
+
if action in {"close_tab", "close_session", "list_tabs", "extract", "new_tab", "switch_tab", "wait_for"}:
|
|
217
|
+
return PermissionResult(behavior=PermissionBehavior.ALLOW, permission_key=permission_key)
|
|
218
|
+
|
|
219
|
+
if action == "screenshot":
|
|
220
|
+
path = str(tool_input.get("path", "")).strip()
|
|
221
|
+
if not path:
|
|
222
|
+
return PermissionResult(behavior=PermissionBehavior.ALLOW, permission_key=permission_key)
|
|
223
|
+
abs_path = Path(path)
|
|
224
|
+
if not abs_path.is_absolute():
|
|
225
|
+
abs_path = Path(context.cwd) / abs_path
|
|
226
|
+
try:
|
|
227
|
+
abs_path.relative_to(Path(context.cwd))
|
|
228
|
+
except ValueError:
|
|
229
|
+
return PermissionResult(
|
|
230
|
+
behavior=PermissionBehavior.ASK,
|
|
231
|
+
reason="Screenshot path is outside the working directory",
|
|
232
|
+
permission_key=permission_key,
|
|
233
|
+
)
|
|
234
|
+
return PermissionResult(behavior=PermissionBehavior.ALLOW, permission_key=permission_key)
|
|
235
|
+
|
|
236
|
+
if action in _SENSITIVE_ACTIONS:
|
|
237
|
+
return PermissionResult(
|
|
238
|
+
behavior=PermissionBehavior.ASK,
|
|
239
|
+
reason=f"Browser action '{action}' is sensitive and requires confirmation",
|
|
240
|
+
permission_key=permission_key,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if action in _SESSION_ACTIONS:
|
|
244
|
+
return PermissionResult(
|
|
245
|
+
behavior=PermissionBehavior.ASK,
|
|
246
|
+
reason=f"Browser action '{action}' requires confirmation",
|
|
247
|
+
permission_key=permission_key,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if action == "click":
|
|
251
|
+
session = self._sessions.get(str(tool_input.get("session_id", "")).strip())
|
|
252
|
+
url = str(tool_input.get("url", "")).strip()
|
|
253
|
+
if url and session:
|
|
254
|
+
target_origin = _origin_from_url(url)
|
|
255
|
+
if target_origin and session.trusted_origins and target_origin not in session.trusted_origins:
|
|
256
|
+
return PermissionResult(
|
|
257
|
+
behavior=PermissionBehavior.ASK,
|
|
258
|
+
reason="Browser click may navigate to a new origin",
|
|
259
|
+
permission_key=permission_key,
|
|
260
|
+
)
|
|
261
|
+
return PermissionResult(behavior=PermissionBehavior.ALLOW, permission_key=permission_key)
|
|
262
|
+
|
|
263
|
+
return PermissionResult(behavior=PermissionBehavior.ALLOW, permission_key=permission_key)
|
|
264
|
+
|
|
265
|
+
async def call(
|
|
266
|
+
self,
|
|
267
|
+
tool_input: dict[str, Any],
|
|
268
|
+
context: ToolContext,
|
|
269
|
+
) -> ToolResult:
|
|
270
|
+
if not self.is_enabled or not self._enabled:
|
|
271
|
+
return ToolResult(
|
|
272
|
+
result_for_model=self._dependency_error or "Browser is disabled in this session.",
|
|
273
|
+
is_error=True,
|
|
274
|
+
)
|
|
275
|
+
if not self._playwright_available or self._playwright_module is None:
|
|
276
|
+
return ToolResult(
|
|
277
|
+
result_for_model=self._dependency_error or "Browser is unavailable.",
|
|
278
|
+
is_error=True,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
action = str(tool_input["action"]).strip()
|
|
282
|
+
try:
|
|
283
|
+
if action == "create_session":
|
|
284
|
+
return await self._create_session(tool_input)
|
|
285
|
+
if action == "close_session":
|
|
286
|
+
return await self._close_session_result(str(tool_input["session_id"]).strip())
|
|
287
|
+
|
|
288
|
+
session = self._require_session(str(tool_input["session_id"]).strip())
|
|
289
|
+
if isinstance(session, ToolResult):
|
|
290
|
+
return session
|
|
291
|
+
|
|
292
|
+
if action == "goto":
|
|
293
|
+
return await self._goto(session, tool_input)
|
|
294
|
+
if action == "click":
|
|
295
|
+
return await self._click(session, tool_input)
|
|
296
|
+
if action == "fill":
|
|
297
|
+
return await self._fill(session, tool_input)
|
|
298
|
+
if action == "press":
|
|
299
|
+
return await self._press(session, tool_input)
|
|
300
|
+
if action == "wait_for":
|
|
301
|
+
return await self._wait_for(session, tool_input)
|
|
302
|
+
if action == "extract":
|
|
303
|
+
return await self._extract(session, tool_input)
|
|
304
|
+
if action == "screenshot":
|
|
305
|
+
return await self._screenshot(session, tool_input, context)
|
|
306
|
+
if action == "evaluate":
|
|
307
|
+
return await self._evaluate(session, tool_input)
|
|
308
|
+
if action == "list_tabs":
|
|
309
|
+
return self._list_tabs(session)
|
|
310
|
+
if action == "new_tab":
|
|
311
|
+
return await self._new_tab(session, tool_input)
|
|
312
|
+
if action == "switch_tab":
|
|
313
|
+
return self._switch_tab(session, tool_input)
|
|
314
|
+
if action == "close_tab":
|
|
315
|
+
return await self._close_tab(session, tool_input)
|
|
316
|
+
except Exception as exc:
|
|
317
|
+
logger.exception("Browser action failed: %s", action)
|
|
318
|
+
return ToolResult(
|
|
319
|
+
result_for_model=f"Browser error during {action}: {exc}",
|
|
320
|
+
is_error=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return ToolResult(result_for_model=f"Unsupported browser action: {action}", is_error=True)
|
|
324
|
+
|
|
325
|
+
def _timeout_ms(self, tool_input: dict[str, Any]) -> int:
|
|
326
|
+
raw = tool_input.get("timeout_seconds")
|
|
327
|
+
if raw is None:
|
|
328
|
+
return self._default_timeout_ms
|
|
329
|
+
return max(1, int(float(raw) * 1000))
|
|
330
|
+
|
|
331
|
+
def _require_session(self, session_id: str) -> _BrowserSession | ToolResult:
|
|
332
|
+
session = self._sessions.get(session_id)
|
|
333
|
+
if session is None:
|
|
334
|
+
return ToolResult(
|
|
335
|
+
result_for_model=f"Unknown browser session: {session_id}",
|
|
336
|
+
is_error=True,
|
|
337
|
+
)
|
|
338
|
+
return session
|
|
339
|
+
|
|
340
|
+
def _get_tab(self, session: _BrowserSession, tool_input: dict[str, Any]) -> tuple[str, Any] | ToolResult:
|
|
341
|
+
tab_id = str(tool_input.get("tab_id", "")).strip() or session.active_tab_id or ""
|
|
342
|
+
page = session.tabs.get(tab_id)
|
|
343
|
+
if page is None:
|
|
344
|
+
return ToolResult(
|
|
345
|
+
result_for_model=f"Unknown browser tab: {tab_id or '(none)'}",
|
|
346
|
+
is_error=True,
|
|
347
|
+
)
|
|
348
|
+
return tab_id, page
|
|
349
|
+
|
|
350
|
+
def _check_domain(self, url: str) -> ToolResult | None:
|
|
351
|
+
parsed = urlparse(url)
|
|
352
|
+
host = parsed.netloc.lower()
|
|
353
|
+
if not host:
|
|
354
|
+
return None
|
|
355
|
+
if self._allowed_domains and host not in self._allowed_domains:
|
|
356
|
+
return ToolResult(result_for_model=f"Blocked by Browser.allowed_domains: {host}", is_error=True)
|
|
357
|
+
if self._blocked_domains and host in self._blocked_domains:
|
|
358
|
+
return ToolResult(result_for_model=f"Blocked by Browser.blocked_domains: {host}", is_error=True)
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
async def _create_session(self, tool_input: dict[str, Any]) -> ToolResult:
|
|
362
|
+
if len(self._sessions) >= self._max_sessions:
|
|
363
|
+
return ToolResult(
|
|
364
|
+
result_for_model=f"Maximum Browser sessions reached ({self._max_sessions})",
|
|
365
|
+
is_error=True,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
playwright = await self._playwright_module().start()
|
|
369
|
+
browser_type = getattr(playwright, self._default_browser, None)
|
|
370
|
+
if browser_type is None:
|
|
371
|
+
await playwright.stop()
|
|
372
|
+
return ToolResult(result_for_model=f"Unsupported browser type: {self._default_browser}", is_error=True)
|
|
373
|
+
|
|
374
|
+
headless = bool(tool_input["headless"]) if "headless" in tool_input else self._headless
|
|
375
|
+
launch_options = dict(self._launch_options)
|
|
376
|
+
launch_options["headless"] = headless
|
|
377
|
+
browser = await browser_type.launch(**launch_options)
|
|
378
|
+
|
|
379
|
+
user_data_dir: str | None = None
|
|
380
|
+
if self._storage_dir:
|
|
381
|
+
base = Path(self._storage_dir)
|
|
382
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
383
|
+
user_data_dir = str(base / f"browser-{uuid.uuid4().hex[:8]}")
|
|
384
|
+
Path(user_data_dir).mkdir(parents=True, exist_ok=True)
|
|
385
|
+
else:
|
|
386
|
+
user_data_dir = tempfile.mkdtemp(prefix="crabcode-browser-")
|
|
387
|
+
|
|
388
|
+
context_options = dict(self._context_options)
|
|
389
|
+
context_options.setdefault("accept_downloads", not self._block_downloads)
|
|
390
|
+
context = await browser.new_context(**context_options)
|
|
391
|
+
page = await context.new_page()
|
|
392
|
+
page.set_default_timeout(self._default_timeout_ms)
|
|
393
|
+
page.set_default_navigation_timeout(self._default_timeout_ms)
|
|
394
|
+
|
|
395
|
+
session_id = str(uuid.uuid4())
|
|
396
|
+
tab_id = str(uuid.uuid4())
|
|
397
|
+
session = _BrowserSession(
|
|
398
|
+
session_id=session_id,
|
|
399
|
+
playwright=playwright,
|
|
400
|
+
browser=browser,
|
|
401
|
+
context=context,
|
|
402
|
+
headless=headless,
|
|
403
|
+
tabs={tab_id: page},
|
|
404
|
+
active_tab_id=tab_id,
|
|
405
|
+
user_data_dir=user_data_dir,
|
|
406
|
+
)
|
|
407
|
+
self._sessions[session_id] = session
|
|
408
|
+
|
|
409
|
+
url = str(tool_input.get("url", "")).strip()
|
|
410
|
+
if url:
|
|
411
|
+
denied = self._check_domain(url)
|
|
412
|
+
if denied is not None:
|
|
413
|
+
await self._close_session(session_id)
|
|
414
|
+
return denied
|
|
415
|
+
await page.goto(url, wait_until=str(tool_input.get("wait_until", "load")), timeout=self._timeout_ms(tool_input))
|
|
416
|
+
origin = _origin_from_url(page.url)
|
|
417
|
+
if origin:
|
|
418
|
+
session.trusted_origins.add(origin)
|
|
419
|
+
|
|
420
|
+
return self._result(
|
|
421
|
+
session=session,
|
|
422
|
+
tab_id=tab_id,
|
|
423
|
+
status="created",
|
|
424
|
+
data={"tabs": [self._tab_summary(tab_id, page)]},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
async def _goto(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
428
|
+
url = str(tool_input.get("url", "")).strip()
|
|
429
|
+
denied = self._check_domain(url)
|
|
430
|
+
if denied is not None:
|
|
431
|
+
return denied
|
|
432
|
+
tab = self._get_tab(session, tool_input)
|
|
433
|
+
if isinstance(tab, ToolResult):
|
|
434
|
+
return tab
|
|
435
|
+
tab_id, page = tab
|
|
436
|
+
await page.goto(url, wait_until=str(tool_input.get("wait_until", "load")), timeout=self._timeout_ms(tool_input))
|
|
437
|
+
origin = _origin_from_url(page.url)
|
|
438
|
+
if origin:
|
|
439
|
+
session.trusted_origins.add(origin)
|
|
440
|
+
session.active_tab_id = tab_id
|
|
441
|
+
return self._result(session=session, tab_id=tab_id, status="navigated")
|
|
442
|
+
|
|
443
|
+
async def _click(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
444
|
+
tab = self._get_tab(session, tool_input)
|
|
445
|
+
if isinstance(tab, ToolResult):
|
|
446
|
+
return tab
|
|
447
|
+
tab_id, page = tab
|
|
448
|
+
selector = str(tool_input.get("selector", "")).strip()
|
|
449
|
+
await page.click(selector, timeout=self._timeout_ms(tool_input))
|
|
450
|
+
origin = _origin_from_url(page.url)
|
|
451
|
+
if origin:
|
|
452
|
+
session.trusted_origins.add(origin)
|
|
453
|
+
return self._result(session=session, tab_id=tab_id, status="clicked")
|
|
454
|
+
|
|
455
|
+
async def _fill(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
456
|
+
tab = self._get_tab(session, tool_input)
|
|
457
|
+
if isinstance(tab, ToolResult):
|
|
458
|
+
return tab
|
|
459
|
+
tab_id, page = tab
|
|
460
|
+
await page.fill(
|
|
461
|
+
str(tool_input.get("selector", "")).strip(),
|
|
462
|
+
str(tool_input.get("text", "")),
|
|
463
|
+
timeout=self._timeout_ms(tool_input),
|
|
464
|
+
)
|
|
465
|
+
return self._result(session=session, tab_id=tab_id, status="filled")
|
|
466
|
+
|
|
467
|
+
async def _press(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
468
|
+
tab = self._get_tab(session, tool_input)
|
|
469
|
+
if isinstance(tab, ToolResult):
|
|
470
|
+
return tab
|
|
471
|
+
tab_id, page = tab
|
|
472
|
+
selector = str(tool_input.get("selector", "")).strip()
|
|
473
|
+
key = str(tool_input.get("text", "")).strip()
|
|
474
|
+
if selector:
|
|
475
|
+
await page.press(selector, key, timeout=self._timeout_ms(tool_input))
|
|
476
|
+
else:
|
|
477
|
+
await page.keyboard.press(key)
|
|
478
|
+
return self._result(session=session, tab_id=tab_id, status="pressed")
|
|
479
|
+
|
|
480
|
+
async def _wait_for(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
481
|
+
tab = self._get_tab(session, tool_input)
|
|
482
|
+
if isinstance(tab, ToolResult):
|
|
483
|
+
return tab
|
|
484
|
+
tab_id, page = tab
|
|
485
|
+
selector = str(tool_input.get("selector", "")).strip()
|
|
486
|
+
if selector:
|
|
487
|
+
await page.wait_for_selector(selector, timeout=self._timeout_ms(tool_input))
|
|
488
|
+
else:
|
|
489
|
+
await page.wait_for_load_state(str(tool_input.get("wait_until", "load")), timeout=self._timeout_ms(tool_input))
|
|
490
|
+
return self._result(session=session, tab_id=tab_id, status="ready")
|
|
491
|
+
|
|
492
|
+
async def _extract(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
493
|
+
tab = self._get_tab(session, tool_input)
|
|
494
|
+
if isinstance(tab, ToolResult):
|
|
495
|
+
return tab
|
|
496
|
+
tab_id, page = tab
|
|
497
|
+
return_format = str(tool_input.get("return_format", "text"))
|
|
498
|
+
if return_format == "html":
|
|
499
|
+
data = await page.content()
|
|
500
|
+
elif return_format == "links":
|
|
501
|
+
data = await page.eval_on_selector_all(
|
|
502
|
+
"a[href]",
|
|
503
|
+
"""els => els.map(el => ({
|
|
504
|
+
text: (el.textContent || '').trim(),
|
|
505
|
+
href: el.href
|
|
506
|
+
}))""",
|
|
507
|
+
)
|
|
508
|
+
elif return_format == "json":
|
|
509
|
+
data = await page.evaluate(
|
|
510
|
+
"""() => ({
|
|
511
|
+
title: document.title,
|
|
512
|
+
text: (document.body?.innerText || '').trim(),
|
|
513
|
+
url: location.href
|
|
514
|
+
})"""
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
data = (await page.text_content("body")) or ""
|
|
518
|
+
return self._result(session=session, tab_id=tab_id, status="extracted", data=data)
|
|
519
|
+
|
|
520
|
+
async def _screenshot(self, session: _BrowserSession, tool_input: dict[str, Any], context: ToolContext) -> ToolResult:
|
|
521
|
+
tab = self._get_tab(session, tool_input)
|
|
522
|
+
if isinstance(tab, ToolResult):
|
|
523
|
+
return tab
|
|
524
|
+
tab_id, page = tab
|
|
525
|
+
path = str(tool_input.get("path", "")).strip()
|
|
526
|
+
if not path:
|
|
527
|
+
session_dir = Path(context.cwd) / ".crabcode" / "browser"
|
|
528
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
529
|
+
path = str(session_dir / f"{session.session_id}-{tab_id}.png")
|
|
530
|
+
out = Path(path)
|
|
531
|
+
if not out.is_absolute():
|
|
532
|
+
out = Path(context.cwd) / out
|
|
533
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
534
|
+
await page.screenshot(path=str(out), full_page=bool(tool_input.get("options", {}).get("full_page", True)))
|
|
535
|
+
images: list[dict[str, str]] = []
|
|
536
|
+
try:
|
|
537
|
+
raw = out.read_bytes()
|
|
538
|
+
if len(raw) <= 20 * 1024 * 1024:
|
|
539
|
+
images.append({
|
|
540
|
+
"media_type": mimetypes.guess_type(out.name)[0] or "image/png",
|
|
541
|
+
"data": base64.b64encode(raw).decode("ascii"),
|
|
542
|
+
})
|
|
543
|
+
except OSError:
|
|
544
|
+
logger.debug("Failed to read browser screenshot for inline display: %s", out, exc_info=True)
|
|
545
|
+
return self._result(
|
|
546
|
+
session=session,
|
|
547
|
+
tab_id=tab_id,
|
|
548
|
+
status="screenshot",
|
|
549
|
+
data={"path": str(out)},
|
|
550
|
+
images=images,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
async def _evaluate(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
554
|
+
tab = self._get_tab(session, tool_input)
|
|
555
|
+
if isinstance(tab, ToolResult):
|
|
556
|
+
return tab
|
|
557
|
+
tab_id, page = tab
|
|
558
|
+
data = await page.evaluate(str(tool_input.get("script", "")))
|
|
559
|
+
return self._result(session=session, tab_id=tab_id, status="evaluated", data=data)
|
|
560
|
+
|
|
561
|
+
def _list_tabs(self, session: _BrowserSession) -> ToolResult:
|
|
562
|
+
tabs = [self._tab_summary(tab_id, page) for tab_id, page in session.tabs.items()]
|
|
563
|
+
return self._result(
|
|
564
|
+
session=session,
|
|
565
|
+
tab_id=session.active_tab_id,
|
|
566
|
+
status="listed_tabs",
|
|
567
|
+
data={"tabs": tabs},
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
async def _new_tab(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
571
|
+
page = await session.context.new_page()
|
|
572
|
+
page.set_default_timeout(self._default_timeout_ms)
|
|
573
|
+
page.set_default_navigation_timeout(self._default_timeout_ms)
|
|
574
|
+
tab_id = str(uuid.uuid4())
|
|
575
|
+
session.tabs[tab_id] = page
|
|
576
|
+
session.active_tab_id = tab_id
|
|
577
|
+
url = str(tool_input.get("url", "")).strip()
|
|
578
|
+
if url:
|
|
579
|
+
denied = self._check_domain(url)
|
|
580
|
+
if denied is not None:
|
|
581
|
+
await page.close()
|
|
582
|
+
session.tabs.pop(tab_id, None)
|
|
583
|
+
return denied
|
|
584
|
+
await page.goto(url, wait_until=str(tool_input.get("wait_until", "load")), timeout=self._timeout_ms(tool_input))
|
|
585
|
+
origin = _origin_from_url(page.url)
|
|
586
|
+
if origin:
|
|
587
|
+
session.trusted_origins.add(origin)
|
|
588
|
+
return self._result(session=session, tab_id=tab_id, status="new_tab")
|
|
589
|
+
|
|
590
|
+
def _switch_tab(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
591
|
+
tab_id = str(tool_input.get("tab_id", "")).strip()
|
|
592
|
+
if tab_id not in session.tabs:
|
|
593
|
+
return ToolResult(result_for_model=f"Unknown browser tab: {tab_id}", is_error=True)
|
|
594
|
+
session.active_tab_id = tab_id
|
|
595
|
+
return self._result(session=session, tab_id=tab_id, status="switched_tab")
|
|
596
|
+
|
|
597
|
+
async def _close_tab(self, session: _BrowserSession, tool_input: dict[str, Any]) -> ToolResult:
|
|
598
|
+
tab = self._get_tab(session, tool_input)
|
|
599
|
+
if isinstance(tab, ToolResult):
|
|
600
|
+
return tab
|
|
601
|
+
tab_id, page = tab
|
|
602
|
+
await page.close()
|
|
603
|
+
session.tabs.pop(tab_id, None)
|
|
604
|
+
session.active_tab_id = next(iter(session.tabs), None)
|
|
605
|
+
return self._result(session=session, tab_id=session.active_tab_id, status="closed_tab")
|
|
606
|
+
|
|
607
|
+
async def _close_session_result(self, session_id: str) -> ToolResult:
|
|
608
|
+
session = self._sessions.get(session_id)
|
|
609
|
+
if session is None:
|
|
610
|
+
return ToolResult(result_for_model=f"Unknown browser session: {session_id}", is_error=True)
|
|
611
|
+
await self._close_session(session_id)
|
|
612
|
+
return ToolResult(
|
|
613
|
+
data={"session_id": session_id, "status": "closed"},
|
|
614
|
+
result_for_model=f"session_id: {session_id}\nstatus: closed",
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
async def _close_session(self, session_id: str) -> None:
|
|
618
|
+
session = self._sessions.pop(session_id, None)
|
|
619
|
+
if session is None:
|
|
620
|
+
return
|
|
621
|
+
try:
|
|
622
|
+
await session.context.close()
|
|
623
|
+
except Exception:
|
|
624
|
+
logger.debug("Failed to close browser context %s", session_id, exc_info=True)
|
|
625
|
+
try:
|
|
626
|
+
await session.browser.close()
|
|
627
|
+
except Exception:
|
|
628
|
+
logger.debug("Failed to close browser %s", session_id, exc_info=True)
|
|
629
|
+
try:
|
|
630
|
+
await session.playwright.stop()
|
|
631
|
+
except Exception:
|
|
632
|
+
logger.debug("Failed to stop playwright %s", session_id, exc_info=True)
|
|
633
|
+
if session.user_data_dir:
|
|
634
|
+
shutil.rmtree(session.user_data_dir, ignore_errors=True)
|
|
635
|
+
|
|
636
|
+
def _tab_summary(self, tab_id: str, page: Any) -> dict[str, Any]:
|
|
637
|
+
return {
|
|
638
|
+
"tab_id": tab_id,
|
|
639
|
+
"url": page.url,
|
|
640
|
+
"title": "",
|
|
641
|
+
"active": False,
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
def _result(
|
|
645
|
+
self,
|
|
646
|
+
*,
|
|
647
|
+
session: _BrowserSession,
|
|
648
|
+
tab_id: str | None,
|
|
649
|
+
status: str,
|
|
650
|
+
data: Any = None,
|
|
651
|
+
images: list[dict[str, str]] | None = None,
|
|
652
|
+
) -> ToolResult:
|
|
653
|
+
page = session.tabs.get(tab_id) if tab_id else None
|
|
654
|
+
url = page.url if page is not None else ""
|
|
655
|
+
payload = {
|
|
656
|
+
"session_id": session.session_id,
|
|
657
|
+
"tab_id": tab_id,
|
|
658
|
+
"url": url,
|
|
659
|
+
"title": "",
|
|
660
|
+
"headless": session.headless,
|
|
661
|
+
"status": status,
|
|
662
|
+
"data": data,
|
|
663
|
+
}
|
|
664
|
+
text = [
|
|
665
|
+
f"session_id: {session.session_id}",
|
|
666
|
+
f"tab_id: {tab_id or '(none)'}",
|
|
667
|
+
f"url: {url or '(blank)'}",
|
|
668
|
+
"title: ",
|
|
669
|
+
f"headless: {str(session.headless).lower()}",
|
|
670
|
+
f"status: {status}",
|
|
671
|
+
]
|
|
672
|
+
if data is not None:
|
|
673
|
+
compact = json.dumps(data, ensure_ascii=False) if not isinstance(data, str) else data
|
|
674
|
+
if len(compact) > 4000:
|
|
675
|
+
compact = compact[:4000] + "…"
|
|
676
|
+
text.append("data:")
|
|
677
|
+
text.append(compact)
|
|
678
|
+
return ToolResult(data=payload, result_for_model="\n".join(text), images=images or [])
|