browser-workspace 0.4.0

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 (108) hide show
  1. package/PRIVACY.md +21 -0
  2. package/README.md +138 -0
  3. package/SKILL.md +301 -0
  4. package/agent-workspace/agent_helpers.py +718 -0
  5. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  6. package/bin/browser-workspace +75 -0
  7. package/extension/manifest.json +26 -0
  8. package/extension/service-worker.mjs +126 -0
  9. package/extension/workspace-manager.mjs +680 -0
  10. package/extension/workspace-page.mjs +7 -0
  11. package/extension/workspace.html +8 -0
  12. package/interaction-skills/connection.md +52 -0
  13. package/interaction-skills/cookies.md +3 -0
  14. package/interaction-skills/cross-origin-iframes.md +3 -0
  15. package/interaction-skills/dialogs.md +64 -0
  16. package/interaction-skills/downloads.md +3 -0
  17. package/interaction-skills/drag-and-drop.md +3 -0
  18. package/interaction-skills/dropdowns.md +3 -0
  19. package/interaction-skills/iframes.md +3 -0
  20. package/interaction-skills/network-requests.md +3 -0
  21. package/interaction-skills/print-as-pdf.md +3 -0
  22. package/interaction-skills/profile-sync.md +90 -0
  23. package/interaction-skills/screenshots.md +17 -0
  24. package/interaction-skills/scrolling.md +22 -0
  25. package/interaction-skills/shadow-dom.md +3 -0
  26. package/interaction-skills/tabs.md +73 -0
  27. package/interaction-skills/uploads.md +1 -0
  28. package/interaction-skills/viewport.md +16 -0
  29. package/licenses/browser-harness-MIT.txt +21 -0
  30. package/managed_chrome.py +244 -0
  31. package/node/admin.mjs +44 -0
  32. package/package.json +36 -0
  33. package/platform_runner.py +187 -0
  34. package/platforms/chatgpt/README.md +60 -0
  35. package/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
  36. package/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
  37. package/platforms/chatgpt/actions/_mcp_app.py +80 -0
  38. package/platforms/chatgpt/actions/_new_thread.py +85 -0
  39. package/platforms/chatgpt/actions/_new_turn.py +33 -0
  40. package/platforms/chatgpt/actions/_project_setup.py +353 -0
  41. package/platforms/chatgpt/actions/_readiness.py +52 -0
  42. package/platforms/chatgpt/actions/_submit_thread.py +129 -0
  43. package/platforms/chatgpt/actions/_temporary_submit.py +571 -0
  44. package/platforms/chatgpt/actions/_thread_turn.py +362 -0
  45. package/platforms/chatgpt/manifest.yaml +34 -0
  46. package/platforms/chatgpt/tests/test_contract.py +176 -0
  47. package/platforms/chrome-web-store/README.md +49 -0
  48. package/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
  49. package/platforms/chrome-web-store/flows/check-status.md +7 -0
  50. package/platforms/chrome-web-store/flows/open-item.md +10 -0
  51. package/platforms/chrome-web-store/flows/submit-review.md +10 -0
  52. package/platforms/chrome-web-store/flows/update-listing.md +18 -0
  53. package/platforms/chrome-web-store/flows/upload-package.md +11 -0
  54. package/platforms/chrome-web-store/flows/verify-published.md +9 -0
  55. package/platforms/chrome-web-store/manifest.yaml +33 -0
  56. package/platforms/chrome-web-store/tests/test_contract.py +83 -0
  57. package/platforms/microsoft-teams/README.md +32 -0
  58. package/platforms/microsoft-teams/actions/_meeting.py +210 -0
  59. package/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
  60. package/platforms/microsoft-teams/manifest.yaml +25 -0
  61. package/platforms/microsoft-teams/tests/test_contract.py +24 -0
  62. package/platforms/tiktok/README.md +10 -0
  63. package/platforms/tiktok/actions/_post.py +180 -0
  64. package/platforms/tiktok/manifest.yaml +99 -0
  65. package/platforms/wechat-channels/README.md +10 -0
  66. package/platforms/wechat-channels/actions/_common.py +163 -0
  67. package/platforms/wechat-channels/actions/_manage.py +130 -0
  68. package/platforms/wechat-channels/actions/_post.py +232 -0
  69. package/platforms/wechat-channels/manifest.yaml +118 -0
  70. package/platforms/wechat-channels/tests/test_contract.py +36 -0
  71. package/platforms/xhs/README.md +10 -0
  72. package/platforms/xhs/actions/_manage.py +355 -0
  73. package/platforms/xhs/actions/_post.py +192 -0
  74. package/platforms/xhs/manifest.yaml +98 -0
  75. package/platforms/youtube/README.md +10 -0
  76. package/platforms/youtube/actions/_post.py +100 -0
  77. package/platforms/youtube/manifest.yaml +94 -0
  78. package/pyproject.toml +29 -0
  79. package/session_client.py +97 -0
  80. package/session_daemon.py +325 -0
  81. package/src/browser_harness/__init__.py +2 -0
  82. package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
  83. package/src/browser_harness/__pycache__/_ipc.cpython-311.pyc +0 -0
  84. package/src/browser_harness/__pycache__/admin.cpython-311.pyc +0 -0
  85. package/src/browser_harness/__pycache__/auth.cpython-311.pyc +0 -0
  86. package/src/browser_harness/__pycache__/daemon.cpython-311.pyc +0 -0
  87. package/src/browser_harness/__pycache__/helpers.cpython-311.pyc +0 -0
  88. package/src/browser_harness/__pycache__/paths.cpython-311.pyc +0 -0
  89. package/src/browser_harness/__pycache__/recorder.cpython-311.pyc +0 -0
  90. package/src/browser_harness/_ipc.py +201 -0
  91. package/src/browser_harness/admin.py +1588 -0
  92. package/src/browser_harness/auth.py +546 -0
  93. package/src/browser_harness/daemon.py +894 -0
  94. package/src/browser_harness/helpers.py +668 -0
  95. package/src/browser_harness/macos.py +131 -0
  96. package/src/browser_harness/mcp_cli.py +16 -0
  97. package/src/browser_harness/paths.py +49 -0
  98. package/src/browser_harness/recorder.py +343 -0
  99. package/src/browser_harness/run.py +413 -0
  100. package/src/browser_harness/telemetry.py +308 -0
  101. package/src/browser_harness/video-template.html +970 -0
  102. package/src/browser_harness/video.py +749 -0
  103. package/src/browser_harness/video_render.py +524 -0
  104. package/src/browser_workspace.egg-info/PKG-INFO +10 -0
  105. package/src/browser_workspace.egg-info/SOURCES.txt +28 -0
  106. package/src/browser_workspace.egg-info/dependency_links.txt +1 -0
  107. package/src/browser_workspace.egg-info/requires.txt +5 -0
  108. package/src/browser_workspace.egg-info/top_level.txt +1 -0
