orb-browser 0.1.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.
- orb_browser-0.1.0/LICENSE +21 -0
- orb_browser-0.1.0/PKG-INFO +126 -0
- orb_browser-0.1.0/README.md +111 -0
- orb_browser-0.1.0/orb_browser/__init__.py +4 -0
- orb_browser-0.1.0/orb_browser/client.py +281 -0
- orb_browser-0.1.0/orb_browser.egg-info/PKG-INFO +126 -0
- orb_browser-0.1.0/orb_browser.egg-info/SOURCES.txt +9 -0
- orb_browser-0.1.0/orb_browser.egg-info/dependency_links.txt +1 -0
- orb_browser-0.1.0/orb_browser.egg-info/top_level.txt +1 -0
- orb_browser-0.1.0/pyproject.toml +23 -0
- orb_browser-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orb-browser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Browser agents that sleep for $0 and wake in 500ms on Orb Cloud
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/nextbysam/orb-browser
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# orb-browser
|
|
17
|
+
|
|
18
|
+
Browser agents that sleep for $0 and wake in 500ms.
|
|
19
|
+
|
|
20
|
+
Deploy headless Chrome on [Orb Cloud](https://orbcloud.dev). Connect [browser-use](https://github.com/browser-use/browser-use) or any CDP client. When idle, checkpoint the browser to NVMe — cookies, DOM, localStorage, everything preserved. Wake it up later in ~500ms, exactly where you left off.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install orb-browser browser-use
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or from source:
|
|
29
|
+
```bash
|
|
30
|
+
pip install git+https://github.com/nextbysam/orb-browser.git
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get an API Key
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Register
|
|
37
|
+
curl -X POST https://api.orbcloud.dev/api/v1/auth/register \
|
|
38
|
+
-H 'Content-Type: application/json' \
|
|
39
|
+
-d '{"email":"you@example.com"}'
|
|
40
|
+
|
|
41
|
+
# Create org key
|
|
42
|
+
curl -X POST https://api.orbcloud.dev/v1/keys \
|
|
43
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
44
|
+
-H 'Content-Type: application/json' \
|
|
45
|
+
-d '{"name":"my-key"}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
from orb_browser import OrbBrowser
|
|
53
|
+
from browser_use import Browser
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
# Deploy a browser on Orb Cloud (~1-3 min first time)
|
|
57
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
58
|
+
cdp_url = orb.deploy()
|
|
59
|
+
|
|
60
|
+
# Connect browser-use
|
|
61
|
+
browser = Browser(cdp_url=cdp_url)
|
|
62
|
+
await browser.start()
|
|
63
|
+
|
|
64
|
+
# Full browser control
|
|
65
|
+
await browser.navigate_to("https://example.com")
|
|
66
|
+
title = await browser.get_current_page_title()
|
|
67
|
+
screenshot = await browser.take_screenshot()
|
|
68
|
+
|
|
69
|
+
# Disconnect before sleep
|
|
70
|
+
await browser.stop()
|
|
71
|
+
|
|
72
|
+
# Sleep — frozen to NVMe, $0/hr
|
|
73
|
+
orb.sleep()
|
|
74
|
+
|
|
75
|
+
# ... hours later ...
|
|
76
|
+
|
|
77
|
+
# Wake — ~500ms, everything restored
|
|
78
|
+
cdp_url = orb.wake()
|
|
79
|
+
|
|
80
|
+
# Reconnect — same page, same cookies
|
|
81
|
+
browser = Browser(cdp_url=cdp_url)
|
|
82
|
+
await browser.start()
|
|
83
|
+
|
|
84
|
+
# Clean up
|
|
85
|
+
await browser.stop()
|
|
86
|
+
orb.destroy()
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from orb_browser import OrbBrowser
|
|
95
|
+
|
|
96
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
|--------|-------------|
|
|
101
|
+
| `orb.deploy()` | Deploy browser VM, returns CDP WebSocket URL |
|
|
102
|
+
| `orb.sleep()` | Checkpoint to NVMe ($0 while sleeping) |
|
|
103
|
+
| `orb.wake()` | Restore from checkpoint (~500ms), returns new CDP URL |
|
|
104
|
+
| `orb.destroy()` | Delete the VM |
|
|
105
|
+
| `orb.connect(computer_id, agent_port)` | Connect to existing VM |
|
|
106
|
+
| `orb.state` | Current state: init, deploying, running, sleeping, destroyed |
|
|
107
|
+
| `orb.vm_url` | HTTPS URL of the VM |
|
|
108
|
+
| `orb.cdp_url` | CDP WebSocket URL |
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
1. `deploy()` creates a VM on Orb Cloud, installs Chromium via Playwright, starts it with CDP debugging enabled
|
|
113
|
+
2. Returns a `wss://` CDP URL that browser-use (or Puppeteer, Playwright, any CDP client) connects to
|
|
114
|
+
3. `sleep()` calls CRIU to checkpoint the entire process tree (Node.js + Chromium + renderers) to NVMe
|
|
115
|
+
4. `wake()` restores everything in ~500ms — the browser doesn't know it was frozen
|
|
116
|
+
|
|
117
|
+
## Works With
|
|
118
|
+
|
|
119
|
+
- [browser-use](https://github.com/browser-use/browser-use) (78K stars) — AI browser agents
|
|
120
|
+
- [Playwright](https://playwright.dev) — `browser = await chromium.connect_over_cdp(cdp_url)`
|
|
121
|
+
- [Puppeteer](https://pptr.dev) — `browser = await puppeteer.connect({ browserWSEndpoint: cdpUrl })`
|
|
122
|
+
- Any CDP client
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# orb-browser
|
|
2
|
+
|
|
3
|
+
Browser agents that sleep for $0 and wake in 500ms.
|
|
4
|
+
|
|
5
|
+
Deploy headless Chrome on [Orb Cloud](https://orbcloud.dev). Connect [browser-use](https://github.com/browser-use/browser-use) or any CDP client. When idle, checkpoint the browser to NVMe — cookies, DOM, localStorage, everything preserved. Wake it up later in ~500ms, exactly where you left off.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install orb-browser browser-use
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or from source:
|
|
14
|
+
```bash
|
|
15
|
+
pip install git+https://github.com/nextbysam/orb-browser.git
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Get an API Key
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Register
|
|
22
|
+
curl -X POST https://api.orbcloud.dev/api/v1/auth/register \
|
|
23
|
+
-H 'Content-Type: application/json' \
|
|
24
|
+
-d '{"email":"you@example.com"}'
|
|
25
|
+
|
|
26
|
+
# Create org key
|
|
27
|
+
curl -X POST https://api.orbcloud.dev/v1/keys \
|
|
28
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
29
|
+
-H 'Content-Type: application/json' \
|
|
30
|
+
-d '{"name":"my-key"}'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from orb_browser import OrbBrowser
|
|
38
|
+
from browser_use import Browser
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
# Deploy a browser on Orb Cloud (~1-3 min first time)
|
|
42
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
43
|
+
cdp_url = orb.deploy()
|
|
44
|
+
|
|
45
|
+
# Connect browser-use
|
|
46
|
+
browser = Browser(cdp_url=cdp_url)
|
|
47
|
+
await browser.start()
|
|
48
|
+
|
|
49
|
+
# Full browser control
|
|
50
|
+
await browser.navigate_to("https://example.com")
|
|
51
|
+
title = await browser.get_current_page_title()
|
|
52
|
+
screenshot = await browser.take_screenshot()
|
|
53
|
+
|
|
54
|
+
# Disconnect before sleep
|
|
55
|
+
await browser.stop()
|
|
56
|
+
|
|
57
|
+
# Sleep — frozen to NVMe, $0/hr
|
|
58
|
+
orb.sleep()
|
|
59
|
+
|
|
60
|
+
# ... hours later ...
|
|
61
|
+
|
|
62
|
+
# Wake — ~500ms, everything restored
|
|
63
|
+
cdp_url = orb.wake()
|
|
64
|
+
|
|
65
|
+
# Reconnect — same page, same cookies
|
|
66
|
+
browser = Browser(cdp_url=cdp_url)
|
|
67
|
+
await browser.start()
|
|
68
|
+
|
|
69
|
+
# Clean up
|
|
70
|
+
await browser.stop()
|
|
71
|
+
orb.destroy()
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from orb_browser import OrbBrowser
|
|
80
|
+
|
|
81
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `orb.deploy()` | Deploy browser VM, returns CDP WebSocket URL |
|
|
87
|
+
| `orb.sleep()` | Checkpoint to NVMe ($0 while sleeping) |
|
|
88
|
+
| `orb.wake()` | Restore from checkpoint (~500ms), returns new CDP URL |
|
|
89
|
+
| `orb.destroy()` | Delete the VM |
|
|
90
|
+
| `orb.connect(computer_id, agent_port)` | Connect to existing VM |
|
|
91
|
+
| `orb.state` | Current state: init, deploying, running, sleeping, destroyed |
|
|
92
|
+
| `orb.vm_url` | HTTPS URL of the VM |
|
|
93
|
+
| `orb.cdp_url` | CDP WebSocket URL |
|
|
94
|
+
|
|
95
|
+
## How It Works
|
|
96
|
+
|
|
97
|
+
1. `deploy()` creates a VM on Orb Cloud, installs Chromium via Playwright, starts it with CDP debugging enabled
|
|
98
|
+
2. Returns a `wss://` CDP URL that browser-use (or Puppeteer, Playwright, any CDP client) connects to
|
|
99
|
+
3. `sleep()` calls CRIU to checkpoint the entire process tree (Node.js + Chromium + renderers) to NVMe
|
|
100
|
+
4. `wake()` restores everything in ~500ms — the browser doesn't know it was frozen
|
|
101
|
+
|
|
102
|
+
## Works With
|
|
103
|
+
|
|
104
|
+
- [browser-use](https://github.com/browser-use/browser-use) (78K stars) — AI browser agents
|
|
105
|
+
- [Playwright](https://playwright.dev) — `browser = await chromium.connect_over_cdp(cdp_url)`
|
|
106
|
+
- [Puppeteer](https://pptr.dev) — `browser = await puppeteer.connect({ browserWSEndpoint: cdpUrl })`
|
|
107
|
+
- Any CDP client
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbBrowser — deploy browser agents on Orb Cloud.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from orb_browser import OrbBrowser
|
|
6
|
+
|
|
7
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
8
|
+
cdp_url = orb.deploy()
|
|
9
|
+
|
|
10
|
+
# Use with browser-use
|
|
11
|
+
from browser_use import Browser
|
|
12
|
+
browser = Browser(cdp_url=cdp_url)
|
|
13
|
+
await browser.start()
|
|
14
|
+
|
|
15
|
+
# Sleep ($0 while frozen)
|
|
16
|
+
orb.sleep()
|
|
17
|
+
|
|
18
|
+
# Wake (~500ms)
|
|
19
|
+
cdp_url = orb.wake()
|
|
20
|
+
browser = Browser(cdp_url=cdp_url)
|
|
21
|
+
await browser.start()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
import urllib.error
|
|
28
|
+
|
|
29
|
+
# The orb.toml config that runs on Orb Cloud
|
|
30
|
+
ORB_TOML = """[agent]
|
|
31
|
+
name = "orb-browser"
|
|
32
|
+
lang = "node"
|
|
33
|
+
entry = "server.js"
|
|
34
|
+
|
|
35
|
+
[source]
|
|
36
|
+
git = "https://github.com/nextbysam/orb-browser.git"
|
|
37
|
+
branch = "main"
|
|
38
|
+
|
|
39
|
+
[build]
|
|
40
|
+
steps = [
|
|
41
|
+
"apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y libnss3 libatk-bridge2.0-0t64 libcups2t64 libdrm2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64 libxshmfence1 libxcomposite1 libxrandr2 libxdamage1 libxfixes3 libxext6 libx11-xcb1 libxcb1 libxkbcommon0 libdbus-1-3",
|
|
42
|
+
"cd /agent/code && npm install",
|
|
43
|
+
"PLAYWRIGHT_BROWSERS_PATH=/opt/browsers npx playwright install chromium"
|
|
44
|
+
]
|
|
45
|
+
working_dir = "/agent/code"
|
|
46
|
+
|
|
47
|
+
[agent.env]
|
|
48
|
+
PLAYWRIGHT_BROWSERS_PATH = "/opt/browsers"
|
|
49
|
+
PORT = "8000"
|
|
50
|
+
CDP_PORT = "9222"
|
|
51
|
+
|
|
52
|
+
[backend]
|
|
53
|
+
provider = "custom"
|
|
54
|
+
|
|
55
|
+
[ports]
|
|
56
|
+
expose = [8000, 9222]
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OrbBrowser:
|
|
61
|
+
"""Deploy and manage a browser on Orb Cloud with sleep/wake support."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, api_key: str, api_url: str = "https://api.orbcloud.dev"):
|
|
64
|
+
self.api_key = api_key
|
|
65
|
+
self.api_url = api_url
|
|
66
|
+
self.computer_id: str | None = None
|
|
67
|
+
self.short_id: str | None = None
|
|
68
|
+
self.agent_port: int | None = None
|
|
69
|
+
self.cdp_url: str | None = None
|
|
70
|
+
self._state = "init"
|
|
71
|
+
|
|
72
|
+
def deploy(
|
|
73
|
+
self,
|
|
74
|
+
name: str | None = None,
|
|
75
|
+
runtime_mb: int = 2048,
|
|
76
|
+
disk_mb: int = 4096,
|
|
77
|
+
wait: bool = True,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Deploy a browser on Orb Cloud. Returns the CDP WebSocket URL.
|
|
81
|
+
|
|
82
|
+
Takes 1-3 minutes on first deploy (installs Chrome).
|
|
83
|
+
"""
|
|
84
|
+
if name is None:
|
|
85
|
+
name = f"orb-browser-{int(time.time())}"
|
|
86
|
+
|
|
87
|
+
self._state = "deploying"
|
|
88
|
+
print(f"[orb-browser] Creating VM...")
|
|
89
|
+
|
|
90
|
+
# 1. Create computer
|
|
91
|
+
res = self._orb("POST", "/v1/computers", {
|
|
92
|
+
"name": name, "runtime_mb": runtime_mb, "disk_mb": disk_mb,
|
|
93
|
+
})
|
|
94
|
+
self.computer_id = res["id"]
|
|
95
|
+
self.short_id = self.computer_id[:8]
|
|
96
|
+
print(f"[orb-browser] VM: {self.short_id}")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# 2. Upload config
|
|
100
|
+
req = urllib.request.Request(
|
|
101
|
+
f"{self.api_url}/v1/computers/{self.computer_id}/config",
|
|
102
|
+
data=ORB_TOML.encode(),
|
|
103
|
+
headers={
|
|
104
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
105
|
+
"Content-Type": "application/toml",
|
|
106
|
+
},
|
|
107
|
+
method="POST",
|
|
108
|
+
)
|
|
109
|
+
urllib.request.urlopen(req)
|
|
110
|
+
|
|
111
|
+
# 3. Build
|
|
112
|
+
print(f"[orb-browser] Building (Chrome + Playwright)...")
|
|
113
|
+
req = urllib.request.Request(
|
|
114
|
+
f"{self.api_url}/v1/computers/{self.computer_id}/build",
|
|
115
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
116
|
+
method="POST",
|
|
117
|
+
)
|
|
118
|
+
build_res = json.loads(urllib.request.urlopen(req, timeout=900).read())
|
|
119
|
+
if not build_res.get("success"):
|
|
120
|
+
failed = next(
|
|
121
|
+
(s for s in build_res.get("steps", []) if s["exit_code"] != 0),
|
|
122
|
+
None,
|
|
123
|
+
)
|
|
124
|
+
raise RuntimeError(f"Build failed: {failed}")
|
|
125
|
+
print(f"[orb-browser] Build OK")
|
|
126
|
+
|
|
127
|
+
# 4. Deploy agent
|
|
128
|
+
deploy_res = self._orb(
|
|
129
|
+
"POST", f"/v1/computers/{self.computer_id}/agents", {}
|
|
130
|
+
)
|
|
131
|
+
self.agent_port = deploy_res["agents"][0]["port"]
|
|
132
|
+
|
|
133
|
+
# 5. Wait for health
|
|
134
|
+
if wait:
|
|
135
|
+
self._wait_for_health(timeout=60)
|
|
136
|
+
|
|
137
|
+
# 6. Set hostname so CDP URLs are correct
|
|
138
|
+
vm_url = f"https://{self.short_id}.orbcloud.dev"
|
|
139
|
+
urllib.request.urlopen(
|
|
140
|
+
f"{vm_url}/set-host?host={self.short_id}.orbcloud.dev"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 7. Get CDP URL
|
|
144
|
+
cdp_data = json.loads(
|
|
145
|
+
urllib.request.urlopen(f"{vm_url}/cdp").read()
|
|
146
|
+
)
|
|
147
|
+
self.cdp_url = cdp_data["cdpUrl"]
|
|
148
|
+
self._state = "running"
|
|
149
|
+
|
|
150
|
+
print(f"[orb-browser] Ready! CDP: {self.cdp_url}")
|
|
151
|
+
return self.cdp_url
|
|
152
|
+
|
|
153
|
+
except Exception:
|
|
154
|
+
self.destroy()
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
def connect(self, computer_id: str, agent_port: int) -> str:
|
|
158
|
+
"""Connect to an existing Orb Browser VM. Returns CDP URL."""
|
|
159
|
+
self.computer_id = computer_id
|
|
160
|
+
self.short_id = computer_id[:8]
|
|
161
|
+
self.agent_port = agent_port
|
|
162
|
+
self._state = "running"
|
|
163
|
+
|
|
164
|
+
vm_url = f"https://{self.short_id}.orbcloud.dev"
|
|
165
|
+
urllib.request.urlopen(
|
|
166
|
+
f"{vm_url}/set-host?host={self.short_id}.orbcloud.dev"
|
|
167
|
+
)
|
|
168
|
+
cdp_data = json.loads(urllib.request.urlopen(f"{vm_url}/cdp").read())
|
|
169
|
+
self.cdp_url = cdp_data["cdpUrl"]
|
|
170
|
+
return self.cdp_url
|
|
171
|
+
|
|
172
|
+
def sleep(self) -> dict:
|
|
173
|
+
"""
|
|
174
|
+
Checkpoint the browser to NVMe. Costs $0 while sleeping.
|
|
175
|
+
WebSocket connections will drop — reconnect after wake().
|
|
176
|
+
"""
|
|
177
|
+
if self._state != "running":
|
|
178
|
+
raise RuntimeError(f"Cannot sleep in state: {self._state}")
|
|
179
|
+
|
|
180
|
+
res = self._orb(
|
|
181
|
+
"POST",
|
|
182
|
+
f"/v1/computers/{self.computer_id}/agents/demote",
|
|
183
|
+
{"port": self.agent_port},
|
|
184
|
+
)
|
|
185
|
+
if res.get("status") != "demoted":
|
|
186
|
+
raise RuntimeError(f"Sleep failed: {res}")
|
|
187
|
+
|
|
188
|
+
self._state = "sleeping"
|
|
189
|
+
print(f"[orb-browser] Sleeping (frozen, $0)")
|
|
190
|
+
return res
|
|
191
|
+
|
|
192
|
+
def wake(self, wait: bool = True) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Restore the browser from NVMe. ~500ms.
|
|
195
|
+
Returns the CDP URL (same as before sleep).
|
|
196
|
+
Reconnect browser-use with the returned URL.
|
|
197
|
+
"""
|
|
198
|
+
if self._state != "sleeping":
|
|
199
|
+
raise RuntimeError(f"Cannot wake in state: {self._state}")
|
|
200
|
+
|
|
201
|
+
res = self._orb(
|
|
202
|
+
"POST",
|
|
203
|
+
f"/v1/computers/{self.computer_id}/agents/promote",
|
|
204
|
+
{"port": self.agent_port},
|
|
205
|
+
)
|
|
206
|
+
if res.get("status") != "promoted":
|
|
207
|
+
raise RuntimeError(f"Wake failed: {res}")
|
|
208
|
+
|
|
209
|
+
if res.get("port"):
|
|
210
|
+
self.agent_port = res["port"]
|
|
211
|
+
|
|
212
|
+
if wait:
|
|
213
|
+
self._wait_for_health(timeout=30)
|
|
214
|
+
|
|
215
|
+
# Refresh CDP URL
|
|
216
|
+
vm_url = f"https://{self.short_id}.orbcloud.dev"
|
|
217
|
+
cdp_data = json.loads(urllib.request.urlopen(f"{vm_url}/cdp").read())
|
|
218
|
+
self.cdp_url = cdp_data["cdpUrl"]
|
|
219
|
+
self._state = "running"
|
|
220
|
+
|
|
221
|
+
print(f"[orb-browser] Awake! CDP: {self.cdp_url}")
|
|
222
|
+
return self.cdp_url
|
|
223
|
+
|
|
224
|
+
def destroy(self):
|
|
225
|
+
"""Delete the VM."""
|
|
226
|
+
if not self.computer_id:
|
|
227
|
+
return
|
|
228
|
+
try:
|
|
229
|
+
req = urllib.request.Request(
|
|
230
|
+
f"{self.api_url}/v1/computers/{self.computer_id}",
|
|
231
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
232
|
+
method="DELETE",
|
|
233
|
+
)
|
|
234
|
+
urllib.request.urlopen(req)
|
|
235
|
+
except urllib.error.HTTPError:
|
|
236
|
+
pass
|
|
237
|
+
self._state = "destroyed"
|
|
238
|
+
print(f"[orb-browser] Destroyed {self.short_id}")
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def vm_url(self) -> str | None:
|
|
242
|
+
return f"https://{self.short_id}.orbcloud.dev" if self.short_id else None
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def state(self) -> str:
|
|
246
|
+
return self._state
|
|
247
|
+
|
|
248
|
+
# -- Internal --
|
|
249
|
+
|
|
250
|
+
def _orb(self, method: str, path: str, body: dict | None = None) -> dict:
|
|
251
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
252
|
+
req = urllib.request.Request(
|
|
253
|
+
f"{self.api_url}{path}",
|
|
254
|
+
data=data,
|
|
255
|
+
headers={
|
|
256
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
},
|
|
259
|
+
method=method,
|
|
260
|
+
)
|
|
261
|
+
try:
|
|
262
|
+
return json.loads(urllib.request.urlopen(req).read())
|
|
263
|
+
except urllib.error.HTTPError as e:
|
|
264
|
+
body_text = e.read().decode() if e.fp else ""
|
|
265
|
+
raise RuntimeError(
|
|
266
|
+
f"Orb API {method} {path} failed ({e.code}): {body_text}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _wait_for_health(self, timeout: int = 60):
|
|
270
|
+
vm_url = f"https://{self.short_id}.orbcloud.dev"
|
|
271
|
+
start = time.time()
|
|
272
|
+
while time.time() - start < timeout:
|
|
273
|
+
try:
|
|
274
|
+
res = urllib.request.urlopen(f"{vm_url}/health", timeout=5)
|
|
275
|
+
data = json.loads(res.read())
|
|
276
|
+
if data.get("status") == "ok" and data.get("browserReady"):
|
|
277
|
+
return
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
time.sleep(2)
|
|
281
|
+
raise TimeoutError(f"Browser not ready after {timeout}s")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orb-browser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Browser agents that sleep for $0 and wake in 500ms on Orb Cloud
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/nextbysam/orb-browser
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# orb-browser
|
|
17
|
+
|
|
18
|
+
Browser agents that sleep for $0 and wake in 500ms.
|
|
19
|
+
|
|
20
|
+
Deploy headless Chrome on [Orb Cloud](https://orbcloud.dev). Connect [browser-use](https://github.com/browser-use/browser-use) or any CDP client. When idle, checkpoint the browser to NVMe — cookies, DOM, localStorage, everything preserved. Wake it up later in ~500ms, exactly where you left off.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install orb-browser browser-use
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or from source:
|
|
29
|
+
```bash
|
|
30
|
+
pip install git+https://github.com/nextbysam/orb-browser.git
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get an API Key
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Register
|
|
37
|
+
curl -X POST https://api.orbcloud.dev/api/v1/auth/register \
|
|
38
|
+
-H 'Content-Type: application/json' \
|
|
39
|
+
-d '{"email":"you@example.com"}'
|
|
40
|
+
|
|
41
|
+
# Create org key
|
|
42
|
+
curl -X POST https://api.orbcloud.dev/v1/keys \
|
|
43
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
44
|
+
-H 'Content-Type: application/json' \
|
|
45
|
+
-d '{"name":"my-key"}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
from orb_browser import OrbBrowser
|
|
53
|
+
from browser_use import Browser
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
# Deploy a browser on Orb Cloud (~1-3 min first time)
|
|
57
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
58
|
+
cdp_url = orb.deploy()
|
|
59
|
+
|
|
60
|
+
# Connect browser-use
|
|
61
|
+
browser = Browser(cdp_url=cdp_url)
|
|
62
|
+
await browser.start()
|
|
63
|
+
|
|
64
|
+
# Full browser control
|
|
65
|
+
await browser.navigate_to("https://example.com")
|
|
66
|
+
title = await browser.get_current_page_title()
|
|
67
|
+
screenshot = await browser.take_screenshot()
|
|
68
|
+
|
|
69
|
+
# Disconnect before sleep
|
|
70
|
+
await browser.stop()
|
|
71
|
+
|
|
72
|
+
# Sleep — frozen to NVMe, $0/hr
|
|
73
|
+
orb.sleep()
|
|
74
|
+
|
|
75
|
+
# ... hours later ...
|
|
76
|
+
|
|
77
|
+
# Wake — ~500ms, everything restored
|
|
78
|
+
cdp_url = orb.wake()
|
|
79
|
+
|
|
80
|
+
# Reconnect — same page, same cookies
|
|
81
|
+
browser = Browser(cdp_url=cdp_url)
|
|
82
|
+
await browser.start()
|
|
83
|
+
|
|
84
|
+
# Clean up
|
|
85
|
+
await browser.stop()
|
|
86
|
+
orb.destroy()
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from orb_browser import OrbBrowser
|
|
95
|
+
|
|
96
|
+
orb = OrbBrowser(api_key="orb_...")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
|--------|-------------|
|
|
101
|
+
| `orb.deploy()` | Deploy browser VM, returns CDP WebSocket URL |
|
|
102
|
+
| `orb.sleep()` | Checkpoint to NVMe ($0 while sleeping) |
|
|
103
|
+
| `orb.wake()` | Restore from checkpoint (~500ms), returns new CDP URL |
|
|
104
|
+
| `orb.destroy()` | Delete the VM |
|
|
105
|
+
| `orb.connect(computer_id, agent_port)` | Connect to existing VM |
|
|
106
|
+
| `orb.state` | Current state: init, deploying, running, sleeping, destroyed |
|
|
107
|
+
| `orb.vm_url` | HTTPS URL of the VM |
|
|
108
|
+
| `orb.cdp_url` | CDP WebSocket URL |
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
1. `deploy()` creates a VM on Orb Cloud, installs Chromium via Playwright, starts it with CDP debugging enabled
|
|
113
|
+
2. Returns a `wss://` CDP URL that browser-use (or Puppeteer, Playwright, any CDP client) connects to
|
|
114
|
+
3. `sleep()` calls CRIU to checkpoint the entire process tree (Node.js + Chromium + renderers) to NVMe
|
|
115
|
+
4. `wake()` restores everything in ~500ms — the browser doesn't know it was frozen
|
|
116
|
+
|
|
117
|
+
## Works With
|
|
118
|
+
|
|
119
|
+
- [browser-use](https://github.com/browser-use/browser-use) (78K stars) — AI browser agents
|
|
120
|
+
- [Playwright](https://playwright.dev) — `browser = await chromium.connect_over_cdp(cdp_url)`
|
|
121
|
+
- [Puppeteer](https://pptr.dev) — `browser = await puppeteer.connect({ browserWSEndpoint: cdpUrl })`
|
|
122
|
+
- Any CDP client
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
orb_browser
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orb-browser"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Browser agents that sleep for $0 and wake in 500ms on Orb Cloud"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/nextbysam/orb-browser"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
include = ["orb_browser*"]
|