grasp-sdk 0.1.6__tar.gz → 0.1.8__tar.gz

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.

Potentially problematic release.


This version of grasp-sdk might be problematic. Click here for more details.

Files changed (31) hide show
  1. {grasp_sdk-0.1.6/grasp_sdk.egg-info → grasp_sdk-0.1.8}/PKG-INFO +1 -1
  2. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/__init__.py +4 -9
  3. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/models/__init__.py +0 -2
  4. grasp_sdk-0.1.8/grasp_sdk/sandbox/bootstrap-chrome-stable.mjs +69 -0
  5. grasp_sdk-0.1.8/grasp_sdk/sandbox/http-proxy.mjs +324 -0
  6. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/services/browser.py +36 -47
  7. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/services/sandbox.py +18 -21
  8. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/utils/config.py +0 -4
  9. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8/grasp_sdk.egg-info}/PKG-INFO +1 -1
  10. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk.egg-info/SOURCES.txt +2 -0
  11. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/pyproject.toml +1 -1
  12. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/MANIFEST.in +0 -0
  13. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/README.md +0 -0
  14. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/build_and_publish.py +0 -0
  15. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/example_usage.py +0 -0
  16. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/sandbox/chrome-stable.mjs +0 -0
  17. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/sandbox/chromium.mjs +0 -0
  18. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/services/__init__.py +0 -0
  19. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/utils/__init__.py +0 -0
  20. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/utils/auth.py +0 -0
  21. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk/utils/logger.py +0 -0
  22. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk.egg-info/dependency_links.txt +0 -0
  23. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk.egg-info/entry_points.txt +0 -0
  24. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk.egg-info/not-zip-safe +0 -0
  25. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk.egg-info/requires.txt +0 -0
  26. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/grasp_sdk.egg-info/top_level.txt +0 -0
  27. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/py.typed +0 -0
  28. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/requirements.txt +0 -0
  29. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/setup.cfg +0 -0
  30. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/setup.py +0 -0
  31. {grasp_sdk-0.1.6 → grasp_sdk-0.1.8}/test_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_sdk
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Python SDK for Grasp E2B - Browser automation and sandbox management
5
5
  Home-page: https://github.com/grasp-team/grasp-e2b
6
6
  Author: Grasp Team
@@ -24,7 +24,7 @@ from .models import (
24
24
  SandboxStatus,
25
25
  )
26
26
 
27
- __version__ = "0.1.6"
27
+ __version__ = "0.1.8"
28
28
  __author__ = "Grasp Team"
29
29
  __email__ = "team@grasp.dev"
30
30
 
@@ -76,9 +76,7 @@ class GraspServer:
76
76
 
77
77
  self.browser_service: Optional[BrowserService] = None
78
78
 
79
- self.logger.info(
80
- f'GraspE2B initialized (templateId: {config["sandbox"]["templateId"]})'
81
- )
79
+ self.logger.info('GraspE2B initialized')
82
80
 
83
81
  async def __aenter__(self):
84
82
  connection = await self.create_browser_task()
@@ -147,7 +145,6 @@ class GraspServer:
147
145
 
148
146
  # Create base browser config
149
147
  browser_config: IBrowserConfig = {
150
- 'cdpPort': 9222,
151
148
  'headless': True,
152
149
  'launchTimeout': 30000,
153
150
  'args': [
@@ -158,8 +155,6 @@ class GraspServer:
158
155
  }
159
156
 
160
157
  # Apply user config overrides with type safety
161
- if 'cdpPort' in config:
162
- browser_config['cdpPort'] = config['cdpPort']
163
158
  if 'headless' in config:
164
159
  browser_config['headless'] = config['headless']
165
160
  if 'launchTimeout' in config:
@@ -173,7 +168,7 @@ class GraspServer:
173
168
  self.config['sandbox'],
174
169
  browser_config
175
170
  )
176
- await self.browser_service.initialize()
171
+ await self.browser_service.initialize(browser_type)
177
172
 
178
173
  # Register server
179
174
  _servers[str(self.browser_service.id)] = self
@@ -182,7 +177,7 @@ class GraspServer:
182
177
  })
183
178
 
184
179
  self.logger.info('🌐 Launching Chromium browser with CDP...')
185
- cdp_connection = await self.browser_service.launch_browser(browser_type)
180
+ cdp_connection = await self.browser_service.launch_browser()
186
181
 
187
182
  self.logger.info('✅ Browser launched successfully!')
