hyperbrowser 0.89.3__tar.gz → 0.90.1__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.
Files changed (87) hide show
  1. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/PKG-INFO +1 -1
  2. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/sandbox.py +18 -10
  3. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +105 -58
  4. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +39 -8
  5. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sandboxes/shared.py +60 -0
  6. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/sandbox.py +18 -10
  7. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +103 -56
  8. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +39 -8
  9. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/crawl.py +2 -2
  10. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/sandbox.py +1 -0
  11. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/scrape.py +3 -3
  12. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/web/common.py +2 -2
  13. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/web/fetch.py +2 -2
  14. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/pyproject.toml +1 -1
  15. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/LICENSE +0 -0
  16. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/README.md +0 -0
  17. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/__init__.py +0 -0
  18. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/async_client.py +0 -0
  19. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/base.py +0 -0
  20. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  21. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  22. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  23. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  24. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  25. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  26. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  27. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  28. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  29. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  30. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  31. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  32. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  33. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
  34. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  35. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  36. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  37. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  38. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  39. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  40. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  41. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  42. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  43. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  44. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  45. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  46. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  47. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  48. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  49. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  50. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  51. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  52. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  53. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  54. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
  55. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  56. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  57. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  58. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  59. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  60. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  61. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/client/sync.py +0 -0
  62. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/config.py +0 -0
  63. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/exceptions.py +0 -0
  64. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/__init__.py +0 -0
  65. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/agents/browser_use.py +0 -0
  66. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  67. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/agents/cua.py +0 -0
  68. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  69. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  70. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/computer_action.py +0 -0
  71. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/consts.py +0 -0
  72. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/extension.py +0 -0
  73. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/extract.py +0 -0
  74. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/profile.py +0 -0
  75. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/session.py +0 -0
  76. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/team.py +0 -0
  77. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/web/batch_fetch.py +0 -0
  78. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/web/crawl.py +0 -0
  79. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/models/web/search.py +0 -0
  80. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/sandbox_common.py +0 -0
  81. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/tools/__init__.py +0 -0
  82. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/tools/anthropic.py +0 -0
  83. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/tools/openai.py +0 -0
  84. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/tools/schema.py +0 -0
  85. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/transport/async_transport.py +0 -0
  86. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/transport/base.py +0 -0
  87. {hyperbrowser-0.89.3 → hyperbrowser-0.90.1}/hyperbrowser/transport/sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 0.89.3
3
+ Version: 0.90.1
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -217,16 +217,24 @@ class SandboxHandle:
217
217
  )
218
218
  return _copy_model(self._runtime_session)
219
219
 
220
- async def exec(self, input: Union[str, SandboxExecParams]):
221
- if isinstance(input, str):
222
- params = SandboxExecParams(command=input)
223
- else:
224
- if not isinstance(input, SandboxExecParams):
225
- raise TypeError(
226
- "input must be a command string or SandboxExecParams instance"
227
- )
228
- params = input
229
- return await self.processes.exec(params)
220
+ async def exec(
221
+ self,
222
+ input: Union[str, SandboxExecParams],
223
+ *,
224
+ cwd: Optional[str] = None,
225
+ env: Optional[Dict[str, str]] = None,
226
+ timeout_ms: Optional[int] = None,
227
+ timeout_sec: Optional[int] = None,
228
+ run_as: Optional[str] = None,
229
+ ):
230
+ return await self.processes.exec(
231
+ input,
232
+ cwd=cwd,
233
+ env=env,
234
+ timeout_ms=timeout_ms,
235
+ timeout_sec=timeout_sec,
236
+ run_as=run_as,
237
+ )
230
238
 
231
239
  async def get_process(self, process_id: str) -> SandboxProcessHandle:
232
240
  return await self.processes.get(process_id)
@@ -5,7 +5,7 @@ import io
5
5
  import json
6
6
  import socket
7
7
  from datetime import datetime
8
- from typing import AsyncIterator, Callable, List, Optional, Union
8
+ from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Union
9
9
  from urllib.parse import urlencode
10
10
 
11
11
  from websockets.asyncio.client import connect as async_ws_connect
@@ -249,10 +249,21 @@ class SandboxFilesApi:
249
249
  transport: RuntimeTransport,
250
250
  get_connection_info,
251
251
  runtime_proxy_override: Optional[str] = None,
252
+ default_run_as: Optional[str] = None,
252
253
  ):
253
254
  self._transport = transport
254
255
  self._get_connection_info = get_connection_info
255
256
  self._runtime_proxy_override = runtime_proxy_override
