-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprovider.py
More file actions
247 lines (212 loc) · 7.97 KB
/
Copy pathprovider.py
File metadata and controls
247 lines (212 loc) · 7.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
from __future__ import annotations
import logging
import os
import shutil
import uuid
from pathlib import Path
from typing import Any
import requests
from agent import browser_provider as hermes_browser_provider
from agent.browser_provider import BrowserProvider
logger = logging.getLogger(__name__)
_DEFAULT_BASE_URL = "https://api.onkernel.com"
_DEFAULT_BROWSER_TIMEOUT_SECONDS = 600
_FALSE_VALUES = {"0", "false", "no", "off"}
def _env_flag(name: str, default: bool) -> bool:
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() not in _FALSE_VALUES
def _optional_env(name: str) -> str | None:
value = os.environ.get(name, "").strip()
return value or None
def _agent_browser_installed() -> bool:
executable = shutil.which("agent-browser")
if executable and Path(executable).exists():
return True
module_path = getattr(hermes_browser_provider, "__file__", None)
if not module_path:
return False
project_root = Path(module_path).resolve().parent.parent
return (project_root / "node_modules" / "agent-browser").is_dir()
class KernelBrowserProvider(BrowserProvider):
@property
def name(self) -> str:
return "kernel"
@property
def display_name(self) -> str:
return "Kernel"
def is_available(self) -> bool:
return bool(os.environ.get("KERNEL_API_KEY"))
def _get_config(self) -> dict[str, str]:
api_key = os.environ.get("KERNEL_API_KEY")
if not api_key:
raise ValueError(
"Kernel requires KERNEL_API_KEY. Get an API key at "
"https://dashboard.onkernel.com/api-keys"
)
return {
"api_key": api_key,
"base_url": os.environ.get("KERNEL_BASE_URL", _DEFAULT_BASE_URL).rstrip(
"/"
),
}
@staticmethod
def _headers(api_key: str) -> dict[str, str]:
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
def _resolve_proxy_id(self, config: dict[str, str]) -> str | None:
proxy_name = _optional_env("KERNEL_PROXY_NAME")
if not proxy_name:
return None
try:
response = requests.get(
f"{config['base_url']}/proxies",
headers=self._headers(config["api_key"]),
params={"name": proxy_name, "limit": 2},
timeout=10,
)
except requests.RequestException as exc:
raise RuntimeError(
f"Failed to resolve Kernel proxy {proxy_name!r}: {exc}"
) from exc
if not response.ok:
raise RuntimeError(
f"Failed to resolve Kernel proxy {proxy_name!r}: "
f"{response.status_code} {response.text[:500]}"
)
try:
proxies: list[dict[str, Any]] = response.json()
proxy_ids = [proxy["id"] for proxy in proxies]
except (KeyError, TypeError, ValueError) as exc:
raise RuntimeError("Kernel API returned an invalid proxy list") from exc
if not proxy_ids:
raise ValueError(f"No Kernel proxy named {proxy_name!r} was found")
if len(proxy_ids) > 1:
raise ValueError(
f"Multiple Kernel proxies are named {proxy_name!r}; "
"rename one so the configured name is unique"
)
return str(proxy_ids[0])
def create_session(self, task_id: str) -> dict[str, object]:
config = self._get_config()
stealth = _env_flag("KERNEL_STEALTH", True)
proxy_id = self._resolve_proxy_id(config)
profile_name = _optional_env("KERNEL_PROFILE_NAME")
session_name = f"hermes_{task_id}_{uuid.uuid4().hex[:8]}"
body: dict[str, object] = {
"stealth": stealth,
"headless": False,
"name": session_name,
"timeout_seconds": _DEFAULT_BROWSER_TIMEOUT_SECONDS,
}
if proxy_id:
body["proxy_id"] = proxy_id
if profile_name:
body["profile"] = {"name": profile_name, "save_changes": True}
try:
response = requests.post(
f"{config['base_url']}/browsers",
headers=self._headers(config["api_key"]),
json=body,
timeout=30,
)
except requests.RequestException as exc:
raise RuntimeError(f"Kernel API connection failed: {exc}") from exc
if not response.ok:
raise RuntimeError(
"Failed to create Kernel browser: "
f"{response.status_code} {response.text[:500]}"
)
try:
data: dict[str, Any] = response.json()
session_id = data["session_id"]
cdp_url = data["cdp_ws_url"]
except (KeyError, TypeError, ValueError) as exc:
raise RuntimeError(
"Kernel API returned an invalid browser payload"
) from exc
live_view_url = data.get("browser_live_view_url")
features: dict[str, object] = {
"stealth": stealth,
"headless": False,
"proxies": bool(proxy_id),
"profile_persistence": bool(profile_name),
"live_view": bool(live_view_url),
}
result: dict[str, object] = {
"session_name": session_name,
"bb_session_id": session_id,
"cdp_url": cdp_url,
"features": features,
}
if live_view_url:
result["live_view_url"] = live_view_url
return result
def close_session(self, session_id: str) -> bool:
try:
config = self._get_config()
except ValueError:
logger.warning(
"Cannot close Kernel browser %s without credentials", session_id
)
return False
try:
response = requests.delete(
f"{config['base_url']}/browsers/{session_id}",
headers=self._headers(config["api_key"]),
timeout=10,
)
except requests.RequestException as exc:
logger.warning("Failed to close Kernel browser %s: %s", session_id, exc)
return False
if response.status_code in {200, 201, 204}:
return True
logger.warning(
"Failed to close Kernel browser %s: HTTP %s - %s",
session_id,
response.status_code,
response.text[:200],
)
return False
def emergency_cleanup(self, session_id: str) -> None:
try:
config = self._get_config()
requests.delete(
f"{config['base_url']}/browsers/{session_id}",
headers=self._headers(config["api_key"]),
timeout=5,
)
except Exception as exc:
logger.debug("Emergency cleanup failed for %s: %s", session_id, exc)
def get_setup_schema(self) -> dict[str, Any]:
schema: dict[str, Any] = {
"name": "Kernel",
"badge": "paid",
"tag": "Cloud browser with stealth, proxies, profiles, and live view",
"env_vars": [
{
"key": "KERNEL_API_KEY",
"prompt": "Kernel API key",
"url": "https://dashboard.onkernel.com/api-keys",
"secret": True,
},
{
"key": "KERNEL_PROXY_NAME",
"prompt": "Default Kernel proxy name (optional)",
"required": False,
"secret": False,
},
{
"key": "KERNEL_PROFILE_NAME",
"prompt": "Default Kernel profile name (optional)",
"required": False,
"secret": False,
},
],
}
if not _agent_browser_installed():
schema["post_setup"] = "agent_browser"
return schema