hyperbrowser 1.11.0__tar.gz → 1.11.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 (132) hide show
  1. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/PKG-INFO +100 -1
  2. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/README.md +98 -0
  3. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/async_client.py +2 -0
  4. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandbox.py +60 -7
  5. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +152 -6
  6. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +9 -1
  7. hyperbrowser-1.11.1/hyperbrowser/client/managers/async_manager/secret.py +208 -0
  8. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/shared.py +52 -1
  9. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandbox.py +58 -7
  10. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +141 -6
  11. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +9 -1
  12. hyperbrowser-1.11.1/hyperbrowser/client/managers/sync_manager/secret.py +200 -0
  13. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/sync.py +2 -0
  14. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/__init__.py +31 -1
  15. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/consts.py +1 -0
  16. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/sandbox.py +27 -0
  17. hyperbrowser-1.11.1/hyperbrowser/models/secret.py +173 -0
  18. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/sandbox_common.py +100 -8
  19. hyperbrowser-1.11.1/hyperbrowser/secrets_common.py +388 -0
  20. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/transport/async_transport.py +6 -7
  21. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/transport/sync.py +6 -7
  22. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/__init__.py +12 -1
  23. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/sandbox.py +4 -0
  24. hyperbrowser-1.11.1/hyperbrowser/types/secret.py +45 -0
  25. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/pyproject.toml +2 -1
  26. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/LICENSE +0 -0
  27. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/__init__.py +0 -0
  28. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/build_context.py +0 -0
  29. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/_request.py +0 -0
  30. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/base.py +0 -0
  31. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  32. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  33. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  34. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  35. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  36. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
  37. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  38. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
  39. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
  40. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  41. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  42. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  43. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  44. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  45. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  46. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
  47. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  48. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  49. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  50. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  51. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
  52. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  53. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  54. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  55. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/webmcp.py +0 -0
  56. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  57. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
  58. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
  59. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
  60. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/image_resolution.py +0 -0
  61. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
  62. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  63. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  64. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  65. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  66. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  67. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
  68. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  69. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
  70. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
  71. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  72. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  73. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  74. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  75. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  76. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  77. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
  78. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  79. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  80. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  81. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  82. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
  83. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  84. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  85. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  86. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/webmcp.py +0 -0
  87. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/config.py +0 -0
  88. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/exceptions.py +0 -0
  89. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/image_builds.py +0 -0
  90. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/_parsers.py +0 -0
  91. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/browser_use.py +0 -0
  92. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  93. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/cua.py +0 -0
  94. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  95. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
  96. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  97. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
  98. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
  99. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/computer_action.py +0 -0
  100. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/crawl.py +0 -0
  101. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/extension.py +0 -0
  102. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/extract.py +0 -0
  103. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/profile.py +0 -0
  104. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/scrape.py +0 -0
  105. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/session.py +0 -0
  106. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/team.py +0 -0
  107. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/volume.py +0 -0
  108. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/batch_fetch.py +0 -0
  109. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/branding.py +0 -0
  110. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/common.py +0 -0
  111. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/crawl.py +0 -0
  112. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/fetch.py +0 -0
  113. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/search.py +0 -0
  114. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/models/webmcp.py +0 -0
  115. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/py.typed +0 -0
  116. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/__init__.py +0 -0
  117. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/anthropic.py +0 -0
  118. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/openai.py +0 -0
  119. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/schema.py +0 -0
  120. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/transport/base.py +0 -0
  121. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/_json.py +0 -0
  122. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/agents.py +0 -0
  123. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/computer_action.py +0 -0
  124. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/crawl.py +0 -0
  125. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/extension.py +0 -0
  126. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/extract.py +0 -0
  127. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/profile.py +0 -0
  128. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/scrape.py +0 -0
  129. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/session.py +0 -0
  130. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/volume.py +0 -0
  131. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/web.py +0 -0
  132. {hyperbrowser-1.11.0 → hyperbrowser-1.11.1}/hyperbrowser/types/webmcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 1.11.0
3
+ Version: 1.11.1
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Programming Language :: Python :: 3.15
20
+ Requires-Dist: cryptography (>=41.0.0)
20
21
  Requires-Dist: httpx (>=0.23.0,<1)
21
22
  Requires-Dist: jsonref (>=1.1.0)
22
23
  Requires-Dist: pydantic (>=2.0,<3)