188
183
  self.logger.debug(
@@ -20,7 +20,6 @@ class SandboxStatus(Enum):
20
20
  class ISandboxConfig(TypedDict):
21
21
  """Sandbox configuration interface."""
22
22
  key: str # Required: Grasp API key
23
- templateId: str # Required: Sandbox template ID
24
23
  timeout: int # Required: Default timeout in milliseconds
25
24
  workspace: NotRequired[str] # Optional: Grasp workspace ID
26
25
  debug: NotRequired[bool] # Optional: Enable debug mode for detailed logging
@@ -28,7 +27,6 @@ class ISandboxConfig(TypedDict):
28
27
 
29
28
  class IBrowserConfig(TypedDict):
30
29
  """Browser service configuration interface."""
31
- cdpPort: int # Required: Port for CDP server (default: 9222)
32
30
  args: List[str] # Required: Chromium launch arguments
33
31
  headless: bool # Required: Headless mode (default: true)
34
32
  launchTimeout: int # Required: Timeout for browser launch (default: 30000ms)
@@ -0,0 +1,69 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ // const asblockPlugin = '/home/user/.config/google-chrome/Default/Extensions/adblock';
4
+
5
+ const args = [
6
+ '--no-sandbox',
7
+ '--disable-setuid-sandbox',
8
+ '--disable-dev-shm-usage',
9
+ '--disable-gpu',
10
+ '--disable-software-rasterizer',
11
+ '--user-data-dir=/home/user/.browser-context'
12
+ ];
13
+
14
+ args.push(
15
+ // 避免缓存积累影响性能
16
+ '--disable-application-cache',
17
+
18
+ // 关闭所有硬件加速特性,防止 GPU 相关崩溃
19
+ '--disable-accelerated-2d-canvas',
20
+ '--disable-accelerated-video-decode',
21
+
22
+ // 禁用后台渲染,减少无关资源消耗
23
+ '--disable-background-timer-throttling',
24
+ '--disable-backgrounding-occluded-windows',
25
+ '--disable-renderer-backgrounding',
26
+
27
+ // 避免过度日志影响性能
28
+ '--disable-logging',
29
+
30
+ // 禁用不必要的多媒体解码
31
+ '--mute-audio',
32
+
33
+ // 避免崩溃时弹窗
34
+ '--no-default-browser-check',
35
+ '--no-first-run',
36
+ '--headless=new',
37
+ );
38
+
39
+ args.push(
40
+ `--remote-debugging-port=9222`,
41
+ '--remote-debugging-address=0.0.0.0',
42
+ 'about:blank',
43
+ );
44
+
45
+ const chromePath = '/usr/bin/google-chrome';
46
+
47
+ // 启动 Chrome 并启用远程调试
48
+ const chrome = spawn(chromePath, args, {
49
+ env: { ...process.env, DISPLAY: ':99' }
50
+ });
51
+
52
+ chrome.stdout.on('data', (data) => {
53
+ console.log(`stdout: ${data}`);
54
+ });
55
+
56
+ chrome.stderr.on('data', (data) => {
57
+ console.error(`stderr: ${data}`);
58
+ });
59
+
60
+ chrome.on('close', (code) => {
61
+ console.log(`Chrome process exited with code ${code}`);
62
+ });
63
+
64
+ console.log('Browser launched and ready...');
65
+
66
+ // 保持进程不退出,并监听中止信号
67
+ process.stdin.resume();
68
+ process.on('SIGINT', () => process.exit());
69
+ process.on('SIGTERM', () => process.exit());
@@ -0,0 +1,324 @@
1
+ import httpProxy from 'http-proxy';
2
+ import http from 'http';
3
+ import fs from 'fs';
4
+
5
+ import { Logtail } from '@logtail/node';
6
+ import * as Sentry from "@sentry/node";
7
+
8
+ const logtail = new Logtail(process.env.BS_SOURCE_TOKEN, {
9
+ endpoint: `https://${process.env.BS_INGESTING_HOST}`,
10
+ });
11
+
12
+ Sentry.init({
13
+ dsn: process.env.SENTRY_DSN,
14
+
15
+ // Setting this option to true will send default PII data to Sentry.
16
+ // For example, automatic IP address collection on events
17
+ sendDefaultPii: true,
18
+ _experiments: {
19
+ enableLogs: true, // 启用日志功能
20
+ },
21
+ });
22
+
23
+ const logger = {
24
+ info: async (message, context) => {
25
+ Sentry.logger.info(message, context);
26
+ return logtail.info(message, context);
27
+ },
28
+ warn: async (message, context) => {
29
+ Sentry.logger.warn(message, context);
30
+ return logtail.warn(message, context);
31
+ },
32
+ error: async (message, context) => {
33
+ Sentry.logger.error(message, context);
34
+ return logtail.error(message, context);
35
+ },
36
+ }
37
+
38
+ function parseWebSocketFrame(buffer) {
39
+ if (buffer.length < 2) {
40
+ throw new Error('Incomplete WebSocket frame.');
41
+ }
42
+
43
+ const firstByte = buffer.readUInt8(0);
44
+ const fin = (firstByte & 0x80) !== 0;
45
+ const opcode = firstByte & 0x0f;
46
+
47
+ // 仅处理文本帧(opcode 为 0x1)
48
+ if (opcode !== 0x1) {
49
+ throw new Error(`Unsupported opcode: ${opcode}`);
50
+ }
51
+
52
+ const secondByte = buffer.readUInt8(1);
53
+ const isMasked = (secondByte & 0x80) !== 0;
54
+ let payloadLength = secondByte & 0x7f;
55
+ let offset = 2;
56
+
57
+ if (payloadLength === 126) {
58
+ if (buffer.length < offset + 2) {
59
+ throw new Error('Incomplete extended payload length.');
60
+ }
61
+ payloadLength = buffer.readUInt16BE(offset);
62
+ offset += 2;
63
+ } else if (payloadLength === 127) {
64
+ if (buffer.length < offset + 8) {
65
+ throw new Error('Incomplete extended payload length.');
66
+ }
67
+ // 注意:JavaScript 无法精确表示超过 2^53 的整数
68
+ const highBits = buffer.readUInt32BE(offset);
69
+ const lowBits = buffer.readUInt32BE(offset + 4);
70
+ payloadLength = highBits * 2 ** 32 + lowBits;
71
+ offset += 8;
72
+ }
73
+
74
+ let maskingKey;
75
+ if (isMasked) {
76
+ if (buffer.length < offset + 4) {
77
+ throw new Error('Incomplete masking key.');
78
+ }
79
+ maskingKey = buffer.slice(offset, offset + 4);
80
+ offset += 4;
81
+ }
82
+
83
+ if (buffer.length < offset + payloadLength) {
84
+ throw new Error('Incomplete payload data.');
85
+ }
86
+
87
+ const payloadData = buffer.slice(offset, offset + payloadLength);
88
+
89
+ if (isMasked) {
90
+ for (let i = 0; i < payloadLength; i++) {
91
+ payloadData[i] ^= maskingKey[i % 4];
92
+ }
93
+ }
94
+
95
+ return payloadData.toString('utf8');
96
+ }
97
+
98
+ let sandboxId = '';
99
+ const cdpPort = Number(process.env.CDP_PORT);
100
+ const headless = process.env.HEADLESS !== 'false';
101
+ const enableAdblock = process.env.ADBLOCK !== 'false';
102
+ const timeoutMS = process.env.SANBOX_TIMEOUT;
103
+ const workspace = process.env.WORKSPACE;
104
+ const keepAliveMS = Number(process.env.KEEP_ALIVE_MS) || 0;
105
+ const args = [];
106
+
107
+ try {
108
+ // 创建代理服务:从 ${this.config.cdpPort! + 1} 转发到 127.0.0.1:${this.config.cdpPort!}
109
+ const proxy = httpProxy.createProxyServer({
110
+ target: `http://127.0.0.1:${cdpPort}`,
111
+ ws: true, // 支持 WebSocket
112
+ changeOrigin: true
113
+ });
114
+
115
+ const clients = new Set();
116
+
117
+ // 监听 WebSocket 事件
118
+ proxy.on('open', () => {
119
+ sandboxId = fs.readFileSync('/home/user/.sandbox_id', 'utf-8');
120
+ console.log('🔌 CDP WebSocket connection established', sandboxId);
121
+ const wsId = Date.now();
122
+ logger.info('CDP WebSocket connection established', { sandboxId, wsId });
123
+ Sentry.addBreadcrumb({
124
+ category: 'websocket',
125
+ message: 'CDP connection established',
126
+ level: 'info',
127
+ data: { sandboxId }
128
+ });
129
+
130
+ clients.add(wsId);
131
+ proxy.once('close', async (req, socket, head) => {
132
+ console.log('🔒 CDP WebSocket connection closed');
133
+ await logger.info('CDP WebSocket connection closed', { sandboxId });
134
+ Sentry.addBreadcrumb({
135
+ category: 'websocket',
136
+ message: 'CDP connection closed',
137
+ level: 'info',
138
+ data: { sandboxId }
139
+ });
140
+ clients.delete(wsId);
141
+
142
+ if(clients.size <= 0) {
143
+ setTimeout(() => {
144
+ if(clients.size <= 0) {
145
+ console.log('❌ Force closed...', wsId);
146
+ process.exit(0);
147
+ }
148
+ }, keepAliveMS);
149
+ }
150
+ });
151
+ });
152
+
153
+ proxy.on('proxyReqWs', (proxyReq, req, socket, options, head) => {
154
+ console.log('📡 New CDP WebSocket connection request:', req.url);
155
+ logger.info('New CDP WebSocket connection request', { url: req.url, sandboxId });
156
+ Sentry.addBreadcrumb({
157
+ category: 'websocket',
158
+ message: 'New CDP connection request',
159
+ level: 'info',
160
+ data: { url: req.url, sandboxId }
161
+ });
162
+ });
163
+
164
+ proxy.on('error', (err, req, res) => {
165
+ console.error('❌ CDP WebSocket proxy error:', err);
166
+ logger.error('CDP WebSocket proxy error', { error: err.message, url: req?.url, sandboxId });
167
+ Sentry.captureException(err, {
168
+ tags: { type: 'websocket_proxy_error', sandboxId },
169
+ extra: { url: req?.url }
170
+ });
171
+ });
172
+
173
+ const server = http.createServer(async (req, res) => {
174
+ if (req.url === '/health') {
175
+ res.end('ok');
176
+ return;
177
+ }
178
+ if (req.url === '/json/version' || req.url === '/json/version/') {
179
+ try {
180
+ // 向本地 CDP 发请求,获取原始 JSON
181
+ const jsonRes = await fetch(`http://127.0.0.1:${cdpPort}/json/version`);
182
+ const data = await jsonRes.json();
183
+ // 替换掉本地的 WebSocket 地址为代理暴露地址
184
+ data.webSocketDebuggerUrl = data.webSocketDebuggerUrl.replace(
185
+ `ws://127.0.0.1:${cdpPort}`,
186
+ `wss://${req.headers.host}`
187
+ );
188
+ await logger.info('CDP version info requested', { url: req.url, response: data, sandboxId });
189
+ Sentry.addBreadcrumb({
190
+ category: 'http',
191
+ message: 'CDP version info requested',
192
+ level: 'info',
193
+ data: { url: req.url, response: data, sandboxId }
194
+ });
195
+ res.writeHead(200, { 'Content-Type': 'application/json' });
196
+ res.end(JSON.stringify(data));
197
+ } catch(ex) {
198
+ console.error('Failed to fetch CDP version:', ex.message);
199
+ await logger.error('Failed to fetch CDP version', { error: ex.message, sandboxId });
200
+ Sentry.captureException(ex, {
201
+ tags: { type: 'cdp_version_error', sandboxId }
202
+ });
203
+ res.writeHead(500);
204
+ res.end('Internal Server Error');
205
+ }
206
+ } else {
207
+ proxy.web(req, res, {}, async (err) => {
208
+ console.error('Proxy error:', err);
209
+ await logger.error('HTTP proxy error', { error: err.message, url: req.url, sandboxId });
210
+ Sentry.captureException(err, {
211
+ tags: { type: 'proxy_error', sandboxId },
212
+ extra: { url: req.url }
213
+ });
214
+ res.writeHead(502);
215
+ res.end('Bad gateway');
216
+ });
217
+ }
218
+ });
219
+
220
+ server.on('upgrade', (req, socket, head) => {
221
+ // 监听 WebSocket 数据
222
+ let _buffers = [];
223
+ socket.on('data', (data) => {
224
+ let message = '';
225
+ try {
226
+ _buffers.push(data);
227
+ // console.log(`💬 ${_buffers.length}`);
228
+ message = parseWebSocketFrame(Buffer.concat(_buffers)); // 复制data不能破坏原始数据
229
+ _buffers.length = 0;
230
+ if (message.startsWith('{')){ // 只解析 JSON 消息
231
+ const parsed = JSON.parse(message);
232
+ console.log('📨 CDP WebSocket message:', parsed);
233
+ logger.info('CDP WebSocket message received', {
234
+ data: parsed,
235
+ sandboxId,
236
+ });
237
+ Sentry.addBreadcrumb({
238
+ category: 'websocket',
239
+ message: 'CDP message received',
240
+ level: 'debug',
241
+ data: { ...parsed, sandboxId }
242
+ });
243
+ }
244
+ } catch (err) {
245
+ const msg = err.message;
246
+ if(!msg.includes('Incomplete')) {
247
+ // 记录解析错误
248
+ console.warn('⚠️ Failed to parse CDP WebSocket message:', err.message, _buffers.length);
249
+ _buffers.length = 0;
250
+ Sentry.captureException(err, {
251
+ tags: { type: 'websocket_error', sandboxId }
252
+ });
253
+ logger.warn('Failed to parse CDP WebSocket message', {
254
+ error: err.message,
255
+ data: message,
256
+ sandboxId,
257
+ });
258
+ }
259
+ }
260
+ });
261
+
262
+ socket.on('error', (err) => {
263
+ console.error('❌ CDP WebSocket error:', err);
264
+ logger.error('CDP WebSocket error', { error: err.message, sandboxId });
265
+ Sentry.captureException(err, {
266
+ tags: { type: 'websocket_error', sandboxId }
267
+ });
268
+ });
269
+
270
+ proxy.ws(req, socket, head);
271
+ });
272
+
273
+ server.listen(cdpPort + 1, '0.0.0.0', () => {
274
+ console.log(`🎯 Proxy server listening on http://0.0.0.0:${cdpPort + 1} → http://127.0.0.1:${cdpPort}`);
275
+ logger.info('Proxy server started', {
276
+ port: cdpPort + 1,
277
+ target: cdpPort,
278
+ sandboxId,
279
+ settings: {
280
+ type: 'chromium',
281
+ args,
282
+ headless,
283
+ enableAdblock,
284
+ timeoutMS,
285
+ workspace,
286
+ sandboxId
287
+ },
288
+ });
289
+ Sentry.addBreadcrumb({
290
+ category: 'server',
291
+ message: 'Proxy server started',
292
+ level: 'info',
293
+ data: {
294
+ port: cdpPort + 1,
295
+ target: cdpPort,
296
+ sandboxId,
297
+ settings: {
298
+ type: 'chromium',
299
+ args,
300
+ headless,
301
+ enableAdblock,
302
+ timeoutMS,
303
+ workspace,
304
+ sandboxId
305
+ },
306
+ }
307
+ });
308
+ });
309
+ } catch (ex) {
310
+ console.error('Failed to launch Chromium:', ex);
311
+ logger.error('Failed to launch Chrome', {
312
+ error: ex.message,
313
+ args,
314
+ headless,
315
+ cdpPort,
316
+ enableAdblock,
317
+ sandboxId,
318
+ });
319
+ Sentry.captureException(ex, {
320
+ tags: { type: 'launch_error', sandboxId },
321
+ extra: { args, headless, cdpPort, enableAdblock }
322
+ });
323
+ process.exit(1);
324
+ }
@@ -63,7 +63,6 @@ class BrowserService:
63
63
 
64
64
  # Set default browser config
65
65
  default_config: IBrowserConfig = {
66
- 'cdpPort': 9222,
67
66
  'headless': True,
68
67
  'launchTimeout': 30000,
69
68
  'args': [],
@@ -92,19 +91,34 @@ class BrowserService:
92
91
  })
