browser-workspace 0.4.2 → 0.4.5
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.
- package/README.md +15 -1
- package/SKILL.md +22 -21
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/package.json +1 -1
- package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
- package/platforms/chatgpt/tests/test_contract.py +7 -0
- package/pyproject.toml +6 -2
- package/session_daemon.py +91 -3
- package/src/browser_workspace.egg-info/PKG-INFO +2 -1
- package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
- package/src/browser_workspace.egg-info/entry_points.txt +2 -0
- package/src/browser_workspace.egg-info/requires.txt +1 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -1
- package/src/browser_workspace_app/__init__.py +1 -0
- package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
- package/src/browser_workspace_app/cli.py +236 -0
- package/src/browser_workspace_app/extension/manifest.json +26 -0
- package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
- package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
- package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
- package/src/browser_workspace_app/extension/workspace.html +8 -0
- package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
- package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
- package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
- package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
- package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
- package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
- package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
- package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
- package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
- package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
- package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
- package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
- package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
- package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
- package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
- package/src/browser_workspace_app/managed_chrome.py +244 -0
- package/src/browser_workspace_app/node/admin.mjs +44 -0
- package/src/browser_workspace_app/package.json +36 -0
- package/src/browser_workspace_app/platform_runner.py +204 -0
- package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
- package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
- package/src/browser_workspace_app/pyproject.toml +33 -0
- package/src/browser_workspace_app/session_client.py +107 -0
- package/src/browser_workspace_app/session_daemon.py +431 -0
- package/src/browser_harness/__init__.py +0 -2
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +0 -201
- package/src/browser_harness/admin.py +0 -1588
- package/src/browser_harness/auth.py +0 -546
- package/src/browser_harness/daemon.py +0 -894
- package/src/browser_harness/helpers.py +0 -668
- package/src/browser_harness/macos.py +0 -131
- package/src/browser_harness/mcp_cli.py +0 -16
- package/src/browser_harness/paths.py +0 -49
- package/src/browser_harness/recorder.py +0 -343
- package/src/browser_harness/run.py +0 -413
- package/src/browser_harness/telemetry.py +0 -308
- package/src/browser_harness/video-template.html +0 -970
- package/src/browser_harness/video.py +0 -749
- package/src/browser_harness/video_render.py +0 -524
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ browser-workspace session stop SESSION_ID
|
|
|
26
26
|
|
|
27
27
|
```text
|
|
28
28
|
browser-workspace
|
|
29
|
-
├──
|
|
29
|
+
├── pyproject.toml pins the external browser-harness runtime dependency
|
|
30
30
|
├── agent-workspace/ Browser Workspace helper layer
|
|
31
31
|
├── extension/ Chrome workspace/tab manager
|
|
32
32
|
├── session_daemon.py persistent Python session service
|
|
@@ -136,3 +136,17 @@ Each platform manifest records explicit verification status and evidence. `verif
|
|
|
136
136
|
## npx skills installation
|
|
137
137
|
|
|
138
138
|
After `npx skills add ...`, invoke `~/.agents/skills/browser-workspace/bin/browser-workspace` directly. The first session start bootstraps the skill-owned Python virtual environment automatically; running `scripts/install.sh` is optional.
|
|
139
|
+
|
|
140
|
+
### Python distribution (PyPI)
|
|
141
|
+
|
|
142
|
+
Browser Workspace is also packaged as a Python CLI. Once its PyPI trusted publisher is configured and a `pypi-v*` release tag is published, invoke it with:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uvx browser-workspace status
|
|
146
|
+
uvx browser-workspace session list
|
|
147
|
+
uvx browser-workspace platform profile wechat-channels
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The package entry point is `browser_workspace_app.cli:main`, and Python dependencies are managed by `pyproject.toml`. This uses the same existing Chrome profile and Session Daemon socket as the previous npm CLI; it does **not** start a second browser or daemon when the existing one is healthy.
|
|
151
|
+
|
|
152
|
+
Before building, run `python tools/sync-pypi-package.py` to copy canonical Python and packaged platform assets to the wheel source. Run `uv build`, then validate with `uvx --from dist/browser_workspace-<version>-py3-none-any.whl browser-workspace status`. The PyPI publishing workflow uses GitHub OIDC trusted publishing; configure `lalalic/browser-workspace` with the `pypi` environment as the publisher on PyPI before the first tag release.
|
package/SKILL.md
CHANGED
|
@@ -9,14 +9,14 @@ Browser Workspace is the **only browser skill agents should load**. It includes
|
|
|
9
9
|
|
|
10
10
|
## Agent contract
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Install the Skill instructions through your preferred skill manager, but run the published Python CLI through **PyPI/uvx**. The Skill directory is documentation, not the executable source of truth:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
bw() { uvx browser-workspace "$@"; }
|
|
16
|
+
bw status
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
On first use, Browser Workspace
|
|
19
|
+
On first use, `uvx` installs Browser Workspace and Python dependencies from PyPI into its managed cache. No local source checkout, `.venv`, `.env`, or `npx` CLI is required. Use `uvx browser-workspace ...` consistently, including in NeoY/Agents Relay workers.
|
|
20
20
|
|
|
21
21
|
Use one session-owned execution path for orchestrated browser work:
|
|
22
22
|
|
|
@@ -61,7 +61,7 @@ Rules:
|
|
|
61
61
|
Default workspace is `Harness`:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
|
|
64
|
+
bw session start
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Normal agents do not choose or manage workspaces. `session start` defaults to `Harness`; the `--workspace` implementation option is hidden from normal CLI help. Workspace lifecycle is not exposed through the agent CLI. Product runtimes that need persistent isolation discover the product-only Node admin helper with `browser-workspace status`, then call `ensureWorkspace` / `deleteWorkspace` through that helper.
|
|
@@ -69,7 +69,7 @@ Normal agents do not choose or manage workspaces. `session start` defaults to `H
|
|
|
69
69
|
Start directly at a URL when known:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
|
|
72
|
+
bw session start --url https://example.com
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
The result is JSON containing `session_id`, `workspace`, `target_id`, the starting `url`, and `workspace_supported`.
|
|
@@ -81,7 +81,7 @@ Browser Workspace self-manages exactly one machine-wide persistent, visible Chro
|
|
|
81
81
|
`session exec` reads Python from stdin. Helpers are already imported; do not import another browser package.
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
|
|
84
|
+
bw session exec <session_id> <<'PY'
|
|
85
85
|
print(page_info())
|
|
86
86
|
print(snapshot())
|
|
87
87
|
PY
|
|
@@ -90,7 +90,7 @@ PY
|
|
|
90
90
|
Python state persists across later calls in the same session:
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
|
|
93
|
+
bw session exec <session_id> <<'PY'
|
|
94
94
|
meeting_name = "Demo"
|
|
95
95
|
print(current_tab())
|
|
96
96
|
PY
|
|
@@ -99,7 +99,7 @@ PY
|
|
|
99
99
|
Then later:
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
|
|
102
|
+
bw session exec <session_id> <<'PY'
|
|
103
103
|
print(meeting_name)
|
|
104
104
|
PY
|
|
105
105
|
```
|
|
@@ -109,7 +109,7 @@ A Python file may also be used with `--code-file`.
|
|
|
109
109
|
## Stop
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
|
-
|
|
112
|
+
bw session stop <session_id>
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
Stopping closes every tab owned or opened by that session—including tabs created with `new_tab()`, `bh.new_tab()`, and detected child/popup tabs—then destroys the session's Python namespace. Raw `cdp("Target.createTarget", ...)` is rejected; use `new_tab()` so Browser Workspace can own and group the tab. It does not close unrelated browser tabs. Do not reuse the ID afterward.
|
|
@@ -117,11 +117,11 @@ Stopping closes every tab owned or opened by that session—including tabs creat
|
|
|
117
117
|
A safe shell structure is:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
SESSION_JSON=$(
|
|
120
|
+
SESSION_JSON=$(bw session start)
|
|
121
121
|
SESSION_ID=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["session_id"])' "$SESSION_JSON")
|
|
122
|
-
trap '
|
|
122
|
+
trap 'bw session stop "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
bw session exec "$SESSION_ID" <<'PY'
|
|
125
125
|
print(snapshot())
|
|
126
126
|
PY
|
|
127
127
|
```
|
|
@@ -231,23 +231,23 @@ Platform rules:
|
|
|
231
231
|
Normal orchestrator workflow:
|
|
232
232
|
|
|
233
233
|
```bash
|
|
234
|
-
SESSION_JSON=$(
|
|
234
|
+
SESSION_JSON=$(bw session start --url https://example.com)
|
|
235
235
|
SESSION_ID=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["session_id"])' "$SESSION_JSON")
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
bw platform run microsoft-teams meeting \
|
|
238
238
|
--session-id "$SESSION_ID" \
|
|
239
239
|
--config /path/to/config.json
|
|
240
240
|
|
|
241
241
|
# If it fails: inspect/self-heal using the same SESSION_ID, write the repair
|
|
242
242
|
# back to the platform implementation, then resume safely.
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
bw session stop "$SESSION_ID"
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
One-shot mode is explicit and is not self-healing:
|
|
248
248
|
|
|
249
249
|
```bash
|
|
250
|
-
|
|
250
|
+
bw platform run microsoft-teams meeting \
|
|
251
251
|
--auto-session \
|
|
252
252
|
--config /path/to/config.json
|
|
253
253
|
```
|
|
@@ -296,13 +296,14 @@ Read the matching platform `README.md` and `manifest.yaml` before invoking or mo
|
|
|
296
296
|
|
|
297
297
|
## Installation
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
Use the published PyPI command (requires `uv`/`uvx`):
|
|
300
300
|
|
|
301
301
|
```bash
|
|
302
|
-
|
|
302
|
+
uvx browser-workspace status
|
|
303
|
+
uvx browser-workspace session list
|
|
303
304
|
```
|
|
304
305
|
|
|
305
|
-
|
|
306
|
+
`uvx` resolves the published Python wheel and installs the dependencies declared in `pyproject.toml`. Do not run the legacy bundled installer or point agents at a checked-out `bin/browser-workspace` script. The installed CLI reuses the same Browser Workspace Chrome profile and Session Daemon socket.
|
|
306
307
|
|
|
307
308
|
There is no required separate Browser Harness installation.
|
|
308
309
|
|
|
@@ -320,4 +321,4 @@ kgbghhigmbpefppgkocgjgnnnbhjchic
|
|
|
320
321
|
|
|
321
322
|
## Internal provenance
|
|
322
323
|
|
|
323
|
-
The low-level browser runtime is
|
|
324
|
+
The low-level browser runtime is the separately maintained `browser-harness` Python dependency. Browser Workspace owns the session/workspace/platform product layer and pins the exact Harness runtime revision it was tested with; Harness source is not vendored into this repository. Upstream licensing remains documented under `licenses/`.
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -439,7 +439,14 @@ def _fill_prompt_if_needed(prompt, mode_label, timeout=30):
|
|
|
439
439
|
deadline = time.time() + timeout
|
|
440
440
|
last_error = None
|
|
441
441
|
while time.time() < deadline:
|
|
442
|
-
|
|
442
|
+
try:
|
|
443
|
+
selector = _wait_for_composer(timeout=min(5, max(1, deadline - time.time())))
|
|
444
|
+
except RuntimeError as exc:
|
|
445
|
+
# Hydration and navigation can temporarily remove the composer.
|
|
446
|
+
# Continue the outer readiness window rather than aborting on the first poll.
|
|
447
|
+
last_error = exc
|
|
448
|
+
time.sleep(.25)
|
|
449
|
+
continue
|
|
443
450
|
if prompt_text_matches(_composer_text(selector), prompt):
|
|
444
451
|
return selector
|
|
445
452
|
|
|
@@ -567,5 +574,18 @@ while time.time() < deadline:
|
|
|
567
574
|
else:
|
|
568
575
|
raise RuntimeError("Temporary Chat activation was not observed")
|
|
569
576
|
|
|
570
|
-
|
|
577
|
+
try:
|
|
578
|
+
result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
|
|
579
|
+
except RuntimeError as exc:
|
|
580
|
+
# A URL-prefilled Temporary Chat can retain the pre-hydration home input
|
|
581
|
+
# without ever mounting the active composer. No send occurred, so retry
|
|
582
|
+
# once on a clean Temporary Chat page without URL prompt prefill.
|
|
583
|
+
if "composer readiness was not observed ready" not in str(exc):
|
|
584
|
+
raise
|
|
585
|
+
goto_url(temporary_chat_entry_url())
|
|
586
|
+
wait_for_load()
|
|
587
|
+
_click_temporary_chat_toggle()
|
|
588
|
+
if not _temporary_chat_enabled():
|
|
589
|
+
raise RuntimeError("Temporary Chat mode lost during composer recovery") from exc
|
|
590
|
+
result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
|
|
571
591
|
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
@@ -174,3 +174,10 @@ def test_new_turn_project_id_is_optional():
|
|
|
174
174
|
assert 'if not thread_id: raise RuntimeError("thread_id is required")' in turn
|
|
175
175
|
assert 'https://chatgpt.com/c/{quote(thread_id' in turn
|
|
176
176
|
assert 'https://chatgpt.com/g/{quote(project_id' in turn
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_temporary_submit_recovers_only_before_send_from_prefill_hydration():
|
|
180
|
+
code=(ACTIONS/"_temporary_submit.py").read_text()
|
|
181
|
+
assert 'goto_url(temporary_chat_entry_url())' in code
|
|
182
|
+
assert '"composer readiness was not observed ready" not in str(exc)' in code
|
|
183
|
+
assert 'if not _temporary_chat_enabled():' in code
|
package/pyproject.toml
CHANGED
|
@@ -4,10 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "browser-workspace"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.5"
|
|
8
8
|
description = "Agent browser workspace with persistent Python sessions, tab leasing, snapshots, and platform workflows."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
|
+
"browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192",
|
|
11
12
|
"cdp-use==1.4.5",
|
|
12
13
|
"fetch-use==0.4.0",
|
|
13
14
|
"pillow==12.3.0",
|
|
@@ -15,6 +16,9 @@ dependencies = [
|
|
|
15
16
|
"websockets==15.0.1",
|
|
16
17
|
]
|
|
17
18
|
|
|
19
|
+
[project.scripts]
|
|
20
|
+
browser-workspace = "browser_workspace_app.cli:main"
|
|
21
|
+
|
|
18
22
|
[tool.setuptools]
|
|
19
23
|
package-dir = {"" = "src"}
|
|
20
24
|
|
|
@@ -22,7 +26,7 @@ package-dir = {"" = "src"}
|
|
|
22
26
|
where = ["src"]
|
|
23
27
|
|
|
24
28
|
[tool.setuptools.package-data]
|
|
25
|
-
|
|
29
|
+
browser_workspace_app = ["**/*"]
|
|
26
30
|
|
|
27
31
|
[tool.pytest.ini_options]
|
|
28
32
|
pythonpath = ["src"]
|
package/session_daemon.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
import ast, atexit, contextlib, io, json, os, secrets, socket, sys, threading, time, traceback
|
|
2
|
+
import ast, atexit, contextlib, io, json, os, secrets, signal, socket, sys, threading, time, traceback
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
ROOT = Path(__file__).resolve().parent
|
|
@@ -14,10 +14,31 @@ os.environ.setdefault('BH_WORKSPACE_NAME', 'Harness')
|
|
|
14
14
|
|
|
15
15
|
from managed_chrome import ensure_managed_chrome
|
|
16
16
|
MANAGED_CHROME = ensure_managed_chrome()
|
|
17
|
-
from browser_harness.admin import ensure_daemon
|
|
18
|
-
|
|
17
|
+
from browser_harness.admin import ensure_daemon, _process_start_time
|
|
18
|
+
from browser_harness import _ipc as harness_ipc
|
|
19
19
|
from browser_harness import helpers as bh
|
|
20
20
|
|
|
21
|
+
HARNESS_OWNER_TOKEN = secrets.token_hex(24)
|
|
22
|
+
HARNESS_OWNER_PID = os.getpid()
|
|
23
|
+
HARNESS_OWNER_STARTED = _process_start_time(HARNESS_OWNER_PID)
|
|
24
|
+
HARNESS_OWNER_PIPE = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def stop_owned_harness():
|
|
28
|
+
"""Only a harness matching this daemon generation may stop."""
|
|
29
|
+
try:
|
|
30
|
+
conn, token = harness_ipc.connect(os.environ.get('BU_NAME', 'browser-workspace'), timeout=1)
|
|
31
|
+
with conn:
|
|
32
|
+
response = harness_ipc.request(conn, token, {'meta': 'owner_shutdown', 'owner_token': HARNESS_OWNER_TOKEN})
|
|
33
|
+
if isinstance(response, dict) and response.get('ok'):
|
|
34
|
+
# Allow the harness its graceful CDP/IPC cleanup before EOF of
|
|
35
|
+
# our owner pipe forces the crash-only exit path.
|
|
36
|
+
deadline = time.monotonic() + 2
|
|
37
|
+
while time.monotonic() < deadline and harness_ipc.ping(os.environ.get('BU_NAME', 'browser-workspace'), timeout=0.1):
|
|
38
|
+
time.sleep(0.05)
|
|
39
|
+
except (OSError, ValueError, TimeoutError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
21
42
|
DEFAULT_WORKSPACE = os.environ.get('BH_WORKSPACE_NAME','Harness')
|
|
22
43
|
RUNTIME_DIR = Path.home()/'.config/browser-workspace/runtime'
|
|
23
44
|
SOCKET_PATH = (Path(os.environ['BROWSER_WORKSPACE_SESSION_SOCKET']) if os.environ.get('BROWSER_WORKSPACE_TEST_MODE')=='1' and os.environ.get('BROWSER_WORKSPACE_SESSION_SOCKET') else RUNTIME_DIR/'session.sock')
|
|
@@ -26,6 +47,47 @@ SESSIONS = {}
|
|
|
26
47
|
SESSIONS_LOCK = threading.Lock()
|
|
27
48
|
BROWSER_LOCK = threading.RLock()
|
|
28
49
|
|
|
50
|
+
SESSION_RECONCILE_SECONDS = 30
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_reclaimed(info):
|
|
54
|
+
url = info.get('url', '')
|
|
55
|
+
if not url.startswith('chrome-extension://'):
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
from urllib.parse import urlparse, parse_qs
|
|
59
|
+
parsed = urlparse(url)
|
|
60
|
+
return parsed.path == '/workspace.html' and parse_qs(parsed.query).get('role') == ['idle']
|
|
61
|
+
except ValueError:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def reconcile_sessions():
|
|
66
|
+
"""Drop records for completely released leases, never release those tabs again."""
|
|
67
|
+
with BROWSER_LOCK:
|
|
68
|
+
try:
|
|
69
|
+
targets = bh.cdp('Target.getTargets')['targetInfos']
|
|
70
|
+
except Exception:
|
|
71
|
+
return []
|
|
72
|
+
pages = {i['targetId']: i for i in targets if i.get('type') == 'page'}
|
|
73
|
+
removed = []
|
|
74
|
+
with SESSIONS_LOCK:
|
|
75
|
+
for sid, session in list(SESSIONS.items()):
|
|
76
|
+
if session.lock.locked():
|
|
77
|
+
continue
|
|
78
|
+
# A leased root may have opened child tabs: keep any live child.
|
|
79
|
+
if all(tid not in pages or is_reclaimed(pages[tid])
|
|
80
|
+
for tid in session.owned_target_ids):
|
|
81
|
+
del SESSIONS[sid]
|
|
82
|
+
removed.append(sid)
|
|
83
|
+
return removed
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def reconciliation_loop():
|
|
87
|
+
while True:
|
|
88
|
+
time.sleep(SESSION_RECONCILE_SECONDS)
|
|
89
|
+
reconcile_sessions()
|
|
90
|
+
|
|
29
91
|
|
|
30
92
|
def compilable_module_body(body):
|
|
31
93
|
"""Return a module body that remains valid after trailing-result extraction.
|
|
@@ -274,6 +336,12 @@ def cleanup():
|
|
|
274
336
|
for sid in ids:
|
|
275
337
|
try: stop_session(sid)
|
|
276
338
|
except Exception: pass
|
|
339
|
+
stop_owned_harness()
|
|
340
|
+
# The write end is close-on-exec; its final close also detects SIGKILL
|
|
341
|
+
# and parent re-exec without relying on a recycled numeric PID.
|
|
342
|
+
if HARNESS_OWNER_PIPE is not None:
|
|
343
|
+
try: os.close(HARNESS_OWNER_PIPE)
|
|
344
|
+
except OSError: pass
|
|
277
345
|
try: remove_owned_socket()
|
|
278
346
|
except Exception: pass
|
|
279
347
|
atexit.register(cleanup)
|
|
@@ -332,9 +400,29 @@ def main():
|
|
|
332
400
|
fcntl.flock(owner,fcntl.LOCK_EX|fcntl.LOCK_NB)
|
|
333
401
|
except BlockingIOError:
|
|
334
402
|
owner.close(); return
|
|
403
|
+
global HARNESS_OWNER_PIPE
|
|
404
|
+
read_fd, write_fd = os.pipe() if os.name == 'posix' else (None, None)
|
|
405
|
+
HARNESS_OWNER_PIPE = write_fd
|
|
406
|
+
try:
|
|
407
|
+
owner_env = {
|
|
408
|
+
'BH_OWNER_PID': str(HARNESS_OWNER_PID),
|
|
409
|
+
'BH_OWNER_STARTED': str(HARNESS_OWNER_STARTED or ''),
|
|
410
|
+
'BH_OWNER_TOKEN': HARNESS_OWNER_TOKEN,
|
|
411
|
+
}
|
|
412
|
+
if read_fd is not None:
|
|
413
|
+
owner_env['BH_OWNER_PIPE_FD'] = str(read_fd)
|
|
414
|
+
ensure_daemon(env=owner_env)
|
|
415
|
+
finally:
|
|
416
|
+
if read_fd is not None:
|
|
417
|
+
os.close(read_fd)
|
|
418
|
+
def terminate(_signum, _frame):
|
|
419
|
+
raise SystemExit(0)
|
|
420
|
+
signal.signal(signal.SIGTERM, terminate)
|
|
421
|
+
signal.signal(signal.SIGINT, terminate)
|
|
335
422
|
# Lock ownership is required before changing the shared socket path.
|
|
336
423
|
SOCKET_PATH.unlink(missing_ok=True)
|
|
337
424
|
srv=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); srv.bind(str(SOCKET_PATH)); os.chmod(SOCKET_PATH,0o600); SOCKET_IDENTITY=(SOCKET_PATH.stat().st_dev, SOCKET_PATH.stat().st_ino); srv.listen(32)
|
|
425
|
+
threading.Thread(target=reconciliation_loop, daemon=True).start()
|
|
338
426
|
try:
|
|
339
427
|
while True:
|
|
340
428
|
conn,_=srv.accept(); threading.Thread(target=serve_conn,args=(conn,),daemon=True).start()
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browser-workspace
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Agent browser workspace with persistent Python sessions, tab leasing, snapshots, and platform workflows.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192
|
|
6
7
|
Requires-Dist: cdp-use==1.4.5
|
|
7
8
|
Requires-Dist: fetch-use==0.4.0
|
|
8
9
|
Requires-Dist: pillow==12.3.0
|
|
@@ -1,28 +1,97 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
|
-
src/browser_harness/__init__.py
|
|
4
|
-
src/browser_harness/_ipc.py
|
|
5
|
-
src/browser_harness/admin.py
|
|
6
|
-
src/browser_harness/auth.py
|
|
7
|
-
src/browser_harness/daemon.py
|
|
8
|
-
src/browser_harness/helpers.py
|
|
9
|
-
src/browser_harness/macos.py
|
|
10
|
-
src/browser_harness/mcp_cli.py
|
|
11
|
-
src/browser_harness/paths.py
|
|
12
|
-
src/browser_harness/recorder.py
|
|
13
|
-
src/browser_harness/run.py
|
|
14
|
-
src/browser_harness/telemetry.py
|
|
15
|
-
src/browser_harness/video-template.html
|
|
16
|
-
src/browser_harness/video.py
|
|
17
|
-
src/browser_harness/video_render.py
|
|
18
3
|
src/browser_workspace.egg-info/PKG-INFO
|
|
19
4
|
src/browser_workspace.egg-info/SOURCES.txt
|
|
20
5
|
src/browser_workspace.egg-info/dependency_links.txt
|
|
6
|
+
src/browser_workspace.egg-info/entry_points.txt
|
|
21
7
|
src/browser_workspace.egg-info/requires.txt
|
|
22
8
|
src/browser_workspace.egg-info/top_level.txt
|
|
9
|
+
src/browser_workspace_app/__init__.py
|
|
10
|
+
src/browser_workspace_app/cli.py
|
|
11
|
+
src/browser_workspace_app/managed_chrome.py
|
|
12
|
+
src/browser_workspace_app/package.json
|
|
13
|
+
src/browser_workspace_app/platform_runner.py
|
|
14
|
+
src/browser_workspace_app/pyproject.toml
|
|
15
|
+
src/browser_workspace_app/session_client.py
|
|
16
|
+
src/browser_workspace_app/session_daemon.py
|
|
17
|
+
src/browser_workspace_app/agent-workspace/agent_helpers.py
|
|
18
|
+
src/browser_workspace_app/extension/manifest.json
|
|
19
|
+
src/browser_workspace_app/extension/service-worker.mjs
|
|
20
|
+
src/browser_workspace_app/extension/workspace-manager.mjs
|
|
21
|
+
src/browser_workspace_app/extension/workspace-page.mjs
|
|
22
|
+
src/browser_workspace_app/extension/workspace.html
|
|
23
|
+
src/browser_workspace_app/interaction-skills/connection.md
|
|
24
|
+
src/browser_workspace_app/interaction-skills/cookies.md
|
|
25
|
+
src/browser_workspace_app/interaction-skills/cross-origin-iframes.md
|
|
26
|
+
src/browser_workspace_app/interaction-skills/dialogs.md
|
|
27
|
+
src/browser_workspace_app/interaction-skills/downloads.md
|
|
28
|
+
src/browser_workspace_app/interaction-skills/drag-and-drop.md
|
|
29
|
+
src/browser_workspace_app/interaction-skills/dropdowns.md
|
|
30
|
+
src/browser_workspace_app/interaction-skills/iframes.md
|
|
31
|
+
src/browser_workspace_app/interaction-skills/network-requests.md
|
|
32
|
+
src/browser_workspace_app/interaction-skills/print-as-pdf.md
|
|
33
|
+
src/browser_workspace_app/interaction-skills/profile-sync.md
|
|
34
|
+
src/browser_workspace_app/interaction-skills/screenshots.md
|
|
35
|
+
src/browser_workspace_app/interaction-skills/scrolling.md
|
|
36
|
+
src/browser_workspace_app/interaction-skills/shadow-dom.md
|
|
37
|
+
src/browser_workspace_app/interaction-skills/tabs.md
|
|
38
|
+
src/browser_workspace_app/interaction-skills/uploads.md
|
|
39
|
+
src/browser_workspace_app/interaction-skills/viewport.md
|
|
40
|
+
src/browser_workspace_app/licenses/browser-harness-MIT.txt
|
|
41
|
+
src/browser_workspace_app/node/admin.mjs
|
|
42
|
+
src/browser_workspace_app/platforms/chatgpt/README.md
|
|
43
|
+
src/browser_workspace_app/platforms/chatgpt/manifest.yaml
|
|
44
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py
|
|
45
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py
|
|
46
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py
|
|
47
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py
|
|
48
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py
|
|
49
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py
|
|
50
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py
|
|
51
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py
|
|
52
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py
|
|
53
|
+
src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py
|
|
54
|
+
src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py
|
|
55
|
+
src/browser_workspace_app/platforms/chrome-web-store/README.md
|
|
56
|
+
src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml
|
|
57
|
+
src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py
|
|
58
|
+
src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md
|
|
59
|
+
src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md
|
|
60
|
+
src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md
|
|
61
|
+
src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md
|
|
62
|
+
src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md
|
|
63
|
+
src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md
|
|
64
|
+
src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py
|
|
65
|
+
src/browser_workspace_app/platforms/microsoft-teams/README.md
|
|
66
|
+
src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml
|
|
67
|
+
src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py
|
|
68
|
+
src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md
|
|
69
|
+
src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py
|
|
70
|
+
src/browser_workspace_app/platforms/tiktok/README.md
|
|
71
|
+
src/browser_workspace_app/platforms/tiktok/manifest.yaml
|
|
72
|
+
src/browser_workspace_app/platforms/tiktok/actions/_post.py
|
|
73
|
+
src/browser_workspace_app/platforms/wechat-channels/README.md
|
|
74
|
+
src/browser_workspace_app/platforms/wechat-channels/manifest.yaml
|
|
75
|
+
src/browser_workspace_app/platforms/wechat-channels/actions/_common.py
|
|
76
|
+
src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py
|
|
77
|
+
src/browser_workspace_app/platforms/wechat-channels/actions/_post.py
|
|
78
|
+
src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py
|
|
79
|
+
src/browser_workspace_app/platforms/xhs/README.md
|
|
80
|
+
src/browser_workspace_app/platforms/xhs/manifest.yaml
|
|
81
|
+
src/browser_workspace_app/platforms/xhs/actions/_manage.py
|
|
82
|
+
src/browser_workspace_app/platforms/xhs/actions/_post.py
|
|
83
|
+
src/browser_workspace_app/platforms/xhs/tests/test_contract.py
|
|
84
|
+
src/browser_workspace_app/platforms/youtube/README.md
|
|
85
|
+
src/browser_workspace_app/platforms/youtube/manifest.yaml
|
|
86
|
+
src/browser_workspace_app/platforms/youtube/actions/_post.py
|
|
87
|
+
test/test_auth_cookie_audit.py
|
|
88
|
+
test/test_auth_keepalive_visit.py
|
|
89
|
+
test/test_harness_ownership.py
|
|
23
90
|
test/test_managed_chrome.py
|
|
24
91
|
test/test_platform_profile.py
|
|
25
92
|
test/test_platform_runner.py
|
|
93
|
+
test/test_pypi_package.py
|
|
26
94
|
test/test_session_cli.py
|
|
27
95
|
test/test_session_execution.py
|
|
96
|
+
test/test_session_reconcile.py
|
|
28
97
|
test/test_workspace_cli.py
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
browser_workspace_app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Browser Workspace Python distribution."""
|