@@ -289,6 +290,104 @@ client.volumes.delete(same_volume.id)
289
290
  client.close()
290
291
  ```
291
292
 
293
+ ### Manage secrets and pass them to sandbox processes
294
+
295
+ Secrets hold values such as API keys for sandbox processes. Values are
296
+ encrypted on your machine to the platform's public key before upload, and the
297
+ API never returns them. Methods that take a secret accept its ID or exact name.
298
+
299
+ ```python
300
+ from pathlib import Path
301
+
302
+ from hyperbrowser import Hyperbrowser
303
+
304
+ client = Hyperbrowser(api_key="test-key")
305
+
306
+ # Values are str or bytes and are stored byte-for-byte, trailing newlines included.
307
+ client.secrets.create(
308
+ {
309
+ "name": "provider-key",
310
+ "description": "Provider API key",
311
+ "value": Path("provider-key.txt").read_bytes(),
312
+ }
313
+ )
314
+ client.secrets.add_version("provider-key", "rotated-value") # becomes version 2
315
+ detail = client.secrets.get("provider-key") # metadata and versions, never the value
316
+ client.secrets.list({"search": "provider"})
317
+
318
+ sandbox = client.sandboxes.create(
319
+ {
320
+ "image_name": "node",
321
+ # Delivered to the image's startup command (image launches only).
322
+ "secrets": {"API_KEY": "provider-key"},
323
+ }
324
+ )
325
+
326
+ # A plain name uses the latest version; {"name": ..., "version": N} pins one.
327
+ result = sandbox.exec(
328
+ "node app.js",
329
+ secrets={"API_KEY": {"name": "provider-key", "version": 2}},
330
+ )
331
+ process = sandbox.processes.start("node worker.js", secrets={"API_KEY": "provider-key"})
332
+ print(process.secrets) # the version each variable received
333
+
334
+ client.secrets.delete_version("provider-key", 1)
335
+ client.secrets.delete("provider-key")
336
+ client.close()
337
+ ```
338
+
339
+ Things to know:
340
+
341
+ - A secret is set only in the environment of the process it was passed to.
342
+ Anything else running as the same user, or as root, in the sandbox can read
343
+ that environment.
344
+ - A sandbox that has received secrets, and snapshots taken of it, report
345
+ `contains_secrets=True`. Restoring such a snapshot needs the same access as
346
+ passing secrets.
347
+ - Deleting a secret or version doesn't revoke a value already delivered to a
348
+ sandbox.
349
+ - Limits: 100 secrets per team, 10 versions per secret, 32 secrets per process,
350
+ and values of 1 byte to 32 KiB of UTF-8 without NUL bytes. Names are 1-128
351
+ letters, digits, `_` or `-`, starting with a letter or digit.
352
+ - Reading secrets needs `secrets.read`; delivering them to a sandbox needs
353
+ `secrets.use`. Creating a secret needs `secrets.manage`. Updating, rotating,
354
+ and deleting secrets or versions through the SDK need both `secrets.read`
355
+ and `secrets.manage`, since it resolves IDs and names through metadata reads.
356
+ `add_version()` also reads the secret's `encryptionContext` to encrypt its
357
+ value. The team needs the secrets feature.
358
+ - Secret-started process handles wait indefinitely by default with `wait()` or
359
+ `result()`. A positive `wait(timeout_ms=...)` or `wait(timeout_sec=...)` limits
360
+ the total local wait and raises `TimeoutError` (`asyncio.TimeoutError` for
361
+ async handles); the process continues and the handle can be used to wait
362
+ again. The start's timeout limits the command's execution separately.
363
+ The sync client checks the limit between requests and caps each request's
364
+ HTTP timeouts at the time left, so a slow response or a runtime token
365
+ refresh can overrun it; use the async client for a strict limit.
366
+ Secret process output comes from the receiver's retained output, so
367
+ `max_output_bytes` does not apply; incomplete retained output raises an error.
368
+ - A process start with secrets that times out, or whose result the API can't
369
+ confirm, raises a non-retryable error with code
370
+ `secret_process_outcome_unknown`: the command may be running, so check
371
+ `sandbox.processes.list()` before retrying. When the API reported the
372
+ unknown outcome itself, its response body, including its own code
373
+ (`SECRET_PROCESS_OUTCOME_UNKNOWN`), is in the error's `details`.
374
+ - `client.sandboxes.create()` with `secrets` raises the same
375
+ `secret_process_outcome_unknown` error when the create may have reached the
376
+ API but its result is unknown (a timeout, a dropped connection, or an
377
+ unreadable response): a sandbox may be running its startup command with the
378
+ secrets, so list sandboxes before retrying.
379
+ - Error codes from the API:
380
+ - `SECRETS_ADMIN_REQUIRED` (403): the request used an `hx auth login`
381
+ session of a team member who isn't an owner or admin. API keys are
382
+ governed by their scopes instead.
383
+ - `SECRETS_FEATURE_DISABLED` (403): the team doesn't have the secrets
384
+ feature.
385
+ - `SECRETS_NOT_CONFIGURED` (503): the platform has no secrets key
386
+ configured. It isn't retried.
387
+ - `SECRET_STARTUP_UNSUPPORTED` (400): the image has no startup command to
388
+ deliver `secrets` to. Create the sandbox without them, then pass them to
389
+ `sandbox.exec()` or `sandbox.processes.start()`.
390
+
292
391
  ### List sandboxes with filters
293
392
 
294
393
  ```python