93
92
  return default_logger.child('BrowserService')
94
93
 
95
- async def initialize(self) -> None:
94
+ async def initialize(self, browser_type: str) -> None:
96
95
  """Initialize the Grasp sandbox.
97
96
 
98
97
  Returns:
99
98
  Promise that resolves when sandbox is ready
100
99
  """
101
100
  self.logger.info('Initializing Browser service')
102
- await self.sandbox_service.create_sandbox()
101
+ envs = {
102
+ 'CDP_PORT': '9222',
103
+ 'BROWSER_ARGS': json.dumps(self.config['args']),
104
+ 'LAUNCH_TIMEOUT': str(self.config['launchTimeout']),
105
+ 'SANDBOX_TIMEOUT': str(self.sandbox_service.timeout),
106
+ 'HEADLESS': str(self.config['headless']).lower(),
107
+ 'NODE_ENV': 'production',
108
+ # 'SANDBOX_ID': self.sandbox_service.id,
109
+ 'WORKSPACE': self.sandbox_service.workspace,
110
+ 'BS_SOURCE_TOKEN': 'Qth8JGboEKVersqr1PSsUFMW',
111
+ 'BS_INGESTING_HOST': 's1363065.eu-nbg-2.betterstackdata.com',
112
+ 'SENTRY_DSN': 'https://21fa729ceb72d7f0adef06b4f786c067@o4509574910509056.ingest.us.sentry.io/4509574913720320',
113
+ **self.config['envs']
114
+ }
115
+ await self.sandbox_service.create_sandbox(f'grasp-run-{browser_type}', envs)
116
+ if(self.sandbox_service.sandbox is not None):
117
+ await self.sandbox_service.sandbox.files.write('/home/user/.sandbox_id', self.id)
103
118
  self.logger.info('Grasp sandbox initialized successfully')
