agentlink-sdk 1.0.0__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.
@@ -0,0 +1,87 @@
1
+ """
2
+ AgentLink Python SDK
3
+
4
+ Official Python SDK for China Mobile Cloud AgentLink MCP Gateway.
5
+ Provides sandbox lifecycle management, browser automation, desktop control,
6
+ and tool invocation capabilities.
7
+
8
+ Usage::
9
+
10
+ from agentlink_sdk import AgentLink
11
+
12
+ al = AgentLink(api_key="your-api-key")
13
+
14
+ # Browser automation
15
+ al.browser.go("https://www.baidu.com")
16
+ shot = al.browser.capture()
17
+ shot.save("page.png")
18
+
19
+ # Windows desktop control
20
+ al.desktop.shell("Get-Date")
21
+ al.desktop.capture().save("desktop.png")
22
+
23
+ # Clean up
24
+ al.cleanup()
25
+ """
26
+
27
+ __version__ = "1.0.0"
28
+
29
+ from .client import AgentLink
30
+ from .browser import BrowserController
31
+ from .desktop import DesktopController
32
+ from .config import AgentLinkConfig
33
+ from .models.sandbox import SandboxSession, SandboxType, IMAGE_BROWSER, IMAGE_WINDOWS
34
+ from .models.results import (
35
+ ActionResult,
36
+ Screenshot,
37
+ SandboxResult,
38
+ ToolResult,
39
+ )
40
+ from .exceptions import (
41
+ AgentLinkError,
42
+ AgentLinkTimeoutError,
43
+ ConfigurationError,
44
+ SandboxError,
45
+ ToolCallError,
46
+ SessionError,
47
+ TransportError,
48
+ TimeoutError,
49
+ BrowserError,
50
+ CommandError,
51
+ FileSystemError,
52
+ )
53
+ from .logger import set_log_level
54
+
55
+ __all__ = [
56
+ "__version__",
57
+ # Client & controllers
58
+ "AgentLink",
59
+ "BrowserController",
60
+ "DesktopController",
61
+ # Config
62
+ "AgentLinkConfig",
63
+ # Models
64
+ "SandboxSession",
65
+ "SandboxType",
66
+ "IMAGE_BROWSER",
67
+ "IMAGE_WINDOWS",
68
+ # Results
69
+ "ActionResult",
70
+ "Screenshot",
71
+ "SandboxResult",
72
+ "ToolResult",
73
+ # Exceptions
74
+ "AgentLinkError",
75
+ "ConfigurationError",
76
+ "SandboxError",
77
+ "ToolCallError",
78
+ "SessionError",
79
+ "TransportError",
80
+ "AgentLinkTimeoutError",
81
+ "TimeoutError",
82
+ "BrowserError",
83
+ "CommandError",
84
+ "FileSystemError",
85
+ # Utilities
86
+ "set_log_level",
87
+ ]
@@ -0,0 +1,617 @@
1
+ """
2
+ Browser automation controller.
3
+
4
+ Wraps the browser sandbox MCP tools into a fluent Python API for
5
+ web navigation, element interaction, screenshots, and JS evaluation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from .models.results import ActionResult, Screenshot
13
+ from .models.sandbox import IMAGE_BROWSER
14
+
15
+ # Avoid circular import at runtime; type-checkers resolve via TYPE_CHECKING.
16
+ from typing import TYPE_CHECKING
17
+ if TYPE_CHECKING:
18
+ from .client import AgentLink
19
+
20
+
21
+ class BrowserController:
22
+ """
23
+ Browser automation controller.
24
+
25
+ Wraps the browser sandbox MCP tools into a fluent Python API for
26
+ web navigation, element interaction, screenshots, and JS evaluation.
27
+ """
28
+
29
+ def __init__(self, client: AgentLink, image_id: str = IMAGE_BROWSER):
30
+ self._client = client
31
+ self._image_id = image_id
32
+
33
+ # ---- Navigation ----
34
+
35
+ def go(self, url: str, *, sandbox_id: Optional[str] = None) -> ActionResult:
36
+ """
37
+ Navigate to *url* in the browser.
38
+
39
+ Args:
40
+ url: Target URL.
41
+ sandbox_id: Explicit sandbox ID.
42
+
43
+ Returns:
44
+ ActionResult: Navigation outcome.
45
+ """
46
+ args: Dict[str, Any] = {"url": url}
47
+ if sandbox_id:
48
+ args["sandboxId"] = sandbox_id
49
+ return self._client._invoke(self._image_id, "browser_navigate", args)
50
+
51
+ def back(self, *, sandbox_id: Optional[str] = None) -> ActionResult:
52
+ """Go back to the previous page in browser history.
53
+
54
+ Returns:
55
+ ActionResult: Navigation outcome.
56
+ """
57
+ args: Dict[str, Any] = {}
58
+ if sandbox_id:
59
+ args["sandboxId"] = sandbox_id
60
+ return self._client._invoke(self._image_id, "browser_navigate_back", args)
61
+
62
+ # ---- Snapshot & Screenshot ----
63
+
64
+ def a11y_tree(
65
+ self,
66
+ *,
67
+ filename: Optional[str] = None,
68
+ depth: Optional[int] = None,
69
+ sandbox_id: Optional[str] = None,
70
+ ) -> ActionResult:
71
+ """
72
+ Capture an accessibility tree of the current page.
73
+
74
+ Each interactive element is tagged with a numeric ``ref`` that
75
+ can be passed to :meth:`click` / :meth:`fill`.
76
+
77
+ Args:
78
+ filename: Save snapshot to markdown file instead of returning inline.
79
+ depth: Limit the depth of the snapshot tree.
80
+ sandbox_id: Explicit sandbox ID.
81
+
82
+ Returns:
83
+ ActionResult: Accessibility text in ``.text``.
84
+ """
85
+ args: Dict[str, Any] = {}
86
+ if filename is not None:
87
+ args["filename"] = filename
88
+ if depth is not None:
89
+ args["depth"] = depth
90
+ if sandbox_id:
91
+ args["sandboxId"] = sandbox_id
92
+ return self._client._invoke(self._image_id, "browser_snapshot", args)
93
+
94
+ def capture(
95
+ self,
96
+ *,
97
+ img_type: str = "png",
98
+ filename: Optional[str] = None,
99
+ element: Optional[str] = None,
100
+ ref: Optional[str] = None,
101
+ full_page: bool = False,
102
+ sandbox_id: Optional[str] = None,
103
+ ) -> Screenshot:
104
+ """
105
+ Capture a screenshot of the current browser page.
106
+
107
+ Args:
108
+ img_type: Image format (``"png"`` or ``"jpeg"``).
109
+ filename: File name to save the screenshot to.
110
+ element: Human-readable element description for element screenshot.
111
+ ref: Element reference for element screenshot.
112
+ full_page: Take screenshot of full scrollable page.
113
+ sandbox_id: Explicit sandbox ID.
114
+
115
+ Returns:
116
+ Screenshot: Image payload saveable via ``.save(path)``.
117
+ """
118
+ args: Dict[str, Any] = {"type": img_type}
119
+ if filename is not None:
120
+ args["filename"] = filename
121
+ if element is not None:
122
+ args["element"] = element
123
+ if ref is not None:
124
+ args["ref"] = ref
125
+ if full_page:
126
+ args["fullPage"] = True
127
+ if sandbox_id:
128
+ args["sandboxId"] = sandbox_id
129
+
130
+ result = self._client._invoke_raw(self._image_id, "browser_take_screenshot", args)
131
+ return Screenshot.from_mcp(result)
132
+
133
+ # ---- Element Interaction ----
134
+
135
+ def click(
136
+ self,
137
+ ref: str,
138
+ *,
139
+ element: Optional[str] = None,
140
+ double_click: bool = False,
141
+ button: str = "left",
142
+ modifiers: Optional[List[str]] = None,
143
+ sandbox_id: Optional[str] = None,
144
+ ) -> ActionResult:
145
+ """
146
+ Click on an element identified by its accessibility ``ref``.
147
+
148
+ Call :meth:`a11y_tree` first to discover element refs.
149
+
150
+ Args:
151
+ ref: Element reference from the accessibility tree.
152
+ element: Human-readable element description.
153
+ double_click: Perform double click.
154
+ button: ``"left"``, ``"right"``, or ``"middle"``.
155
+ modifiers: Modifier keys (e.g. ``["Shift", "Control"]``).
156
+ sandbox_id: Explicit sandbox ID.
157
+
158
+ Returns:
159
+ ActionResult: Click outcome.
160
+ """
161
+ args: Dict[str, Any] = {"target": ref}
162
+ if element is not None:
163
+ args["element"] = element
164
+ if double_click:
165
+ args["doubleClick"] = True
166
+ if button != "left":
167
+ args["button"] = button
168
+ if modifiers:
169
+ args["modifiers"] = modifiers
170
+ if sandbox_id:
171
+ args["sandboxId"] = sandbox_id
172
+ return self._client._invoke(self._image_id, "browser_click", args)
173
+
174
+ def hover(self, ref: str, *, element: Optional[str] = None, sandbox_id: Optional[str] = None) -> ActionResult:
175
+ """Hover over an element.
176
+
177
+ Args:
178
+ ref: Element reference from the accessibility tree.
179
+ element: Human-readable element description.
180
+ sandbox_id: Explicit sandbox ID.
181
+
182
+ Returns:
183
+ ActionResult: Hover outcome.
184
+ """
185
+ args: Dict[str, Any] = {"target": ref}
186
+ if element is not None:
187
+ args["element"] = element
188
+ if sandbox_id:
189
+ args["sandboxId"] = sandbox_id
190
+ return self._client._invoke(self._image_id, "browser_hover", args)
191
+
192
+ def drag(
193
+ self,
194
+ start_ref: str,
195
+ end_ref: str,
196
+ *,
197
+ start_element: Optional[str] = None,
198
+ end_element: Optional[str] = None,
199
+ sandbox_id: Optional[str] = None,
200
+ ) -> ActionResult:
201
+ """Drag and drop between two elements.
202
+
203
+ Args:
204
+ start_ref: Source element reference.
205
+ end_ref: Target element reference.
206
+ start_element: Human-readable source element description.
207
+ end_element: Human-readable target element description.
208
+ sandbox_id: Explicit sandbox ID.
209
+
210
+ Returns:
211
+ ActionResult: Drag outcome.
212
+ """
213
+ args: Dict[str, Any] = {
214
+ "startTarget": start_ref,
215
+ "endTarget": end_ref,
216
+ }
217
+ if start_element is not None:
218
+ args["startElement"] = start_element
219
+ if end_element is not None:
220
+ args["endElement"] = end_element
221
+ if sandbox_id:
222
+ args["sandboxId"] = sandbox_id
223
+ return self._client._invoke(self._image_id, "browser_drag", args)
224
+
225
+ # ---- Typing & Form ----
226
+
227
+ def fill(
228
+ self,
229
+ ref: str,
230
+ text: str,
231
+ *,
232
+ element: Optional[str] = None,
233
+ submit: bool = False,
234
+ slowly: bool = False,
235
+ sandbox_id: Optional[str] = None,
236
+ ) -> ActionResult:
237
+ """
238
+ Type *text* into the input element identified by ``ref``.
239
+
240
+ Args:
241
+ ref: Element reference from the accessibility tree.
242
+ text: Text to type.
243
+ element: Human-readable element description.
244
+ submit: Press Enter after typing.
245
+ slowly: Type one character at a time (triggers key handlers).
246
+ sandbox_id: Explicit sandbox ID.
247
+
248
+ Returns:
249
+ ActionResult: Typing outcome.
250
+ """
251
+ args: Dict[str, Any] = {"target": ref, "text": text}
252
+ if element is not None:
253
+ args["element"] = element
254
+ if submit:
255
+ args["submit"] = True
256
+ if slowly:
257
+ args["slowly"] = True
258
+ if sandbox_id:
259
+ args["sandboxId"] = sandbox_id
260
+ return self._client._invoke(self._image_id, "browser_type", args)
261
+
262
+ def fill_form(
263
+ self,
264
+ fields: List[Dict[str, Any]],
265
+ *,
266
+ sandbox_id: Optional[str] = None,
267
+ ) -> ActionResult:
268
+ """Fill multiple form fields at once.
269
+
270
+ Args:
271
+ fields: List of field dicts with keys ``name``, ``type``,
272
+ ``ref`` (or ``selector``), and ``value``.
273
+ ``type`` is one of: ``textbox``, ``checkbox``,
274
+ ``radio``, ``combobox``, ``slider``.
275
+ sandbox_id: Explicit sandbox ID.
276
+
277
+ Returns:
278
+ ActionResult: Fill outcome.
279
+ """
280
+ args: Dict[str, Any] = {"fields": fields}
281
+ if sandbox_id:
282
+ args["sandboxId"] = sandbox_id
283
+ return self._client._invoke(self._image_id, "browser_fill_form", args)
284
+
285
+ def select_option(
286
+ self,
287
+ ref: str,
288
+ values: List[str],
289
+ *,
290
+ element: Optional[str] = None,
291
+ sandbox_id: Optional[str] = None,
292
+ ) -> ActionResult:
293
+ """Select option(s) in a dropdown.
294
+
295
+ Args:
296
+ ref: Element reference from the accessibility tree.
297
+ values: Array of values to select.
298
+ element: Human-readable element description.
299
+ sandbox_id: Explicit sandbox ID.
300
+
301
+ Returns:
302
+ ActionResult: Selection outcome.
303
+ """
304
+ args: Dict[str, Any] = {"target": ref, "values": values}
305
+ if element is not None:
306
+ args["element"] = element
307
+ if sandbox_id:
308
+ args["sandboxId"] = sandbox_id
309
+ return self._client._invoke(self._image_id, "browser_select_option", args)
310
+
311
+ def press_key(self, key: str, *, sandbox_id: Optional[str] = None) -> ActionResult:
312
+ """Press a key on the keyboard.
313
+
314
+ Args:
315
+ key: Key name (e.g. ``"Enter"``, ``"Tab"``, ``"ArrowLeft"``, ``"a"``).
316
+ sandbox_id: Explicit sandbox ID.
317
+
318
+ Returns:
319
+ ActionResult: Key press outcome.
320
+ """
321
+ args: Dict[str, Any] = {"key": key}
322
+ if sandbox_id:
323
+ args["sandboxId"] = sandbox_id
324
+ return self._client._invoke(self._image_id, "browser_press_key", args)
325
+
326
+ # ---- JavaScript & Code ----
327
+
328
+ def evaluate(
329
+ self,
330
+ script: str,
331
+ *,
332
+ element: Optional[str] = None,
333
+ ref: Optional[str] = None,
334
+ filename: Optional[str] = None,
335
+ sandbox_id: Optional[str] = None,
336
+ ) -> ActionResult:
337
+ """
338
+ Execute JavaScript in the browser context and return the result.
339
+
340
+ Args:
341
+ script: JavaScript function: ``() => { ... }`` or ``(el) => { ... }``.
342
+ element: Human-readable element description (used with ref).
343
+ ref: Element reference to evaluate on.
344
+ filename: Save result to file instead of returning inline.
345
+ sandbox_id: Explicit sandbox ID.
346
+
347
+ Returns:
348
+ ActionResult: Return value of the script in ``.data``.
349
+ """
350
+ args: Dict[str, Any] = {"function": script}
351
+ if element is not None:
352
+ args["element"] = element
353
+ if ref is not None:
354
+ args["ref"] = ref
355
+ if filename is not None:
356
+ args["filename"] = filename
357
+ if sandbox_id:
358
+ args["sandboxId"] = sandbox_id
359
+ return self._client._invoke(self._image_id, "browser_evaluate", args)
360
+
361
+ def run_code(self, code: str, *, filename: Optional[str] = None, sandbox_id: Optional[str] = None) -> ActionResult:
362
+ """Run a Playwright code snippet on the page.
363
+
364
+ Args:
365
+ code: Async JavaScript function receiving ``page`` parameter,
366
+ e.g. ``async (page) => { await page.getByRole('button').click(); }``
367
+ filename: Load code from file instead of inline.
368
+ sandbox_id: Explicit sandbox ID.
369
+
370
+ Returns:
371
+ ActionResult: Code execution result.
372
+ """
373
+ args: Dict[str, Any] = {"code": code}
374
+ if filename is not None:
375
+ args["filename"] = filename
376
+ if sandbox_id:
377
+ args["sandboxId"] = sandbox_id
378
+ return self._client._invoke(self._image_id, "browser_run_code", args)
379
+
380
+ # ---- Window & Tab Management ----
381
+
382
+ def close_tab(self, *, sandbox_id: Optional[str] = None) -> ActionResult:
383
+ """Close the current browser tab/page.
384
+
385
+ This method closes the current browser tab. It is intentionally named
386
+ ``close_tab`` (rather than ``close``) to avoid confusion with
387
+ :meth:`AgentLink.close`, which closes the client connection itself.
388
+
389
+ Args:
390
+ sandbox_id: Explicit sandbox ID.
391
+
392
+ Returns:
393
+ ActionResult: Close outcome.
394
+ """
395
+ args: Dict[str, Any] = {}
396
+ if sandbox_id:
397
+ args["sandboxId"] = sandbox_id
398
+ return self._client._invoke(self._image_id, "browser_close", args)
399
+
400
+ def resize(self, width: int, height: int, *, sandbox_id: Optional[str] = None) -> ActionResult:
401
+ """Resize the browser window.
402
+
403
+ Args:
404
+ width: New width in pixels.
405
+ height: New height in pixels.
406
+ sandbox_id: Explicit sandbox ID.
407
+
408
+ Returns:
409
+ ActionResult: Resize outcome.
410
+ """
411
+ args: Dict[str, Any] = {"width": width, "height": height}
412
+ if sandbox_id:
413
+ args["sandboxId"] = sandbox_id
414
+ return self._client._invoke(self._image_id, "browser_resize", args)
415
+
416
+ def tabs(
417
+ self,
418
+ action: str = "list",
419
+ *,
420
+ index: Optional[int] = None,
421
+ sandbox_id: Optional[str] = None,
422
+ ) -> ActionResult:
423
+ """List, create, close, or select browser tabs.
424
+
425
+ Args:
426
+ action: ``"list"``, ``"new"``, ``"close"``, or ``"select"``.
427
+ index: Tab index (for close/select).
428
+ sandbox_id: Explicit sandbox ID.
429
+
430
+ Returns:
431
+ ActionResult: Tab operation outcome.
432
+ """
433
+ args: Dict[str, Any] = {"action": action}
434
+ if index is not None:
435
+ args["index"] = index
436
+ if sandbox_id:
437
+ args["sandboxId"] = sandbox_id
438
+ return self._client._invoke(self._image_id, "browser_tabs", args)
439
+
440
+ # ---- Dialog & File Upload ----
441
+
442
+ def handle_dialog(
443
+ self,
444
+ accept: bool,
445
+ *,
446
+ prompt_text: Optional[str] = None,
447
+ sandbox_id: Optional[str] = None,
448
+ ) -> ActionResult:
449
+ """Handle a browser dialog (alert, confirm, prompt).
450
+
451
+ Args:
452
+ accept: Whether to accept the dialog.
453
+ prompt_text: Text for prompt dialog.
454
+ sandbox_id: Explicit sandbox ID.
455
+
456
+ Returns:
457
+ ActionResult: Dialog handling outcome.
458
+ """
459
+ args: Dict[str, Any] = {"accept": accept}
460
+ if prompt_text is not None:
461
+ args["promptText"] = prompt_text
462
+ if sandbox_id:
463
+ args["sandboxId"] = sandbox_id
464
+ return self._client._invoke(self._image_id, "browser_handle_dialog", args)
465
+
466
+ def file_upload(self, paths: Optional[List[str]] = None, *, sandbox_id: Optional[str] = None) -> ActionResult:
467
+ """Upload one or multiple files.
468
+
469
+ Args:
470
+ paths: Absolute paths to files. If omitted, file chooser is cancelled.
471
+ sandbox_id: Explicit sandbox ID.
472
+
473
+ Returns:
474
+ ActionResult: Upload outcome.
475
+ """
476
+ args: Dict[str, Any] = {}
477
+ if paths is not None:
478
+ args["paths"] = paths
479
+ if sandbox_id:
480
+ args["sandboxId"] = sandbox_id
481
+ return self._client._invoke(self._image_id, "browser_file_upload", args)
482
+
483
+ # ---- Diagnostics ----
484
+
485
+ def console_messages(
486
+ self,
487
+ level: str = "info",
488
+ *,
489
+ all_messages: bool = False,
490
+ filename: Optional[str] = None,
491
+ sandbox_id: Optional[str] = None,
492
+ ) -> ActionResult:
493
+ """Get browser console messages.
494
+
495
+ Args:
496
+ level: Minimum severity level: ``"error"``, ``"warning"``,
497
+ ``"info"``, or ``"debug"``.
498
+ all_messages: Return all messages since session start.
499
+ filename: Save messages to file instead of returning inline.
500
+ sandbox_id: Explicit sandbox ID.
501
+
502
+ Returns:
503
+ ActionResult: Console messages in ``.text``.
504
+ """
505
+ args: Dict[str, Any] = {"level": level}
506
+ if all_messages:
507
+ args["all"] = True
508
+ if filename is not None:
509
+ args["filename"] = filename
510
+ if sandbox_id:
511
+ args["sandboxId"] = sandbox_id
512
+ return self._client._invoke(self._image_id, "browser_console_messages", args)
513
+
514
+ def network_requests(
515
+ self,
516
+ *,
517
+ static: bool = False,
518
+ request_body: bool = False,
519
+ request_headers: bool = False,
520
+ filter_pattern: Optional[str] = None,
521
+ filename: Optional[str] = None,
522
+ sandbox_id: Optional[str] = None,
523
+ ) -> ActionResult:
524
+ """Get network requests since page load.
525
+
526
+ Args:
527
+ static: Include static resources (images, fonts, scripts).
528
+ request_body: Include request bodies.
529
+ request_headers: Include request headers.
530
+ filter_pattern: Regex filter for URLs.
531
+ filename: Save to file instead of returning inline.
532
+ sandbox_id: Explicit sandbox ID.
533
+
534
+ Returns:
535
+ ActionResult: Network request data in ``.text``.
536
+ """
537
+ args: Dict[str, Any] = {
538
+ "static": static,
539
+ "requestBody": request_body,
540
+ "requestHeaders": request_headers,
541
+ }
542
+ if filter_pattern is not None:
543
+ args["filter"] = filter_pattern
544
+ if filename is not None:
545
+ args["filename"] = filename
546
+ if sandbox_id:
547
+ args["sandboxId"] = sandbox_id
548
+ return self._client._invoke(self._image_id, "browser_network_requests", args)
549
+
550
+ # ---- Wait ----
551
+
552
+ def wait_for(
553
+ self,
554
+ *,
555
+ time_secs: Optional[float] = None,
556
+ text: Optional[str] = None,
557
+ text_gone: Optional[str] = None,
558
+ sandbox_id: Optional[str] = None,
559
+ ) -> ActionResult:
560
+ """Wait for text to appear, disappear, or a time to pass.
561
+
562
+ Args:
563
+ time_secs: Time to wait in seconds.
564
+ text: Text to wait for.
565
+ text_gone: Text to wait for to disappear.
566
+ sandbox_id: Explicit sandbox ID.
567
+
568
+ Returns:
569
+ ActionResult: Wait outcome.
570
+
571
+ Raises:
572
+ ValueError: If none of *time_secs*, *text*, or *text_gone* is provided.
573
+ """
574
+ if time_secs is None and text is None and text_gone is None:
575
+ raise ValueError("At least one of 'time_secs', 'text', or 'text_gone' must be provided.")
576
+ args: Dict[str, Any] = {}
577
+ if time_secs is not None:
578
+ args["time"] = time_secs
579
+ if text is not None:
580
+ args["text"] = text
581
+ if text_gone is not None:
582
+ args["textGone"] = text_gone
583
+ if sandbox_id:
584
+ args["sandboxId"] = sandbox_id
585
+ return self._client._invoke(self._image_id, "browser_wait_for", args)
586
+
587
+ # ---- Shell (cross-cutting) ----
588
+
589
+ def shell(self, command: str, *, timeout: int = 30, sandbox_id: Optional[str] = None) -> ActionResult:
590
+ """Execute a shell command in the browser sandbox.
591
+
592
+ Args:
593
+ command: Shell command to execute.
594
+ timeout: Command timeout in seconds (default: 30).
595
+ sandbox_id: Explicit sandbox ID.
596
+
597
+ Returns:
598
+ ActionResult: Command output in ``.text``.
599
+ """
600
+ args: Dict[str, Any] = {"command": command, "timeout": timeout}
601
+ if sandbox_id:
602
+ args["sandboxId"] = sandbox_id
603
+ return self._client._invoke(self._image_id, "shell_exec", args)
604
+
605
+ def get_os(self, *, sandbox_id: Optional[str] = None) -> ActionResult:
606
+ """Get the operating system of the sandbox.
607
+
608
+ Returns:
609
+ ActionResult: OS information in ``.text``.
610
+ """
611
+ args: Dict[str, Any] = {}
612
+ if sandbox_id:
613
+ args["sandboxId"] = sandbox_id
614
+ return self._client._invoke(self._image_id, "get_os", args)
615
+
616
+ def __repr__(self) -> str:
617
+ return f"BrowserController(image_id={self._image_id!r})"