@@ -261,6 +261,104 @@ client.volumes.delete(same_volume.id)
261
261
  client.close()
262
262
  ```
263
263
 
264
+ ### Manage secrets and pass them to sandbox processes
265
+
266
+ Secrets hold values such as API keys for sandbox processes. Values are
267
+ encrypted on your machine to the platform's public key before upload, and the
268
+ API never returns them. Methods that take a secret accept its ID or exact name.
269
+
270
+ ```python
271
+ from pathlib import Path
272
+
273
+ from hyperbrowser import Hyperbrowser
274
+
275
+ client = Hyperbrowser(api_key="test-key")
276
+
277
+ # Values are str or bytes and are stored byte-for-byte, trailing newlines included.
278
+ client.secrets.create(
279
+ {
280
+ "name": "provider-key",
281
+ "description": "Provider API key",
282
+ "value": Path("provider-key.txt").read_bytes(),
283
+ }
284
+ )
285
+ client.secrets.add_version("provider-key", "rotated-value") # becomes version 2
286
+ detail = client.secrets.get("provider-key") # metadata and versions, never the value
287
+ client.secrets.list({"search": "provider"})
288
+
289
+ sandbox = client.sandboxes.create(
290
+ {
291
+ "image_name": "node",
292
+ # Delivered to the image's startup command (image launches only).
293
+ "secrets": {"API_KEY": "provider-key"},
294
+ }
295
+ )
296
+
297
+ # A plain name uses the latest version; {"name": ..., "version": N} pins one.
298
+ result = sandbox.exec(
299
+ "node app.js",
300
+ secrets={"API_KEY": {"name": "provider-key", "version": 2}},
301
+ )
302
+ process = sandbox.processes.start("node worker.js", secrets={"API_KEY": "provider-key"})
303
+ print(process.secrets) # the version each variable received
304
+
305
+ client.secrets.delete_version("provider-key", 1)
306
+ client.secrets.delete("provider-key")
307
+ client.close()
308
+ ```
309
+
310
+ Things to know:
311
+
312
+ - A secret is set only in the environment of the process it was passed to.
313
+ Anything else running as the same user, or as root, in the sandbox can read
314
+ that environment.
315
+ - A sandbox that has received secrets, and snapshots taken of it, report
316
+ `contains_secrets=True`. Restoring such a snapshot needs the same access as
317
+ passing secrets.
318
+ - Deleting a secret or version doesn't revoke a value already delivered to a
319
+ sandbox.
320
+ - Limits: 100 secrets per team, 10 versions per secret, 32 secrets per process,
321
+ and values of 1 byte to 32 KiB of UTF-8 without NUL bytes. Names are 1-128
322
+ letters, digits, `_` or `-`, starting with a letter or digit.
323
+ - Reading secrets needs `secrets.read`; delivering them to a sandbox needs
324
+ `secrets.use`. Creating a secret needs `secrets.manage`. Updating, rotating,
325
+ and deleting secrets or versions through the SDK need both `secrets.read`
326
+ and `secrets.manage`, since it resolves IDs and names through metadata reads.
327
+ `add_version()` also reads the secret's `encryptionContext` to encrypt its
328
+ value. The team needs the secrets feature.
329
+ - Secret-started process handles wait indefinitely by default with `wait()` or
330
+ `result()`. A positive `wait(timeout_ms=...)` or `wait(timeout_sec=...)` limits
331
+ the total local wait and raises `TimeoutError` (`asyncio.TimeoutError` for
332
+ async handles); the process continues and the handle can be used to wait
333
+ again. The start's timeout limits the command's execution separately.
334
+ The sync client checks the limit between requests and caps each request's
335
+ HTTP timeouts at the time left, so a slow response or a runtime token
336
+ refresh can overrun it; use the async client for a strict limit.
337
+ Secret process output comes from the receiver's retained output, so
338
+ `max_output_bytes` does not apply; incomplete retained output raises an error.
339
+ - A process start with secrets that times out, or whose result the API can't
340
+ confirm, raises a non-retryable error with code
341
+ `secret_process_outcome_unknown`: the command may be running, so check
342
+ `sandbox.processes.list()` before retrying. When the API reported the
343
+ unknown outcome itself, its response body, including its own code
344
+ (`SECRET_PROCESS_OUTCOME_UNKNOWN`), is in the error's `details`.
345
+ - `client.sandboxes.create()` with `secrets` raises the same
346
+ `secret_process_outcome_unknown` error when the create may have reached the
347
+ API but its result is unknown (a timeout, a dropped connection, or an
348
+ unreadable response): a sandbox may be running its startup command with the
349
+ secrets, so list sandboxes before retrying.
350
+ - Error codes from the API:
351
+ - `SECRETS_ADMIN_REQUIRED` (403): the request used an `hx auth login`
352
+ session of a team member who isn't an owner or admin. API keys are
353
+ governed by their scopes instead.
354
+ - `SECRETS_FEATURE_DISABLED` (403): the team doesn't have the secrets
355
+ feature.
356
+ - `SECRETS_NOT_CONFIGURED` (503): the platform has no secrets key
357
+ configured. It isn't retried.
358
+ - `SECRET_STARTUP_UNSUPPORTED` (400): the image has no startup command to
359
+ deliver `secrets` to. Create the sandbox without them, then pass them to
360
+ `sandbox.exec()` or `sandbox.processes.start()`.
361
+
264
362
  ### List sandboxes with filters
265
363
 
266
364
  ```python