104
119
 
105
120
  async def launch_browser(
106
121
  self,
107
- browser_type: str = 'chromium'
108
122
  ) -> CDPConnection:
109
123
  """Launch Chromium browser with CDP server.
110
124
 
@@ -122,43 +136,18 @@ class BrowserService:
122
136
 
123
137
  try:
124
138
  self.logger.info(
125
- f'Launching Chromium browser with CDP (port: {self.config["cdpPort"]}, headless: {self.config["headless"]})'
126
- )
127
-
128
- # Check if adblock is enabled and adjust browser type
129
- if (
130
- self.config['envs'].get('ADBLOCK') == 'true' and
131
- browser_type == 'chromium'
132
- ):
133
- self.logger.warn(
134
- '⚠️ Adblock is enabled. Should use chrome-stable instead.'
135
- )
136
- browser_type = 'chrome-stable'
139
+ f'Launching Chromium browser with CDP (port: 9222, headless: {self.config["headless"]})')
137
140
 
138
141
  # Read the Playwright script
139
- script_path = Path(__file__).parent.parent / 'sandbox' / f'{browser_type}.mjs'
140
-
141
- try:
142
- with open(script_path, 'r', encoding='utf-8') as f:
143
- playwright_script = f.read()
144
- except FileNotFoundError:
145
- raise RuntimeError(f'Browser script not found: {script_path}')
142
+ # script_path = Path(__file__).parent.parent / 'sandbox' / 'http-proxy.mjs'
146
143
 
147
- # Prepare environment variables
148
- envs = {
149
- 'CDP_PORT': str(self.config['cdpPort']),
150
- 'BROWSER_ARGS': json.dumps(self.config['args']),
151
- 'LAUNCH_TIMEOUT': str(self.config['launchTimeout']),
152
- 'SANDBOX_TIMEOUT': str(self.sandbox_service.timeout),
153
- 'HEADLESS': str(self.config['headless']).lower(),
154
- 'NODE_ENV': 'production',
155
- 'SANDBOX_ID': self.sandbox_service.id,
156
- 'WORKSPACE': self.sandbox_service.workspace,
157
- 'BS_SOURCE_TOKEN': 'Qth8JGboEKVersqr1PSsUFMW',
158
- 'BS_INGESTING_HOST': 's1363065.eu-nbg-2.betterstackdata.com',
159
- 'SENTRY_DSN': 'https://21fa729ceb72d7f0adef06b4f786c067@o4509574910509056.ingest.us.sentry.io/4509574913720320',
160
- **self.config['envs']
161
- }
144
+ # try:
145
+ # with open(script_path, 'r', encoding='utf-8') as f:
146
+ # playwright_script = f.read()
147
+ # except FileNotFoundError:
148
+ # raise RuntimeError(f'Browser script not found: {script_path}')
149
+
150
+ playwright_script = '/home/user/http-proxy.mjs'
162
151
 
163
152
  # Prepare script options
164
153
  from ..models import IScriptOptions
@@ -167,7 +156,6 @@ class BrowserService:
167
156
  'background': True,
168
157
  'nohup': not self.sandbox_service.is_debug,
169
158
  'timeoutMs': 0,
170
- 'envs': envs,
171
159
  'preCommand': '' if self.config['headless'] else 'xvfb-run -a -s "-screen 0 1280x1024x24" '
172
160
  }
173
161
 
@@ -187,7 +175,7 @@ class BrowserService:
187
175
  self.cdp_connection = result
188
176
 
189
177
  self.logger.info(
190
- f'Chromium browser launched successfully (cdpPort: {self.config["cdpPort"]}, wsUrl: {self.cdp_connection.ws_url})'
178
+ f'Chromium browser launched successfully (cdpPort: 9222, wsUrl: {self.cdp_connection.ws_url})'
191
179
  )
192
180
 
193
181
  # Start health check if not in debug mode
@@ -238,7 +226,7 @@ class BrowserService:
238
226
  Raises:
239
227
  RuntimeError: If CDP server fails to become ready within timeout
240
228
  """
