browser-workspace 0.4.5 → 0.4.9

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 (68) hide show
  1. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  2. package/package.json +1 -1
  3. package/platform_runner.py +11 -2
  4. package/platforms/chatgpt/actions/_lifecycle_common.py +73 -20
  5. package/platforms/chatgpt/actions/_new_turn.py +1 -0
  6. package/platforms/chatgpt/actions/_readiness.py +29 -3
  7. package/platforms/chatgpt/actions/_submit_thread.py +20 -3
  8. package/platforms/chatgpt/actions/_thread_turn.py +31 -2
  9. package/platforms/chatgpt/tests/test_contract.py +107 -1
  10. package/platforms/iyf/AIRPLAY.md +25 -0
  11. package/platforms/iyf/actions/_find.py +18 -0
  12. package/platforms/iyf/actions/_open.py +10 -0
  13. package/platforms/iyf/manifest.yaml +11 -0
  14. package/platforms/iyf/safari-airplay +57 -0
  15. package/pyproject.toml +2 -2
  16. package/session_client.py +10 -0
  17. package/src/browser_workspace.egg-info/PKG-INFO +2 -2
  18. package/src/browser_workspace.egg-info/SOURCES.txt +53 -46
  19. package/src/browser_workspace.egg-info/requires.txt +1 -1
  20. package/src/browser_workspace_app/package.json +1 -1
  21. package/src/browser_workspace_app/pyproject.toml +2 -2
  22. package/src/browser_workspace_app/session_client.py +10 -0
  23. package/src/browser_workspace_app/platform_runner.py +0 -204
  24. package/src/browser_workspace_app/platforms/chatgpt/README.md +0 -60
  25. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +0 -58
  26. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +0 -119
  27. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +0 -80
  28. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +0 -85
  29. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +0 -33
  30. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +0 -353
  31. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +0 -52
  32. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +0 -129
  33. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +0 -591
  34. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +0 -362
  35. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +0 -34
  36. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +0 -183
  37. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +0 -49
  38. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +0 -425
  39. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +0 -7
  40. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +0 -10
  41. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +0 -10
  42. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +0 -18
  43. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +0 -11
  44. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +0 -9
  45. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +0 -33
  46. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +0 -83
  47. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +0 -32
  48. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +0 -210
  49. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +0 -21
  50. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +0 -25
  51. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +0 -24
  52. package/src/browser_workspace_app/platforms/tiktok/README.md +0 -10
  53. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +0 -180
  54. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +0 -99
  55. package/src/browser_workspace_app/platforms/wechat-channels/README.md +0 -10
  56. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +0 -163
  57. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +0 -130
  58. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +0 -231
  59. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +0 -124
  60. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +0 -36
  61. package/src/browser_workspace_app/platforms/xhs/README.md +0 -10
  62. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +0 -355
  63. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +0 -281
  64. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +0 -107
  65. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +0 -34
  66. package/src/browser_workspace_app/platforms/youtube/README.md +0 -10
  67. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +0 -103
  68. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +0 -94
package/session_client.py CHANGED
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  ROOT=Path(__file__).resolve().parent
7
7
  RUNTIME_DIR=Path.home()/'.config/browser-workspace/runtime'
8
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
+ HARNESS_RUNTIME_SPEC='browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192'
9
10
 
10
11
  def package_version():
11
12
  try:
@@ -30,6 +31,13 @@ def runtime_source():
30
31
  destination=tmp/name
31
32
  if source.is_dir(): shutil.copytree(source,destination)
32
33
  else: shutil.copy2(source,destination)
34
+ # A published wheel installs directly under browser_workspace_app/ without
35
+ # a nested src/ tree. Reconstruct the build layout only in the runtime
36
+ # cache, never in the repository or published source tree.
37
+ if not (tmp/'src').exists():
38
+ package_root = tmp/'src'/'browser_workspace_app'
39
+ package_root.parent.mkdir(parents=True, exist_ok=True)
40
+ shutil.copytree(ROOT, package_root, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.DS_Store'))
33
41
  (tmp/'.ready').write_text(package_version())
34
42
  shutil.rmtree(target,ignore_errors=True)
35
43
  tmp.rename(target)
@@ -53,6 +61,7 @@ def ensure_runtime_python():
53
61
  if uv:
54
62
  subprocess.run([uv,'venv','--python','3.11',str(PYTHON.parent.parent)],check=True)
55
63
  subprocess.run([uv,'pip','install','--python',str(PYTHON),'-e',str(SOURCE)],check=True)
64
+ subprocess.run([uv,'pip','install','--python',str(PYTHON),'--reinstall',HARNESS_RUNTIME_SPEC],check=True)
56
65
  return PYTHON
57
66
  if sys.version_info < (3,11):
58
67
  raise RuntimeError(
@@ -61,6 +70,7 @@ def ensure_runtime_python():
61
70
  )
62
71
  subprocess.run([sys.executable,'-m','venv',str(PYTHON.parent.parent)],check=True)
63
72
  subprocess.run([str(PYTHON),'-m','pip','install','-e',str(SOURCE)],check=True)
73
+ subprocess.run([str(PYTHON),'-m','pip','install','--force-reinstall',HARNESS_RUNTIME_SPEC],check=True)
64
74
  return PYTHON
65
75
 
66
76
  def request(payload, ensure=True):
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browser-workspace
3
- Version: 0.4.5
3
+ Version: 0.4.9
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
+ Requires-Dist: browser-harness==0.1.13
7
7
  Requires-Dist: cdp-use==1.4.5
8
8
  Requires-Dist: fetch-use==0.4.0
9
9
  Requires-Dist: pillow==12.3.0
@@ -1,5 +1,58 @@
1
+ MANIFEST.in
1
2
  README.md
3
+ platform_runner.py
2
4
  pyproject.toml
5
+ setup.py
6
+ platforms/chatgpt/README.md
7
+ platforms/chatgpt/manifest.yaml
8
+ platforms/chatgpt/actions/_bootstrap_start.py
9
+ platforms/chatgpt/actions/_lifecycle_common.py
10
+ platforms/chatgpt/actions/_mcp_app.py
11
+ platforms/chatgpt/actions/_new_thread.py
12
+ platforms/chatgpt/actions/_new_turn.py
13
+ platforms/chatgpt/actions/_project_setup.py
14
+ platforms/chatgpt/actions/_readiness.py
15
+ platforms/chatgpt/actions/_submit_thread.py
16
+ platforms/chatgpt/actions/_temporary_submit.py
17
+ platforms/chatgpt/actions/_thread_turn.py
18
+ platforms/chatgpt/tests/test_contract.py
19
+ platforms/chrome-web-store/README.md
20
+ platforms/chrome-web-store/manifest.yaml
21
+ platforms/chrome-web-store/actions/_chrome_web_store.py
22
+ platforms/chrome-web-store/flows/check-status.md
23
+ platforms/chrome-web-store/flows/open-item.md
24
+ platforms/chrome-web-store/flows/submit-review.md
25
+ platforms/chrome-web-store/flows/update-listing.md
26
+ platforms/chrome-web-store/flows/upload-package.md
27
+ platforms/chrome-web-store/flows/verify-published.md
28
+ platforms/chrome-web-store/tests/test_contract.py
29
+ platforms/iyf/AIRPLAY.md
30
+ platforms/iyf/manifest.yaml
31
+ platforms/iyf/safari-airplay
32
+ platforms/iyf/actions/_find.py
33
+ platforms/iyf/actions/_open.py
34
+ platforms/microsoft-teams/README.md
35
+ platforms/microsoft-teams/manifest.yaml
36
+ platforms/microsoft-teams/actions/_meeting.py
37
+ platforms/microsoft-teams/flows/instant-meeting.md
38
+ platforms/microsoft-teams/tests/test_contract.py
39
+ platforms/tiktok/README.md
40
+ platforms/tiktok/manifest.yaml
41
+ platforms/tiktok/actions/_post.py
42
+ platforms/wechat-channels/README.md
43
+ platforms/wechat-channels/manifest.yaml
44
+ platforms/wechat-channels/actions/_common.py
45
+ platforms/wechat-channels/actions/_manage.py
46
+ platforms/wechat-channels/actions/_post.py
47
+ platforms/wechat-channels/tests/test_contract.py
48
+ platforms/xhs/README.md
49
+ platforms/xhs/manifest.yaml
50
+ platforms/xhs/actions/_manage.py
51
+ platforms/xhs/actions/_post.py
52
+ platforms/xhs/tests/test_contract.py
53
+ platforms/youtube/README.md
54
+ platforms/youtube/manifest.yaml
55
+ platforms/youtube/actions/_post.py
3
56
  src/browser_workspace.egg-info/PKG-INFO
4
57
  src/browser_workspace.egg-info/SOURCES.txt
5
58
  src/browser_workspace.egg-info/dependency_links.txt
@@ -10,7 +63,6 @@ src/browser_workspace_app/__init__.py
10
63
  src/browser_workspace_app/cli.py
11
64
  src/browser_workspace_app/managed_chrome.py
12
65
  src/browser_workspace_app/package.json
13
- src/browser_workspace_app/platform_runner.py
14
66
  src/browser_workspace_app/pyproject.toml
15
67
  src/browser_workspace_app/session_client.py
16
68
  src/browser_workspace_app/session_daemon.py
@@ -39,51 +91,6 @@ src/browser_workspace_app/interaction-skills/uploads.md
39
91
  src/browser_workspace_app/interaction-skills/viewport.md
40
92
  src/browser_workspace_app/licenses/browser-harness-MIT.txt
41
93
  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
94
  test/test_auth_cookie_audit.py
88
95
  test/test_auth_keepalive_visit.py
89
96
  test/test_harness_ownership.py
@@ -1,4 +1,4 @@
1
- browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192
1
+ browser-harness==0.1.13
2
2
  cdp-use==1.4.5
3
3
  fetch-use==0.4.0
4
4
  pillow==12.3.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-workspace",
3
- "version": "0.4.5",
3
+ "version": "0.4.8",
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"
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "browser-workspace"
7
- version = "0.4.5"
7
+ version = "0.4.8"
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
+ "browser-harness==0.1.13",
12
12
  "cdp-use==1.4.5",
13
13
  "fetch-use==0.4.0",
14
14
  "pillow==12.3.0",
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  ROOT=Path(__file__).resolve().parent
7
7
  RUNTIME_DIR=Path.home()/'.config/browser-workspace/runtime'
8
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
+ HARNESS_RUNTIME_SPEC='browser-harness @ git+https://github.com/lalalic/browser-harness.git@80b74e116b9126f5159c84e5e05a750573e5f192'
9
10
 
10
11
  def package_version():
11
12
  try:
@@ -30,6 +31,13 @@ def runtime_source():
30
31
  destination=tmp/name
31
32
  if source.is_dir(): shutil.copytree(source,destination)
32
33
  else: shutil.copy2(source,destination)
34
+ # A published wheel installs directly under browser_workspace_app/ without
35
+ # a nested src/ tree. Reconstruct the build layout only in the runtime
36
+ # cache, never in the repository or published source tree.
37
+ if not (tmp/'src').exists():
38
+ package_root = tmp/'src'/'browser_workspace_app'
39
+ package_root.parent.mkdir(parents=True, exist_ok=True)
40
+ shutil.copytree(ROOT, package_root, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.DS_Store'))
33
41
  (tmp/'.ready').write_text(package_version())
34
42
  shutil.rmtree(target,ignore_errors=True)
35
43
  tmp.rename(target)
@@ -53,6 +61,7 @@ def ensure_runtime_python():
53
61
  if uv:
54
62
  subprocess.run([uv,'venv','--python','3.11',str(PYTHON.parent.parent)],check=True)
55
63
  subprocess.run([uv,'pip','install','--python',str(PYTHON),'-e',str(SOURCE)],check=True)
64
+ subprocess.run([uv,'pip','install','--python',str(PYTHON),'--reinstall',HARNESS_RUNTIME_SPEC],check=True)
56
65
  return PYTHON
57
66
  if sys.version_info < (3,11):
58
67
  raise RuntimeError(
@@ -61,6 +70,7 @@ def ensure_runtime_python():
61
70
  )
62
71
  subprocess.run([sys.executable,'-m','venv',str(PYTHON.parent.parent)],check=True)
63
72
  subprocess.run([str(PYTHON),'-m','pip','install','-e',str(SOURCE)],check=True)
73
+ subprocess.run([str(PYTHON),'-m','pip','install','--force-reinstall',HARNESS_RUNTIME_SPEC],check=True)
64
74
  return PYTHON
65
75
 
66
76
  def request(payload, ensure=True):
@@ -1,204 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import json
5
- import re
6
- from pathlib import Path
7
-
8
- import yaml
9
-
10
- from session_client import request
11
-
12
- ROOT = Path(__file__).resolve().parent
13
- PLATFORMS = ROOT / "platforms"
14
-
15
-
16
- def platform_manifest(platform: str) -> dict:
17
- platform = platform.strip()
18
- manifest = PLATFORMS / platform / "manifest.yaml"
19
- if not manifest.is_file():
20
- raise FileNotFoundError(f"unknown platform: {platform}")
21
- data = yaml.safe_load(manifest.read_text())
22
- if not isinstance(data, dict):
23
- raise ValueError(f"invalid platform manifest: {platform}")
24
- return data
25
-
26
-
27
- def platform_profile(platform: str) -> dict:
28
- data = platform_manifest(platform)
29
- keys = (
30
- "platform", "display_name", "aliases", "implementation_status",
31
- "media", "fields", "capabilities", "content_mapping", "notes",
32
- )
33
- return {key: data[key] for key in keys if key in data}
34
-
35
-
36
- def _validate_string(name: str, value, spec: dict) -> None:
37
- if value is None:
38
- return
39
- text = str(value)
40
- minimum = spec.get("min_chars")
41
- maximum = spec.get("max_chars") or spec.get("max_chars_including_tags")
42
- if minimum is not None and text and len(text) < int(minimum):
43
- raise ValueError(f"{name} too short: {len(text)} chars (min {minimum})")
44
- if maximum is not None and len(text) > int(maximum):
45
- raise ValueError(f"{name} too long: {len(text)} chars (max {maximum})")
46
-
47
-
48
- def validate_platform_config(platform: str, action: str, config: dict) -> None:
49
- if action != "post":
50
- return
51
- profile = platform_profile(platform)
52
- fields = profile.get("fields") or {}
53
- for name, spec in fields.items():
54
- if not isinstance(spec, dict) or spec.get("type") == "operation":
55
- continue
56
- value = config.get(name)
57
- if spec.get("required") and (value is None or value == "" or value == []):
58
- raise ValueError(f"{platform}: missing required field {name}")
59
- if value is None:
60
- continue
61
- kind = spec.get("type")
62
- if kind == "string":
63
- _validate_string(name, value, spec)
64
- elif kind == "string_list":
65
- values = value if isinstance(value, list) else [x.strip() for x in str(value).split(",") if x.strip()]
66
- maximum = spec.get("max_count")
67
- if maximum is not None and len(values) > int(maximum):
68
- raise ValueError(f"{name} has {len(values)} items (max {maximum})")
69
- each = spec.get("max_chars_each")
70
- if each is not None:
71
- for item in values:
72
- if len(str(item)) > int(each):
73
- raise ValueError(f"{name} item too long: {item!r} (max {each})")
74
- total = spec.get("max_total_chars")
75
- if total is not None and sum(len(str(x)) for x in values) > int(total):
76
- raise ValueError(f"{name} total length exceeds {total}")
77
- elif kind == "enum":
78
- values = spec.get("values") or []
79
- if value not in values:
80
- raise ValueError(f"{name} must be one of: {', '.join(map(str, values))}")
81
-
82
- images_spec = (profile.get("media") or {}).get("images") or {}
83
- images = config.get("images") or []
84
- maximum = images_spec.get("max_count")
85
- if maximum is not None and len(images) > int(maximum):
86
- raise ValueError(f"images has {len(images)} items (max {maximum})")
87
-
88
-
89
- def declared_actions(platform: str) -> dict[str, Path]:
90
- platform = platform.strip()
91
- manifest = PLATFORMS / platform / "manifest.yaml"
92
- if not manifest.is_file():
93
- raise FileNotFoundError(f"unknown platform: {platform}")
94
-
95
- text = manifest.read_text()
96
- match = re.search(
97
- r"^actions:\s*\n(?P<body>(?:^[ \t]+.*\n?)*)",
98
- text,
99
- flags=re.MULTILINE,
100
- )
101
- if not match:
102
- raise ValueError(f"{platform}/manifest.yaml has no actions block")
103
-
104
- actions: dict[str, Path] = {}
105
- for line in match.group("body").splitlines():
106
- entry = re.match(r'^\s{2}([\w-]+):\s*(.+?)\s*$', line)
107
- if not entry:
108
- continue
109
- name, relative = entry.groups()
110
- if len(relative) >= 2 and relative[0] == relative[-1] and relative[0] in {"\"", "'"}:
111
- relative = relative[1:-1]
112
- # Actions are simple relative file paths; comments and nested mappings are not runnable actions.
113
- if not relative or relative.startswith(("#", "{", "[")) or " #" in relative:
114
- continue
115
- path = PLATFORMS / platform / relative
116
- if not path.is_file():
117
- raise FileNotFoundError(f"declared platform action missing: {platform}/{name} -> {relative}")
118
- actions[name] = path
119
- if not actions:
120
- raise ValueError(f"{platform}/manifest.yaml declares no runnable actions")
121
- return actions
122
-
123
-
124
- def action_path(platform: str, action: str) -> Path:
125
- platform = platform.strip()
126
- action = action.strip().replace("_", "-")
127
- actions = declared_actions(platform)
128
- if action not in actions:
129
- raise FileNotFoundError(
130
- f"unknown platform action {platform}/{action}; "
131
- f"available: {', '.join(sorted(actions)) or 'none'}"
132
- )
133
- return actions[action]
134
-
135
-
136
- def prepare_action(path: Path, config_path: Path | None = None) -> str:
137
- source = path.read_text()
138
- source = "import sys\nsys.path.insert(0, " + repr(str(path.parent.resolve())) + ")\n" + source
139
- if "__CFG_PATH__" in source:
140
- if config_path is None:
141
- raise ValueError(f"{path.parent.parent.name}/{path.stem}: --config is required")
142
- escaped = str(config_path.resolve()).replace("\\", "\\\\").replace('"', '\\"')
143
- source = source.replace("__CFG_PATH__", escaped)
144
- return source
145
-
146
-
147
- def run_platform_action(
148
- platform: str,
149
- action: str,
150
- *,
151
- config_path: str | None = None,
152
- url: str | None = None,
153
- session_id: str | None = None,
154
- auto_session: bool = False,
155
- ) -> dict:
156
- path = action_path(platform, action)
157
- config_file = Path(config_path) if config_path else None
158
- if config_file is not None:
159
- config = json.loads(config_file.read_text())
160
- if not isinstance(config, dict):
161
- raise ValueError("platform config must be a JSON object")
162
- validate_platform_config(platform, action, config)
163
- code = prepare_action(path, config_file)
164
-
165
- if not session_id and not auto_session:
166
- raise ValueError(
167
- "platform run requires session_id; orchestrators should use: "
168
- "session start -> platform run --session-id SESSION_ID -> self-heal in the same session -> session stop. "
169
- "Use auto_session=True only for a one-shot run without orchestrator self-heal."
170
- )
171
- if session_id and auto_session:
172
- raise ValueError("session_id and auto_session are mutually exclusive")
173
-
174
- owned_session = bool(auto_session)
175
- started = request({"op": "start", "workspace": None, "url": url}) if owned_session else {"session_id": session_id, "workspace_supported": None}
176
- sid = started["session_id"]
177
- result = None
178
- cleanup = None
179
- try:
180
- raw_result = request({"op": "exec", "session_id": sid, "code": code})
181
- result = {k: v for k, v in raw_result.items() if k not in {"session_id", "workspace", "target_id"}}
182
- output = {
183
- "platform": platform,
184
- "action": action,
185
- "workspace_supported": started.get("workspace_supported"),
186
- "session_mode": "auto_session_no_self_heal" if owned_session else "caller_owned_self_heal_capable",
187
- "self_heal_capable": not owned_session,
188
- "result": result,
189
- }
190
- if started.get("warning"):
191
- output["warning"] = started["warning"]
192
- output["extension_url"] = started.get("extension_url")
193
- return output
194
- finally:
195
- if owned_session:
196
- try:
197
- raw_cleanup = request({"op": "stop", "session_id": sid})
198
- cleanup = {k: v for k, v in raw_cleanup.items() if k not in {"session_id", "workspace", "target_id"}}
199
- except Exception as exc:
200
- cleanup = {"release_error": f"{type(exc).__name__}: {exc}"}
201
- else:
202
- cleanup = {"caller_owned_session": True}
203
- if result is not None:
204
- result["cleanup"] = cleanup
@@ -1,60 +0,0 @@
1
- # ChatGPT browser platform
2
-
3
- Canonical reusable browser mechanics for ChatGPT.
4
-
5
- This platform owns ChatGPT-specific page behavior. Callers own lifecycle:
6
-
7
- - Agents Relay: disposable worker lifecycle and event/barrier ownership.
8
- - NeoY Tutor Workspace: persistent learner/thread lifecycle.
9
-
10
- Use Browser Workspace as the only browser-control engine. Do not duplicate ChatGPT
11
- selectors or submit/result-completion logic in callers.
12
-
13
- `temporary-submit` owns the reusable Temporary Chat page mechanics and verified submit receipt. Callers that need delayed cleanup should render the action source into a caller-owned Browser Workspace session and stop that session at their lifecycle barrier.
14
-
15
- ## Web ChatGPT MCP apps
16
-
17
- Use the `mcp-app` action for repeatable inspection of an installed custom MCP app and for a safe tool refresh. It deliberately uses semantic text/role discovery rather than transient snapshot refs such as `e31`.
18
-
19
- ```bash
20
- cat > /tmp/chatgpt-mcp-app.json <<'JSON'
21
- {"app_name":"neo","operation":"status"}
22
- JSON
23
- browser-workspace platform run chatgpt mcp-app --config /tmp/chatgpt-mcp-app.json
24
- ```
25
-
26
- A connected app is verified by its detail page exposing `Permissions`, `Disconnect`, and `Refresh tools`. The action returns these as structured booleans.
27
-
28
- ### Configure a custom MCP app
29
-
30
- The proven Web ChatGPT flow is:
31
-
32
- 1. Start/reuse a Browser Workspace session on `https://chatgpt.com` and take a fresh `snapshot()` before interacting. Never reuse `eNN` refs from an earlier snapshot.
33
- 2. Open the visible profile menu by semantic label `Open profile menu`, choose `Settings`, then choose `Plugins`. The durable settings route observed in October 2026 is `/settings/plugins-settings`, but prefer semantic navigation because routes can drift.
34
- 3. From Plugins, use the create/add-custom-app control. Enter the app display name and its public HTTPS MCP endpoint. Keep endpoint/client/OAuth values in caller configuration; never commit credentials to this platform.
35
- 4. Ask ChatGPT to scan/discover the MCP tools. If OAuth is advertised by the MCP server, continue the OAuth authorization flow and return to ChatGPT.
36
- 5. Finish creating/installing the app, then reopen Settings -> Plugins and select the app by exact display name.
37
- 6. Verify the detail page exposes `Permissions`, `Disconnect`, and `Refresh tools`. `Disconnect` is the positive signal that the account/OAuth connection is established. Run `mcp-app` with `operation=status` to capture this verification programmatically.
38
- 7. After an MCP server descriptor/tool change, use `operation=refresh-tools`, then inspect status again before testing tool calls.
39
-
40
- ### Snapshot-first fallback for UI drift
41
-
42
- If a semantic selector no longer resolves, do not guess coordinates or hard-code old refs. In the same Browser Workspace session:
43
-
44
- ```python
45
- print(snapshot(interactive_only=False))
46
- ```
47
-
48
- Use the fresh accessibility/DOM output to rediscover the visible control by role, accessible label, or stable text. When a menu is lazy-loaded, trigger the visible semantic control, wait for hydration, then snapshot again. This is how the profile menu -> Settings -> Plugins path was recovered during the live MCP setup.
49
-
50
- Destructive controls (`Disconnect`, `Uninstall`, `Delete app`) are intentionally not exposed as `mcp-app` operations.
51
-
52
-
53
- ## XChat lifecycle control transfer
54
-
55
- Browser Workspace owns the ChatGPT UI mechanics for orchestrator lifecycle handoff.
56
-
57
- - `new-turn`: given stable `project_id`, `thread_id`, and `message`, wait until the current assistant turn is no longer generating, submit the continuation as the next user turn in the same thread, verify acceptance, and return without waiting for the new assistant answer.
58
- - `new-thread`: given stable `project_id` and `message` (plus optional `source_thread_id` provenance), open the same Project, submit the continuation to create a fresh thread, and return the durable new `thread_id` once ChatGPT replaces any temporary local id.
59
-
60
- These actions are control-transfer primitives. The generic Browser Workspace runner owns session start/stop and cleanup. Product code should pass stable Project/Thread IDs rather than persist ChatGPT URLs or DOM identities.
@@ -1,58 +0,0 @@
1
- import json
2
- import time
3
- from urllib.parse import urlsplit
4
-
5
- CFG=json.load(open("__CFG_PATH__",encoding="utf-8"))
6
-
7
- def wait(pred,label,timeout=30):
8
- deadline=time.time()+timeout
9
- while time.time()<deadline:
10
- value=pred()
11
- if value:return value
12
- time.sleep(.2)
13
- raise RuntimeError(f"{label} not observed")
14
-
15
- def composer():
16
- return js("""(() => {
17
- const sels=['#prompt-textarea','[contenteditable="true"][data-composer-markdown]','[contenteditable="true"][data-lexical-editor="true"]','textarea'];
18
- for(const s of sels){const e=document.querySelector(s);if(!e)continue;const r=e.getBoundingClientRect();if(r.width&&r.height)return s;}
19
- return null;
20
- })()""")
21
-
22
- def set_value(selector,text):
23
- ok=js(f"""(() => {{
24
- const e=document.querySelector({json.dumps(selector)});
25
- if(!e)return false;e.focus();
26
- if(e instanceof HTMLTextAreaElement || e instanceof HTMLInputElement){{
27
- const p=e instanceof HTMLTextAreaElement?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;
28
- Object.getOwnPropertyDescriptor(p,'value')?.set?.call(e,{json.dumps(text)});
29
- }}else{{
30
- const sel=window.getSelection(),r=document.createRange();r.selectNodeContents(e);sel.removeAllRanges();sel.addRange(r);
31
- document.execCommand('delete',false,null);document.execCommand('insertText',false,{json.dumps(text)});sel.removeAllRanges();
32
- }}
33
- e.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:{json.dumps(text)}}}));
34
- return true;
35
- }})()""")
36
- if not ok: raise RuntimeError("bootstrap composer could not be filled")
37
-
38
- target_id=new_tab("https://chatgpt.com/")
39
- wait_for_load()
40
- selector=wait(composer,"ChatGPT bootstrap composer",45)
41
- set_value(selector,CFG["instructions"])
42
- button=wait(lambda: js("""(() => {
43
- const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"],button[aria-label="Send"],form button[type="submit"]');
44
- if(!b||b.disabled||b.getAttribute('aria-disabled')==='true')return null;
45
- b.setAttribute('data-neoy-bootstrap-send','1');return '[data-neoy-bootstrap-send="1"]';
46
- })()"""),"ChatGPT bootstrap send button",20)
47
- if not js("""(() => {const b=document.querySelector('[data-neoy-bootstrap-send="1"]');if(!b)return false;b.click();return true;})()"""):
48
- raise RuntimeError("bootstrap send failed")
49
- deadline=time.time()+90
50
- thread_url=None
51
- while time.time()<deadline:
52
- current=page_info().get("url","")
53
- if "/c/" in urlsplit(current).path and "local-chatgpt" not in current:
54
- thread_url=current.split("?",1)[0]
55
- break
56
- time.sleep(.25)
57
- if not thread_url: raise RuntimeError("bootstrap thread durable URL not observed")
58
- print(json.dumps({"status":"started","thread_url":thread_url,"target_id":target_id},ensure_ascii=False),flush=True)