@@ -10,6 +10,7 @@ from .managers.async_manager.extension import ExtensionManager
10
10
  from .managers.async_manager.extract import ExtractManager
11
11
  from .managers.async_manager.profile import ProfileManager
12
12
  from .managers.async_manager.sandbox import SandboxManager
13
+ from .managers.async_manager.secret import SecretManager
13
14
  from .managers.async_manager.scrape import ScrapeManager
14
15
  from .managers.async_manager.session import SessionManager
15
16
  from .managers.async_manager.team import TeamManager
@@ -49,6 +50,7 @@ class AsyncHyperbrowser(HyperbrowserBase):
49
50
  self.computer_action = ComputerActionManager(self)
50
51
  self.sandboxes = SandboxManager(self)
51
52
  self.volumes = VolumeManager(self)
53
+ self.secrets = SecretManager(self)
52
54
 
53
55
  async def close(self) -> None:
54
56
  await self.transport.close()
@@ -41,6 +41,13 @@ from ....models.sandbox import (
41
41
  StartSandboxFromSnapshotParams,
42
42
  )
43
43
  from ....models.session import BasicResponse
44
+ from ....secrets_common import (
45
+ missing_process_id_error,
46
+ request_may_have_arrived,
47
+ sandbox_create_outcome_unknown_error,
48
+ secret_process_id,
49
+ secret_process_start_error,
50
+ )
44
51
  from ....types import (
45
52
  CompleteSandboxImageBuildParams as CompleteSandboxImageBuildParamsDict,
46
53
  CreateSandboxImageBuildParams as CreateSandboxImageBuildParamsDict,
@@ -72,6 +79,7 @@ from ..sandboxes.image_resolution import (
72
79
  completed_image_id,
73
80
  )
74
81
  from ..sandboxes.shared import (
82
+ SecretReferences,
75
83
  _build_sandbox_exposed_url,
76
84
  _copy_model,
77
85
  _expires_within_buffer,
@@ -143,7 +151,11 @@ class SandboxHandle:
143
151
  service.runtime_timeout,
144
152
  service.runtime_proxy_override,
145
153
  )
146
- self.processes = SandboxProcessesApi(self._transport)
154
+ self.processes = SandboxProcessesApi(
155
+ self._transport,
156
+ self._start_secret_process,
157
+ detail.id,
158
+ )
147
159
  self.files = SandboxFilesApi(
148
160
  self._transport,
149
161
  self._resolve_runtime_socket_info,
@@ -318,7 +330,16 @@ class SandboxHandle:
318
330
  timeout_sec: Optional[int] = None,
319
331
  run_as: Optional[str] = None,
320
332
  max_output_bytes: int = 64 * 1024 * 1024,
333
+ secrets: Optional[SecretReferences] = None,
321
334
  ):
335
+ """Run a command to completion.
336
+
337
+ ``secrets`` maps environment variable names to stored secrets (a name,
338
+ or ``{"name": ..., "version": N}``). Anything else running as the
339
+ same user, or as root, in the sandbox can read a process's
340
+ environment, and the sandbox and its later snapshots are marked as
341
+ containing secrets.
342
+ """
322
343
  return await self.processes.exec(
323
344
  input,
324
345
  cwd=cwd,
@@ -327,8 +348,12 @@ class SandboxHandle:
327
348
  timeout_sec=timeout_sec,
328
349
  run_as=run_as,
329
350
  max_output_bytes=max_output_bytes,
351
+ secrets=secrets,
330
352
  )
331
353
 
354
+ async def _start_secret_process(self, payload: Dict[str, object]) -> dict:
355
+ return await self._service.start_secret_process(self.id, payload)
356
+
332
357
  async def get_process(self, process_id: str) -> SandboxProcessHandle:
333
358
  return await self.processes.get(process_id)
334
359
 
@@ -1127,16 +1152,44 @@ class SandboxManager:
1127
1152
  )