241
- delay_ms = 200
229
+ delay_ms = 100
242
230
  max_attempts = self.config['launchTimeout'] // delay_ms
243
231
 
244
232
  for attempt in range(1, max_attempts + 1):
@@ -246,6 +234,10 @@ class BrowserService:
246
234
  self.logger.debug(
247
235
  f'Checking CDP availability (attempt {attempt}/{max_attempts})'
248
236
  )
237
+
238
+ host = self.sandbox_service.get_sandbox_host(
239
+ 9223
240
+ )
249
241
 
250
242
  # Check if CDP endpoint is responding
251
243
  options: ICommandOptions = {
@@ -253,7 +245,7 @@ class BrowserService:
253
245
  'inBackground': False
254
246
  }
255
247
  result = await self.sandbox_service.run_command(
256
- f'curl -s http://localhost:{self.config["cdpPort"]}/json/version',
248
+ f'curl -s https://{host}/json/version',
257
249
  options,
258
250
  True,
259
251
  )
@@ -264,15 +256,12 @@ class BrowserService:
264
256
  ):
265
257
  stdout_content = getattr(result, 'stdout', '')
266
258
  metadata = json.loads(stdout_content)
267
- host = self.sandbox_service.get_sandbox_host(
268
- self.config['cdpPort'] + 1
269
- )
270
259
 
