strix-agent 0.4.0__py3-none-any.whl → 0.6.2__py3-none-any.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.
- strix/agents/StrixAgent/strix_agent.py +3 -3
- strix/agents/StrixAgent/system_prompt.jinja +30 -26
- strix/agents/base_agent.py +159 -75
- strix/agents/state.py +5 -2
- strix/config/__init__.py +12 -0
- strix/config/config.py +172 -0
- strix/interface/assets/tui_styles.tcss +195 -230
- strix/interface/cli.py +16 -41
- strix/interface/main.py +151 -74
- strix/interface/streaming_parser.py +119 -0
- strix/interface/tool_components/__init__.py +4 -0
- strix/interface/tool_components/agent_message_renderer.py +190 -0
- strix/interface/tool_components/agents_graph_renderer.py +54 -38
- strix/interface/tool_components/base_renderer.py +68 -36
- strix/interface/tool_components/browser_renderer.py +106 -91
- strix/interface/tool_components/file_edit_renderer.py +117 -36
- strix/interface/tool_components/finish_renderer.py +43 -10
- strix/interface/tool_components/notes_renderer.py +63 -38
- strix/interface/tool_components/proxy_renderer.py +133 -92
- strix/interface/tool_components/python_renderer.py +121 -8
- strix/interface/tool_components/registry.py +19 -12
- strix/interface/tool_components/reporting_renderer.py +196 -28
- strix/interface/tool_components/scan_info_renderer.py +22 -19
- strix/interface/tool_components/terminal_renderer.py +270 -90
- strix/interface/tool_components/thinking_renderer.py +8 -6
- strix/interface/tool_components/todo_renderer.py +225 -0
- strix/interface/tool_components/user_message_renderer.py +26 -19
- strix/interface/tool_components/web_search_renderer.py +7 -6
- strix/interface/tui.py +907 -262
- strix/interface/utils.py +236 -4
- strix/llm/__init__.py +6 -2
- strix/llm/config.py +8 -5
- strix/llm/dedupe.py +217 -0
- strix/llm/llm.py +209 -356
- strix/llm/memory_compressor.py +6 -5
- strix/llm/utils.py +17 -8
- strix/runtime/__init__.py +12 -3
- strix/runtime/docker_runtime.py +121 -202
- strix/runtime/tool_server.py +55 -95
- strix/skills/README.md +64 -0
- strix/skills/__init__.py +110 -0
- strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
- strix/skills/scan_modes/deep.jinja +145 -0
- strix/skills/scan_modes/quick.jinja +63 -0
- strix/skills/scan_modes/standard.jinja +91 -0
- strix/telemetry/README.md +38 -0
- strix/telemetry/__init__.py +7 -1
- strix/telemetry/posthog.py +137 -0
- strix/telemetry/tracer.py +194 -54
- strix/tools/__init__.py +11 -4
- strix/tools/agents_graph/agents_graph_actions.py +20 -21
- strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
- strix/tools/browser/browser_actions.py +10 -6
- strix/tools/browser/browser_actions_schema.xml +6 -1
- strix/tools/browser/browser_instance.py +96 -48
- strix/tools/browser/tab_manager.py +121 -102
- strix/tools/context.py +12 -0
- strix/tools/executor.py +63 -4
- strix/tools/file_edit/file_edit_actions.py +6 -3
- strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
- strix/tools/finish/finish_actions.py +80 -105
- strix/tools/finish/finish_actions_schema.xml +121 -14
- strix/tools/notes/notes_actions.py +6 -33
- strix/tools/notes/notes_actions_schema.xml +50 -46
- strix/tools/proxy/proxy_actions.py +14 -2
- strix/tools/proxy/proxy_actions_schema.xml +0 -1
- strix/tools/proxy/proxy_manager.py +28 -16
- strix/tools/python/python_actions.py +2 -2
- strix/tools/python/python_actions_schema.xml +9 -1
- strix/tools/python/python_instance.py +39 -37
- strix/tools/python/python_manager.py +43 -31
- strix/tools/registry.py +73 -12
- strix/tools/reporting/reporting_actions.py +218 -31
- strix/tools/reporting/reporting_actions_schema.xml +256 -8
- strix/tools/terminal/terminal_actions.py +2 -2
- strix/tools/terminal/terminal_actions_schema.xml +6 -0
- strix/tools/terminal/terminal_manager.py +41 -30
- strix/tools/thinking/thinking_actions_schema.xml +27 -25
- strix/tools/todo/__init__.py +18 -0
- strix/tools/todo/todo_actions.py +568 -0
- strix/tools/todo/todo_actions_schema.xml +225 -0
- strix/utils/__init__.py +0 -0
- strix/utils/resource_paths.py +13 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
- strix_agent-0.6.2.dist-info/RECORD +134 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
- strix/llm/request_queue.py +0 -87
- strix/prompts/README.md +0 -64
- strix/prompts/__init__.py +0 -109
- strix_agent-0.4.0.dist-info/RECORD +0 -118
- /strix/{prompts → skills}/cloud/.gitkeep +0 -0
- /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
- /strix/{prompts → skills}/custom/.gitkeep +0 -0
- /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
- /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
- /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
- /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
- /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -190,36 +190,35 @@ def create_agent(
|
|
|
190
190
|
task: str,
|
|
191
191
|
name: str,
|
|
192
192
|
inherit_context: bool = True,
|
|
193
|
-
|
|
193
|
+
skills: str | None = None,
|
|
194
194
|
) -> dict[str, Any]:
|
|
195
195
|
try:
|
|
196
196
|
parent_id = agent_state.agent_id
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
if
|
|
200
|
-
|
|
198
|
+
skill_list = []
|
|
199
|
+
if skills:
|
|
200
|
+
skill_list = [s.strip() for s in skills.split(",") if s.strip()]
|
|
201
201
|
|
|
202
|
-
if len(
|
|
202
|
+
if len(skill_list) > 5:
|
|
203
203
|
return {
|
|
204
204
|
"success": False,
|
|
205
205
|
"error": (
|
|
206
|
-
"Cannot specify more than 5
|
|
207
|
-
"(use comma-separated format)"
|
|
206
|
+
"Cannot specify more than 5 skills for an agent (use comma-separated format)"
|
|
208
207
|
),
|
|
209
208
|
"agent_id": None,
|
|
210
209
|
}
|
|
211
210
|
|
|
212
|
-
if
|
|
213
|
-
from strix.
|
|
211
|
+
if skill_list:
|
|
212
|
+
from strix.skills import get_all_skill_names, validate_skill_names
|
|
214
213
|
|
|
215
|
-
validation =
|
|
214
|
+
validation = validate_skill_names(skill_list)
|
|
216
215
|
if validation["invalid"]:
|
|
217
|
-
|
|
216
|
+
available_skills = list(get_all_skill_names())
|
|
218
217
|
return {
|
|
219
218
|
"success": False,
|
|
220
219
|
"error": (
|
|
221
|
-
f"Invalid
|
|
222
|
-
f"Available
|
|
220
|
+
f"Invalid skills: {validation['invalid']}. "
|
|
221
|
+
f"Available skills: {', '.join(available_skills)}"
|
|
223
222
|
),
|
|
224
223
|
"agent_id": None,
|
|
225
224
|
}
|
|
@@ -233,14 +232,14 @@ def create_agent(
|
|
|
233
232
|
parent_agent = _agent_instances.get(parent_id)
|
|
234
233
|
|
|
235
234
|
timeout = None
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
llm_config = LLMConfig(
|
|
235
|
+
scan_mode = "deep"
|
|
236
|
+
if parent_agent and hasattr(parent_agent, "llm_config"):
|
|
237
|
+
if hasattr(parent_agent.llm_config, "timeout"):
|
|
238
|
+
timeout = parent_agent.llm_config.timeout
|
|
239
|
+
if hasattr(parent_agent.llm_config, "scan_mode"):
|
|
240
|
+
scan_mode = parent_agent.llm_config.scan_mode
|
|
241
|
+
|
|
242
|
+
llm_config = LLMConfig(skills=skill_list, timeout=timeout, scan_mode=scan_mode)
|
|
244
243
|
|
|
245
244
|
agent_config = {
|
|
246
245
|
"llm_config": llm_config,
|
|
@@ -79,8 +79,8 @@ Only create a new agent if no existing agent is handling the specific task.</des
|
|
|
79
79
|
<parameter name="inherit_context" type="boolean" required="false">
|
|
80
80
|
<description>Whether the new agent should inherit parent's conversation history and context</description>
|
|
81
81
|
</parameter>
|
|
82
|
-
<parameter name="
|
|
83
|
-
<description>Comma-separated list of
|
|
82
|
+
<parameter name="skills" type="string" required="false">
|
|
83
|
+
<description>Comma-separated list of skills to use for the agent (MAXIMUM 5 skills allowed). Most agents should have at least one skill in order to be useful. Agents should be highly specialized - use 1-3 related skills; up to 5 for complex contexts. {{DYNAMIC_SKILLS_DESCRIPTION}}</description>
|
|
84
84
|
</parameter>
|
|
85
85
|
</parameters>
|
|
86
86
|
<returns type="Dict[str, Any]">
|
|
@@ -92,30 +92,30 @@ Only create a new agent if no existing agent is handling the specific task.</des
|
|
|
92
92
|
<parameter=task>Validate and exploit the suspected SQL injection vulnerability found in
|
|
93
93
|
the login form. Confirm exploitability and document proof of concept.</parameter>
|
|
94
94
|
<parameter=name>SQLi Validator</parameter>
|
|
95
|
-
<parameter=
|
|
95
|
+
<parameter=skills>sql_injection</parameter>
|
|
96
96
|
</function>
|
|
97
97
|
|
|
98
98
|
<function=create_agent>
|
|
99
99
|
<parameter=task>Test authentication mechanisms, JWT implementation, and session management
|
|
100
100
|
for security vulnerabilities and bypass techniques.</parameter>
|
|
101
101
|
<parameter=name>Auth Specialist</parameter>
|
|
102
|
-
<parameter=
|
|
102
|
+
<parameter=skills>authentication_jwt, business_logic</parameter>
|
|
103
103
|
</function>
|
|
104
104
|
|
|
105
|
-
# Example of single-
|
|
105
|
+
# Example of single-skill specialization (most focused)
|
|
106
106
|
<function=create_agent>
|
|
107
107
|
<parameter=task>Perform comprehensive XSS testing including reflected, stored, and DOM-based
|
|
108
108
|
variants across all identified input points.</parameter>
|
|
109
109
|
<parameter=name>XSS Specialist</parameter>
|
|
110
|
-
<parameter=
|
|
110
|
+
<parameter=skills>xss</parameter>
|
|
111
111
|
</function>
|
|
112
112
|
|
|
113
|
-
# Example of up to 5 related
|
|
113
|
+
# Example of up to 5 related skills (borderline acceptable)
|
|
114
114
|
<function=create_agent>
|
|
115
115
|
<parameter=task>Test for server-side vulnerabilities including SSRF, XXE, and potential
|
|
116
116
|
RCE vectors in file upload and XML processing endpoints.</parameter>
|
|
117
117
|
<parameter=name>Server-Side Attack Specialist</parameter>
|
|
118
|
-
<parameter=
|
|
118
|
+
<parameter=skills>ssrf, xxe, rce</parameter>
|
|
119
119
|
</function>
|
|
120
120
|
</examples>
|
|
121
121
|
</tool>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
from typing import Any, Literal, NoReturn
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Literal, NoReturn
|
|
2
2
|
|
|
3
3
|
from strix.tools.registry import register_tool
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .tab_manager import BrowserTabManager
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
BrowserAction = Literal[
|
|
@@ -71,7 +73,7 @@ def _validate_file_path(action_name: str, file_path: str | None) -> None:
|
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
def _handle_navigation_actions(
|
|
74
|
-
manager: BrowserTabManager,
|
|
76
|
+
manager: "BrowserTabManager",
|
|
75
77
|
action: str,
|
|
76
78
|
url: str | None = None,
|
|
77
79
|
tab_id: str | None = None,
|
|
@@ -90,7 +92,7 @@ def _handle_navigation_actions(
|
|
|
90
92
|
|
|
91
93
|
|
|
92
94
|
def _handle_interaction_actions(
|
|
93
|
-
manager: BrowserTabManager,
|
|
95
|
+
manager: "BrowserTabManager",
|
|
94
96
|
action: str,
|
|
95
97
|
coordinate: str | None = None,
|
|
96
98
|
text: str | None = None,
|
|
@@ -128,7 +130,7 @@ def _raise_unknown_action(action: str) -> NoReturn:
|
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
def _handle_tab_actions(
|
|
131
|
-
manager: BrowserTabManager,
|
|
133
|
+
manager: "BrowserTabManager",
|
|
132
134
|
action: str,
|
|
133
135
|
url: str | None = None,
|
|
134
136
|
tab_id: str | None = None,
|
|
@@ -149,7 +151,7 @@ def _handle_tab_actions(
|
|
|
149
151
|
|
|
150
152
|
|
|
151
153
|
def _handle_utility_actions(
|
|
152
|
-
manager: BrowserTabManager,
|
|
154
|
+
manager: "BrowserTabManager",
|
|
153
155
|
action: str,
|
|
154
156
|
duration: float | None = None,
|
|
155
157
|
js_code: str | None = None,
|
|
@@ -191,6 +193,8 @@ def browser_action(
|
|
|
191
193
|
file_path: str | None = None,
|
|
192
194
|
clear: bool = False,
|
|
193
195
|
) -> dict[str, Any]:
|
|
196
|
+
from .tab_manager import get_browser_tab_manager
|
|
197
|
+
|
|
194
198
|
manager = get_browser_tab_manager()
|
|
195
199
|
|
|
196
200
|
try:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<?xml version="1.0" ?>
|
|
2
1
|
<tools>
|
|
3
2
|
<tool name="browser_action">
|
|
4
3
|
<description>Perform browser actions using a Playwright-controlled browser with multiple tabs.
|
|
@@ -92,6 +91,12 @@
|
|
|
92
91
|
code normally. It can be single line or multi-line.
|
|
93
92
|
13. For form filling, click on the field first, then use 'type' to enter text.
|
|
94
93
|
14. The browser runs in headless mode using Chrome engine for security and performance.
|
|
94
|
+
15. RESOURCE MANAGEMENT:
|
|
95
|
+
- ALWAYS close tabs you no longer need using 'close_tab' action.
|
|
96
|
+
- ALWAYS close the browser with 'close' action when you have completely finished
|
|
97
|
+
all browser-related tasks. Do not leave the browser running if you're done with it.
|
|
98
|
+
- If you opened multiple tabs, close them as soon as you've extracted the needed
|
|
99
|
+
information from each one.
|
|
95
100
|
</notes>
|
|
96
101
|
<examples>
|
|
97
102
|
# Launch browser at URL (creates tab_1)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
+
import contextlib
|
|
3
4
|
import logging
|
|
4
5
|
import threading
|
|
5
6
|
from pathlib import Path
|
|
@@ -17,13 +18,82 @@ MAX_CONSOLE_LOGS_COUNT = 200
|
|
|
17
18
|
MAX_JS_RESULT_LENGTH = 5_000
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
class _BrowserState:
|
|
22
|
+
"""Singleton state for the shared browser instance."""
|
|
23
|
+
|
|
24
|
+
lock = threading.Lock()
|
|
25
|
+
event_loop: asyncio.AbstractEventLoop | None = None
|
|
26
|
+
event_loop_thread: threading.Thread | None = None
|
|
27
|
+
playwright: Playwright | None = None
|
|
28
|
+
browser: Browser | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_state = _BrowserState()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_event_loop() -> None:
|
|
35
|
+
if _state.event_loop is not None:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def run_loop() -> None:
|
|
39
|
+
_state.event_loop = asyncio.new_event_loop()
|
|
40
|
+
asyncio.set_event_loop(_state.event_loop)
|
|
41
|
+
_state.event_loop.run_forever()
|
|
42
|
+
|
|
43
|
+
_state.event_loop_thread = threading.Thread(target=run_loop, daemon=True)
|
|
44
|
+
_state.event_loop_thread.start()
|
|
45
|
+
|
|
46
|
+
while _state.event_loop is None:
|
|
47
|
+
threading.Event().wait(0.01)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _create_browser() -> Browser:
|
|
51
|
+
if _state.browser is not None and _state.browser.is_connected():
|
|
52
|
+
return _state.browser
|
|
53
|
+
|
|
54
|
+
if _state.browser is not None:
|
|
55
|
+
with contextlib.suppress(Exception):
|
|
56
|
+
await _state.browser.close()
|
|
57
|
+
_state.browser = None
|
|
58
|
+
if _state.playwright is not None:
|
|
59
|
+
with contextlib.suppress(Exception):
|
|
60
|
+
await _state.playwright.stop()
|
|
61
|
+
_state.playwright = None
|
|
62
|
+
|
|
63
|
+
_state.playwright = await async_playwright().start()
|
|
64
|
+
_state.browser = await _state.playwright.chromium.launch(
|
|
65
|
+
headless=True,
|
|
66
|
+
args=[
|
|
67
|
+
"--no-sandbox",
|
|
68
|
+
"--disable-dev-shm-usage",
|
|
69
|
+
"--disable-gpu",
|
|
70
|
+
"--disable-web-security",
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
return _state.browser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_browser() -> tuple[asyncio.AbstractEventLoop, Browser]:
|
|
77
|
+
with _state.lock:
|
|
78
|
+
_ensure_event_loop()
|
|
79
|
+
assert _state.event_loop is not None
|
|
80
|
+
|
|
81
|
+
if _state.browser is None or not _state.browser.is_connected():
|
|
82
|
+
future = asyncio.run_coroutine_threadsafe(_create_browser(), _state.event_loop)
|
|
83
|
+
future.result(timeout=30)
|
|
84
|
+
|
|
85
|
+
assert _state.browser is not None
|
|
86
|
+
return _state.event_loop, _state.browser
|
|
87
|
+
|
|
88
|
+
|
|
20
89
|
class BrowserInstance:
|
|
21
90
|
def __init__(self) -> None:
|
|
22
91
|
self.is_running = True
|
|
23
92
|
self._execution_lock = threading.Lock()
|
|
24
93
|
|
|
25
|
-
self.
|
|
26
|
-
self.
|
|
94
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
95
|
+
self._browser: Browser | None = None
|
|
96
|
+
|
|
27
97
|
self.context: BrowserContext | None = None
|
|
28
98
|
self.pages: dict[str, Page] = {}
|
|
29
99
|
self.current_page_id: str | None = None
|
|
@@ -31,23 +101,6 @@ class BrowserInstance:
|
|
|
31
101
|
|
|
32
102
|
self.console_logs: dict[str, list[dict[str, Any]]] = {}
|
|
33
103
|
|
|
34
|
-
self._loop: asyncio.AbstractEventLoop | None = None
|
|
35
|
-
self._loop_thread: threading.Thread | None = None
|
|
36
|
-
|
|
37
|
-
self._start_event_loop()
|
|
38
|
-
|
|
39
|
-
def _start_event_loop(self) -> None:
|
|
40
|
-
def run_loop() -> None:
|
|
41
|
-
self._loop = asyncio.new_event_loop()
|
|
42
|
-
asyncio.set_event_loop(self._loop)
|
|
43
|
-
self._loop.run_forever()
|
|
44
|
-
|
|
45
|
-
self._loop_thread = threading.Thread(target=run_loop, daemon=True)
|
|
46
|
-
self._loop_thread.start()
|
|
47
|
-
|
|
48
|
-
while self._loop is None:
|
|
49
|
-
threading.Event().wait(0.01)
|
|
50
|
-
|
|
51
104
|
def _run_async(self, coro: Any) -> dict[str, Any]:
|
|
52
105
|
if not self._loop or not self.is_running:
|
|
53
106
|
raise RuntimeError("Browser instance is not running")
|
|
@@ -77,21 +130,10 @@ class BrowserInstance:
|
|
|
77
130
|
|
|
78
131
|
page.on("console", handle_console)
|
|
79
132
|
|
|
80
|
-
async def
|
|
81
|
-
self.
|
|
82
|
-
|
|
83
|
-
self.browser = await self.playwright.chromium.launch(
|
|
84
|
-
headless=True,
|
|
85
|
-
args=[
|
|
86
|
-
"--no-sandbox",
|
|
87
|
-
"--disable-dev-shm-usage",
|
|
88
|
-
"--disable-gpu",
|
|
89
|
-
"--disable-web-security",
|
|
90
|
-
"--disable-features=VizDisplayCompositor",
|
|
91
|
-
],
|
|
92
|
-
)
|
|
133
|
+
async def _create_context(self, url: str | None = None) -> dict[str, Any]:
|
|
134
|
+
assert self._browser is not None
|
|
93
135
|
|
|
94
|
-
self.context = await self.
|
|
136
|
+
self.context = await self._browser.new_context(
|
|
95
137
|
viewport={"width": 1280, "height": 720},
|
|
96
138
|
user_agent=(
|
|
97
139
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
|
@@ -148,10 +190,11 @@ class BrowserInstance:
|
|
|
148
190
|
|
|
149
191
|
def launch(self, url: str | None = None) -> dict[str, Any]:
|
|
150
192
|
with self._execution_lock:
|
|
151
|
-
if self.
|
|
193
|
+
if self.context is not None:
|
|
152
194
|
raise ValueError("Browser is already launched")
|
|
153
195
|
|
|
154
|
-
|
|
196
|
+
self._loop, self._browser = _get_browser()
|
|
197
|
+
return self._run_async(self._create_context(url))
|
|
155
198
|
|
|
156
199
|
def goto(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
157
200
|
with self._execution_lock:
|
|
@@ -512,22 +555,27 @@ class BrowserInstance:
|
|
|
512
555
|
def close(self) -> None:
|
|
513
556
|
with self._execution_lock:
|
|
514
557
|
self.is_running = False
|
|
515
|
-
if self._loop:
|
|
516
|
-
asyncio.run_coroutine_threadsafe(self.
|
|
517
|
-
|
|
518
|
-
|
|
558
|
+
if self._loop and self.context:
|
|
559
|
+
future = asyncio.run_coroutine_threadsafe(self._close_context(), self._loop)
|
|
560
|
+
with contextlib.suppress(Exception):
|
|
561
|
+
future.result(timeout=5)
|
|
519
562
|
|
|
520
|
-
|
|
521
|
-
|
|
563
|
+
self.pages.clear()
|
|
564
|
+
self.console_logs.clear()
|
|
565
|
+
self.current_page_id = None
|
|
566
|
+
self.context = None
|
|
522
567
|
|
|
523
|
-
async def
|
|
568
|
+
async def _close_context(self) -> None:
|
|
524
569
|
try:
|
|
525
|
-
if self.
|
|
526
|
-
await self.
|
|
527
|
-
if self.playwright:
|
|
528
|
-
await self.playwright.stop()
|
|
570
|
+
if self.context:
|
|
571
|
+
await self.context.close()
|
|
529
572
|
except (OSError, RuntimeError) as e:
|
|
530
|
-
logger.warning(f"Error closing
|
|
573
|
+
logger.warning(f"Error closing context: {e}")
|
|
531
574
|
|
|
532
575
|
def is_alive(self) -> bool:
|
|
533
|
-
return
|
|
576
|
+
return (
|
|
577
|
+
self.is_running
|
|
578
|
+
and self.context is not None
|
|
579
|
+
and self._browser is not None
|
|
580
|
+
and self._browser.is_connected()
|
|
581
|
+
)
|