1128
1153
  return SandboxUnexposeResult(**payload)
1129
1154
 
1155
+ async def start_secret_process(
1156
+ self, sandbox_id: str, payload: Dict[str, object]
1157
+ ) -> dict:
1158
+ """Start a sandbox process with secrets through the control API.
1159
+
1160
+ Returns the raw ``{"process": ..., "secrets": [...]}`` response. Prefer
1161
+ ``sandbox.exec(..., secrets=...)`` or ``sandbox.processes.start``.
1162
+ """
1163
+ try:
1164
+ response = await self._request(
1165
+ "POST", f"/sandbox/{sandbox_id}/processes", data=payload
1166
+ )
1167
+ except HyperbrowserError as error:
1168
+ raise secret_process_start_error(sandbox_id, error)
1169
+ process = response.get("process") if isinstance(response, dict) else None
1170
+ process_id = secret_process_id(process)
1171
+ if not process_id:
1172
+ raise missing_process_id_error(sandbox_id)
1173
+ return dict(response, process=dict(process, id=process_id))
1174
+
1130
1175
  async def _create_detail(
1131
1176
  self,
1132
1177
  params: Union[CreateSandboxParamsDict, CreateSandboxParams],
1133
1178
  ) -> SandboxDetail:
1134
- payload = await self._request(
1135
- "POST",
1136
- "/sandbox",
1137
- data=dump_request(params, CreateSandboxParams),
1138
- )
1139
- return SandboxDetail(**payload)
1179
+ normalized = coerce_request(params, CreateSandboxParams)
1180
+ data = dump_request(normalized, CreateSandboxParams)
1181
+ try:
1182
+ payload = await self._request("POST", "/sandbox", data=data)
1183
+ detail = SandboxDetail(**payload)
1184
+ if normalized.secrets and not detail.id.strip():
1185
+ raise ValueError("The API accepted the create but returned no sandbox ID")
1186
+ return detail
1187
+ except Exception as error:
1188
+ # A create that may have reached the API may be running its
1189
+ # startup command with the secrets, so it must not look retryable.
1190
+ if normalized.secrets and request_may_have_arrived(error):
1191
+ raise sandbox_create_outcome_unknown_error(error)
1192
+ raise
1140
1193
 