271
260
  # Update URLs for external access
272
261
  ws_url = metadata['webSocketDebuggerUrl'].replace(
273
262
  'ws://', 'wss://'
274
263
  ).replace(
275
- f'localhost:{self.config["cdpPort"]}', host
264
+ f'localhost:9222', host
276
265
  )
277
266
 
278
267
  http_url = f'https://{host}'
@@ -280,7 +269,7 @@ class BrowserService:
280
269
  connection = CDPConnection(
281
270
  ws_url=ws_url,
282
271
  http_url=http_url,
283
- port=self.config['cdpPort']
272
+ port=9222
284
273
  )
285
274
 
286
275
  self.logger.info(f'CDP server is ready (metadata: {metadata})')
@@ -123,7 +123,7 @@ class SandboxService:
123
123
  """Get timeout value."""
124
124
  return self.config['timeout']
125
125
 
126
- async def create_sandbox(self) -> None:
126
+ async def create_sandbox(self, template_id: str, envs: Optional[Dict[str, str]] = None) -> None:
127
127
  """
128
128
  Creates and starts a new sandbox.
129
129
 
@@ -143,13 +143,14 @@ class SandboxService:
143
143
 
144
144
  # Create sandbox
145
145
  self.logger.info('Creating Grasp sandbox', {
146
- 'templateId': self.config['templateId'],
146
+ 'templateId': template_id,
147
147
  })
148
148
  # Use Sandbox constructor directly (e2b SDK 1.5.3+)
149
149
  self.sandbox = await AsyncSandbox.create(
150
- template=self.config['templateId'],
150
+ template=template_id,
151
151
  api_key=api_key,
152
- timeout=self.config['timeout'] // 1000 # Convert ms to seconds
152
+ timeout=self.config['timeout'] // 1000, # Convert ms to seconds
153
+ envs=envs,
153
154
  )
154
155
 
155
156
  self.status = SandboxStatus.RUNNING
@@ -322,7 +323,7 @@ class SandboxService:
322
323
  Runs JavaScript code in the sandbox.
323
324
 
324
325
  Args:
325
- code: JavaScript code to execute
326
+ code: JavaScript code to execute, or file path starting with '/home/user/'
326
327
  options: Script execution options
327
328
 
328
329
  Returns:
@@ -331,6 +332,10 @@ class SandboxService:
331
332
 
332
333
  Raises:
333
334
  RuntimeError: If sandbox is not running or script execution fails
335
+
336
+ Note:
337
+ If code starts with '/home/user/', it will be treated as a file path.
338
+ Otherwise, it will be treated as JavaScript code and written to a temporary file.
334
339
  """
