hyperbrowser 1.10.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.
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/PKG-INFO +134 -1
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/README.md +132 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/async_client.py +2 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandbox.py +60 -7
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +152 -6
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +9 -1
- hyperbrowser-1.11.1/hyperbrowser/client/managers/async_manager/secret.py +208 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/session.py +3 -0
- hyperbrowser-1.11.1/hyperbrowser/client/managers/async_manager/webmcp.py +98 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/shared.py +52 -1
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandbox.py +58 -7
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +141 -6
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +9 -1
- hyperbrowser-1.11.1/hyperbrowser/client/managers/sync_manager/secret.py +200 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/session.py +3 -0
- hyperbrowser-1.11.1/hyperbrowser/client/managers/sync_manager/webmcp.py +98 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/sync.py +2 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/__init__.py +59 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/consts.py +1 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/sandbox.py +27 -0
- hyperbrowser-1.11.1/hyperbrowser/models/secret.py +173 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/session.py +6 -0
- hyperbrowser-1.11.1/hyperbrowser/models/webmcp.py +126 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/sandbox_common.py +100 -8
- hyperbrowser-1.11.1/hyperbrowser/secrets_common.py +388 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/transport/async_transport.py +18 -3
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/transport/base.py +8 -1
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/transport/sync.py +18 -3
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/__init__.py +20 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/sandbox.py +4 -0
- hyperbrowser-1.11.1/hyperbrowser/types/secret.py +45 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/session.py +1 -0
- hyperbrowser-1.11.1/hyperbrowser/types/webmcp.py +22 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/pyproject.toml +2 -1
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/LICENSE +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/build_context.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/image_resolution.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/image_builds.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/agents.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/types/web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperbrowser
|
|
3
|
-
Version: 1.
|
|
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)
|
|
@@ -62,6 +63,40 @@ Profile names must match `^[A-Za-z0-9._-]+$`.
|
|
|
62
63
|
|
|
63
64
|
`base_url` and `HYPERBROWSER_BASE_URL` accept either `https://host` or `https://host/api`. The client normalizes both to the same control-plane base URL.
|
|
64
65
|
|
|
66
|
+
## WebMCP page tools
|
|
67
|
+
|
|
68
|
+
Create a session with `{"enable_web_mcp": True}` (default: false), then navigate
|
|
69
|
+
to a page that exposes WebMCP tools using Playwright. With that session open:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
discovery = client.sessions.webmcp.list_tools(session.id)
|
|
73
|
+
# Select a tool using its name, source page/frame, and input schema.
|
|
74
|
+
tool = next(tool for tool in discovery.tools if tool.name == "search")
|
|
75
|
+
result = client.sessions.webmcp.invoke(session.id, {
|
|
76
|
+
"tool_ref": tool.tool_ref,
|
|
77
|
+
"input": {"query": "flights to Tokyo"}, # Match the selected tool's schema.
|
|
78
|
+
})
|
|
79
|
+
print(result.status, result.output)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For long-running tools or forms awaiting human submission, use
|
|
83
|
+
`client.sessions.webmcp.start(session_id, params)`, then
|
|
84
|
+
`client.sessions.webmcp.get_result(session_id, invocation_id, {"wait_seconds": 30})`
|
|
85
|
+
until the status is terminal. `client.sessions.webmcp.cancel(session_id, invocation_id)`
|
|
86
|
+
requests best-effort cancellation. Blocking invocation defaults to 60 seconds
|
|
87
|
+
(maximum 120); `start` defaults to 300 seconds (maximum 3,600) via `timeout_seconds`.
|
|
88
|
+
All five methods also work with `AsyncHyperbrowser` by adding `await`.
|
|
89
|
+
|
|
90
|
+
Discover tools again after navigation. Invocations never automatically retry;
|
|
91
|
+
losing a response can leave an unknown outcome. Handles are ephemeral and
|
|
92
|
+
terminal results expire after 10 minutes or earlier eviction. Check result
|
|
93
|
+
status and truncation metadata before using output. Tool input and output keys
|
|
94
|
+
are not renamed. Request TypedDicts are available in `hyperbrowser.types`, and
|
|
95
|
+
Pydantic request/response models in `hyperbrowser.models`.
|
|
96
|
+
|
|
97
|
+
See the [WebMCP guide](https://hyperbrowser.ai/docs/sessions/webmcp) for complete
|
|
98
|
+
examples, cancellation behavior, human submission, and limits.
|
|
99
|
+
|
|
65
100
|
## Usage
|
|
66
101
|
|
|
67
102
|
Hyperbrowser 1.0 accepts plain dictionaries for request parameters. Method
|
|
@@ -255,6 +290,104 @@ client.volumes.delete(same_volume.id)
|
|
|
255
290
|
client.close()
|
|
256
291
|
```
|
|
257
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
|
+
|
|
258
391
|
### List sandboxes with filters
|
|
259
392
|
|
|
260
393
|
```python
|
|
@@ -34,6 +34,40 @@ Profile names must match `^[A-Za-z0-9._-]+$`.
|
|
|
34
34
|
|
|
35
35
|
`base_url` and `HYPERBROWSER_BASE_URL` accept either `https://host` or `https://host/api`. The client normalizes both to the same control-plane base URL.
|
|
36
36
|
|
|
37
|
+
## WebMCP page tools
|
|
38
|
+
|
|
39
|
+
Create a session with `{"enable_web_mcp": True}` (default: false), then navigate
|
|
40
|
+
to a page that exposes WebMCP tools using Playwright. With that session open:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
discovery = client.sessions.webmcp.list_tools(session.id)
|
|
44
|
+
# Select a tool using its name, source page/frame, and input schema.
|
|
45
|
+
tool = next(tool for tool in discovery.tools if tool.name == "search")
|
|
46
|
+
result = client.sessions.webmcp.invoke(session.id, {
|
|
47
|
+
"tool_ref": tool.tool_ref,
|
|
48
|
+
"input": {"query": "flights to Tokyo"}, # Match the selected tool's schema.
|
|
49
|
+
})
|
|
50
|
+
print(result.status, result.output)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For long-running tools or forms awaiting human submission, use
|
|
54
|
+
`client.sessions.webmcp.start(session_id, params)`, then
|
|
55
|
+
`client.sessions.webmcp.get_result(session_id, invocation_id, {"wait_seconds": 30})`
|
|
56
|
+
until the status is terminal. `client.sessions.webmcp.cancel(session_id, invocation_id)`
|
|
57
|
+
requests best-effort cancellation. Blocking invocation defaults to 60 seconds
|
|
58
|
+
(maximum 120); `start` defaults to 300 seconds (maximum 3,600) via `timeout_seconds`.
|
|
59
|
+
All five methods also work with `AsyncHyperbrowser` by adding `await`.
|
|
60
|
+
|
|
61
|
+
Discover tools again after navigation. Invocations never automatically retry;
|
|
62
|
+
losing a response can leave an unknown outcome. Handles are ephemeral and
|
|
63
|
+
terminal results expire after 10 minutes or earlier eviction. Check result
|
|
64
|
+
status and truncation metadata before using output. Tool input and output keys
|
|
65
|
+
are not renamed. Request TypedDicts are available in `hyperbrowser.types`, and
|
|
66
|
+
Pydantic request/response models in `hyperbrowser.models`.
|
|
67
|
+
|
|
68
|
+
See the [WebMCP guide](https://hyperbrowser.ai/docs/sessions/webmcp) for complete
|
|
69
|
+
examples, cancellation behavior, human submission, and limits.
|
|
70
|
+
|
|
37
71
|
## Usage
|
|
38
72
|
|
|
39
73
|
Hyperbrowser 1.0 accepts plain dictionaries for request parameters. Method
|
|
@@ -227,6 +261,104 @@ client.volumes.delete(same_volume.id)
|
|
|
227
261
|
client.close()
|
|
228
262
|
```
|
|
229
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
|
+
|
|
230
362
|
### List sandboxes with filters
|
|
231
363
|
|
|
232
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()
|
{hyperbrowser-1.10.0 → hyperbrowser-1.11.1}/hyperbrowser/client/managers/async_manager/sandbox.py
RENAMED
|
@@ -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(
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional, Union
|
|
4
5
|
|
|
5
|
-
from ...._request import coerce_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
|
|
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__(
|
|
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__(
|
|
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=
|
|
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(
|