257
+ self._default_run_as = default_run_as.strip() if default_run_as else None
258
+
259
+ def with_run_as(self, run_as: Optional[str]):
260
+ normalized = run_as.strip() if run_as else None
261
+ return SandboxFilesApi(
262
+ self._transport,
263
+ self._get_connection_info,
264
+ self._runtime_proxy_override,
265
+ default_run_as=normalized,
266
+ )
256
267
 
257
268
  async def list(
258
269
  self,
@@ -266,17 +277,19 @@ class SandboxFilesApi:
266
277
 
267
278
  payload = await self._transport.request_json(
268
279
  "/sandbox/files",
269
- params={
270
- "path": path,
271
- "depth": depth,
272
- },
280
+ params=self._with_run_as_params(
281
+ {
282
+ "path": path,
283
+ "depth": depth,
284
+ }
285
+ ),
273
286
  )
274
287
  return [_normalize_file_info(entry) for entry in payload.get("entries", [])]
275
288
 
276
289
  async def get_info(self, path: str) -> SandboxFileInfo:
277
290
  payload = await self._transport.request_json(
278
291
  "/sandbox/files/stat",
279
- params={"path": path},
292
+ params=self._with_run_as_params({"path": path}),
280
293
  )
281
294
  return _normalize_file_info(payload["file"])
282
295
 
@@ -357,10 +370,12 @@ class SandboxFilesApi:
357
370
  payload = await self._transport.request_json(
358
371
  "/sandbox/files/write",
359
372
  method="POST",
360
- json_body={
361
- "path": path_or_files,
362
- **_encode_write_data(data),
363
- },
373
+ json_body=self._with_run_as_body(
374
+ {
375
+ "path": path_or_files,
376
+ **_encode_write_data(data),
377
+ }
378
+ ),
364
379
  headers={"content-type": "application/json"},
365
380
  )
366
381
  return _normalize_write_info(payload["files"][0])
@@ -377,7 +392,7 @@ class SandboxFilesApi:
377
392
  payload = await self._transport.request_json(
378
393
  "/sandbox/files/write",
379
394
  method="POST",
380
- json_body={"files": encoded_files},
395
+ json_body=self._with_run_as_body({"files": encoded_files}),
381
396
  headers={"content-type": "application/json"},
382
397
  )
383
398
  return [_normalize_write_info(entry) for entry in payload.get("files", [])]
@@ -419,7 +434,7 @@ class SandboxFilesApi:
419
434
  payload = await self._transport.request_json(
420
435
  "/sandbox/files/upload",
421
436
  method="PUT",
422
- params={"path": path},
437
+ params=self._with_run_as_params({"path": path}),
423
438
  content=body,
424
439
  )
425
440
  return SandboxFileTransferResult(**payload)
@@ -427,7 +442,7 @@ class SandboxFilesApi:
427
442
  async def download(self, path: str) -> bytes:
428
443
  return await self._transport.request_bytes(
429
444
  "/sandbox/files/download",
430
- params={"path": path},
445
+ params=self._with_run_as_params({"path": path}),
431
446
  )
432
447
 
433
448
  async def make_dir(
@@ -440,11 +455,13 @@ class SandboxFilesApi:
440
455
  payload = await self._transport.request_json(
441
456
  "/sandbox/files/mkdir",
442
457
  method="POST",
443
- json_body={
444
- "path": path,
445
- "parents": parents,
446
- "mode": mode,
447
- },
458
+ json_body=self._with_run_as_body(
459
+ {
460
+ "path": path,
461
+ "parents": parents,
462
+ "mode": mode,
463
+ }
464
+ ),
448
465
  headers={"content-type": "application/json"},
449
466
  )
450
467
  return bool(payload.get("created"))
@@ -475,7 +492,7 @@ class SandboxFilesApi:
475
492
  payload = await self._transport.request_json(
476
493
  "/sandbox/files/move",
477
494
  method="POST",
478
- json_body=payload,
495
+ json_body=self._with_run_as_body(payload),
479
496
  headers={"content-type": "application/json"},
480
497
  )
481
498
  return _normalize_file_info(payload["entry"])
@@ -493,10 +510,12 @@ class SandboxFilesApi:
493
510
  await self._transport.request_json(
494
511
  "/sandbox/files/delete",
495
512
  method="POST",
496
- json_body=SandboxFileDeleteParams(
497
- path=path,
498
- recursive=recursive,
499
- ).model_dump(exclude_none=True),
513
+ json_body=self._with_run_as_body(
514
+ SandboxFileDeleteParams(
515
+ path=path,
516
+ recursive=recursive,
517
+ ).model_dump(exclude_none=True)
518
+ ),
500
519
  headers={"content-type": "application/json"},
501
520
  )
502
521
 
@@ -527,12 +546,14 @@ class SandboxFilesApi:
527
546
  payload = await self._transport.request_json(
528
547
  "/sandbox/files/copy",
529
548
  method="POST",
530
- json_body={
531
- "from": normalized.source,
532
- "to": normalized.destination,
533
- "recursive": normalized.recursive,
534
- "overwrite": normalized.overwrite,
535
- },
549
+ json_body=self._with_run_as_body(
550
+ {
551
+ "from": normalized.source,
552
+ "to": normalized.destination,
553
+ "recursive": normalized.recursive,
554
+ "overwrite": normalized.overwrite,
555
+ }
556
+ ),
536
557
  headers={"content-type": "application/json"},
537
558
  )
538
559
  return _normalize_file_info(payload["entry"])
@@ -558,7 +579,7 @@ class SandboxFilesApi:
558
579
  await self._transport.request_json(
559
580
  "/sandbox/files/chmod",
560
581
  method="POST",
561
- json_body=normalized.model_dump(exclude_none=True),
582
+ json_body=self._with_run_as_body(normalized.model_dump(exclude_none=True)),
562
583
  headers={"content-type": "application/json"},
563
584
  )
564
585
 
@@ -585,7 +606,7 @@ class SandboxFilesApi:
585
606
  await self._transport.request_json(
586
607
  "/sandbox/files/chown",
587
608
  method="POST",
588
- json_body=normalized.model_dump(exclude_none=True),
609
+ json_body=self._with_run_as_body(normalized.model_dump(exclude_none=True)),
589
610
  headers={"content-type": "application/json"},
590
611
  )
591
612
 
@@ -593,10 +614,12 @@ class SandboxFilesApi:
593
614
  payload = await self._transport.request_json(
594
615
  "/sandbox/files/watch",
595
616
  method="POST",
596
- json_body={
597
- "path": path,
598
- "recursive": recursive,
599
- },
617
+ json_body=self._with_run_as_body(
618
+ {
619
+ "path": path,
620
+ "recursive": recursive,
621
+ }
622
+ ),
600
623
  headers={"content-type": "application/json"},
601
624
  )
602
625
  return SandboxFileWatchHandle(
@@ -646,11 +669,13 @@ class SandboxFilesApi:
646
669
  payload = await self._transport.request_json(
647
670
  "/sandbox/files/presign-upload",
648
671
  method="POST",
649
- json_body=SandboxPresignFileParams(
650
- path=path,
651
- expires_in_seconds=expires_in_seconds,
652
- one_time=one_time,
653
- ).model_dump(exclude_none=True, by_alias=True),
672
+ json_body=self._with_run_as_body(
673
+ SandboxPresignFileParams(
674
+ path=path,
675
+ expires_in_seconds=expires_in_seconds,
676
+ one_time=one_time,
677
+ ).model_dump(exclude_none=True, by_alias=True)
678
+ ),
654
679
  headers={"content-type": "application/json"},
655
680
  )
656
681
  return SandboxPresignedUrl(**payload)
@@ -665,11 +690,13 @@ class SandboxFilesApi:
665
690
  payload = await self._transport.request_json(
666
691
  "/sandbox/files/presign-download",
667
692
  method="POST",
668
- json_body=SandboxPresignFileParams(
669
- path=path,
670
- expires_in_seconds=expires_in_seconds,
671
- one_time=one_time,
672
- ).model_dump(exclude_none=True, by_alias=True),
693
+ json_body=self._with_run_as_body(
694
+ SandboxPresignFileParams(
695
+ path=path,
696
+ expires_in_seconds=expires_in_seconds,
697
+ one_time=one_time,
698
+ ).model_dump(exclude_none=True, by_alias=True)
699
+ ),
673
700
  headers={"content-type": "application/json"},
674
701
  )
675
702
  return SandboxPresignedUrl(**payload)
@@ -685,12 +712,14 @@ class SandboxFilesApi:
685
712
  payload = await self._transport.request_json(
686
713
  "/sandbox/files/read",
687
714
  method="POST",
688
- json_body={
689
- "path": path,
690
- "offset": offset,
691
- "length": length,
692
- "encoding": encoding,
693
- },
715
+ json_body=self._with_run_as_body(
716
+ {
717
+ "path": path,
718
+ "offset": offset,
719
+ "length": length,
720
+ "encoding": encoding,
721
+ }
722
+ ),
694
723
  headers={"content-type": "application/json"},
695
724
  )
696
725
  return SandboxFileReadResult(**payload)
@@ -707,13 +736,31 @@ class SandboxFilesApi:
707
736
  payload = await self._transport.request_json(
708
737
  "/sandbox/files/write",
709
738
  method="POST",
710
- json_body={
711
- "path": path,
712
- "data": data,
713
- "append": append,
714
- "mode": mode,
715
- "encoding": encoding,
716
- },
739
+ json_body=self._with_run_as_body(
740
+ {
741
+ "path": path,
742
+ "data": data,
743
+ "append": append,
744
+ "mode": mode,
745
+ "encoding": encoding,
746
+ }
747
+ ),
717
748
  headers={"content-type": "application/json"},
718
749
  )
719
750
  return _normalize_write_info(payload["files"][0])
751
+
752
+ def _with_run_as_params(
753
+ self, params: Dict[str, Union[str, int, bool, None]]
754
+ ) -> Dict[str, Union[str, int, bool, None]]:
755
+ if not self._default_run_as:
756
+ return params
757
+ enriched = dict(params)
758
+ enriched["runAs"] = self._default_run_as
759
+ return enriched
760
+
761
+ def _with_run_as_body(self, body: Dict[str, Any]) -> Dict[str, Any]:
762
+ if not self._default_run_as:
763
+ return body
764
+ enriched = dict(body)
765
+ enriched["runAs"] = self._default_run_as
766
+ return enriched
@@ -10,6 +10,7 @@ from .....models.sandbox import (
10
10
  SandboxProcessStdinParams,
11
11
  SandboxProcessSummary,
12
12
  )
13
+ from ...sandboxes.shared import _normalize_exec_params
13
14
  from .sandbox_transport import RuntimeTransport
14
15
 
15
16
  DEFAULT_PROCESS_KILL_WAIT_SECONDS = 5.0
@@ -147,24 +148,54 @@ class SandboxProcessesApi:
147
148
  def __init__(self, transport: RuntimeTransport):
148
149
  self._transport = transport
149
150
 
150
- async def exec(self, input: SandboxExecParams) -> SandboxProcessResult:
151
- if not isinstance(input, SandboxExecParams):
152
- raise TypeError("input must be a SandboxExecParams instance")
151
+ async def exec(
152
+ self,
153
+ input: Union[str, SandboxExecParams],
154
+ *,
155
+ cwd: Optional[str] = None,
156
+ env: Optional[Dict[str, str]] = None,
157
+ timeout_ms: Optional[int] = None,
158
+ timeout_sec: Optional[int] = None,
159
+ run_as: Optional[str] = None,
160
+ ) -> SandboxProcessResult:
161
+ params = _normalize_exec_params(
162
+ input,
163
+ cwd=cwd,
164
+ env=env,
165
+ timeout_ms=timeout_ms,
166
+ timeout_sec=timeout_sec,
167
+ run_as=run_as,
168
+ )
153
169
  payload = await self._transport.request_json(
154
170
  "/sandbox/exec",
155
171
  method="POST",
156
- json_body=input.model_dump(exclude_none=True, by_alias=True),
172
+ json_body=params.model_dump(exclude_none=True, by_alias=True),
157
173
  headers={"content-type": "application/json"},
158
174
  )
159
175
  return SandboxProcessResult(**payload["result"])
160
176
 
161
- async def start(self, input: SandboxExecParams) -> SandboxProcessHandle:
162
- if not isinstance(input, SandboxExecParams):
163
- raise TypeError("input must be a SandboxExecParams instance")
177
+ async def start(
178
+ self,
179
+ input: Union[str, SandboxExecParams],
180
+ *,
181
+ cwd: Optional[str] = None,
182
+ env: Optional[Dict[str, str]] = None,
183
+ timeout_ms: Optional[int] = None,
184
+ timeout_sec: Optional[int] = None,
185
+ run_as: Optional[str] = None,
186
+ ) -> SandboxProcessHandle:
187
+ params = _normalize_exec_params(
188
+ input,
189
+ cwd=cwd,
190
+ env=env,
191
+ timeout_ms=timeout_ms,
192
+ timeout_sec=timeout_sec,
193
+ run_as=run_as,
194
+ )
164
195
  payload = await self._transport.request_json(
165
196
  "/sandbox/processes",
166
197
  method="POST",
167
- json_body=input.model_dump(exclude_none=True, by_alias=True),
198
+ json_body=params.model_dump(exclude_none=True, by_alias=True),
168
199
  headers={"content-type": "application/json"},
169
200
  )
170
201
  return SandboxProcessHandle(
@@ -1,11 +1,13 @@
1
1
  import base64
2
2
  import posixpath
3
+ import re
3
4
  from datetime import datetime, timedelta, timezone
4
5
  from typing import Dict, Optional, Union
5
6
  from urllib.parse import urlencode, urlsplit, urlunsplit
6
7
 
7
8
  from ....exceptions import HyperbrowserError
8
9
  from ....models.sandbox import (
10
+ SandboxExecParams,
9
11
  SandboxFileInfo,
10
12
  SandboxFileWriteEntry,
11
13
  SandboxFileWriteInfo,
@@ -18,12 +20,70 @@ from ....sandbox_common import (
18
20
  )
19
21
 
20
22
  DEFAULT_WATCH_TIMEOUT_MS = 60_000
23
+ SHELL_SAFE_TOKEN_PATTERN = re.compile(r"^[A-Za-z0-9_@%+=:,./-]+$")
21
24
 
22
25
 
23
26
  def _copy_model(model):
24
27
  return model.model_copy(deep=True)
25
28
 
26
29
 
30
+ def _quote_shell_token(token: str) -> str:
31
+ if token == "":
32
+ return "''"
33
+ if SHELL_SAFE_TOKEN_PATTERN.fullmatch(token):
34
+ return token
35
+ return "'" + token.replace("'", "'\"'\"'") + "'"
36
+
37
+
38
+ def _normalize_legacy_process_fields(params: SandboxExecParams) -> SandboxExecParams:
39
+ updates = {}
40
+
41
+ if params.args:
42
+ updates["command"] = " ".join(
43
+ _quote_shell_token(token) for token in [params.command, *params.args]
44
+ )
45
+
46
+ if params.args is not None:
47
+ updates["args"] = None
48
+
49
+ if params.use_shell is not None:
50
+ updates["use_shell"] = None
51
+
52
+ return params.model_copy(update=updates) if updates else params
53
+
54
+
55
+ def _normalize_exec_params(
56
+ input: Union[str, SandboxExecParams],
57
+ *,
58
+ cwd: Optional[str] = None,
59
+ env: Optional[Dict[str, str]] = None,
60
+ timeout_ms: Optional[int] = None,
61
+ timeout_sec: Optional[int] = None,
62
+ run_as: Optional[str] = None,
63
+ ) -> SandboxExecParams:
64
+ if isinstance(input, str):
65
+ params = SandboxExecParams(command=input)
66
+ elif isinstance(input, SandboxExecParams):
67
+ params = input
68
+ else:
69
+ raise TypeError("input must be a command string or SandboxExecParams instance")
70
+
71
+ updates = {}
72
+ if cwd is not None:
73
+ updates["cwd"] = cwd
74
+ if env is not None:
75
+ updates["env"] = env
76
+ if timeout_ms is not None:
77
+ updates["timeout_ms"] = timeout_ms
78
+ if timeout_sec is not None:
79
+ updates["timeout_sec"] = timeout_sec
80
+ if run_as is not None:
81
+ updates["run_as"] = run_as
82
+
83
+ normalized = params.model_copy(update=updates) if updates else params
84
+ return _normalize_legacy_process_fields(normalized)
85
+
86
+
27
87
  def _build_sandbox_exposed_url(runtime, port: int) -> str:
28
88
  parsed = urlsplit(runtime.base_url)
29
89
  hostname = parsed.hostname
@@ -217,16 +217,24 @@ class SandboxHandle:
217
217
  )
218
218
  return _copy_model(self._runtime_session)
219
219
 
220
- def exec(self, input: Union[str, SandboxExecParams]):
221
- if isinstance(input, str):
222
- params = SandboxExecParams(command=input)
223
- else:
224
- if not isinstance(input, SandboxExecParams):
225
- raise TypeError(
226
- "input must be a command string or SandboxExecParams instance"
227
- )
228
- params = input
229
- return self.processes.exec(params)
220
+ def exec(
221
+ self,
222
+ input: Union[str, SandboxExecParams],
223
+ *,
224
+ cwd: Optional[str] = None,
225
+ env: Optional[Dict[str, str]] = None,
226
+ timeout_ms: Optional[int] = None,
227
+ timeout_sec: Optional[int] = None,
228
+ run_as: Optional[str] = None,
229
+ ):
230
+ return self.processes.exec(
231
+ input,
232
+ cwd=cwd,
233
+ env=env,
234
+ timeout_ms=timeout_ms,
235
+ timeout_sec=timeout_sec,
236
+ run_as=run_as,
237
+ )
230
238
 
231
239
  def get_process(self, process_id: str) -> SandboxProcessHandle:
232
240
  return self.processes.get(process_id)