335
340
  if not self.sandbox or self.status != SandboxStatus.RUNNING:
336
341
  raise RuntimeError('Sandbox is not running. Call create_sandbox() first.')
@@ -341,9 +346,14 @@ class SandboxService:
341
346
  try:
342
347
  # Generate temporary file name in working directory
343
348
  timestamp = int(time.time() * 1000)
344
- extension = 'mjs' if options['type'] == 'esm' else 'js'
345
- working_dir = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
346
- script_path = f'{working_dir}/script_{timestamp}.{extension}'
349
+ script_path = code
350
+
351
+ if not code.startswith('/home/user/'):
352
+ extension = 'mjs' if options['type'] == 'esm' else 'js'
353
+ working_dir = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
354
+ script_path = f'{working_dir}/script_{timestamp}.{extension}'
355
+ # Write code to temporary file
356
+ await self.sandbox.files.write(script_path, code)
347
357
 
348
358
  self.logger.debug('Running JavaScript code in sandbox', {
349
359
  'type': options['type'],
@@ -351,9 +361,6 @@ class SandboxService:
351
361
  'codeLength': len(code),
352
362
  })
353
363
 
354
- # Write code to temporary file
355
- await self.sandbox.files.write(script_path, code)
356
-
357
364
  # Choose execution command based on type
