hyperbrowser 1.12.0__tar.gz → 1.13.0__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.13.0/PKG-INFO +1074 -0
- hyperbrowser-1.13.0/README.md +1044 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/async_client.py +2 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/sandbox.py +29 -8
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +16 -4
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/secret.py +46 -19
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/session.py +2 -0
- hyperbrowser-1.13.0/hyperbrowser/client/managers/async_manager/session_credentials.py +53 -0
- hyperbrowser-1.13.0/hyperbrowser/client/managers/async_manager/vault.py +106 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/shared.py +29 -4
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +29 -8
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +16 -4
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/secret.py +46 -19
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/session.py +2 -0
- hyperbrowser-1.13.0/hyperbrowser/client/managers/sync_manager/session_credentials.py +53 -0
- hyperbrowser-1.13.0/hyperbrowser/client/managers/sync_manager/vault.py +104 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/sync.py +2 -0
- hyperbrowser-1.13.0/hyperbrowser/credentials_common.py +33 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/__init__.py +67 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/gemini_computer_use.py +4 -1
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/consts.py +15 -1
- hyperbrowser-1.13.0/hyperbrowser/models/credential.py +115 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/sandbox.py +42 -4
- hyperbrowser-1.13.0/hyperbrowser/models/secret.py +350 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/session.py +127 -0
- hyperbrowser-1.13.0/hyperbrowser/models/vault.py +107 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/secrets_common.py +185 -3
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/tools/__init__.py +115 -1
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/tools/anthropic.py +8 -0
- hyperbrowser-1.13.0/hyperbrowser/tools/gemini.py +55 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/tools/openai.py +12 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/tools/schema.py +94 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/__init__.py +42 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/agents.py +2 -0
- hyperbrowser-1.13.0/hyperbrowser/types/credential.py +53 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/sandbox.py +5 -1
- hyperbrowser-1.13.0/hyperbrowser/types/secret.py +101 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/session.py +52 -2
- hyperbrowser-1.13.0/hyperbrowser/types/vault.py +44 -0
- hyperbrowser-1.13.0/hyperbrowser/vaults_common.py +57 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/pyproject.toml +1 -1
- hyperbrowser-1.12.0/PKG-INFO +0 -605
- hyperbrowser-1.12.0/README.md +0 -575
- hyperbrowser-1.12.0/hyperbrowser/models/secret.py +0 -173
- hyperbrowser-1.12.0/hyperbrowser/types/secret.py +0 -45
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/LICENSE +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/build_context.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/async_manager/webmcp.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/image_resolution.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/client/managers/sync_manager/webmcp.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/image_builds.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/models/webmcp.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/web.py +0 -0
- {hyperbrowser-1.12.0 → hyperbrowser-1.13.0}/hyperbrowser/types/webmcp.py +0 -0
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperbrowser
|
|
3
|
+
Version: 1.13.0
|
|
4
|
+
Summary: Python SDK for hyperbrowser
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Nikhil Shahi
|
|
8
|
+
Author-email: nshahi1998@gmail.com
|
|
9
|
+
Requires-Python: >=3.8,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Requires-Dist: cryptography (>=41.0.0)
|
|
21
|
+
Requires-Dist: httpx (>=0.23.0,<1)
|
|
22
|
+
Requires-Dist: jsonref (>=1.1.0)
|
|
23
|
+
Requires-Dist: pydantic (>=2.0,<3)
|
|
24
|
+
Requires-Dist: typing-extensions (>=4.0,<5)
|
|
25
|
+
Requires-Dist: websockets (>=13,<18)
|
|
26
|
+
Project-URL: Homepage, https://github.com/hyperbrowserai/python-sdk
|
|
27
|
+
Project-URL: Repository, https://github.com/hyperbrowserai/python-sdk
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Hyperbrowser Python SDK
|
|
31
|
+
|
|
32
|
+
Checkout the full documentation [here](https://hyperbrowser.ai/docs)
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Currently Hyperbrowser supports creating a browser session in two ways:
|
|
37
|
+
|
|
38
|
+
- Async Client
|
|
39
|
+
- Sync Client
|
|
40
|
+
|
|
41
|
+
It can be installed from `pypi` by running :
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
pip install hyperbrowser
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The browser-control examples below also use Playwright:
|
|
48
|
+
|
|
49
|
+
```shell
|
|
50
|
+
pip install playwright
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Both the sync and async client follow similar configuration params
|
|
56
|
+
|
|
57
|
+
### API Key
|
|
58
|
+
The API key can be configured either from the constructor arguments or environment variables using `HYPERBROWSER_API_KEY`
|
|
59
|
+
|
|
60
|
+
If no API key is provided, the client falls back to a saved OAuth session created by `hx auth login`. By default it reads `~/.hx_config/auth/default.json`, or `~/.hx_config/auth/<profile>.json` when `HYPERBROWSER_PROFILE` or `ClientConfig(profile=...)` is set.
|
|
61
|
+
|
|
62
|
+
Profile names must match `^[A-Za-z0-9._-]+$`.
|
|
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.
|
|
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
|
+
|
|
100
|
+
## Web Bot Auth
|
|
101
|
+
|
|
102
|
+
A session can sign its requests with Web Bot Auth so websites can verify that they
|
|
103
|
+
come from Hyperbrowser. Web Bot Auth is enabled for teams on request.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
session = client.sessions.create({"web_bot_auth": True})
|
|
107
|
+
print(session.launch_state.web_bot_auth.signature_agent) # https://www.hyperbrowser.ai
|
|
108
|
+
|
|
109
|
+
# Sign only requests to these hosts and their subdomains:
|
|
110
|
+
client.sessions.create({"web_bot_auth": {"only_domains": ["example.com"]}})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Agent tasks accept `web_bot_auth` in `session_options`; scrape, crawl, and extract
|
|
114
|
+
jobs do not. It cannot be combined with `use_ultra_stealth` or
|
|
115
|
+
`start_from_snapshot`. See the
|
|
116
|
+
[Web Bot Auth guide](https://hyperbrowser.ai/docs/sessions/web-bot-auth).
|
|
117
|
+
|
|
118
|
+
## Proxy routes
|
|
119
|
+
|
|
120
|
+
`proxy_routes` sends some hosts' traffic straight out instead of through the
|
|
121
|
+
session's proxy, or the other way round. The first route that matches a host
|
|
122
|
+
decides; hosts that no route matches use the proxy. Proxy routes require
|
|
123
|
+
`use_proxy` and are enabled for teams on request.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
session = client.sessions.create({
|
|
127
|
+
"use_proxy": True,
|
|
128
|
+
"proxy_routes": [
|
|
129
|
+
{"match": "api.example.com", "proxy": "default"}, # stays on the proxy
|
|
130
|
+
{"match": ["*.example.com", "cdn.example.net"], "proxy": "direct"},
|
|
131
|
+
],
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A pattern is a hostname, `*.example.com` (its subdomains, not `example.com`
|
|
136
|
+
itself), or `*` (every host). Proxy routes only apply to `client.sessions.create`
|
|
137
|
+
and cannot be combined with `allow_internet_access=False`, `allow_out`, or
|
|
138
|
+
`deny_out`. See the
|
|
139
|
+
[proxy routes guide](https://hyperbrowser.ai/docs/sessions/proxy-routes).
|
|
140
|
+
|
|
141
|
+
## Fill credentials into browser sessions
|
|
142
|
+
|
|
143
|
+
Any secret with allowed origins can be typed into pages of a browser session.
|
|
144
|
+
A website login is usually two such secrets grouped in a vault, plus a TOTP
|
|
145
|
+
secret when the site asks for a one-time code. Values are encrypted on your
|
|
146
|
+
machine; after upload, neither the API, your code, nor an LLM sees them again.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from playwright.sync_api import sync_playwright
|
|
150
|
+
|
|
151
|
+
from hyperbrowser import Hyperbrowser
|
|
152
|
+
|
|
153
|
+
client = Hyperbrowser(api_key="test-key")
|
|
154
|
+
|
|
155
|
+
origins = ["https://github.com"]
|
|
156
|
+
client.secrets.create(
|
|
157
|
+
{"name": "github-username", "value": "ada@example.com", "allowed_origins": origins}
|
|
158
|
+
)
|
|
159
|
+
client.secrets.create(
|
|
160
|
+
{"name": "github-password", "value": "correct horse", "allowed_origins": origins}
|
|
161
|
+
)
|
|
162
|
+
# A TOTP seed: base32 as the site shows it, or an otpauth://totp/ URI.
|
|
163
|
+
client.secrets.create(
|
|
164
|
+
{
|
|
165
|
+
"name": "github-otp",
|
|
166
|
+
"type": "totp",
|
|
167
|
+
"value": "JBSW Y3DP EHPK 3PXP JBSW Y3DP EHPK 3PXP",
|
|
168
|
+
"allowed_origins": origins,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
client.vaults.create(
|
|
172
|
+
{
|
|
173
|
+
"name": "github-login",
|
|
174
|
+
"entries": {
|
|
175
|
+
"USERNAME": {"name": "github-username"},
|
|
176
|
+
"PASSWORD": {"name": "github-password"},
|
|
177
|
+
"OTP": {"name": "github-otp"},
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Secrets and vaults can only be attached when the session is created.
|
|
183
|
+
session = client.sessions.create({"credentials": [{"vault": "github-login"}]})
|
|
184
|
+
|
|
185
|
+
with sync_playwright() as p:
|
|
186
|
+
browser = p.chromium.connect_over_cdp(session.ws_endpoint)
|
|
187
|
+
page = browser.contexts[0].pages[0]
|
|
188
|
+
page.goto("https://github.com/login")
|
|
189
|
+
|
|
190
|
+
# With a vault, each field names the entry to type.
|
|
191
|
+
result = client.sessions.credentials.fill(
|
|
192
|
+
session.id,
|
|
193
|
+
{
|
|
194
|
+
"vault": "github-login",
|
|
195
|
+
"fields": [
|
|
196
|
+
{"field": "USERNAME", "selector": "#login_field"},
|
|
197
|
+
{"field": "PASSWORD", "selector": "#password"},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
if all(field.outcome == "filled" for field in result.fields):
|
|
202
|
+
page.click("input[type=submit]") # Fill only types; submitting is up to you.
|
|
203
|
+
|
|
204
|
+
page.wait_for_selector("#app_totp")
|
|
205
|
+
# A TOTP entry types the code that is current at fill time.
|
|
206
|
+
client.sessions.credentials.fill(
|
|
207
|
+
session.id,
|
|
208
|
+
{"vault": "github-login", "fields": [{"field": "OTP", "selector": "#app_totp"}]},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
client.sessions.stop(session.id)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A single secret works without a vault: attach it by name (`"credentials":
|
|
215
|
+
["github-password"]` or `[{"secret": "github-password"}]`) and fill it with
|
|
216
|
+
`{"credential": "github-password", "fields": [{"selector": "#password"}]}`.
|
|
217
|
+
|
|
218
|
+
`fill()` returns the filled page's `page_id` and `origin`, the `version` for a
|
|
219
|
+
single-secret fill, and per input its `selector`, `outcome`, `secret_id`,
|
|
220
|
+
`version`, `field` (vault fills), `reference` (1Password fills) and
|
|
221
|
+
`expires_at` (TOTP codes). Values are never returned.
|
|
222
|
+
|
|
223
|
+
| Outcome | Meaning |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| `filled` | The input now holds the value. |
|
|
226
|
+
| `not_found` | No input matched the selector. |
|
|
227
|
+
| `ambiguous` | Several visible inputs matched the selector. |
|
|
228
|
+
| `not_editable` | The input is disabled, read-only, hidden, or not a text input. |
|
|
229
|
+
| `not_accepted` | The page didn't keep the value, for example because of `maxlength`. |
|
|
230
|
+
|
|
231
|
+
Things to know:
|
|
232
|
+
|
|
233
|
+
- A secret can be filled only on its allowed origins (`scheme://host[:port]`;
|
|
234
|
+
`https://*.example.com` matches any subdomain but not `example.com` itself).
|
|
235
|
+
A secret takes 1-20 of at most 512 characters, https only (http for
|
|
236
|
+
`localhost`, `127.0.0.1` and `[::1]`), with no path and `*.` only on a
|
|
237
|
+
registrable domain such as `example.com`, never `*.com` or an IP address.
|
|
238
|
+
The API validates and normalizes them.
|
|
239
|
+
Change them with `client.secrets.set_allowed_origins("github-password",
|
|
240
|
+
[...])`; `None` clears them, after which it can't be filled.
|
|
241
|
+
`client.secrets.update()` takes `{"metadata": {"browser": None}}` too: each
|
|
242
|
+
metadata member sent replaces the stored one and `None` removes it.
|
|
243
|
+
- Secret types are `secret` (the default), `totp` and `op_token`, fixed at
|
|
244
|
+
creation; `client.secrets.list({"type": "totp"})` filters by type. TOTP
|
|
245
|
+
values are checked locally before upload: a base32 seed (spaces, hyphens, line breaks,
|
|
246
|
+
lowercase and trailing `=` are fine) or an `otpauth://totp/` URI with
|
|
247
|
+
`algorithm` SHA1/SHA256/SHA512, `digits` 6 or 8 and `period` 1-300. Seeds
|
|
248
|
+
shorter than 10 bytes (16 characters) are accepted but weak. The platform
|
|
249
|
+
can't read values, so a bad seed would otherwise only fail when used. A TOTP
|
|
250
|
+
secret delivered to a sandbox holds the code current at process start.
|
|
251
|
+
- A fill takes one of `credential`, `vault` and `one_password`, and 1-16
|
|
252
|
+
fields with unique selectors; `field` is required with a vault and left out
|
|
253
|
+
without one, and `ref` is required with `one_password` and left out without
|
|
254
|
+
it.
|
|
255
|
+
Without `page_id`, fill considers the tabs whose URL starts with `page_url`
|
|
256
|
+
(all tabs when it's not given). If one is left, that's the target. If several
|
|
257
|
+
are left, it prefers the tabs on an origin every secret allows (skipped with
|
|
258
|
+
`frame_url`, since a login frame's tab is usually on another origin), then
|
|
259
|
+
the visible one among them; if more than one still qualifies, it fails with
|
|
260
|
+
`CREDENTIAL_PAGE_AMBIGUOUS`. The chosen tab's origin is then checked: on any
|
|
261
|
+
other origin nothing is typed and the fill fails with
|
|
262
|
+
`CREDENTIAL_ORIGIN_NOT_ALLOWED`. Under Playwright every tab reports as visible, so pass
|
|
263
|
+
`page_url` (a URL prefix) or the tab's CDP target ID as `page_id`:
|
|
264
|
+
`page.context.new_cdp_session(page).send("Target.getTargetInfo")["targetInfo"]["targetId"]`.
|
|
265
|
+
For a login form in an iframe, pass `frame_url` (a URL prefix).
|
|
266
|
+
- Fill is safe to retry and never retried automatically. It waits at least
|
|
267
|
+
30 seconds for the API (the client timeout when longer).
|
|
268
|
+
- Attaching secrets needs an API key with `secrets.use`; filling also needs
|
|
269
|
+
`browser.sessions.update`. The team needs the secrets feature. Sessions with
|
|
270
|
+
credentials run as Firecracker browsers with Chrome's password manager off.
|
|
271
|
+
A vault attaches the secrets its entries hold when the session is created.
|
|
272
|
+
- Fills are recorded in the session's event log as `credential_filled` and
|
|
273
|
+
`credential_fill_refused`. Their metadata names the `credential` or `vault`;
|
|
274
|
+
for 1Password fills, `onePasswordToken` (the op_token's name) and its
|
|
275
|
+
`secretId`.
|
|
276
|
+
- Errors are raised as `HyperbrowserError` with the API's `code`, `retryable`,
|
|
277
|
+
and `status_code`. `details` holds the API's response body, so the
|
|
278
|
+
candidate tabs of an ambiguous fill are in `error.details["details"]["pages"]`
|
|
279
|
+
(each with `pageId`, `url` and `title`). Error codes from the API:
|
|
280
|
+
- `CREDENTIAL_PAGE_AMBIGUOUS` (409): several tabs could be the target; pass
|
|
281
|
+
one's `pageId` as `page_id`, or a `page_url`. `CREDENTIAL_FRAME_AMBIGUOUS`
|
|
282
|
+
(409) is the same for frames.
|
|
283
|
+
- `CREDENTIAL_ORIGIN_NOT_ALLOWED` (403): the page's origin (in `details`)
|
|
284
|
+
isn't allowed; nothing was typed.
|
|
285
|
+
- `CREDENTIAL_NOT_ATTACHED` (403): the secret (or a 1Password fill's
|
|
286
|
+
op_token) wasn't attached when the session was created, or a vault entry
|
|
287
|
+
now points at another secret.
|
|
288
|
+
- `CREDENTIAL_NOT_READY` (409): the secret has no allowed origins or no
|
|
289
|
+
value.
|
|
290
|
+
- `SECRET_TYPE_MISMATCH` (409): an `op_token` can't be attached or filled
|
|
291
|
+
as a secret (by name, `{"secret": ...}` or a vault entry); attach it with
|
|
292
|
+
`{"one_password": ...}` instead. Also another type named as one.
|
|
293
|
+
- `SECRET_VALUE_INVALID` (422): the stored value is unusable, such as a bad
|
|
294
|
+
TOTP seed; add a new version.
|
|
295
|
+
- `CREDENTIAL_PAGE_NOT_FOUND`, `CREDENTIAL_FRAME_NOT_FOUND`,
|
|
296
|
+
`SECRET_NOT_FOUND`, `SECRET_VERSION_NOT_FOUND`, `VAULT_NOT_FOUND`,
|
|
297
|
+
`VAULT_ENTRY_NOT_FOUND` (404), `SESSION_NOT_ACTIVE` (409).
|
|
298
|
+
- `CREDENTIAL_FILL_TIMEOUT` (503/504, retryable) and
|
|
299
|
+
`CREDENTIAL_FILL_FAILED` (500/502/503, retryable as flagged).
|
|
300
|
+
- `CREDENTIALS_UNSUPPORTED_RUNTIME` (400), `CREDENTIAL_FILL_UNSUPPORTED`
|
|
301
|
+
(501) and `CREDENTIAL_VALUE_UNSUPPORTED` (501): the session's browser
|
|
302
|
+
can't fill this, such as 1Password fields on an older host.
|
|
303
|
+
- `SECRET_ONEPASSWORD_REFERENCE_FAILED` (422): a 1Password item or field
|
|
304
|
+
couldn't be read, so nothing was filled. `details` has the failing
|
|
305
|
+
`selectors` and `reasons` by selector. `SECRET_ONEPASSWORD_AUTH_FAILED`
|
|
306
|
+
(422): 1Password rejected the token, or the token is malformed or doesn't
|
|
307
|
+
sign in to `1password.com`, `.ca` or `.eu` (or a subdomain), so it wasn't
|
|
308
|
+
sent to 1Password. `SECRET_ONEPASSWORD_RATE_LIMITED` (429) and
|
|
309
|
+
`SECRET_ONEPASSWORD_FAILED` (502/503) are retryable. Each has the
|
|
310
|
+
token's `tokenSecretId` and `tokenVersion` in `details`.
|
|
311
|
+
- Local fill shape errors raise `invalid_request`; invalid secret values
|
|
312
|
+
raise `invalid_secret_value` before upload. Fill, origin, vault-entry and
|
|
313
|
+
1Password policy errors come from the API. Inspect the status, code (when
|
|
314
|
+
supplied) and details; schema errors may have no machine code.
|
|
315
|
+
|
|
316
|
+
### Fill from 1Password
|
|
317
|
+
|
|
318
|
+
A fill can also type fields of a 1Password vault, read with a 1Password
|
|
319
|
+
service account token stored as an `op_token` secret (see
|
|
320
|
+
[Manage secrets](#manage-secrets-and-pass-them-to-sandbox-processes)). Attach
|
|
321
|
+
the token when creating the session, then name the vault, the origins the
|
|
322
|
+
values may be typed on, and each input's field:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
session = client.sessions.create({"credentials": [{"one_password": "op-token"}]})
|
|
326
|
+
# ... open https://www.scrapingcourse.com/login in the session ...
|
|
327
|
+
result = client.sessions.credentials.fill(
|
|
328
|
+
session.id,
|
|
329
|
+
{
|
|
330
|
+
"one_password": {
|
|
331
|
+
"token": "op-token", # the op_token secret; its latest version signs in
|
|
332
|
+
"vault": "Dev Vault", # the 1Password vault's name or ID
|
|
333
|
+
# Nothing stored says where a 1Password value belongs, so the
|
|
334
|
+
# request names the origins it may be typed on.
|
|
335
|
+
"allowed_origins": ["https://www.scrapingcourse.com"],
|
|
336
|
+
},
|
|
337
|
+
"fields": [
|
|
338
|
+
{"selector": "#email", "ref": {"item": "scrapingcourse login", "field": "username"}},
|
|
339
|
+
{"selector": "#password", "ref": {"item": "scrapingcourse login", "field": "password"}},
|
|
340
|
+
{
|
|
341
|
+
"selector": "#otp",
|
|
342
|
+
"ref": {"item": "scrapingcourse login", "section": "Security", "field": "otp?attribute=totp"},
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
"page_url": "https://www.scrapingcourse.com/login",
|
|
346
|
+
},
|
|
347
|
+
)
|
|
348
|
+
for field in result.fields:
|
|
349
|
+
print(field.selector, field.outcome, field.reference) # op://Dev Vault/...
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- `token` names the op_token secret, which must be attached to the session
|
|
353
|
+
(`{"one_password": name}` counts toward the 20 credentials). An op_token is
|
|
354
|
+
never typed itself, and attaching or filling it as a plain secret fails with
|
|
355
|
+
`SECRET_TYPE_MISMATCH`.
|
|
356
|
+
- `allowed_origins` takes 1-20 origins with the same rules as a secret's.
|
|
357
|
+
A page on another origin fails with
|
|
358
|
+
`CREDENTIAL_ORIGIN_NOT_ALLOWED`, as for stored secrets.
|
|
359
|
+
- `ref` is `{"item", "section"?, "field"}` by name or ID, with the same rules
|
|
360
|
+
as a sandbox's `one_password` fields: a field may end with a query such as
|
|
361
|
+
`?attribute=otp`, and `op://<vault>/<item>/[<section>/]<field>` is at most
|
|
362
|
+
512 bytes. Every field takes `ref` and none takes `field`.
|
|
363
|
+
- The result has no top-level `version`; each input's `secret_id` and
|
|
364
|
+
`version` are the op_token's, and `reference` is the `op://` reference it
|
|
365
|
+
was typed from.
|
|
366
|
+
- The API validates these rules. Request models still reject unknown members
|
|
367
|
+
and invalid shapes locally.
|
|
368
|
+
|
|
369
|
+
### Use with LLM SDKs
|
|
370
|
+
|
|
371
|
+
`CredentialFillTool` lets a model fill a secret or vault by name while your
|
|
372
|
+
application binds the session ID. It doesn't offer 1Password fills: their
|
|
373
|
+
request names the allowed origins, which a model shouldn't choose, so call
|
|
374
|
+
`client.sessions.credentials.fill()` with `one_password` directly. The model chooses only `credential` or
|
|
375
|
+
`vault`, the fields (`selector`, and `field` with a vault), and optionally
|
|
376
|
+
`page_id`, `page_url` or `frame_url`; it never sees the values. `runnable()`
|
|
377
|
+
returns JSON text for the model: the fill result, or `{"error": {"code",
|
|
378
|
+
"message", "retryable", "details"}}` when the fill fails, so the model can
|
|
379
|
+
react (for example by choosing a `page_id`). Give the model your other
|
|
380
|
+
browser tools too, since it must submit the form itself. `async_runnable()`
|
|
381
|
+
does the same with `AsyncHyperbrowser`.
|
|
382
|
+
|
|
383
|
+
OpenAI (Chat Completions):
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
import json
|
|
387
|
+
|
|
388
|
+
from openai import OpenAI
|
|
389
|
+
|
|
390
|
+
from hyperbrowser.tools import CredentialFillTool
|
|
391
|
+
|
|
392
|
+
openai_client = OpenAI()
|
|
393
|
+
tools = [CredentialFillTool.openai_tool_definition] # plus your browser tools
|
|
394
|
+
messages = [{"role": "user", "content": "Sign in to GitHub with the vault named github-login."}]
|
|
395
|
+
while True:
|
|
396
|
+
response = openai_client.chat.completions.create(
|
|
397
|
+
model="gpt-5", messages=messages, tools=tools
|
|
398
|
+
)
|
|
399
|
+
message = response.choices[0].message
|
|
400
|
+
messages.append(message)
|
|
401
|
+
if not message.tool_calls:
|
|
402
|
+
break
|
|
403
|
+
for call in message.tool_calls:
|
|
404
|
+
if call.function.name == "fill_credential":
|
|
405
|
+
output = CredentialFillTool.runnable(
|
|
406
|
+
client, session.id, json.loads(call.function.arguments)
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
output = run_browser_tool(call) # your click/press/navigate tools
|
|
410
|
+
messages.append({"role": "tool", "tool_call_id": call.id, "content": output})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Anthropic (Messages):
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
import anthropic
|
|
417
|
+
|
|
418
|
+
from hyperbrowser.tools import CredentialFillTool
|
|
419
|
+
|
|
420
|
+
anthropic_client = anthropic.Anthropic()
|
|
421
|
+
tools = [CredentialFillTool.anthropic_tool_definition] # plus your browser tools
|
|
422
|
+
messages = [{"role": "user", "content": "Sign in to GitHub with the vault named github-login."}]
|
|
423
|
+
while True:
|
|
424
|
+
response = anthropic_client.messages.create(
|
|
425
|
+
model="claude-sonnet-5-5", max_tokens=1024, tools=tools, messages=messages
|
|
426
|
+
)
|
|
427
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
428
|
+
if response.stop_reason != "tool_use":
|
|
429
|
+
break
|
|
430
|
+
results = []
|
|
431
|
+
for block in response.content:
|
|
432
|
+
if block.type != "tool_use":
|
|
433
|
+
continue
|
|
434
|
+
if block.name == "fill_credential":
|
|
435
|
+
output = CredentialFillTool.runnable(client, session.id, block.input)
|
|
436
|
+
else:
|
|
437
|
+
output = run_browser_tool(block)
|
|
438
|
+
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
|
|
439
|
+
messages.append({"role": "user", "content": results})
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Gemini (google-genai):
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
from google import genai
|
|
446
|
+
from google.genai import types
|
|
447
|
+
|
|
448
|
+
from hyperbrowser.tools import CredentialFillTool
|
|
449
|
+
|
|
450
|
+
gemini = genai.Client()
|
|
451
|
+
config = types.GenerateContentConfig(
|
|
452
|
+
tools=[
|
|
453
|
+
types.Tool(function_declarations=[CredentialFillTool.gemini_tool_definition])
|
|
454
|
+
] # plus your browser tools
|
|
455
|
+
)
|
|
456
|
+
contents = [
|
|
457
|
+
types.Content(
|
|
458
|
+
role="user",
|
|
459
|
+
parts=[types.Part.from_text(text="Sign in to GitHub with the vault named github-login.")],
|
|
460
|
+
)
|
|
461
|
+
]
|
|
462
|
+
while True:
|
|
463
|
+
response = gemini.models.generate_content(
|
|
464
|
+
model="gemini-3.5-flash", contents=contents, config=config
|
|
465
|
+
)
|
|
466
|
+
contents.append(response.candidates[0].content)
|
|
467
|
+
if not response.function_calls:
|
|
468
|
+
break
|
|
469
|
+
parts = []
|
|
470
|
+
for call in response.function_calls:
|
|
471
|
+
if call.name == "fill_credential":
|
|
472
|
+
output = CredentialFillTool.runnable(client, session.id, call.args or {})
|
|
473
|
+
else:
|
|
474
|
+
output = run_browser_tool(call)
|
|
475
|
+
parts.append(
|
|
476
|
+
types.Part.from_function_response(name=call.name, response={"result": output})
|
|
477
|
+
)
|
|
478
|
+
contents.append(types.Content(role="user", parts=parts))
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## Tools for LLM function calling
|
|
482
|
+
|
|
483
|
+
`hyperbrowser.tools` has ready-made tools: `WebsiteScrapeTool`,
|
|
484
|
+
`WebsiteScreenshotTool`, `WebsiteCrawlTool`, `WebsiteExtractTool`,
|
|
485
|
+
`BrowserUseTool` and `CredentialFillTool`. Each has `openai_tool_definition`
|
|
486
|
+
(a strict Chat Completions function tool), `anthropic_tool_definition`, and
|
|
487
|
+
`runnable()` / `async_runnable()` to run the model's input and return text for
|
|
488
|
+
the model. `CredentialFillTool` also provides `gemini_tool_definition`, a
|
|
489
|
+
function declaration dict for google-genai.
|
|
490
|
+
|
|
491
|
+
## Usage
|
|
492
|
+
|
|
493
|
+
Hyperbrowser 1.0 accepts plain dictionaries for request parameters. Method
|
|
494
|
+
signatures use `TypedDict` definitions, so editors can autocomplete keys at
|
|
495
|
+
every nested level:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
from hyperbrowser import Hyperbrowser
|
|
499
|
+
|
|
500
|
+
client = Hyperbrowser(api_key="test-key")
|
|
501
|
+
|
|
502
|
+
# Preferred in 1.0: autocomplete works directly in the dictionary.
|
|
503
|
+
session = client.sessions.create(
|
|
504
|
+
{
|
|
505
|
+
"use_stealth": True,
|
|
506
|
+
"screen": {"width": 1920, "height": 1080},
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Browser sessions can also use an outbound network policy. Omitting these fields
|
|
512
|
+
keeps the default unrestricted behavior:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
session = client.sessions.create(
|
|
516
|
+
{
|
|
517
|
+
"allow_internet_access": False,
|
|
518
|
+
"allow_out": ["example.com"],
|
|
519
|
+
"deny_out": ["0.0.0.0/0"],
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Direct browser policies accept domains, IPv4 addresses, and CIDR ranges in
|
|
525
|
+
`allow_out`; `deny_out` accepts IPv4 addresses and CIDR ranges. With a proxy,
|
|
526
|
+
allow rules must be domains and the only supported deny rule is `0.0.0.0/0`.
|
|
527
|
+
|
|
528
|
+
Existing Pydantic request classes remain accepted, so upgrading does not require
|
|
529
|
+
an immediate rewrite:
|
|
530
|
+
|
|
531
|
+
```python
|
|
532
|
+
from hyperbrowser.models import CreateSessionParams, ScreenConfig
|
|
533
|
+
|
|
534
|
+
session = client.sessions.create(
|
|
535
|
+
CreateSessionParams(
|
|
536
|
+
use_stealth=True,
|
|
537
|
+
screen=ScreenConfig(width=1920, height=1080),
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Import request annotations from `hyperbrowser.types` when a named variable is
|
|
543
|
+
useful. The same names under `hyperbrowser.models` refer to the legacy Pydantic
|
|
544
|
+
request classes. Responses remain Pydantic models.
|
|
545
|
+
|
|
546
|
+
JSON Schema fields accept raw schema values, including object schemas with
|
|
547
|
+
`$defs`, `$ref`, or custom keywords, and boolean schemas where the API supports
|
|
548
|
+
them. Those schemas and other user-owned mappings are preserved as data; only
|
|
549
|
+
SDK-owned request keys are translated to their API aliases. Schema fields
|
|
550
|
+
documented as accepting a model class can also generate a schema from a Pydantic
|
|
551
|
+
model.
|
|
552
|
+
|
|
553
|
+
See the
|
|
554
|
+
[Hyperbrowser Python SDK 1.0 migration guide](https://hyperbrowser.ai/docs/sdks/python-1-0-migration)
|
|
555
|
+
for the complete compatibility details and migration checklist.
|
|
556
|
+
|
|
557
|
+
### Async
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
import asyncio
|
|
561
|
+
from hyperbrowser import AsyncHyperbrowser
|
|
562
|
+
from playwright.async_api import async_playwright
|
|
563
|
+
|
|
564
|
+
HYPERBROWSER_API_KEY = "test-key"
|
|
565
|
+
|
|
566
|
+
async def main():
|
|
567
|
+
async with AsyncHyperbrowser(api_key=HYPERBROWSER_API_KEY) as client:
|
|
568
|
+
session = await client.sessions.create()
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
async with async_playwright() as playwright:
|
|
572
|
+
browser = await playwright.chromium.connect_over_cdp(
|
|
573
|
+
session.ws_endpoint
|
|
574
|
+
)
|
|
575
|
+
context = browser.contexts[0]
|
|
576
|
+
page = context.pages[0]
|
|
577
|
+
|
|
578
|
+
print("Navigating to Hacker News...")
|
|
579
|
+
await page.goto("https://news.ycombinator.com/")
|
|
580
|
+
print("Page title:", await page.title())
|
|
581
|
+
finally:
|
|
582
|
+
await client.sessions.stop(session.id)
|
|
583
|
+
|
|
584
|
+
# Run the asyncio event loop
|
|
585
|
+
asyncio.run(main())
|
|
586
|
+
```
|
|
587
|
+
### Sync
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
from playwright.sync_api import sync_playwright
|
|
591
|
+
from hyperbrowser import Hyperbrowser
|
|
592
|
+
|
|
593
|
+
HYPERBROWSER_API_KEY = "test-key"
|
|
594
|
+
|
|
595
|
+
def main():
|
|
596
|
+
client = Hyperbrowser(api_key=HYPERBROWSER_API_KEY)
|
|
597
|
+
session = client.sessions.create()
|
|
598
|
+
|
|
599
|
+
ws_endpoint = session.ws_endpoint
|
|
600
|
+
|
|
601
|
+
# Launch Playwright and connect to the remote browser
|
|
602
|
+
with sync_playwright() as p:
|
|
603
|
+
browser = p.chromium.connect_over_cdp(ws_endpoint)
|
|
604
|
+
context = browser.new_context()
|
|
605
|
+
|
|
606
|
+
# Get the first page or create a new one
|
|
607
|
+
if len(context.pages) == 0:
|
|
608
|
+
page = context.new_page()
|
|
609
|
+
else:
|
|
610
|
+
page = context.pages[0]
|
|
611
|
+
|
|
612
|
+
# Navigate to a website
|
|
613
|
+
print("Navigating to Hacker News...")
|
|
614
|
+
page.goto("https://news.ycombinator.com/")
|
|
615
|
+
page_title = page.title()
|
|
616
|
+
print("Page title:", page_title)
|
|
617
|
+
|
|
618
|
+
page.close()
|
|
619
|
+
browser.close()
|
|
620
|
+
print("Session completed!")
|
|
621
|
+
client.sessions.stop(session.id)
|
|
622
|
+
|
|
623
|
+
# Run the asyncio event loop
|
|
624
|
+
main()
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Sandboxes
|
|
628
|
+
|
|
629
|
+
The sync and async clients expose the same sandbox APIs through `client.sandboxes`.
|
|
630
|
+
|
|
631
|
+
### Create a sandbox with pre-exposed ports
|
|
632
|
+
|
|
633
|
+
```python
|
|
634
|
+
from hyperbrowser import Hyperbrowser
|
|
635
|
+
|
|
636
|
+
client = Hyperbrowser(api_key="test-key")
|
|
637
|
+
sandbox = client.sandboxes.create(
|
|
638
|
+
{
|
|
639
|
+
"image_name": "node",
|
|
640
|
+
"cpu": 2,
|
|
641
|
+
"memory_mib": 2048,
|
|
642
|
+
"disk_mib": 8192,
|
|
643
|
+
"exposed_ports": [{"port": 3000, "auth": True}],
|
|
644
|
+
}
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
print(sandbox.exposed_ports[0].browser_url)
|
|
648
|
+
print(sandbox.cpu, sandbox.memory_mib, sandbox.disk_mib)
|
|
649
|
+
sandbox.stop()
|
|
650
|
+
client.close()
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
`cpu`, `memory_mib`, and `disk_mib` are only supported for image launches.
|
|
654
|
+
|
|
655
|
+
### Manage volumes and mount them in a sandbox
|
|
656
|
+
|
|
657
|
+
```python
|
|
658
|
+
from hyperbrowser import Hyperbrowser
|
|
659
|
+
|
|
660
|
+
client = Hyperbrowser(api_key="test-key")
|
|
661
|
+
|
|
662
|
+
volume = client.volumes.create({"name": "project-cache"})
|
|
663
|
+
all_volumes = client.volumes.list()
|
|
664
|
+
same_volume = client.volumes.get(volume.id)
|
|
665
|
+
|
|
666
|
+
sandbox = client.sandboxes.create(
|
|
667
|
+
{
|
|
668
|
+
"image_name": "node",
|
|
669
|
+
"mounts": {
|
|
670
|
+
"/workspace/cache": {
|
|
671
|
+
"id": same_volume.id,
|
|
672
|
+
"type": "rw",
|
|
673
|
+
"shared": True,
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
sandbox.stop()
|
|
680
|
+
client.volumes.delete(same_volume.id)
|
|
681
|
+
client.close()
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Manage secrets and pass them to sandbox processes
|
|
685
|
+
|
|
686
|
+
Secrets hold values such as API keys for sandbox processes. Values are
|
|
687
|
+
encrypted on your machine to the platform's public key before upload, and the
|
|
688
|
+
API never returns them. Methods that take a secret accept its ID or exact name.
|
|
689
|
+
|
|
690
|
+
```python
|
|
691
|
+
from pathlib import Path
|
|
692
|
+
|
|
693
|
+
from hyperbrowser import Hyperbrowser
|
|
694
|
+
|
|
695
|
+
client = Hyperbrowser(api_key="test-key")
|
|
696
|
+
|
|
697
|
+
# Values are str or bytes and are stored byte-for-byte, trailing newlines included.
|
|
698
|
+
client.secrets.create(
|
|
699
|
+
{
|
|
700
|
+
"name": "provider-key",
|
|
701
|
+
"description": "Provider API key",
|
|
702
|
+
"value": Path("provider-key.txt").read_bytes(),
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
client.secrets.add_version("provider-key", "rotated-value") # becomes version 2
|
|
706
|
+
detail = client.secrets.get("provider-key") # metadata and versions, never the value
|
|
707
|
+
client.secrets.list({"search": "provider"})
|
|
708
|
+
|
|
709
|
+
# A vault groups variables; sandboxes and processes take it by name.
|
|
710
|
+
client.vaults.create(
|
|
711
|
+
{"name": "app-env", "entries": {"API_KEY": {"name": "provider-key"}}}
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
sandbox = client.sandboxes.create(
|
|
715
|
+
{
|
|
716
|
+
"image_name": "node",
|
|
717
|
+
# Delivered to the image's startup command (image launches only).
|
|
718
|
+
"vault": "app-env",
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
print(sandbox.startup_secrets) # what each variable received, never values
|
|
722
|
+
|
|
723
|
+
# A plain name uses the latest version; {"name": ..., "version": N} pins one.
|
|
724
|
+
# `secrets` are merged over the vault's entries and win on a clash.
|
|
725
|
+
result = sandbox.exec(
|
|
726
|
+
"node app.js",
|
|
727
|
+
vault="app-env",
|
|
728
|
+
secrets={"API_KEY": {"name": "provider-key", "version": 2}},
|
|
729
|
+
)
|
|
730
|
+
process = sandbox.processes.start("node worker.js", secrets={"API_KEY": "provider-key"})
|
|
731
|
+
print(process.secrets) # the version each variable received
|
|
732
|
+
|
|
733
|
+
client.vaults.delete("app-env")
|
|
734
|
+
client.secrets.delete_version("provider-key", 1)
|
|
735
|
+
client.secrets.delete("provider-key")
|
|
736
|
+
client.close()
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Values can also come straight from a 1Password vault. Store a 1Password
|
|
740
|
+
service account token (it starts with `ops_`) as an `op_token` secret: it is
|
|
741
|
+
only used to sign in to 1Password, never set as a variable or typed into a
|
|
742
|
+
browser itself, and can't have allowed origins. Browser sessions can fill
|
|
743
|
+
1Password fields too; see [Fill from 1Password](#fill-from-1password). Then name the fields each variable
|
|
744
|
+
takes with `one_password` when creating a sandbox or starting a process.
|
|
745
|
+
Nothing about them is stored: the sandbox reads the fields when the process
|
|
746
|
+
starts.
|
|
747
|
+
|
|
748
|
+
```python
|
|
749
|
+
client.secrets.create(
|
|
750
|
+
{"name": "op-token", "type": "op_token", "value": Path("op-token.txt").read_text().strip()}
|
|
751
|
+
)
|
|
752
|
+
one_password = {
|
|
753
|
+
"token": "op-token", # the op_token secret's name; its latest version signs in
|
|
754
|
+
"vault": "Engineering", # the 1Password vault's name or ID
|
|
755
|
+
# Items, sections and fields by name or ID: op://Engineering/Postgres/prod/password
|
|
756
|
+
"fields": {
|
|
757
|
+
"DB_PASS": {"item": "Postgres", "section": "prod", "field": "password"},
|
|
758
|
+
"DB_OTP": {"item": "Postgres", "field": "one-time password?attribute=totp"},
|
|
759
|
+
},
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
# Delivered to the image's startup command (image launches only).
|
|
763
|
+
sandbox = client.sandboxes.create({"image_name": "node", "one_password": one_password})
|
|
764
|
+
print(sandbox.startup_secrets) # each with its op:// reference and token_secret
|
|
765
|
+
|
|
766
|
+
result = sandbox.exec("psql -c 'select 1'", one_password=one_password)
|
|
767
|
+
process = sandbox.processes.start("node worker.js", one_password=one_password)
|
|
768
|
+
print(process.secrets) # source.type is "onePassword" for these variables
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
`one_password` takes one token and vault per request and 1-32 fields, named
|
|
772
|
+
like environment variables. A field may end with a query such as
|
|
773
|
+
`?attribute=otp`, `?attribute=totp` or `?ssh-format=openssh`. Vaults, items,
|
|
774
|
+
sections and fields are 1-256 characters (UTF-16 code units) without `/`, `?` or control characters, and
|
|
775
|
+
not `.` or `..`; the whole `op://` reference is at most 512 bytes. These are
|
|
776
|
+
checked by the API, as is a variable that `secrets`, `env` or the request's
|
|
777
|
+
`vault` also sets: a variable from 1Password must come from it alone.
|
|
778
|
+
|
|
779
|
+
Things to know:
|
|
780
|
+
|
|
781
|
+
- A secret is set only in the environment of the process it was passed to.
|
|
782
|
+
Anything else running as the same user, or as root, in the sandbox can read
|
|
783
|
+
that environment.
|
|
784
|
+
- A sandbox that has received secrets, and snapshots taken of it, report
|
|
785
|
+
`contains_secrets=True`. Restoring such a snapshot needs the same access as
|
|
786
|
+
passing secrets.
|
|
787
|
+
- Deleting a secret or version doesn't revoke a value already delivered to a
|
|
788
|
+
sandbox.
|
|
789
|
+
- Limits: 100 secrets per team, 10 versions per secret, 32 secrets per process,
|
|
790
|
+
and values of 1 byte to 32 KiB of UTF-8 without NUL bytes. Names are 1-128
|
|
791
|
+
letters, digits, `_` or `-`, starting with a letter or digit.
|
|
792
|
+
- Reading secrets needs `secrets.read`; delivering them to a sandbox needs
|
|
793
|
+
`secrets.use`. Creating a secret needs `secrets.manage`. Updating, rotating,
|
|
794
|
+
and deleting secrets or versions through the SDK need both `secrets.read`
|
|
795
|
+
and `secrets.manage`, since it resolves IDs and names through metadata reads.
|
|
796
|
+
`add_version()` also reads the secret's `encryptionContext` to encrypt its
|
|
797
|
+
value. The team needs the secrets feature.
|
|
798
|
+
- Secret-started process handles wait indefinitely by default with `wait()` or
|
|
799
|
+
`result()`. A positive `wait(timeout_ms=...)` or `wait(timeout_sec=...)` limits
|
|
800
|
+
the total local wait and raises `TimeoutError` (`asyncio.TimeoutError` for
|
|
801
|
+
async handles); the process continues and the handle can be used to wait
|
|
802
|
+
again. The start's timeout limits the command's execution separately.
|
|
803
|
+
The sync client checks the limit between requests and caps each request's
|
|
804
|
+
HTTP timeouts at the time left, so a slow response or a runtime token
|
|
805
|
+
refresh can overrun it; use the async client for a strict limit.
|
|
806
|
+
Secret process output comes from the receiver's retained output, so
|
|
807
|
+
`max_output_bytes` does not apply; incomplete retained output raises an error.
|
|
808
|
+
- A process start with secrets that times out, or whose result the API can't
|
|
809
|
+
confirm, raises a non-retryable error with code
|
|
810
|
+
`secret_process_outcome_unknown`: the command may be running, so check
|
|
811
|
+
`sandbox.processes.list()` before retrying. When the API reported the
|
|
812
|
+
unknown outcome itself, its response body, including its own code
|
|
813
|
+
(`SECRET_PROCESS_OUTCOME_UNKNOWN`), is in the error's `details`.
|
|
814
|
+
- `client.sandboxes.create()` with `secrets`, `vault` or `one_password`
|
|
815
|
+
raises the same
|
|
816
|
+
`secret_process_outcome_unknown` error when the create may have reached the
|
|
817
|
+
API but its result is unknown (a timeout, a dropped connection, or an
|
|
818
|
+
unreadable response): a sandbox may be running its startup command with the
|
|
819
|
+
secrets, so list sandboxes before retrying.
|
|
820
|
+
- Vaults hold up to 32 stored secrets, named like environment variables; a
|
|
821
|
+
team has up to 100. Methods that take a vault accept its ID or exact name,
|
|
822
|
+
and vaults need the same scopes as secrets. A vault, `secrets` and
|
|
823
|
+
`one_password` together deliver at most 32 variables, and a process's `env`
|
|
824
|
+
may not set one of them.
|
|
825
|
+
- Error codes from the API:
|
|
826
|
+
- `SECRETS_ADMIN_REQUIRED` (403): the request used an `hx auth login`
|
|
827
|
+
session of a team member who isn't an owner or admin. API keys are
|
|
828
|
+
governed by their scopes instead.
|
|
829
|
+
- `SECRETS_FEATURE_DISABLED` (403): the team doesn't have the secrets
|
|
830
|
+
feature.
|
|
831
|
+
- `SECRETS_NOT_CONFIGURED` (503): the platform has no secrets key
|
|
832
|
+
configured. It isn't retried.
|
|
833
|
+
- `SECRET_STARTUP_UNSUPPORTED` (400): the image has no startup command to
|
|
834
|
+
deliver `secrets`, a `vault` or `one_password` to. Create the sandbox without them, then pass them to
|
|
835
|
+
`sandbox.exec()` or `sandbox.processes.start()`.
|
|
836
|
+
- `VAULT_NOT_FOUND` (404), `VAULT_NAME_TAKEN`, `VAULT_LIMIT_REACHED`,
|
|
837
|
+
`VAULT_ENTRY_LIMIT_REACHED` (409).
|
|
838
|
+
- `SECRET_IN_USE` (409): a vault uses the secret as an entry;
|
|
839
|
+
`SECRET_VERSION_IN_USE` (409): a vault pins the version. Only vaults
|
|
840
|
+
hold secrets, so an `op_token` can always be deleted.
|
|
841
|
+
- `SECRET_TYPE_MISMATCH` (409): an `op_token` used as a variable or vault
|
|
842
|
+
entry, or another type named as `one_password`'s `token`.
|
|
843
|
+
- `SECRET_SOURCES_INVALID` (400): for example `env` sets a variable that
|
|
844
|
+
`vault` or `secrets` also sets, a `one_password` variable is also set by
|
|
845
|
+
`vault`, `secrets` or `env`, or the sources deliver more than 32
|
|
846
|
+
variables.
|
|
847
|
+
- `SECRET_VALUE_INVALID` (422): a stored value is unusable, such as a bad
|
|
848
|
+
TOTP seed; fix it by adding a new version.
|
|
849
|
+
- `SECRET_TOTP_UNSUPPORTED` and `SECRET_ONEPASSWORD_UNSUPPORTED` (501): the
|
|
850
|
+
sandbox's host predates TOTP or 1Password, so nothing started. Retryable
|
|
851
|
+
on sandbox create, which may land on a newer host; not on a process start.
|
|
852
|
+
- `SECRET_ONEPASSWORD_REFERENCE_FAILED` (422, `details` names the
|
|
853
|
+
variables) and `SECRET_ONEPASSWORD_AUTH_FAILED` (422, also a malformed
|
|
854
|
+
token, or one that doesn't sign in to `1password.com`, `.ca` or `.eu`,
|
|
855
|
+
which is never sent to 1Password): fix the fields or the token.
|
|
856
|
+
`SECRET_ONEPASSWORD_RATE_LIMITED` (429) and
|
|
857
|
+
`SECRET_ONEPASSWORD_FAILED` (502/503) are retryable.
|
|
858
|
+
- `retryable` is the API's flag when it sends one.
|
|
859
|
+
|
|
860
|
+
### List sandboxes with filters
|
|
861
|
+
|
|
862
|
+
```python
|
|
863
|
+
from hyperbrowser import Hyperbrowser
|
|
864
|
+
|
|
865
|
+
client = Hyperbrowser(api_key="test-key")
|
|
866
|
+
result = client.sandboxes.list(
|
|
867
|
+
{
|
|
868
|
+
"status": "active",
|
|
869
|
+
"search": "sandbox",
|
|
870
|
+
"start": 1711929600000,
|
|
871
|
+
"end": 1712016000000,
|
|
872
|
+
"limit": 20,
|
|
873
|
+
}
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
for sandbox in result.sandboxes:
|
|
877
|
+
print(sandbox.id, sandbox.status)
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### List snapshots for a specific image
|
|
881
|
+
|
|
882
|
+
```python
|
|
883
|
+
from hyperbrowser import Hyperbrowser
|
|
884
|
+
|
|
885
|
+
client = Hyperbrowser(api_key="test-key")
|
|
886
|
+
snapshots = client.sandboxes.list_snapshots(
|
|
887
|
+
{"image_name": "node", "status": "created", "limit": 10}
|
|
888
|
+
)
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### Expose and unexpose ports
|
|
892
|
+
|
|
893
|
+
```python
|
|
894
|
+
from hyperbrowser import Hyperbrowser
|
|
895
|
+
|
|
896
|
+
client = Hyperbrowser(api_key="test-key")
|
|
897
|
+
sandbox = client.sandboxes.create(
|
|
898
|
+
{"image_name": "node", "cpu": 2, "memory_mib": 2048, "disk_mib": 8192}
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
result = sandbox.expose({"port": 8080, "auth": True})
|
|
902
|
+
print(result.url, result.browser_url)
|
|
903
|
+
|
|
904
|
+
sandbox.unexpose(8080)
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
### Batch file writes with per-file options
|
|
908
|
+
|
|
909
|
+
```python
|
|
910
|
+
from hyperbrowser import Hyperbrowser
|
|
911
|
+
|
|
912
|
+
client = Hyperbrowser(api_key="test-key")
|
|
913
|
+
sandbox = client.sandboxes.create({"image_name": "node"})
|
|
914
|
+
|
|
915
|
+
sandbox.files.write(
|
|
916
|
+
[
|
|
917
|
+
{
|
|
918
|
+
"path": "/tmp/config.json",
|
|
919
|
+
"data": '{"debug":true}\n',
|
|
920
|
+
"append": True,
|
|
921
|
+
"mode": "600",
|
|
922
|
+
},
|
|
923
|
+
{"path": "/tmp/blob.bin", "data": b"\x00\x01\x02"},
|
|
924
|
+
]
|
|
925
|
+
)
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Run commands and collect output
|
|
929
|
+
|
|
930
|
+
`sandbox.exec()` and `sandbox.processes.start()` stream command output from the
|
|
931
|
+
receiver as soon as execution starts. The SDK collects stdout and stderr in
|
|
932
|
+
memory, so a completed result is not limited to the receiver's replay buffer.
|
|
933
|
+
This requires a receiver supporting streaming `POST /sandbox/processes`; roll
|
|
934
|
+
out the receiver before upgrading the SDK.
|
|
935
|
+
|
|
936
|
+
```python
|
|
937
|
+
result = sandbox.exec("make test", max_output_bytes=128 * 1024 * 1024)
|
|
938
|
+
print(result.stdout, result.stderr, result.exit_code)
|
|
939
|
+
|
|
940
|
+
process = sandbox.processes.start("make test")
|
|
941
|
+
try:
|
|
942
|
+
for event in process.stream():
|
|
943
|
+
if event.type == "stdout":
|
|
944
|
+
print(event.data, end="")
|
|
945
|
+
result = process.wait()
|
|
946
|
+
finally:
|
|
947
|
+
process.disconnect()
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
The combined output limit defaults to 64 MiB per command and can be adjusted
|
|
951
|
+
with `max_output_bytes`. Exceeding it raises `output_limit_exceeded`. A broken
|
|
952
|
+
stream, missing output, or receiver truncation raises `incomplete_output`.
|
|
953
|
+
These errors include the process ID and do not automatically rerun the command.
|
|
954
|
+
|
|
955
|
+
Process streams use a separate 60-second read-idle timeout once response headers
|
|
956
|
+
arrive. Output and the receiver's 15-second heartbeats reset this timeout, so quiet
|
|
957
|
+
commands can run longer than the client's ordinary HTTP timeout. That ordinary
|
|
958
|
+
timeout still applies to connection setup and waiting for response headers.
|
|
959
|
+
|
|
960
|
+
`start()` returns after the process starts and collects in the background.
|
|
961
|
+
`wait(timeout_sec=...)` limits the local wait; collection continues after a wait
|
|
962
|
+
timeout. The timeout passed to `start()` or `exec()` limits command execution.
|
|
963
|
+
A local wait timeout raises `TimeoutError` (`asyncio.TimeoutError` in the async API).
|
|
964
|
+
`disconnect()` stops collection and leaves the command running. Use `kill()` to
|
|
965
|
+
stop it. Reattaching with `get()` can retrieve only retained receiver output;
|
|
966
|
+
`wait()` raises if that output has been truncated.
|
|
967
|
+
|
|
968
|
+
The async API has the same behavior: await `exec()`, `start()`, `wait()`, and
|
|
969
|
+
`disconnect()`, and use `async for` with `stream()`.
|
|
970
|
+
|
|
971
|
+
### Resume terminal output after reconnect
|
|
972
|
+
|
|
973
|
+
```python
|
|
974
|
+
from hyperbrowser import Hyperbrowser
|
|
975
|
+
|
|
976
|
+
client = Hyperbrowser(api_key="test-key")
|
|
977
|
+
sandbox = client.sandboxes.create({"image_name": "node"})
|
|
978
|
+
terminal = sandbox.terminal.create({"command": "bash"})
|
|
979
|
+
|
|
980
|
+
connection = terminal.attach(cursor=10)
|
|
981
|
+
for event in connection.events():
|
|
982
|
+
print(event)
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### Cache remote Dockerfile builds
|
|
986
|
+
|
|
987
|
+
Use the public context fingerprint when deriving a cache name. It uses the same
|
|
988
|
+
Dockerfile source selection and `.dockerignore` rules as remote packaging,
|
|
989
|
+
including file contents, modes, paths, and symlinks. It ignores timestamps and
|
|
990
|
+
does not compress or stage the context on disk.
|
|
991
|
+
|
|
992
|
+
```python
|
|
993
|
+
from hyperbrowser.build_context import docker_build_context_fingerprint
|
|
994
|
+
|
|
995
|
+
fingerprint = docker_build_context_fingerprint("./app")
|
|
996
|
+
# Include build options such as platform and image_init in your cache key too.
|
|
997
|
+
image_name = f"app-{fingerprint[:32]}"
|
|
998
|
+
build = client.sandboxes.build_image_from_dockerfile(
|
|
999
|
+
context_path="./app",
|
|
1000
|
+
image_name=image_name,
|
|
1001
|
+
expected_context_fingerprint=fingerprint,
|
|
1002
|
+
)
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
If the archived inputs differ from the fingerprint, the SDK raises
|
|
1006
|
+
`DockerBuildContextChangedError` before creating or uploading a build. Compute a
|
|
1007
|
+
fresh fingerprint and repeat the lookup/build operation. Use the same `dockerfile`
|
|
1008
|
+
and `force_full_context` selection when fingerprinting and building (the latter
|
|
1009
|
+
is named `remote_full_context` on the build method). The expected fingerprint is
|
|
1010
|
+
supported only for remote builds.
|
|
1011
|
+
|
|
1012
|
+
Fingerprinting streams included files and performs blocking I/O. Async callers
|
|
1013
|
+
should use `await asyncio.to_thread(docker_build_context_fingerprint, "./app")`
|
|
1014
|
+
(Python 3.9+) or an executor. It does not resolve mutable base-image tags or
|
|
1015
|
+
network resources fetched by a Dockerfile; rebuild explicitly when those change.
|
|
1016
|
+
|
|
1017
|
+
Image listings distinguish `ready` from `uploaded`: a completed team image can
|
|
1018
|
+
be ready to launch before its durability backup is uploaded. `ready` is `None`
|
|
1019
|
+
when talking to an older server. Keep the returned image ID to pin that revision.
|
|
1020
|
+
|
|
1021
|
+
### Reuse an image or join a build
|
|
1022
|
+
|
|
1023
|
+
`get_or_build_image` provides the same operation on sync and async clients. Give
|
|
1024
|
+
it either a remote Dockerfile context or a local Docker image. It derives a name
|
|
1025
|
+
from the input identity and image initialization options, reuses a ready team
|
|
1026
|
+
image, or submits a build and joins a compatible concurrent build automatically.
|
|
1027
|
+
The optional prefix is a namespace, not a fixed image alias: different inputs
|
|
1028
|
+
produce different names under the same prefix.
|
|
1029
|
+
|
|
1030
|
+
```python
|
|
1031
|
+
resolved = client.sandboxes.get_or_build_image(
|
|
1032
|
+
context_path="./app", # alternatively: docker_image="local/app:latest"
|
|
1033
|
+
image_name_prefix="my-app",
|
|
1034
|
+
wait_timeout=3600,
|
|
1035
|
+
)
|
|
1036
|
+
print(resolved.outcome) # "reused", "joined", or "created"
|
|
1037
|
+
sandbox = client.sandboxes.create({
|
|
1038
|
+
"image_name": resolved.image_name,
|
|
1039
|
+
"image_id": resolved.image_id,
|
|
1040
|
+
})
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
With `wait=False`, a submitted/joined build is returned as `resolved.build`;
|
|
1044
|
+
`image_id` is populated only when ready. `find_ready_image(name)` exposes the
|
|
1045
|
+
exact-name lookup separately. Older servers fall back to uploaded-image reuse.
|
|
1046
|
+
The public `hyperbrowser.image_builds.image_build_name` helper lets integrations
|
|
1047
|
+
derive the same name from an existing context fingerprint or Docker image digest.
|
|
1048
|
+
Passing `expected_context_fingerprint` or `expected_image_digest` avoids repeating
|
|
1049
|
+
identity discovery; supply a fresh identity for each resolution request. Changes
|
|
1050
|
+
between identity discovery and packaging are rejected instead of published under
|
|
1051
|
+
the wrong name. Local Docker images must already be available in the daemon.
|
|
1052
|
+
|
|
1053
|
+
Automatic local-image identity discovery requires a Docker CLI and Engine
|
|
1054
|
+
supporting **API 1.49 or newer (Docker 28.1+)** for platform-specific inspection.
|
|
1055
|
+
Upgrade Docker and check for an older `DOCKER_API_VERSION` override if the helper
|
|
1056
|
+
reports this requirement. Remote Dockerfile builds do not require local Docker.
|
|
1057
|
+
The existing explicit-name import method retains its inspection fallback.
|
|
1058
|
+
|
|
1059
|
+
`force_build=True` skips ready-image lookup but still joins matching active builds
|
|
1060
|
+
and permits existing layer/artifact caches. Use it to refresh mutable base tags or
|
|
1061
|
+
external Dockerfile downloads. Joining does not change an existing builder's
|
|
1062
|
+
resources. Lookup and creation use separate API calls; if another build completes
|
|
1063
|
+
between them, an additional revision can be submitted.
|
|
1064
|
+
|
|
1065
|
+
Each caller owns its polling timeout. Canceling that wait does not cancel an
|
|
1066
|
+
accepted backend build. Uploads have a separate inactivity allowance
|
|
1067
|
+
(`upload_timeout=600` by default), not a total upload-duration limit. The existing
|
|
1068
|
+
`build_image_from_dockerfile` and `build_image_from_docker_image` methods retain
|
|
1069
|
+
their explicit-name behavior and continue to report build conflicts directly.
|
|
1070
|
+
|
|
1071
|
+
## License
|
|
1072
|
+
|
|
1073
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1074
|
+
|