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.
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/package.json +1 -1
- package/platform_runner.py +11 -2
- package/platforms/chatgpt/actions/_lifecycle_common.py +73 -20
- package/platforms/chatgpt/actions/_new_turn.py +1 -0
- package/platforms/chatgpt/actions/_readiness.py +29 -3
- package/platforms/chatgpt/actions/_submit_thread.py +20 -3
- package/platforms/chatgpt/actions/_thread_turn.py +31 -2
- package/platforms/chatgpt/tests/test_contract.py +107 -1
- package/platforms/iyf/AIRPLAY.md +25 -0
- package/platforms/iyf/actions/_find.py +18 -0
- package/platforms/iyf/actions/_open.py +10 -0
- package/platforms/iyf/manifest.yaml +11 -0
- package/platforms/iyf/safari-airplay +57 -0
- package/pyproject.toml +2 -2
- package/session_client.py +10 -0
- package/src/browser_workspace.egg-info/PKG-INFO +2 -2
- package/src/browser_workspace.egg-info/SOURCES.txt +53 -46
- package/src/browser_workspace.egg-info/requires.txt +1 -1
- package/src/browser_workspace_app/package.json +1 -1
- package/src/browser_workspace_app/pyproject.toml +2 -2
- package/src/browser_workspace_app/session_client.py +10 -0
- package/src/browser_workspace_app/platform_runner.py +0 -204
- package/src/browser_workspace_app/platforms/chatgpt/README.md +0 -60
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +0 -58
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +0 -119
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +0 -80
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +0 -85
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +0 -33
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +0 -353
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +0 -52
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +0 -129
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +0 -591
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +0 -362
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +0 -34
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +0 -183
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +0 -49
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +0 -425
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +0 -7
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +0 -10
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +0 -10
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +0 -18
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +0 -11
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +0 -9
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +0 -33
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +0 -83
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +0 -32
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +0 -210
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +0 -21
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +0 -25
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +0 -24
- package/src/browser_workspace_app/platforms/tiktok/README.md +0 -10
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +0 -180
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +0 -99
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +0 -10
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +0 -163
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +0 -130
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +0 -231
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +0 -124
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +0 -36
- package/src/browser_workspace_app/platforms/xhs/README.md +0 -10
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +0 -355
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +0 -281
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +0 -107
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +0 -34
- package/src/browser_workspace_app/platforms/youtube/README.md +0 -10
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +0 -103
- 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.
|
|
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
|
|
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
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "browser-workspace"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.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
|
|
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)
|