358
365
  pre_command = options.get('preCommand', '')
359
366
  command = f'{pre_command}node {script_path}'
@@ -374,16 +381,6 @@ class SandboxService:
374
381
  # Execute the script
375
382
  result = await self.run_command(command, cmd_options)
376
383
 
377
- # Cleanup temporary file (if not background execution)
378
- if not options.get('background', False):
379
- try:
380
- await self.run_command(f'rm -f {script_path}')
381
- except Exception as cleanup_error:
382
- self.logger.warn('Failed to cleanup script file', {
383
- 'scriptPath': script_path,
384
- 'error': cleanup_error,
385
- })
386
-
387
384
  return result
388
385
 
389
386
  except Exception as error:
@@ -23,7 +23,6 @@ def get_config() -> Dict[str, Any]:
23
23
  return {
24
24
  'sandbox': {
25
25
  'key': os.getenv('GRASP_KEY', ''),
26
- 'templateId': 'playwright-pnpm-template',
27
26
  'timeout': int(os.getenv('GRASP_SERVICE_TIMEOUT', '900000')),
28
27
  'debug': os.getenv('GRASP_DEBUG', 'false').lower() == 'true',
29
28
  },
@@ -44,7 +43,6 @@ def get_sandbox_config() -> ISandboxConfig:
44
43
  config = get_config()
45
44
  sandbox_config = ISandboxConfig(
46
45
  key=config['sandbox']['key'],
47
- templateId=config['sandbox']['templateId'],
48
46
  timeout=config['sandbox']['timeout'],
49
47
  debug=config['sandbox'].get('debug', False),
50
48
  )
@@ -64,7 +62,6 @@ def get_browser_config() -> IBrowserConfig:
64
62
  IBrowserConfig: Browser configuration object.
65
63
  """
66
64
  return IBrowserConfig(
67
- cdpPort=int(os.getenv('GRASP_CDP_PORT', '9222')),
68
65
  args=[
69
66
  '--no-sandbox',
70
67
  '--disable-setuid-sandbox',
@@ -102,7 +99,6 @@ ENV_VARS = {
102
99
  'GRASP_DEBUG': 'GRASP_DEBUG',
103
100
  'GRASP_LOG_LEVEL': 'GRASP_LOG_LEVEL',
104
101
  'GRASP_LOG_FILE': 'GRASP_LOG_FILE',
105
- 'GRASP_CDP_PORT': 'GRASP_CDP_PORT',
106
102
  'GRASP_HEADLESS': 'GRASP_HEADLESS',
107
103
  'GRASP_LAUNCH_TIMEOUT': 'GRASP_LAUNCH_TIMEOUT',
108
104
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_sdk
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Python SDK for Grasp E2B - Browser automation and sandbox management
5
5
  Home-page: https://github.com/grasp-team/grasp-e2b
6
6
  Author: Grasp Team
@@ -30,8 +30,10 @@ grasp_sdk.egg-info/not-zip-safe
30
30
  grasp_sdk.egg-info/requires.txt
31
31
  grasp_sdk.egg-info/top_level.txt
32
32
  grasp_sdk/models/__init__.py
33
+ grasp_sdk/sandbox/bootstrap-chrome-stable.mjs
33
34
  grasp_sdk/sandbox/chrome-stable.mjs
34
35
  grasp_sdk/sandbox/chromium.mjs
36
+ grasp_sdk/sandbox/http-proxy.mjs
35
37
  grasp_sdk/services/__init__.py
36
38
  grasp_sdk/services/browser.py
37
39
  grasp_sdk/services/sandbox.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "grasp_sdk"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  authors = [
9
9
  {name = "Grasp Team", email = "team@grasp.com"},
10
10
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes