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
@@ -0,0 +1,107 @@
1
+ platform: xhs
2
+ status: migrated_unverified
3
+ last_verified: null
4
+ actions:
5
+ post: "actions/_post.py"
6
+ manage: "actions/_manage.py"
7
+ verification:
8
+ status: migrated_unverified
9
+ last_verified: null
10
+ evidence: Existing post and creator-manager mechanics migrated from the working
11
+ post adapter. Current Browser Workspace live verification pending.
12
+ url_patterns:
13
+ - https://creator.xiaohongshu.com/*
14
+ authentication:
15
+ required: true
16
+ notes: Use the caller's authenticated browser context; never store credentials.
17
+ provenance:
18
+ source: skills/post/scripts/platforms/xhs
19
+ migrated_code: true
20
+ flows:
21
+ post:
22
+ status: migrated_unverified
23
+ evidence: Migrated action; current Browser Workspace live verification pending.
24
+ manage:
25
+ status: migrated_unverified
26
+ evidence: Migrated manager action; current Browser Workspace live verification
27
+ pending.
28
+ display_name: Xiaohongshu
29
+ aliases:
30
+ - xiaohongshu
31
+ media:
32
+ modes:
33
+ - video
34
+ - images
35
+ video:
36
+ required_when_mode: video
37
+ formats:
38
+ - mp4
39
+ images:
40
+ required_when_mode: images
41
+ formats:
42
+ - jpg
43
+ - png
44
+ - webp
45
+ max_count: 18
46
+ mutually_exclusive:
47
+ - video
48
+ - images
49
+ fields:
50
+ title:
51
+ cli: --title
52
+ required: true
53
+ type: string
54
+ max_chars: 20
55
+ implemented: true
56
+ body:
57
+ cli: --body
58
+ required: true
59
+ type: string
60
+ max_chars: 1000
61
+ implemented: true
62
+ tags:
63
+ cli: --tags
64
+ required: false
65
+ type: string_list
66
+ max_count: 5
67
+ implemented: true
68
+ render: 'append as #tag'
69
+ publish:
70
+ cli: --publish
71
+ required: false
72
+ type: boolean
73
+ default: false
74
+ implemented: true
75
+ collection:
76
+ required: false
77
+ type: string
78
+ max_chars: 20
79
+ implemented: true
80
+ collection_description:
81
+ required: false
82
+ type: string
83
+ max_chars: 50
84
+ implemented: true
85
+ capabilities:
86
+ draft: true
87
+ publish: true
88
+ visibility: false
89
+ custom_thumbnail: false
90
+ series_or_collection: true
91
+ post_status: true
92
+ read_comments: true
93
+ write_comment: true
94
+ reply_comments: true
95
+ content_mapping:
96
+ headline: title
97
+ main_text: body
98
+ discovery_terms: tags
99
+ notes:
100
+ - Without --publish, the adapter saves a draft.
101
+ - After --publish, the XHS adapter automatically verifies the created note in creator
102
+ note manager and reports published/reviewing/rejected state.
103
+ - comments/comment/reply resolve the post from the signed-in My Posts profile list
104
+ and use that card's live xsec detail route.
105
+ - The opened post detail view is the canonical place to read comments, write a top-level
106
+ comment, and reply to comments.
107
+ - Optional `collection` selects an existing exact-name collection or creates and joins it when absent.
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+
4
+ HERE = Path(__file__).resolve().parents[1]
5
+
6
+
7
+ def test_actions_do_not_use_page_info_as_tab_url_authority():
8
+ for name in ("_post.py", "_manage.py"):
9
+ source = (HERE / "actions" / name).read_text()
10
+ assert 'page_info()["url"]' not in source
11
+
12
+
13
+ def test_actions_use_current_tab_url_for_navigation_identity():
14
+ post = (HERE / "actions" / "_post.py").read_text()
15
+ manage = (HERE / "actions" / "_manage.py").read_text()
16
+ assert '(current_tab().get("url") or "") != url' in post
17
+ assert '(current_tab().get("url") or "") != url' in manage
18
+
19
+
20
+ def test_video_readiness_uses_editor_state_not_preview():
21
+ source=(HERE/'actions'/'_post.py').read_text()
22
+ assert '上传中' in source
23
+ assert '重新上传' in source
24
+ assert 'Upload processed and editor-ready!' in source
25
+
26
+ def test_collection_and_receipt_contracts_are_present():
27
+ source=(HERE/'actions'/'_post.py').read_text()
28
+ manifest=(HERE/'manifest.yaml').read_text()
29
+ assert 'choose_collection' in source
30
+ assert 'creator_manager_receipt' in source
31
+ assert 'XHS_RECEIPT' in source
32
+ assert 'duplicate_count' in source
33
+ assert 'series_or_collection: true' in manifest
34
+ assert 'collection:' in manifest
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: youtube-platform
3
+ description: "YouTube Studio Browser Workspace upload mechanics."
4
+ ---
5
+
6
+ # YouTube
7
+
8
+ Use the parent Browser Workspace skill contract first. The
9
+ `actions/` directory contains the canonical migrated Studio upload
10
+ mechanics; `skills/post` retains payload and publishing orchestration.
@@ -0,0 +1,103 @@
1
+ import time, json
2
+
3
+ CFG = json.load(open("__CFG_PATH__"))
4
+
5
+ tags = CFG.get("tags", [])
6
+ thumbnail = CFG.get("thumbnail", "")
7
+ has_extras = bool(tags or thumbnail)
8
+ total_steps = 7 + (1 if tags else 0) + (1 if thumbnail else 0)
9
+ step = [0]
10
+ def next_step(label):
11
+ step[0] += 1
12
+ print(f"[{step[0]}/{total_steps}] {label}")
13
+
14
+ def click_btn(text):
15
+ text_s = json.dumps(text)
16
+ for _ in range(5):
17
+ clicked = js(
18
+ '(function(){ var btns=document.querySelectorAll("ytcp-button"); '
19
+ 'var b=Array.from(btns).find(function(el){return el.textContent.trim()===' + text_s + ' && !el.disabled;}); '
20
+ 'if(!b)return false; b.click(); return true; })()'
21
+ )
22
+ if clicked:
23
+ return True
24
+ time.sleep(1)
25
+ return False
26
+
27
+ next_step("Opening YouTube Studio upload...")
28
+ new_tab("https://www.youtube.com/upload")
29
+ wait_for_load()
30
+ time.sleep(3)
31
+
32
+ url = page_info()["url"]
33
+ if "accounts.google.com" in url:
34
+ print("ERROR: Not logged in")
35
+ capture_screenshot()
36
+ raise SystemExit(1)
37
+
38
+ next_step("Uploading video...")
39
+ upload_file("input[type=file]", CFG["video"])
40
+ time.sleep(5)
41
+
42
+ next_step("Waiting for processing...")
43
+ time.sleep(3)
44
+
45
+ next_step("Setting title: " + CFG["title"])
46
+ title_escaped = json.dumps(CFG["title"])
47
+ js('var b=document.querySelectorAll("#textbox")[0];b.textContent=' + title_escaped + ';b.dispatchEvent(new Event("input",{bubbles:true}));""')
48
+ time.sleep(0.5)
49
+
50
+ next_step("Setting description...")
51
+ desc_escaped = json.dumps(CFG["desc"])
52
+ js('var b=document.querySelectorAll("#textbox")[1];b.textContent=' + desc_escaped + ';b.dispatchEvent(new Event("input",{bubbles:true}));""')
53
+ time.sleep(0.5)
54
+
55
+ # Upload thumbnail if provided
56
+ if thumbnail:
57
+ next_step("Uploading thumbnail...")
58
+ # Click "Upload thumbnail" button to reveal file input
59
+ js('var btn=document.querySelector("#still-picker ytcp-button,#upload-prompt");if(btn)btn.click();""')
60
+ time.sleep(1)
61
+ upload_file("#file-loader input[type=file],input[accept*=image]", thumbnail)
62
+ time.sleep(3)
63
+
64
+ # Add tags via "Show more" → tags input
65
+ if tags:
66
+ next_step("Adding tags: " + ", ".join(tags))
67
+ # Expand "Show more" section
68
+ js('var btn=document.querySelector("#toggle-button ytcp-button,ytcp-button#toggle-button");if(btn)btn.click();""')
69
+ time.sleep(1)
70
+ # Find tags input and type comma-separated tags
71
+ tag_str = ",".join(tags)
72
+ tag_escaped = json.dumps(tag_str)
73
+ js('var inp=document.querySelector("input.tags-input,input[aria-label*=Tag],input[placeholder*=Tag]");if(inp){var ns=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value").set;ns.call(inp,' + tag_escaped + ');inp.dispatchEvent(new Event("input",{bubbles:true}));inp.dispatchEvent(new Event("change",{bubbles:true}))};"done"')
74
+ time.sleep(0.5)
75
+
76
+ next_step("Setting audience" + (" (MADE FOR KIDS)" if CFG.get("made_for_kids") else "") + " + advancing wizard...")
77
+ if CFG.get("made_for_kids"):
78
+ # COPPA: select explicitly by name — never rely on radio order
79
+ js('var r=document.querySelector("tp-yt-paper-radio-button[name=VIDEO_MADE_FOR_KIDS_MFK]");if(r){r.click()}')
80
+ else:
81
+ js('var g=document.querySelector(".made-for-kids-group");if(g){g.querySelectorAll("tp-yt-paper-radio-button")[1].click()}')
82
+ time.sleep(1)
83
+
84
+ for w_step in ["Video elements", "Initial check", "Visibility"]:
85
+ print(" -> " + w_step)
86
+ click_btn("Next")
87
+ time.sleep(2)
88
+
89
+ vis = CFG["vis"]
90
+ next_step("Setting visibility to " + vis + " and saving...")
91
+ for _ in range(5):
92
+ found = js('document.querySelector("[name=' + vis + ']") ? "found" : "notfound"')
93
+ if found == "found":
94
+ js('document.querySelector("[name=' + vis + ']").click()')
95
+ time.sleep(1)
96
+ break
97
+ time.sleep(1)
98
+
99
+ click_btn("Save")
100
+ time.sleep(5)
101
+
102
+ capture_screenshot()
103
+ print("Done! Video saved as " + vis + ".")
@@ -0,0 +1,94 @@
1
+ platform: youtube
2
+ status: migrated_unverified
3
+ last_verified: null
4
+ actions:
5
+ post: actions/_post.py
6
+ verification:
7
+ status: migrated_unverified
8
+ last_verified: null
9
+ evidence: Existing Studio upload mechanics migrated from the working post adapter.
10
+ Current Browser Workspace live verification pending.
11
+ url_patterns:
12
+ - https://www.youtube.com/upload
13
+ - https://studio.youtube.com/*
14
+ authentication:
15
+ required: true
16
+ notes: Use the caller's authenticated browser context; never store credentials.
17
+ provenance:
18
+ source: skills/post/scripts/platforms/youtube
19
+ migrated_code: true
20
+ flows:
21
+ post:
22
+ status: migrated_unverified
23
+ evidence: Migrated action; current Browser Workspace live verification pending.
24
+ display_name: YouTube
25
+ aliases:
26
+ - yt
27
+ media:
28
+ modes:
29
+ - video
30
+ video:
31
+ required: true
32
+ fields:
33
+ title:
34
+ cli: --title
35
+ required: true
36
+ type: string
37
+ max_chars: 100
38
+ implemented: true
39
+ desc:
40
+ cli: --desc
41
+ required: false
42
+ type: string
43
+ max_chars: 5000
44
+ implemented: true
45
+ tags:
46
+ cli: --tags
47
+ required: false
48
+ type: string_list
49
+ max_total_chars: 500
50
+ max_chars_each: 30
51
+ implemented: true
52
+ thumbnail:
53
+ cli: --thumbnail
54
+ required: false
55
+ type: file
56
+ formats:
57
+ - jpg
58
+ - png
59
+ max_size_mb: 2
60
+ implemented: true
61
+ visibility:
62
+ cli_group:
63
+ - --private
64
+ - --unlisted
65
+ - --public
66
+ required: false
67
+ type: enum
68
+ values:
69
+ - PRIVATE
70
+ - UNLISTED
71
+ - PUBLIC
72
+ default: PRIVATE
73
+ implemented: true
74
+ made_for_kids:
75
+ cli: --made-for-kids
76
+ required: false
77
+ type: boolean
78
+ default: false
79
+ implemented: true
80
+ capabilities:
81
+ draft: false
82
+ publish: true
83
+ visibility: true
84
+ custom_thumbnail: true
85
+ audience_made_for_kids: true
86
+ post_status: false
87
+ read_comments: false
88
+ write_comment: false
89
+ reply_comments: false
90
+ content_mapping:
91
+ headline: title
92
+ main_text: desc
93
+ discovery_terms: tags
94
+ visual_packaging: thumbnail
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools==84.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "browser-workspace"
7
+ version = "0.4.5"
8
+ description = "Agent browser workspace with persistent Python sessions, tab leasing, snapshots, and platform workflows."
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192",
12
+ "cdp-use==1.4.5",
13
+ "fetch-use==0.4.0",
14
+ "pillow==12.3.0",
15
+ "PyYAML==6.0.2",
16
+ "websockets==15.0.1",
17
+ ]
18
+
19
+ [project.scripts]
20
+ browser-workspace = "browser_workspace_app.cli:main"
21
+
22
+ [tool.setuptools]
23
+ package-dir = {"" = "src"}
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ browser_workspace_app = ["**/*"]
30
+
31
+ [tool.pytest.ini_options]
32
+ pythonpath = ["src"]
33
+ addopts = "--import-mode=importlib"
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ import fcntl, json, os, shutil, socket, subprocess, sys, time
4
+ from pathlib import Path
5
+
6
+ ROOT=Path(__file__).resolve().parent
7
+ RUNTIME_DIR=Path.home()/'.config/browser-workspace/runtime'
8
+ 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')
9
+
10
+ def package_version():
11
+ try:
12
+ return json.loads((ROOT/'package.json').read_text()).get('version','dev')
13
+ except Exception:
14
+ return 'dev'
15
+
16
+ def runtime_source():
17
+ configured=os.environ.get('BROWSER_WORKSPACE_RUNTIME_SOURCE')
18
+ if configured: return Path(configured)
19
+ if not (ROOT/'package.json').exists(): return ROOT
20
+ target=RUNTIME_DIR/'packages'/package_version()
21
+ marker=target/'.ready'
22
+ if marker.exists(): return target
23
+ target.parent.mkdir(parents=True,exist_ok=True)
24
+ tmp=target.with_name(target.name+'.tmp')
25
+ shutil.rmtree(tmp,ignore_errors=True)
26
+ tmp.mkdir(parents=True)
27
+ for name in ['pyproject.toml','session_daemon.py','session_client.py','platform_runner.py','managed_chrome.py','src','agent-workspace','interaction-skills','platforms','extension','licenses']:
28
+ source=ROOT/name
29
+ if not source.exists(): continue
30
+ destination=tmp/name
31
+ if source.is_dir(): shutil.copytree(source,destination)
32
+ else: shutil.copy2(source,destination)
33
+ (tmp/'.ready').write_text(package_version())
34
+ shutil.rmtree(target,ignore_errors=True)
35
+ tmp.rename(target)
36
+ return target
37
+
38
+ SOURCE=runtime_source()
39
+ PYTHON=Path(os.environ.get('BROWSER_WORKSPACE_SESSION_PYTHON',RUNTIME_DIR/'venv'/package_version()/'bin/python'))
40
+
41
+
42
+
43
+ def ensure_runtime_python():
44
+ if PYTHON.exists():
45
+ return PYTHON
46
+ RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
47
+ lock_path=RUNTIME_DIR/'runtime-bootstrap.lock'
48
+ with open(lock_path,'a+b') as lock:
49
+ fcntl.flock(lock,fcntl.LOCK_EX)
50
+ if PYTHON.exists():
51
+ return PYTHON
52
+ uv=shutil.which('uv')
53
+ if uv:
54
+ subprocess.run([uv,'venv','--python','3.11',str(PYTHON.parent.parent)],check=True)
55
+ subprocess.run([uv,'pip','install','--python',str(PYTHON),'-e',str(SOURCE)],check=True)
56
+ return PYTHON
57
+ if sys.version_info < (3,11):
58
+ raise RuntimeError(
59
+ 'Browser Workspace needs Python 3.11+ for first-use bootstrap. '
60
+ 'Install uv or Python 3.11+, then retry.'
61
+ )
62
+ subprocess.run([sys.executable,'-m','venv',str(PYTHON.parent.parent)],check=True)
63
+ subprocess.run([str(PYTHON),'-m','pip','install','-e',str(SOURCE)],check=True)
64
+ return PYTHON
65
+
66
+ def request(payload, ensure=True):
67
+ if ensure: ensure_daemon()
68
+ s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
69
+ try: s.connect(str(SOCKET_PATH))
70
+ except Exception:
71
+ s.close(); raise
72
+ with s:
73
+ f=s.makefile('rwb'); f.write((json.dumps(payload,ensure_ascii=False)+'\n').encode()); f.flush(); line=f.readline()
74
+ if not line: raise RuntimeError('browser-workspace session daemon closed without a response')
75
+ return json.loads(line)
76
+
77
+ def ping():
78
+ try: return request({'op':'ping'},ensure=False)
79
+ except Exception: return None
80
+
81
+ def owner_is_running():
82
+ """An exclusive owner lock means a live daemon already exists."""
83
+ owner_path=RUNTIME_DIR/'session.lock'
84
+ with open(owner_path,'a+b') as owner:
85
+ try:
86
+ fcntl.flock(owner,fcntl.LOCK_EX|fcntl.LOCK_NB)
87
+ except BlockingIOError:
88
+ return True
89
+ finally:
90
+ try: fcntl.flock(owner,fcntl.LOCK_UN)
91
+ except OSError: pass
92
+ return False
93
+
94
+ def ensure_daemon():
95
+ p=ping()
96
+ if p and p.get('ok'): return p
97
+ RUNTIME_DIR.mkdir(parents=True,exist_ok=True)
98
+ if owner_is_running():
99
+ raise RuntimeError('browser-workspace daemon is alive but its socket is unreachable; refusing duplicate startup')
100
+ runtime_python=ensure_runtime_python()
101
+ log=open(RUNTIME_DIR/'session-daemon.log','ab',buffering=0)
102
+ subprocess.Popen([str(runtime_python),str(SOURCE/'session_daemon.py')],stdin=subprocess.DEVNULL,stdout=log,stderr=log,start_new_session=True,close_fds=True)
103
+ deadline=time.time()+15
104
+ while time.time()<deadline:
105
+ time.sleep(.1); p=ping()
106
+ if p and p.get('ok'): return p
107
+ raise RuntimeError(f'browser-workspace session daemon did not start; see {RUNTIME_DIR / "session-daemon.log"}')