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.
Files changed (107) hide show
  1. package/README.md +15 -1
  2. package/SKILL.md +22 -21
  3. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  4. package/package.json +1 -1
  5. package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
  6. package/platforms/chatgpt/tests/test_contract.py +7 -0
  7. package/pyproject.toml +6 -2
  8. package/session_daemon.py +91 -3
  9. package/src/browser_workspace.egg-info/PKG-INFO +2 -1
  10. package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
  11. package/src/browser_workspace.egg-info/entry_points.txt +2 -0
  12. package/src/browser_workspace.egg-info/requires.txt +1 -0
  13. package/src/browser_workspace.egg-info/top_level.txt +1 -1
  14. package/src/browser_workspace_app/__init__.py +1 -0
  15. package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
  16. package/src/browser_workspace_app/cli.py +236 -0
  17. package/src/browser_workspace_app/extension/manifest.json +26 -0
  18. package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
  19. package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
  20. package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
  21. package/src/browser_workspace_app/extension/workspace.html +8 -0
  22. package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
  23. package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
  24. package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
  25. package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
  26. package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
  27. package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
  28. package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
  29. package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
  30. package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
  31. package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
  32. package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
  33. package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
  34. package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
  35. package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
  36. package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
  37. package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
  38. package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
  39. package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
  40. package/src/browser_workspace_app/managed_chrome.py +244 -0
  41. package/src/browser_workspace_app/node/admin.mjs +44 -0
  42. package/src/browser_workspace_app/package.json +36 -0
  43. package/src/browser_workspace_app/platform_runner.py +204 -0
  44. package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
  45. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
  46. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
  47. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
  48. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
  49. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
  50. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
  51. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
  52. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
  53. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
  54. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
  55. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
  56. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
  57. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
  58. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
  59. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
  60. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
  61. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
  62. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
  63. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
  64. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
  65. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
  66. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
  67. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
  68. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
  69. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
  70. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
  71. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
  72. package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
  73. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
  74. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
  75. package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
  76. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
  77. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
  78. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
  79. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
  80. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
  81. package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
  82. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
  83. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
  84. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
  85. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
  86. package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
  87. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
  88. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
  89. package/src/browser_workspace_app/pyproject.toml +33 -0
  90. package/src/browser_workspace_app/session_client.py +107 -0
  91. package/src/browser_workspace_app/session_daemon.py +431 -0
  92. package/src/browser_harness/__init__.py +0 -2
  93. package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
  94. package/src/browser_harness/_ipc.py +0 -201
  95. package/src/browser_harness/admin.py +0 -1588
  96. package/src/browser_harness/auth.py +0 -546
  97. package/src/browser_harness/daemon.py +0 -894
  98. package/src/browser_harness/helpers.py +0 -668
  99. package/src/browser_harness/macos.py +0 -131
  100. package/src/browser_harness/mcp_cli.py +0 -16
  101. package/src/browser_harness/paths.py +0 -49
  102. package/src/browser_harness/recorder.py +0 -343
  103. package/src/browser_harness/run.py +0 -413
  104. package/src/browser_harness/telemetry.py +0 -308
  105. package/src/browser_harness/video-template.html +0 -970
  106. package/src/browser_harness/video.py +0 -749
  107. 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
- ├── src/browser_harness/ low-level browser/CDP runtime (forked internally)
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
- When installed with `npx skills`, use the CLI from the skill itself; no separate install step is required:
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
- BW_CLI="$HOME/.agents/skills/browser-workspace/bin/browser-workspace"
16
- $BW_CLI status
15
+ bw() { uvx browser-workspace "$@"; }
16
+ bw status
17
17
  ```
18
18
 
19
- On first use, Browser Workspace bootstraps its own `.venv` automatically.
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
- $BW_CLI session start
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
- $BW_CLI session start --url https://example.com
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
- $BW_CLI session exec <session_id> <<'PY'
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
- $BW_CLI session exec <session_id> <<'PY'
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
- $BW_CLI session exec <session_id> <<'PY'
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
- $BW_CLI session stop <session_id>
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=$($BW_CLI session start)
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 '$BW_CLI session stop "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
122
+ trap 'bw session stop "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
123
123
 
124
- $BW_CLI session exec "$SESSION_ID" <<'PY'
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=$($BW_CLI session start --url https://example.com)
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
- $BW_CLI platform run microsoft-teams meeting \
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
- $BW_CLI session stop "$SESSION_ID"
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
- $BW_CLI platform run microsoft-teams meeting \
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
- Run the bundled installer from the skill directory:
299
+ Use the published PyPI command (requires `uv`/`uvx`):
300
300
 
301
301
  ```bash
302
- ./scripts/install.sh
302
+ uvx browser-workspace status
303
+ uvx browser-workspace session list
303
304
  ```
304
305
 
305
- It creates a Browser Workspace-owned `.venv`, installs the vendored browser runtime and dependencies, writes Browser Workspace environment configuration to `agent-workspace/.env`, and links `browser-workspace` into `~/.local/bin` by default.
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 forked from `browser-use/browser-harness` and kept internally under `src/browser_harness/`. Browser Workspace is the public product/skill/CLI surface. Upstream licensing is preserved under `licenses/`.
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/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-workspace",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
4
4
  "description": "Agent browser workspace with persistent sessions, tab leasing, snapshots, and platform workflows.",
5
5
  "bin": {
6
6
  "browser-workspace": "bin/browser-workspace"
@@ -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
- selector = _wait_for_composer(timeout=min(5, max(1, deadline - time.time())))
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
- result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
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.0"
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
- browser_harness = ["video-template.html"]
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
- ensure_daemon()
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.0
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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ browser-workspace = browser_workspace_app.cli:main
@@ -1,3 +1,4 @@
1
+ browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192
1
2
  cdp-use==1.4.5
2
3
  fetch-use==0.4.0
3
4
  pillow==12.3.0
@@ -1 +1 @@
1
- browser_harness
1
+ browser_workspace_app
@@ -0,0 +1 @@
1
+ """Browser Workspace Python distribution."""