@@ -0,0 +1,413 @@
1
+ import json, os, sys, time, urllib.request
2
+ from io import StringIO
3
+
4
+ # Windows default stdout/stderr encoding is cp1252
5
+ # which can't encode the 🐴 marker helpers prepend to tab titles (or anything
6
+ # else outside the locale charset). Force UTF-8 so `print(page_info())` and
7
+ # tracebacks carrying page titles don't UnicodeEncodeError on Windows. #124(4).
8
+ for _stream in (sys.stdout, sys.stderr):
9
+ if hasattr(_stream, "reconfigure"):
10
+ try: _stream.reconfigure(encoding="utf-8", errors="replace")
11
+ except Exception: pass
12
+
13
+ from .admin import (
14
+ _version,
15
+ NAME,
16
+ daemon_alive,
17
+ daemon_browser_kind,
18
+ ensure_daemon,
19
+ list_cloud_profiles,
20
+ list_local_profiles,
21
+ print_update_banner,
22
+ require_existing_daemon,
23
+ restart_daemon,
24
+ run_doctor,
25
+ run_doctor_fix_snap,
26
+ run_doctor_json,
27
+ run_update,
28
+ start_remote_daemon,
29
+ stop_remote_daemon,
30
+ sync_local_profile,
31
+ )
32
+ from . import auth, recorder, telemetry
33
+ from .helpers import *
34
+
35
+ HELP = """Browser Harness
36
+
37
+ Read SKILL.md for the default workflow and examples.
38
+
39
+ Typical usage:
40
+ browser-harness <<'PY'
41
+ ensure_real_tab()
42
+ print(page_info())
43
+ PY
44
+
45
+ Helpers are pre-imported. The daemon auto-starts and connects to the running browser.
46
+
47
+ Commands:
48
+ browser-harness --version print the installed version
49
+ browser-harness --doctor diagnose install, daemon, and browser state
50
+ browser-harness doctor same as --doctor
51
+ browser-harness doctor --json [--require-existing-daemon]
52
+ print machine-readable runtime health
53
+ browser-harness doctor --fix-snap print how to fix Snap Chromium blocking CDP (Linux)
54
+ browser-harness mac-approve approve Chrome's macOS remote debugging sheet
55
+ browser-harness auth login sign in to Browser Use Cloud for cloud browsers
56
+ browser-harness auth login --device-code sign in from SSH/headless environments
57
+ browser-harness auth status show Browser Use Cloud auth state
58
+ browser-harness auth logout remove stored Browser Use Cloud auth
59
+ browser-harness skill print the browser-harness skill text
60
+ browser-harness recordings show recording status and recent sessions
61
+ browser-harness recordings --latest print the newest recording directory
62
+ browser-harness recordings enable save browser actions locally by default
63
+ browser-harness recordings disable stop saving browser actions by default
64
+ browser-harness video init <recording> prepare a recording for editing
65
+ browser-harness video review <recording> compile and review the video
66
+ browser-harness video export <recording> --reviewed export a verified MP4
67
+ browser-harness telemetry status show anonymous telemetry opt-out state
68
+ browser-harness --update [-y] pull the latest version (agents: pass -y)
69
+ browser-harness --reload stop the daemon so next call picks up code changes
70
+ """
71
+
72
+ USAGE = """Usage:
73
+ browser-harness <<'PY'
74
+ print(page_info())
75
+ PY
76
+ """
77
+
78
+
79
+ # Probe /json/version (not a bare TCP connect) so a non-Chrome process bound to
80
+ # 9222/9223 doesn't masquerade as Chrome and skip the cloud bootstrap. Mirrors
81
+ # daemon.py's fallback probe.
82
+ def _local_chrome_listening():
83
+ for port in (9222, 9223):
84
+ try:
85
+ with urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version", timeout=0.3) as response:
86
+ version = json.loads(response.read())
87
+ if isinstance(version, dict) and isinstance(version.get("webSocketDebuggerUrl"), str) and version["webSocketDebuggerUrl"]:
88
+ return True
89
+ except (OSError, TypeError, ValueError):
90
+ pass
91
+ return False
92
+
93
+
94
+ # BU_CDP_URL / BU_CDP_WS are documented to override local Chrome discovery
95
+ # (install.md:58-59), so they must also block cloud auto-bootstrap. Without this
96
+ # guard, start_remote_daemon() in admin.py overwrites BU_CDP_WS in the daemon
97
+ # env with a cloud WebSocket URL, silently replacing the user's explicit endpoint
98
+ # *and* billing them for a cloud browser they never asked for.
99
+ def _explicit_cdp_configured():
100
+ return bool(os.environ.get("BU_CDP_URL") or os.environ.get("BU_CDP_WS"))
101
+
102
+
103
+ def _cloud_auth_configured():
104
+ try:
105
+ auth.get_browser_use_api_key()
106
+ return True
107
+ except (auth.CloudAuthRequired, auth.AuthError, OSError):
108
+ return False
109
+
110
+
111
+ def _print_skill():
112
+ from importlib import resources
113
+ # SKILL.md is UTF-8 (contains emoji); locale-codec read crashes on gbk Windows
114
+ print(resources.files("browser_harness").joinpath("SKILL.md").read_text(encoding="utf-8"), end="")
115
+
116
+
117
+ def _telemetry_command(args):
118
+ if not args:
119
+ return "script"
120
+ first = args[0]
121
+ if first in {"-h", "--help"}:
122
+ return "help"
123
+ if first == "--version":
124
+ return "version"
125
+ if first in {"--doctor", "doctor"}:
126
+ return "doctor"
127
+ if first == "--update":
128
+ return "update"
129
+ if first == "--reload":
130
+ return "reload"
131
+ if first == "--debug-clicks":
132
+ return "debug-clicks"
133
+ if first in {"auth", "skill", "mac-approve", "recordings", "telemetry", "video"}:
134
+ return first
135
+ return "usage"
136
+
137
+
138
+ def _exit_code(result) -> int:
139
+ if result is None:
140
+ return 0
141
+ if isinstance(result, int):
142
+ return result
143
+ return 1
144
+
145
+ _MAX_TRACED_STEPS = 500
146
+ _MAX_STEP_ARGS_LENGTH = 300
147
+ _helper_trace = []
148
+ _helper_call_count = 0
149
+
150
+
151
+ def _step_args(args, kwargs):
152
+ parts = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
153
+ return ", ".join(parts)[:_MAX_STEP_ARGS_LENGTH]
154
+
155
+
156
+ def _traced(name, fn):
157
+ import functools
158
+
159
+ @functools.wraps(fn)
160
+ def wrapper(*args, **kwargs):
161
+ global _helper_call_count
162
+ _helper_call_count += 1
163
+ entry = {"helper": name, "args": _step_args(args, kwargs)}
164
+ if len(_helper_trace) < _MAX_TRACED_STEPS:
165
+ _helper_trace.append(entry)
166
+ step_start = time.monotonic()
167
+ try:
168
+ result = fn(*args, **kwargs)
169
+ except BaseException as exc:
170
+ entry["duration_seconds"] = round(time.monotonic() - step_start, 3)
171
+ entry["error"] = str(exc)[:300]
172
+ raise
173
+ entry["duration_seconds"] = round(time.monotonic() - step_start, 3)
174
+ recorder.observe(name, args, kwargs, entry["duration_seconds"])
175
+ return result
176
+
177
+ wrapper.__bh_traced__ = True
178
+ return wrapper
179
+
180
+
181
+ def _install_helper_trace():
182
+ from . import helpers
183
+
184
+ g = globals()
185
+ for name in dir(helpers):
186
+ if name.startswith("_"):
187
+ continue
188
+ fn = g.get(name)
189
+ if callable(fn) and not isinstance(fn, type) and not getattr(fn, "__bh_traced__", False):
190
+ g[name] = _traced(name, fn)
191
+
192
+
193
+ _MAX_OUTPUT_LENGTH = 20_000
194
+
195
+
196
+ class _StreamTail:
197
+ """Pass-through stream wrapper that remembers the tail and total length."""
198
+
199
+ def __init__(self, wrapped, limit=500):
200
+ self._wrapped = wrapped
201
+ self._limit = limit
202
+ self.tail = ""
203
+ self.length = 0
204
+
205
+ def write(self, text):
206
+ text = str(text)
207
+ self.length += len(text)
208
+ self.tail = (self.tail + text)[-self._limit :]
209
+ return self._wrapped.write(text)
210
+
211
+ def __getattr__(self, name):
212
+ return getattr(self._wrapped, name)
213
+
214
+
215
+ def _read_task(args):
216
+ if args and args[0] == "--debug-clicks":
217
+ args = args[1:]
218
+ if args or sys.stdin.isatty():
219
+ return None
220
+ code = sys.stdin.read()
221
+ sys.stdin = StringIO(code)
222
+ return code
223
+
224
+
225
+ def _traced_steps():
226
+ return _helper_trace or None
227
+
228
+
229
+ def _telemetry_browser(task):
230
+ """'cloud' | 'cdp' | 'local', self-reported by the daemon the task ran on.
231
+ None when no browser was involved (non-script commands, daemon never up)."""
232
+ if not task or not telemetry.is_enabled():
233
+ return None
234
+ try:
235
+ return daemon_browser_kind()
236
+ except Exception:
237
+ return None
238
+
239
+
240
+ def main():
241
+ global _helper_call_count
242
+ args = sys.argv[1:]
243
+ if args and args[0] == "telemetry":
244
+ sys.exit(telemetry.run_telemetry_cli(args[1:]))
245
+ _helper_trace.clear()
246
+ _helper_call_count = 0
247
+ start_time = time.monotonic()
248
+ command = _telemetry_command(args)
249
+ task = _read_task(args)
250
+ stderr_tail = _StreamTail(sys.stderr)
251
+ stdout_tail = _StreamTail(sys.stdout, limit=_MAX_OUTPUT_LENGTH)
252
+ sys.stderr = stderr_tail
253
+ sys.stdout = stdout_tail
254
+ try:
255
+ _run(args)
256
+ except SystemExit as exc:
257
+ code = _exit_code(exc.code)
258
+ telemetry.capture_cli_event(
259
+ action="error" if code else "completed",
260
+ command=command,
261
+ task=task,
262
+ browser=_telemetry_browser(task),
263
+ output=stdout_tail.tail or None,
264
+ output_length=stdout_tail.length or None,
265
+ steps=_traced_steps(),
266
+ step_count=_helper_call_count or None,
267
+ duration_seconds=time.monotonic() - start_time,
268
+ exit_code=code,
269
+ error_message=str(exc.code) if isinstance(exc.code, str) else (stderr_tail.tail.strip() or None) if code else None,
270
+ )
271
+ raise
272
+ except Exception as exc:
273
+ telemetry.capture_cli_event(
274
+ action="error",
275
+ command=command,
276
+ task=task,
277
+ browser=_telemetry_browser(task),
278
+ output=stdout_tail.tail or None,
279
+ output_length=stdout_tail.length or None,
280
+ steps=_traced_steps(),
281
+ step_count=_helper_call_count or None,
282
+ duration_seconds=time.monotonic() - start_time,
283
+ exit_code=1,
284
+ error_message=str(exc),
285
+ )
286
+ raise
287
+ finally:
288
+ sys.stderr = stderr_tail._wrapped
289
+ sys.stdout = stdout_tail._wrapped
290
+ telemetry.capture_cli_event(
291
+ action="completed",
292
+ command=command,
293
+ task=task,
294
+ browser=_telemetry_browser(task),
295
+ output=stdout_tail.tail or None,
296
+ output_length=stdout_tail.length or None,
297
+ steps=_traced_steps(),
298
+ step_count=_helper_call_count or None,
299
+ duration_seconds=time.monotonic() - start_time,
300
+ exit_code=0,
301
+ )
302
+
303
+
304
+ def _run(args):
305
+ if args and args[0] in {"-h", "--help"}:
306
+ print(HELP)
307
+ return
308
+ if args and args[0] == "--version":
309
+ print(_version() or "unknown")
310
+ return
311
+ if args and args[0] == "--doctor":
312
+ sys.exit(run_doctor())
313
+ if args and args[0] == "doctor":
314
+ rest = args[1:]
315
+ if rest == ["--fix-snap"]:
316
+ sys.exit(run_doctor_fix_snap())
317
+ if rest and set(rest).issubset({"--json", "--require-existing-daemon"}) \
318
+ and "--json" in rest and len(rest) == len(set(rest)):
319
+ sys.exit(run_doctor_json(require_existing_daemon="--require-existing-daemon" in rest))
320
+ if rest:
321
+ print("usage: browser-harness doctor [--fix-snap|--json [--require-existing-daemon]]", file=sys.stderr)
322
+ sys.exit(2)
323
+ sys.exit(run_doctor())
324
+ if args and args[0] == "auth":
325
+ sys.exit(auth.run_auth_cli(args[1:]))
326
+ if args and args[0] == "mac-approve":
327
+ from . import macos
328
+
329
+ sys.exit(macos.run_cli(args[1:]))
330
+ if args and args[0] == "skill":
331
+ if len(args) != 1:
332
+ print("usage: browser-harness skill", file=sys.stderr)
333
+ sys.exit(2)
334
+ _print_skill()
335
+ return
336
+ if args and args[0] == "recordings":
337
+ rest = args[1:]
338
+ if rest == ["--latest"]:
339
+ latest = recorder.latest_recording()
340
+ if latest is None:
341
+ print("no recordings found", file=sys.stderr)
342
+ sys.exit(1)
343
+ print(latest)
344
+ return
345
+ if rest in (["enable"], ["disable"]):
346
+ enabled = rest == ["enable"]
347
+ recorder.set_auto_recording(enabled)
348
+ print(f"auto-recording preference {'enabled' if enabled else 'disabled'}")
349
+ return
350
+ if rest:
351
+ print("usage: browser-harness recordings [--latest|enable|disable]", file=sys.stderr)
352
+ sys.exit(2)
353
+ enabled, source = recorder.auto_recording_setting()
354
+ print(f"auto-recording: {'on' if enabled else 'off'} ({source})")
355
+ active = recorder.recording_dir()
356
+ print(f"active: {active or 'none'}")
357
+ recent = recorder.recordings()
358
+ print(f"latest: {recent[0] if recent else 'none'}")
359
+ return
360
+ if args and args[0] == "video":
361
+ from . import video
362
+
363
+ sys.exit(video.run_cli(args[1:]))
364
+ if args and args[0] == "--update":
365
+ rest = args[1:]
366
+ if not set(rest).issubset({"-y", "--yes"}):
367
+ print("usage: browser-harness --update [-y|--yes]", file=sys.stderr)
368
+ sys.exit(2)
369
+ sys.exit(run_update(yes=bool(rest)))
370
+ if args and args[0] == "--reload":
371
+ restart_daemon()
372
+ print("daemon stopped — will restart fresh on next call")
373
+ return
374
+ if args and args[0] == "--debug-clicks":
375
+ os.environ["BH_DEBUG_CLICKS"] = "1"
376
+ args = args[1:]
377
+ if not args and not sys.stdin.isatty():
378
+ code = sys.stdin.read()
379
+ if not code.strip():
380
+ sys.exit(USAGE)
381
+ else:
382
+ sys.exit(USAGE)
383
+ print_update_banner()
384
+ # Auto-bootstrap a cloud browser is opt-in via BU_AUTOSPAWN — BROWSER_USE_API_KEY alone
385
+ # is not enough, since the key is commonly set for unrelated reasons (profile sync,
386
+ # cloud API calls, parent agents managing their own session). An explicit BU_CDP_URL
387
+ # or BU_CDP_WS also blocks the spawn so we honour the precedence install.md promises.
388
+ cloud_admin = code.lstrip().startswith(("start_remote_daemon(", "stop_remote_daemon("))
389
+ if not cloud_admin:
390
+ require_existing = os.environ.get("BH_REQUIRE_EXISTING_DAEMON") == "1"
391
+ try:
392
+ if require_existing:
393
+ require_existing_daemon()
394
+ else:
395
+ if (
396
+ not daemon_alive()
397
+ and not _local_chrome_listening()
398
+ and not _explicit_cdp_configured()
399
+ and _cloud_auth_configured()
400
+ and os.environ.get("BU_AUTOSPAWN")
401
+ ):
402
+ start_remote_daemon(NAME)
403
+ ensure_daemon()
404
+ except RuntimeError as e:
405
+ # Setup/permission errors are instructions for calling agent
406
+ print(f"browser-harness: {e}", file=sys.stderr)
407
+ sys.exit(1)
408
+ _install_helper_trace()
409
+ exec(code, globals())
410
+
411
+
412
+ if __name__ == "__main__":
413
+ main()