1141
1194
  async def _request(
1142
1195
  self,
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
2
  import base64
3
- from typing import AsyncIterator, Dict, Optional, Union
3
+ import time
4
+ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, Union
4
5
 
5
- from ...._request import coerce_request, dump_request
6
+ from ...._request import coerce_request
6
7
  from .....models.sandbox import (
7
8
  SandboxExecParams,
8
9
  SandboxProcessExitEvent,
@@ -12,11 +13,24 @@ from .....models.sandbox import (
12
13
  SandboxProcessStdinParams,
13
14
  SandboxProcessSummary,
14
15
  )
16
+ from .....exceptions import HyperbrowserError
17
+ from .....models.secret import SandboxResolvedSecret
18
+ from .....secrets_common import (
19
+ SecretProcessWait,
20
+ SecretProcessWaitContext,
21
+ secret_process_failed_error,
22
+ secret_process_id,
23
+ )
15
24
  from .....types import (
16
25
  SandboxExecParams as SandboxExecParamsDict,
17
26
  SandboxProcessStdinParams as SandboxProcessStdinParamsDict,
18
27
  )
19
- from ...sandboxes.shared import _normalize_exec_params
28
+ from ...sandboxes.shared import (
29
+ SecretReferences,
30
+ _normalize_exec_params,
31
+ _runtime_process_payload,
32
+ _secret_process_payload,
33
+ )
20
34
  from ...sandboxes.process_output import (
21
35
  DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
22
36
  ProcessOutput,
@@ -28,13 +42,22 @@ DEFAULT_PROCESS_KILL_WAIT_SECONDS = 5.0
28
42
 
29
43
 
30
44
  class SandboxProcessHandle:
31
- def __init__(self, transport: RuntimeTransport, summary: SandboxProcessSummary):
45
+ def __init__(
46
+ self,
47
+ transport: RuntimeTransport,
48
+ summary: SandboxProcessSummary,
49
+ secret_wait_context: Optional[SecretProcessWaitContext] = None,
50
+ ):
32
51
  self._transport = transport
33
52
  self._summary = summary
53
+ self._secret_wait_context = secret_wait_context
34
54
  self._output = None
35
55
  self._collector = None
36
56
  self._events = None
37
57
  self._changed = asyncio.Event()
58
+ # The secret versions this process received, when it was started
59
+ # with secrets; None otherwise.
60
+ self.secrets: Optional[List[SandboxResolvedSecret]] = None
38
61
 
39
62
  @property
40
63
  def id(self) -> str:
@@ -69,6 +92,53 @@ class SandboxProcessHandle:
69
92
  if not self._collector.done():
70
93
  await asyncio.wait_for(asyncio.shield(self._collector), timeout)
71
94
  return self._collected_result()
95
+ if self._secret_wait_context is not None:
96
+ return await self._wait_with_secrets(timeout_ms, timeout_sec)
97
+ return await self._wait_once(timeout_ms, timeout_sec)
98
+
99
+ async def _wait_with_secrets(
100
+ self, timeout_ms: Optional[int], timeout_sec: Optional[int]
101
+ ) -> SandboxProcessResult:
102
+ context = self._secret_wait_context
103
+ assert context is not None
104
+ wait = SecretProcessWait(
105
+ context, timeout_ms, timeout_sec, timeout_error=asyncio.TimeoutError
106
+ )
107
+ while True:
108
+ chunk_ms, request_timeout, remaining = wait.request_budget()
109
+ started_at = time.monotonic()
110
+ try:
111
+ request = self._wait_once(chunk_ms, None, request_timeout)
112
+ result = await asyncio.wait_for(request, remaining)
113
+ wait.remaining()
114
+ return result
115
+ except (TimeoutError, asyncio.TimeoutError) as error:
116
+ if wait.deadline is not None:
117
+ raise asyncio.TimeoutError(
118
+ "Timed out waiting for command output"
119
+ ) from error
120
+ raise secret_process_failed_error(
121
+ context.sandbox_id, self.id, "waiting for its result", error
122
+ )
123
+ except Exception as error:
124
+ # An explicit local deadline keeps TimeoutError semantics. It
125
+ # ends only this wait; callers can wait again on the handle.
126
+ wait.remaining()
127
+ if not isinstance(error, HyperbrowserError) or error.status_code != 408:
128
+ raise secret_process_failed_error(
129
+ context.sandbox_id, self.id, "waiting for its result", error
130
+ )
131
+ await asyncio.sleep(wait.retry_delay(started_at))
132
+
133
+ async def _wait_once(
134
+ self,
135
+ timeout_ms: Optional[int],
136
+ timeout_sec: Optional[int],
137
+ request_timeout: Optional[float] = None,
138
+ ) -> SandboxProcessResult:
139
+ request_options: Dict[str, Any] = {}
140
+ if request_timeout is not None:
141
+ request_options["request_timeout"] = request_timeout
72
142
  payload = await self._transport.request_json(
73
143
  f"/sandbox/processes/{self.id}/wait",
74
144
  method="POST",
@@ -77,6 +147,7 @@ class SandboxProcessHandle:
77
147
  "timeout_sec": timeout_sec,
78
148
  },
79
149
  headers={"content-type": "application/json"},
150
+ **request_options,
80
151
  )
81
152
  result = SandboxProcessResult(**payload["result"])
82
153
  if result.output_truncated:
@@ -260,8 +331,17 @@ class SandboxProcessHandle:
260
331
 
261
332
 
262
333
  class SandboxProcessesApi:
263
- def __init__(self, transport: RuntimeTransport):
334
+ def __init__(
335
+ self,
336
+ transport: RuntimeTransport,
337
+ start_secret_process: Optional[
338
+ Callable[[Dict[str, object]], Awaitable[dict]]
339
+ ] = None,
340
+ sandbox_id: Optional[str] = None,
341
+ ):
264
342
  self._transport = transport
343
+ self._start_secret_process = start_secret_process
344
+ self._sandbox_id = sandbox_id
265
345
 
266
346
  async def exec(
267
347
  self,
@@ -273,6 +353,7 @@ class SandboxProcessesApi:
273
353
  timeout_sec: Optional[int] = None,
274
354
  run_as: Optional[str] = None,
275
355
  max_output_bytes: int = DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
356
+ secrets: Optional[SecretReferences] = None,
276
357
  ) -> SandboxProcessResult:
277
358
  handle = await self.start(
278
359
  input,
@@ -282,7 +363,10 @@ class SandboxProcessesApi:
282
363
  timeout_sec=timeout_sec,
283
364
  run_as=run_as,
284
365
  max_output_bytes=max_output_bytes,
366
+ secrets=secrets,
285
367
  )
368
+ if handle.secrets is not None:
369
+ return await self._finish_secret_exec(handle)
286
370
  try:
287
371
  return await handle.wait()
288
372
  finally:
@@ -298,7 +382,14 @@ class SandboxProcessesApi:
298
382
  timeout_sec: Optional[int] = None,
299
383
  run_as: Optional[str] = None,
300
384
  max_output_bytes: int = DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
385
+ secrets: Optional[SecretReferences] = None,
301
386
  ) -> SandboxProcessHandle:
387
+ """Start a process.
388
+
389
+ With ``secrets``, the start goes through the control API, which has the
390
+ platform decrypt them into the process environment; output is then
391
+ read with ``wait()`` rather than collected from the start.
392
+ """
302
393
  validate_output_limit(max_output_bytes)
303
394
  params = _normalize_exec_params(
304
395
  input,
@@ -307,11 +398,14 @@ class SandboxProcessesApi:
307
398
  timeout_ms=timeout_ms,
308
399
  timeout_sec=timeout_sec,
309
400
  run_as=run_as,
401
+ secrets=secrets,
310
402
  )
403
+ if params.secrets:
404
+ return await self._start_with_secrets(params)
311
405
  events = self._transport.stream_sse(
312
406
  "/sandbox/processes",
313
407
  method="POST",
314
- json_body=dump_request(params, SandboxExecParams),
408
+ json_body=_runtime_process_payload(params),
315
409
  )
316
410
  try:
317
411
  started = await events.__anext__()
@@ -328,6 +422,58 @@ class SandboxProcessesApi:
328
422
  handle._collector = asyncio.create_task(handle._collect())
329
423
  return handle
330
424
 
425
+ async def _start_with_secrets(
426
+ self, params: SandboxExecParams
427
+ ) -> SandboxProcessHandle:
428
+ if self._start_secret_process is None:
429
+ raise HyperbrowserError(
430
+ "Starting a process with secrets needs a sandbox handle; use "
431
+ "client.sandboxes.get() and its processes API",
432
+ code="secrets_unsupported",
433
+ )
434
+ payload = await self._start_secret_process(_secret_process_payload(params))
435
+ process_id = secret_process_id(payload["process"])
436
+ try:
437
+ handle = SandboxProcessHandle(
438
+ self._transport,
439
+ SandboxProcessSummary(**dict(payload["process"], id=process_id)),
440
+ SecretProcessWaitContext(
441
+ self._sandbox_id or "", getattr(self._transport, "_timeout", None)
442
+ ),
443
+ )
444
+ handle.secrets = [
445
+ SandboxResolvedSecret(**entry) for entry in payload.get("secrets") or []
446
+ ]
447
+ return handle
448
+ except Exception as error:
449
+ raise secret_process_failed_error(
450
+ self._sandbox_id or "", process_id, "reading its start response", error
451
+ )
452
+
453
+ async def _finish_secret_exec(
454
+ self, handle: SandboxProcessHandle
455
+ ) -> SandboxProcessResult:
456
+ """Close stdin and wait for the result, as one-shot exec does.
457
+
458
+ The process may still be running with its secrets after a failure here,
459
+ so failures name it instead of looking like a safe retry.
460
+ """
461
+ sandbox_id = self._sandbox_id or ""
462
+ try:
463
+ await handle.write_stdin(eof=True)
464
+ except HyperbrowserError as error:
465
+ # 409: the command already exited, like a pipe whose reader quit
466
+ # early. Its result is still reported.
467
+ if error.status_code != 409:
468
+ raise secret_process_failed_error(
469
+ sandbox_id, handle.id, "closing its stdin", error
470
+ )
471
+ except Exception as error:
472
+ raise secret_process_failed_error(
473
+ sandbox_id, handle.id, "closing its stdin", error
474
+ )
475
+ return await handle.wait()
476
+
331
477
  async def get(self, process_id: str) -> SandboxProcessHandle:
332
478
  payload = await self._transport.request_json(f"/sandbox/processes/{process_id}")
333
479
  return SandboxProcessHandle(
@@ -40,6 +40,7 @@ class RuntimeTransport:
40
40
  json_body: Optional[Dict[str, object]] = None,
41
41
  content: Optional[Any] = None,
42
42
  headers: Optional[Dict[str, str]] = None,
43
+ request_timeout: Optional[float] = None,
43
44
  ):
44
45
  response = await self._request(
45
46
  path,
@@ -48,6 +49,7 @@ class RuntimeTransport:
48
49
  json_body=json_body,
49
50
  content=content,
50
51
  headers=headers,
52
+ request_timeout=request_timeout,
51
53
  )
52
54
  return parse_json_response(response, "runtime")
53
55
 
@@ -168,6 +170,7 @@ class RuntimeTransport:
168
170
  content: Optional[Any] = None,
169
171
  headers: Optional[Dict[str, str]] = None,
170
172
  allow_refresh: bool = True,
173
+ request_timeout: Optional[float] = None,
171
174
  ) -> httpx.Response:
172
175
  connection = await self._resolve_connection(False)
173
176
  response = await self._send(
@@ -178,6 +181,7 @@ class RuntimeTransport:
178
181
  json_body=json_body,
179
182
  content=content,
180
183
  headers=headers,
184
+ request_timeout=request_timeout,
181
185
  )
182
186
 
183
187
  if response.status_code == 401 and allow_refresh:
@@ -197,6 +201,7 @@ class RuntimeTransport:
197
201
  json_body=json_body,
198
202
  content=content,
199
203
  headers=headers,
204
+ request_timeout=request_timeout,
200
205
  )
201
206
  return ensure_response_ok(retry, "runtime")
202
207
 
@@ -293,6 +298,7 @@ class RuntimeTransport:
293
298
  json_body: Optional[Dict[str, object]],
294
299
  content: Optional[Any],
295
300
  headers: Optional[Dict[str, str]],
301
+ request_timeout: Optional[float] = None,
296
302
  ) -> httpx.Response:
297
303
  request_path = _build_query_path(path, params)
298
304
  target = resolve_runtime_transport_target(
@@ -301,7 +307,9 @@ class RuntimeTransport:
301
307
  self._runtime_proxy_override,
302
308
  )
303
309
  merged_headers = build_headers(connection.token, headers, target.host_header)
304
- client = httpx.AsyncClient(timeout=self._timeout)
310
+ client = httpx.AsyncClient(
311
+ timeout=self._timeout if request_timeout is None else request_timeout
312
+ )
305
313
 
306
314
  try:
307
315
  response = await client.request(