browser-workspace 0.4.1 → 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.
- package/README.md +16 -2
- package/SKILL.md +60 -37
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/bin/browser-workspace +174 -13
- package/package.json +1 -1
- package/platform_runner.py +23 -6
- package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
- package/platforms/chatgpt/tests/test_contract.py +7 -0
- package/platforms/wechat-channels/actions/_common.py +1 -1
- package/platforms/wechat-channels/actions/_post.py +37 -38
- package/platforms/wechat-channels/manifest.yaml +19 -13
- package/platforms/xhs/actions/_post.py +109 -20
- package/platforms/xhs/manifest.yaml +14 -5
- package/platforms/xhs/tests/test_contract.py +17 -0
- package/platforms/youtube/actions/_post.py +7 -4
- package/pyproject.toml +6 -2
- package/session_client.py +21 -11
- package/session_daemon.py +115 -9
- package/src/browser_workspace.egg-info/PKG-INFO +2 -1
- package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
- package/src/browser_workspace.egg-info/entry_points.txt +2 -0
- package/src/browser_workspace.egg-info/requires.txt +1 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -1
- package/src/browser_workspace_app/__init__.py +1 -0
- package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
- package/src/browser_workspace_app/cli.py +236 -0
- package/src/browser_workspace_app/extension/manifest.json +26 -0
- package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
- package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
- package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
- package/src/browser_workspace_app/extension/workspace.html +8 -0
- package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
- package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
- package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
- package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
- package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
- package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
- package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
- package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
- package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
- package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
- package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
- package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
- package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
- package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
- package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
- package/src/browser_workspace_app/managed_chrome.py +244 -0
- package/src/browser_workspace_app/node/admin.mjs +44 -0
- package/src/browser_workspace_app/package.json +36 -0
- package/src/browser_workspace_app/platform_runner.py +204 -0
- package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
- package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
- package/src/browser_workspace_app/pyproject.toml +33 -0
- package/src/browser_workspace_app/session_client.py +107 -0
- package/src/browser_workspace_app/session_daemon.py +431 -0
- package/src/browser_harness/__init__.py +0 -2
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +0 -201
- package/src/browser_harness/admin.py +0 -1588
- package/src/browser_harness/auth.py +0 -546
- package/src/browser_harness/daemon.py +0 -894
- package/src/browser_harness/helpers.py +0 -668
- package/src/browser_harness/macos.py +0 -131
- package/src/browser_harness/mcp_cli.py +0 -16
- package/src/browser_harness/paths.py +0 -49
- package/src/browser_harness/recorder.py +0 -343
- package/src/browser_harness/run.py +0 -413
- package/src/browser_harness/telemetry.py +0 -308
- package/src/browser_harness/video-template.html +0 -970
- package/src/browser_harness/video.py +0 -749
- package/src/browser_harness/video_render.py +0 -524
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Chrome Web Store browser mechanics.
|
|
2
|
+
|
|
3
|
+
This action contains no credentials and defaults to read-only/dry-run behavior.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
import zipfile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CFG = json.load(open("__CFG_PATH__", encoding="utf-8"))
|
|
13
|
+
ACTION = CFG["action"]
|
|
14
|
+
ITEM_ID = CFG.get("item_id", "").strip()
|
|
15
|
+
PRODUCT_NAME = CFG.get("product_name", "").strip()
|
|
16
|
+
EXPECTED_VERSION = CFG.get("expected_version", "").strip()
|
|
17
|
+
PUBLIC_LISTING_URL = CFG.get("public_listing_url", "").strip()
|
|
18
|
+
LISTING_URL = CFG.get("listing_url", "").strip()
|
|
19
|
+
COMMIT = bool(CFG.get("commit", False))
|
|
20
|
+
ALLOW_EXTERNAL_SUBMIT = bool(CFG.get("allow_external_submit", False))
|
|
21
|
+
|
|
22
|
+
CONSOLE_URL = "https://chromewebstore.google.com/devconsole"
|
|
23
|
+
ALLOWED_ACTIONS = {
|
|
24
|
+
"open-item",
|
|
25
|
+
"resolve-item",
|
|
26
|
+
"create-item",
|
|
27
|
+
"verify-draft",
|
|
28
|
+
"save-draft",
|
|
29
|
+
"upload-package",
|
|
30
|
+
"update-listing",
|
|
31
|
+
"submit-review",
|
|
32
|
+
"check-status",
|
|
33
|
+
"verify-published",
|
|
34
|
+
}
|
|
35
|
+
STATES = {"draft", "submitted", "pending", "rejected", "published", "unknown"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fail(message):
|
|
39
|
+
print(json.dumps({"action": ACTION, "status": "unknown", "error": message}))
|
|
40
|
+
raise SystemExit(2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def package_version(path):
|
|
44
|
+
try:
|
|
45
|
+
with zipfile.ZipFile(path) as package:
|
|
46
|
+
manifest = json.loads(package.read("manifest.json"))
|
|
47
|
+
except (OSError, KeyError, ValueError, zipfile.BadZipFile) as exc:
|
|
48
|
+
fail(f"invalid extension package: {exc}")
|
|
49
|
+
version = str(manifest.get("version", "")).strip()
|
|
50
|
+
if not version:
|
|
51
|
+
fail("package manifest is missing version")
|
|
52
|
+
return version
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_package():
|
|
56
|
+
package = CFG.get("package_path", "").strip()
|
|
57
|
+
if not package:
|
|
58
|
+
fail("package_path is required")
|
|
59
|
+
if not EXPECTED_VERSION:
|
|
60
|
+
fail("expected_version is required")
|
|
61
|
+
observed = package_version(package)
|
|
62
|
+
if observed != EXPECTED_VERSION:
|
|
63
|
+
fail(f"package version {observed} does not match expected_version {EXPECTED_VERSION}")
|
|
64
|
+
return observed
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def evidence(status, **extra):
|
|
68
|
+
if status not in STATES:
|
|
69
|
+
fail(f"invalid evidence status: {status}")
|
|
70
|
+
record = {
|
|
71
|
+
"action": ACTION,
|
|
72
|
+
"status": status,
|
|
73
|
+
"item_id": ITEM_ID or None,
|
|
74
|
+
"url": page_info().get("url"),
|
|
75
|
+
**extra,
|
|
76
|
+
}
|
|
77
|
+
print(json.dumps(record, ensure_ascii=False))
|
|
78
|
+
return record
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def text_exists(needles):
|
|
82
|
+
values = json.dumps([n.lower() for n in needles])
|
|
83
|
+
return js(
|
|
84
|
+
"""(() => {
|
|
85
|
+
const text = (document.body?.innerText || '').toLowerCase();
|
|
86
|
+
return %s.some(value => text.includes(value));
|
|
87
|
+
})()""" % values
|
|
88
|
+
) is True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def visible_version():
|
|
92
|
+
value = js("""(() => document.body?.innerText || '')()""") or ""
|
|
93
|
+
versions = re.findall(r"\b\d+\.\d+(?:\.\d+){1,2}\b", value)
|
|
94
|
+
return EXPECTED_VERSION if EXPECTED_VERSION in versions else (versions[0] if versions else None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def click_text(label):
|
|
98
|
+
wanted = label.lower()
|
|
99
|
+
return js(
|
|
100
|
+
"""(() => {
|
|
101
|
+
const nodes = [...document.querySelectorAll('button,[role=button],a')];
|
|
102
|
+
const node = nodes.find(el => {
|
|
103
|
+
const text = (el.innerText || el.getAttribute('aria-label') || '').trim().toLowerCase();
|
|
104
|
+
return text === %r || text.includes(%r);
|
|
105
|
+
});
|
|
106
|
+
if (!node) return false;
|
|
107
|
+
node.click();
|
|
108
|
+
return true;
|
|
109
|
+
})()""" % (wanted, wanted)
|
|
110
|
+
) is True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def field_by_label(keys):
|
|
114
|
+
"""Find a form control by its accessible/visible label, not generated IDs."""
|
|
115
|
+
return js(
|
|
116
|
+
"""(() => {
|
|
117
|
+
const keys = %s;
|
|
118
|
+
const controls = [...document.querySelectorAll('input,textarea,select,[role=combobox],[contenteditable=true]')];
|
|
119
|
+
const labelText = (node) => {
|
|
120
|
+
const ids = (node.getAttribute('aria-labelledby') || '').split(/\\s+/).filter(Boolean);
|
|
121
|
+
const labelled = ids.map(id => document.getElementById(id)?.innerText || '').join(' ');
|
|
122
|
+
const explicit = node.id ? document.querySelector(`label[for="${CSS.escape(node.id)}"]`)?.innerText || '' : '';
|
|
123
|
+
const parent = node.closest('label')?.innerText || node.parentElement?.innerText || '';
|
|
124
|
+
return [node.getAttribute('aria-label'), node.getAttribute('placeholder'), node.name, node.id,
|
|
125
|
+
labelled, explicit, parent].filter(Boolean).join(' ').toLowerCase();
|
|
126
|
+
};
|
|
127
|
+
const node = controls.find(node => keys.some(key => labelText(node).includes(key)));
|
|
128
|
+
return node?.id ? `#${CSS.escape(node.id)}` : null;
|
|
129
|
+
})()""" % json.dumps([key.lower() for key in keys])
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def require_publisher_login():
|
|
134
|
+
url = page_info().get("url", "")
|
|
135
|
+
if "accounts.google.com" in url or text_exists(["sign in", "log in"]):
|
|
136
|
+
fail("authenticated Chrome Web Store publisher login required")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def publisher_id_from_url(url):
|
|
140
|
+
match = re.search(
|
|
141
|
+
r"https://(?:chrome\.google\.com/webstore/devconsole|chromewebstore\.google\.com/devconsole)/([^/]+)(?:/|$)",
|
|
142
|
+
url,
|
|
143
|
+
)
|
|
144
|
+
return match.group(1) if match else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def open_item():
|
|
148
|
+
if ACTION == "verify-published":
|
|
149
|
+
url = PUBLIC_LISTING_URL or ("https://chromewebstore.google.com/detail/" + ITEM_ID)
|
|
150
|
+
else:
|
|
151
|
+
url = LISTING_URL
|
|
152
|
+
if not url:
|
|
153
|
+
publisher_id = publisher_id_from_url(page_info().get("url", ""))
|
|
154
|
+
if not publisher_id:
|
|
155
|
+
# The canonical console URL redirects to the publisher-scoped
|
|
156
|
+
# dashboard. Resolve that scope before constructing an item
|
|
157
|
+
# route; the generic /store-item path redirects to the public
|
|
158
|
+
# store and cannot prove the requested item was opened.
|
|
159
|
+
goto_url(CONSOLE_URL)
|
|
160
|
+
wait_for_load()
|
|
161
|
+
require_publisher_login()
|
|
162
|
+
publisher_id = publisher_id_from_url(page_info().get("url", ""))
|
|
163
|
+
if not publisher_id:
|
|
164
|
+
fail("publisher-scoped developer console route not observed")
|
|
165
|
+
url = f"https://chrome.google.com/webstore/devconsole/{publisher_id}/{ITEM_ID}/edit"
|
|
166
|
+
# Browser automation can briefly expose more than one target for a
|
|
167
|
+
# requested URL. Reuse the harness-verified real tab instead of asking the
|
|
168
|
+
# browser controller to create another target; this keeps navigation stable
|
|
169
|
+
# without changing unrelated browser state.
|
|
170
|
+
if ensure_real_tab():
|
|
171
|
+
goto_url(url)
|
|
172
|
+
else:
|
|
173
|
+
# A fresh isolated browser may not have a usable page yet.
|
|
174
|
+
new_tab(url)
|
|
175
|
+
wait_for_load()
|
|
176
|
+
require_publisher_login()
|
|
177
|
+
if ITEM_ID and ITEM_ID not in page_info().get("url", ""):
|
|
178
|
+
fail("observed URL does not contain the requested item id")
|
|
179
|
+
return evidence("unknown", observed_item_id=ITEM_ID or None)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def dashboard_candidates():
|
|
183
|
+
return js("""(() => [...document.querySelectorAll('a[href]')].map(node => ({href: node.href, text: (node.innerText || node.getAttribute('aria-label') || '').trim()})).filter(item => /\\/devconsole\\/[^/]+\\/[a-z]{32}/.test(item.href)))()""") or []
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def item_id_from_url(url):
|
|
187
|
+
match = re.search(r"/devconsole/[^/]+/([a-z]{32})/(?:edit|details)?(?:[/?#]|$)", url)
|
|
188
|
+
return match.group(1) if match else None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def dashboard_item_version(item_id):
|
|
192
|
+
for candidate in dashboard_candidates():
|
|
193
|
+
if item_id in candidate.get("href", ""):
|
|
194
|
+
versions = re.findall(r"\b\d+\.\d+(?:\.\d+){1,2}\b", candidate.get("text", ""))
|
|
195
|
+
return versions[0] if versions else None
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def open_dashboard():
|
|
200
|
+
goto_url(CONSOLE_URL)
|
|
201
|
+
wait_for_load()
|
|
202
|
+
require_publisher_login()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def create_or_resolve_item():
|
|
206
|
+
global ITEM_ID
|
|
207
|
+
if ITEM_ID:
|
|
208
|
+
open_item()
|
|
209
|
+
return evidence("draft", observed_item_id=ITEM_ID, reused_item=True)
|
|
210
|
+
if not PRODUCT_NAME:
|
|
211
|
+
fail("product_name is required when item_id is not supplied")
|
|
212
|
+
open_dashboard()
|
|
213
|
+
wanted = PRODUCT_NAME.lower()
|
|
214
|
+
for candidate in dashboard_candidates():
|
|
215
|
+
if wanted in candidate.get("text", "").lower():
|
|
216
|
+
ITEM_ID = item_id_from_url(candidate.get("href", ""))
|
|
217
|
+
if ITEM_ID:
|
|
218
|
+
return evidence("draft", observed_item_id=ITEM_ID, reused_item=True, listing_url=candidate.get("href"))
|
|
219
|
+
if not COMMIT:
|
|
220
|
+
return evidence("draft", dry_run=True, would_create=True, product_name=PRODUCT_NAME)
|
|
221
|
+
create_item()
|
|
222
|
+
ITEM_ID = item_id_from_url(page_info().get("url", ""))
|
|
223
|
+
return evidence("draft", created_item=True, observed_item_id=ITEM_ID, package_version=EXPECTED_VERSION,
|
|
224
|
+
browser_version=visible_version(), version_verification="reopen-required")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def verify_draft():
|
|
228
|
+
validate_package()
|
|
229
|
+
open_item()
|
|
230
|
+
if not text_exists(["status: draft", "draft"]):
|
|
231
|
+
fail("draft status not observed after reopening exact item")
|
|
232
|
+
open_dashboard()
|
|
233
|
+
observed = dashboard_item_version(ITEM_ID)
|
|
234
|
+
if observed != EXPECTED_VERSION:
|
|
235
|
+
fail(f"reopened draft version {observed or 'unknown'} does not match expected_version {EXPECTED_VERSION}")
|
|
236
|
+
return evidence("draft", observed_item_id=ITEM_ID, observed_version=observed, exact_version_verified=True, reopened=True)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def save_draft():
|
|
240
|
+
open_item()
|
|
241
|
+
if not click_text("Save draft") and not click_text("Save"):
|
|
242
|
+
fail("save draft control not observed")
|
|
243
|
+
return evidence("draft", saved=True, observed_item_id=ITEM_ID)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def create_item():
|
|
247
|
+
observed_version = validate_package()
|
|
248
|
+
if not COMMIT:
|
|
249
|
+
return evidence("draft", dry_run=True, would_create=True,
|
|
250
|
+
package_path=CFG["package_path"], package_version=observed_version)
|
|
251
|
+
goto_url(CONSOLE_URL)
|
|
252
|
+
wait_for_load()
|
|
253
|
+
require_publisher_login()
|
|
254
|
+
if not publisher_id_from_url(page_info().get("url", "")):
|
|
255
|
+
fail("publisher-scoped developer console route not observed")
|
|
256
|
+
if not click_text("New item") and not click_text("Add a new item"):
|
|
257
|
+
fail("new item control not observed")
|
|
258
|
+
upload_file("input[type=file]", CFG["package_path"].strip())
|
|
259
|
+
wait_for_load()
|
|
260
|
+
require_publisher_login()
|
|
261
|
+
if text_exists(["key field is not allowed"]):
|
|
262
|
+
fail("new item upload rejected: manifest key field is not allowed")
|
|
263
|
+
if text_exists(["there was a problem uploading your file"]):
|
|
264
|
+
fail("new item upload rejected by Chrome Web Store")
|
|
265
|
+
url = page_info().get("url", "")
|
|
266
|
+
match = re.search(r"/devconsole/[^/]+/([^/]+)/edit(?:$|[?#])", url)
|
|
267
|
+
if not match:
|
|
268
|
+
fail("created item editor route not observed")
|
|
269
|
+
return evidence("draft", created_item_id=match.group(1),
|
|
270
|
+
package_version=observed_version, browser_version=visible_version())
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def upload_package():
|
|
274
|
+
package = CFG["package_path"].strip()
|
|
275
|
+
observed_version = validate_package()
|
|
276
|
+
if not COMMIT:
|
|
277
|
+
return evidence("draft", dry_run=True, would_upload=package, package_version=observed_version)
|
|
278
|
+
if not click_text("Package") and not click_text("Upload new package"):
|
|
279
|
+
fail("package upload control not observed")
|
|
280
|
+
upload_file("input[type=file]", package)
|
|
281
|
+
time.sleep(2)
|
|
282
|
+
browser_version = visible_version()
|
|
283
|
+
if browser_version != observed_version:
|
|
284
|
+
fail(f"uploaded package version {browser_version or 'unknown'} does not match expected_version {observed_version}")
|
|
285
|
+
return evidence("draft", uploaded_package=package, package_version=observed_version,
|
|
286
|
+
browser_version=browser_version)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def update_listing():
|
|
290
|
+
market = CFG.get("market_package") or {}
|
|
291
|
+
required = ["title", "short_description", "long_description", "support_url"]
|
|
292
|
+
missing = [key for key in required if not str(market.get(key, "")).strip()]
|
|
293
|
+
if missing:
|
|
294
|
+
fail("market_package missing: " + ", ".join(missing))
|
|
295
|
+
if not COMMIT:
|
|
296
|
+
return evidence("draft", dry_run=True, fields=sorted(market))
|
|
297
|
+
# Chrome's editor is a dynamic form. Use labels/aria names rather than
|
|
298
|
+
# brittle generated class names; the caller still receives observed evidence.
|
|
299
|
+
mapped = {key: market[key] for key in market if market[key] not in (None, "", [], {})}
|
|
300
|
+
selectors = {
|
|
301
|
+
"short_description": ["short description", "summary"],
|
|
302
|
+
"long_description": ["long description", "description"],
|
|
303
|
+
"release_notes": ["release notes", "what's new"],
|
|
304
|
+
"reviewer_test_instructions": ["reviewer", "test instructions"],
|
|
305
|
+
"support_url": ["support url", "support website"],
|
|
306
|
+
"privacy_policy_url": ["privacy policy", "privacy url"],
|
|
307
|
+
"privacy_practices": ["privacy practices", "privacy"],
|
|
308
|
+
"data_use": ["data use", "data usage"],
|
|
309
|
+
"distribution": ["distribution", "visibility"],
|
|
310
|
+
"payment": ["payment", "paid"],
|
|
311
|
+
"permissions": ["permissions", "permission justification"],
|
|
312
|
+
"host_permissions": ["host permissions", "host permission"],
|
|
313
|
+
"host_permission_justifications": ["host permission justification", "host permissions"],
|
|
314
|
+
"title": ["title", "name"],
|
|
315
|
+
}
|
|
316
|
+
for label, value in mapped.items():
|
|
317
|
+
if label in {"screenshots", "posters", "feature_graphic", "promotional_graphic", "demo_video"}:
|
|
318
|
+
continue
|
|
319
|
+
keys = selectors.get(label, [label.replace("_", " ")])
|
|
320
|
+
node = field_by_label(keys)
|
|
321
|
+
if not node and label in {"title", "short_description"}:
|
|
322
|
+
# Chrome may expose package-owned title/summary as read-only text.
|
|
323
|
+
# The caller supplied the factual value; verify other editable fields
|
|
324
|
+
# without trying to mutate package metadata through a nonexistent control.
|
|
325
|
+
continue
|
|
326
|
+
if not node:
|
|
327
|
+
fail(f"listing field not observed: {label}")
|
|
328
|
+
changed = js(
|
|
329
|
+
"""(() => {
|
|
330
|
+
const node = document.querySelector(%s);
|
|
331
|
+
if (!node) return false;
|
|
332
|
+
node.focus();
|
|
333
|
+
if (node.type === 'checkbox' || node.type === 'radio') node.checked = Boolean(%s);
|
|
334
|
+
else if (node.tagName === 'SELECT') {
|
|
335
|
+
const wanted = String(%s).toLowerCase();
|
|
336
|
+
const option = [...node.options].find(item => (item.text || item.value).toLowerCase() === wanted);
|
|
337
|
+
if (!option) return false;
|
|
338
|
+
node.value = option.value;
|
|
339
|
+
} else if ('value' in node) node.value = %s;
|
|
340
|
+
else node.textContent = %s;
|
|
341
|
+
node.dispatchEvent(new Event('input', {bubbles: true}));
|
|
342
|
+
node.dispatchEvent(new Event('change', {bubbles: true}));
|
|
343
|
+
return true;
|
|
344
|
+
})()""" % (json.dumps(str(node)), json.dumps(bool(value)), json.dumps(str(value)),
|
|
345
|
+
json.dumps(str(value)), json.dumps(str(value)))
|
|
346
|
+
)
|
|
347
|
+
if changed is not True:
|
|
348
|
+
fail(f"listing field not observed: {label}")
|
|
349
|
+
for field in ("screenshots", "posters", "feature_graphic", "promotional_graphic", "demo_video"):
|
|
350
|
+
for media in (mapped.get(field) if isinstance(mapped.get(field), list) else [mapped.get(field)]):
|
|
351
|
+
if media:
|
|
352
|
+
try:
|
|
353
|
+
upload_file("input[type=file]", str(media))
|
|
354
|
+
except (OSError, RuntimeError) as exc:
|
|
355
|
+
fail(f"listing media upload failed: {field}")
|
|
356
|
+
if not click_text("Save draft") and not click_text("Save"):
|
|
357
|
+
fail("save control not observed")
|
|
358
|
+
return evidence("draft", fields=sorted(mapped))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def submit_review():
|
|
362
|
+
if not COMMIT or not ALLOW_EXTERNAL_SUBMIT:
|
|
363
|
+
return evidence("draft", blocked="explicit submit authorization required")
|
|
364
|
+
if not click_text("Submit for review"):
|
|
365
|
+
fail("submit-for-review control not observed")
|
|
366
|
+
time.sleep(2)
|
|
367
|
+
return evidence("submitted")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def check_status():
|
|
371
|
+
if text_exists(["rejected", "needs changes"]):
|
|
372
|
+
return evidence("rejected", observed_version=visible_version())
|
|
373
|
+
if text_exists(["published", "public"]):
|
|
374
|
+
return evidence("published", observed_version=visible_version())
|
|
375
|
+
if text_exists(["submitted", "under review", "pending"]):
|
|
376
|
+
return evidence("pending", observed_version=visible_version())
|
|
377
|
+
return evidence("unknown", observed_version=visible_version())
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def verify_published():
|
|
381
|
+
url = page_info().get("url", "")
|
|
382
|
+
if not url.startswith("https://chromewebstore.google.com/detail/"):
|
|
383
|
+
return evidence("unknown", reason="public listing URL not observed")
|
|
384
|
+
if ITEM_ID and ITEM_ID not in url:
|
|
385
|
+
return evidence("unknown", reason="public URL does not match item id")
|
|
386
|
+
observed = visible_version()
|
|
387
|
+
if EXPECTED_VERSION and observed != EXPECTED_VERSION:
|
|
388
|
+
return evidence("unknown", reason="expected version not observed")
|
|
389
|
+
return evidence("published", expected_version=EXPECTED_VERSION or None, observed_version=observed)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if ACTION not in ALLOWED_ACTIONS:
|
|
393
|
+
fail("unsupported action: " + ACTION)
|
|
394
|
+
if ACTION in {"resolve-item", "create-item"}:
|
|
395
|
+
if ACTION == "create-item" and not (PRODUCT_NAME or ITEM_ID):
|
|
396
|
+
fail("product_name is required for create-item")
|
|
397
|
+
validate_package()
|
|
398
|
+
create_or_resolve_item()
|
|
399
|
+
raise SystemExit(0)
|
|
400
|
+
if ACTION in {"verify-draft", "save-draft", "upload-package", "update-listing", "submit-review", "check-status", "verify-published"} and not ITEM_ID:
|
|
401
|
+
fail("item_id is required for this action")
|
|
402
|
+
if ACTION == "open-item" and not (ITEM_ID or LISTING_URL):
|
|
403
|
+
fail("item_id or listing_url is required for open-item")
|
|
404
|
+
if ACTION in {"create-item", "upload-package"}:
|
|
405
|
+
validate_package()
|
|
406
|
+
|
|
407
|
+
if ACTION == "verify-draft":
|
|
408
|
+
verify_draft()
|
|
409
|
+
raise SystemExit(0)
|
|
410
|
+
if ACTION == "save-draft":
|
|
411
|
+
save_draft()
|
|
412
|
+
raise SystemExit(0)
|
|
413
|
+
if ACTION == "upload-package":
|
|
414
|
+
open_item()
|
|
415
|
+
upload_package()
|
|
416
|
+
else:
|
|
417
|
+
open_item()
|
|
418
|
+
if ACTION == "update-listing":
|
|
419
|
+
update_listing()
|
|
420
|
+
elif ACTION == "submit-review":
|
|
421
|
+
submit_review()
|
|
422
|
+
elif ACTION == "check-status":
|
|
423
|
+
check_status()
|
|
424
|
+
elif ACTION == "verify-published":
|
|
425
|
+
verify_published()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Check status
|
|
2
|
+
|
|
3
|
+
Status: `working` — platform action implemented; live account verification pending.
|
|
4
|
+
|
|
5
|
+
Open the explicitly selected item and read the visible package/review status.
|
|
6
|
+
Return the observed status and version, including any warning or rejection
|
|
7
|
+
message. This flow is read-only.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Open item
|
|
2
|
+
|
|
3
|
+
Status: `working` — platform action implemented; live account verification pending.
|
|
4
|
+
|
|
5
|
+
Preconditions: an authenticated Chrome Web Store publisher session and an
|
|
6
|
+
explicit item identifier or listing URL.
|
|
7
|
+
|
|
8
|
+
Use Browser Workspace to open the publisher console, navigate to the selected
|
|
9
|
+
item, and verify that the item title and identifier match the request. Do not
|
|
10
|
+
change listing state.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Submit review
|
|
2
|
+
|
|
3
|
+
Status: `working` — platform action implemented; live account verification pending.
|
|
4
|
+
|
|
5
|
+
Preconditions: the requested package and listing changes are complete, and the
|
|
6
|
+
caller explicitly authorizes submission.
|
|
7
|
+
|
|
8
|
+
Review the platform's final summary, confirm the selected item and version,
|
|
9
|
+
submit once with `Browser Workspace`, and verify the resulting review/pending
|
|
10
|
+
state. Never submit merely because a draft is valid.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Update listing
|
|
2
|
+
|
|
3
|
+
Status: `working` — platform action implemented; live account verification pending.
|
|
4
|
+
|
|
5
|
+
Preconditions: an explicit item and exact listing fields to change.
|
|
6
|
+
|
|
7
|
+
Open the item, edit only the approved Market Package fields with
|
|
8
|
+
`Browser Workspace`, and verify the saved values in the publisher console.
|
|
9
|
+
Supported field groups are:
|
|
10
|
+
|
|
11
|
+
- title, short/long description, release notes, public/support URLs, and
|
|
12
|
+
reviewer test instructions;
|
|
13
|
+
- screenshots, promotional/feature graphics (posters), and demo/preview video;
|
|
14
|
+
- privacy practices, data-use declarations, distribution/payment designation;
|
|
15
|
+
- permissions and host-permission justifications.
|
|
16
|
+
|
|
17
|
+
Listing metadata changes are external side effects and must not be inferred
|
|
18
|
+
from navigation success.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Upload package
|
|
2
|
+
|
|
3
|
+
Status: `working` — platform action implemented; live account verification pending.
|
|
4
|
+
|
|
5
|
+
Preconditions: an explicit item, a locally available extension package, and
|
|
6
|
+
confirmation that uploading it is intended.
|
|
7
|
+
|
|
8
|
+
Open the item with `open-item`, use the platform's package upload control via
|
|
9
|
+
`Browser Workspace`, and verify the displayed package/version against the
|
|
10
|
+
approved release candidate before leaving the draft state. Uploading changes
|
|
11
|
+
external state; do not submit for review here.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Verify published
|
|
2
|
+
|
|
3
|
+
Status: `working` — platform action implemented; live account verification pending.
|
|
4
|
+
|
|
5
|
+
After the publisher console reports publication, open the public listing with
|
|
6
|
+
`Browser Workspace` and verify the expected item identity, exact version, and
|
|
7
|
+
visible listing fields. Treat the public page as the source of observed
|
|
8
|
+
publication; do not infer it from a successful submit action. A submitted or
|
|
9
|
+
pending review is not published evidence.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
platform: chrome-web-store
|
|
2
|
+
status: partially_verified
|
|
3
|
+
last_verified: 2026-09-26
|
|
4
|
+
actions:
|
|
5
|
+
chrome-web-store: "actions/_chrome_web_store.py"
|
|
6
|
+
verification:
|
|
7
|
+
status: partially_verified
|
|
8
|
+
last_verified: 2026-09-26
|
|
9
|
+
evidence: "Live verification covers new-item draft creation/reopen plus Teammate and Family Tutor existing-item draft reopen/save regressions. Submit/review/publication lifecycle remains implemented but not live-verified."
|
|
10
|
+
url_patterns:
|
|
11
|
+
- "https://chromewebstore.google.com/devconsole/*"
|
|
12
|
+
- "https://chrome.google.com/webstore/developer/dashboard*"
|
|
13
|
+
authentication:
|
|
14
|
+
required: true
|
|
15
|
+
notes: "Use an already authenticated publisher browser context; never store credentials or browser state."
|
|
16
|
+
side_effects:
|
|
17
|
+
- "upload-package changes a draft/package upload"
|
|
18
|
+
- "update-listing edits listing metadata"
|
|
19
|
+
- "submit-review sends a listing for review"
|
|
20
|
+
provenance:
|
|
21
|
+
source: "Neo platform skeleton"
|
|
22
|
+
migrated_code: false
|
|
23
|
+
flows:
|
|
24
|
+
resolve-item: {status: verified, evidence: "Live runs resolved and reopened the intended Browser Workspace, Teammate, and Family Tutor developer items."}
|
|
25
|
+
create-item: {status: verified, evidence: "Browser Workspace was created as a new developer item from a store-compatible package, then reopened at exact version 0.2.3."}
|
|
26
|
+
verify-draft: {status: verified, evidence: "Draft/version was verified after reopen; Teammate and Family Tutor existing-item drafts were also reopened successfully."}
|
|
27
|
+
save-draft: {status: verified, evidence: "Draft save/reopen was exercised on Browser Workspace plus existing-item regressions for Teammate and Family Tutor."}
|
|
28
|
+
open-item: {status: verified, evidence: "Publisher-scoped item navigation was exercised across the three real extension items."}
|
|
29
|
+
upload-package: {status: verified, evidence: "Initial package upload was exercised during real new-item creation."}
|
|
30
|
+
update-listing: {status: implemented_unverified, evidence: "Action maps approved Market Package fields, but complete live listing-field coverage has not been verified as a release gate."}
|
|
31
|
+
submit-review: {status: implemented_unverified, evidence: "Action requires commit plus explicit external-submit authorization; no real review submission has been performed by this verification."}
|
|
32
|
+
check-status: {status: implemented_unverified, evidence: "Read-only review/listing-state classification is implemented; post-submission external review states have not been live-verified."}
|
|
33
|
+
verify-published: {status: implemented_unverified, evidence: "Public exact-version verification is implemented; no authorized published listing lifecycle has been exercised by this verification."}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ROOT = Path(__file__).parents[1]
|
|
7
|
+
MANIFEST = ROOT / "manifest.yaml"
|
|
8
|
+
ACTION = ROOT / "actions" / "_chrome_web_store.py"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_action_has_explicit_action_and_side_effect_guards():
|
|
12
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
13
|
+
assert '"submit-review"' in source
|
|
14
|
+
assert "COMMIT" in source
|
|
15
|
+
assert "ALLOW_EXTERNAL_SUBMIT" in source
|
|
16
|
+
assert '"published"' in source
|
|
17
|
+
assert "page_info" in source
|
|
18
|
+
assert "package_version" in source
|
|
19
|
+
assert "allow_external_submit" in source
|
|
20
|
+
assert "screenshots" in source
|
|
21
|
+
assert "reviewer_test_instructions" in source
|
|
22
|
+
assert '"resolve-item"' in source
|
|
23
|
+
assert '"verify-draft"' in source
|
|
24
|
+
assert '"save-draft"' in source
|
|
25
|
+
assert "product_name" in source
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_manifest_covers_release_lifecycle_without_overstating_evidence():
|
|
29
|
+
text = MANIFEST.read_text(encoding="utf-8")
|
|
30
|
+
for flow in ("resolve-item", "create-item", "verify-draft", "save-draft", "open-item", "upload-package", "update-listing", "submit-review", "check-status", "verify-published"):
|
|
31
|
+
assert flow in text
|
|
32
|
+
assert "status: partially_verified" in text
|
|
33
|
+
assert "submit-review: {status: implemented_unverified" in text
|
|
34
|
+
assert "check-status: {status: implemented_unverified" in text
|
|
35
|
+
assert "verify-published: {status: implemented_unverified" in text
|
|
36
|
+
assert "no real review submission has been performed" in text
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_action_requires_exact_package_version_before_upload():
|
|
40
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
41
|
+
assert "manifest.json" in source
|
|
42
|
+
assert "does not match expected_version" in source
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_action_reuses_harness_real_tab_for_stable_navigation():
|
|
46
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
47
|
+
assert "ensure_real_tab()" in source
|
|
48
|
+
assert "goto_url(url)" in source
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_action_uses_accessible_labels_and_draft_save_control():
|
|
52
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
53
|
+
assert "aria-labelledby" in source
|
|
54
|
+
assert "label[for=" in source
|
|
55
|
+
assert "Save draft" in source
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_action_derives_publisher_scoped_item_route():
|
|
59
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
60
|
+
assert "publisher_id_from_url" in source
|
|
61
|
+
assert 'f"https://chrome.google.com/webstore/devconsole/{publisher_id}/{ITEM_ID}/edit"' in source
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_action_bootstraps_publisher_scope_before_item_navigation():
|
|
65
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
66
|
+
assert 'goto_url(CONSOLE_URL)' in source
|
|
67
|
+
assert 'publisher-scoped developer console route not observed' in source
|
|
68
|
+
assert 'CONSOLE_URL + f"/store-item/{ITEM_ID}"' not in source
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_action_supports_dry_run_new_item_creation():
|
|
72
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
73
|
+
assert '"create-item"' in source
|
|
74
|
+
assert 'would_create=True' in source
|
|
75
|
+
assert 'created item editor route not observed' in source
|
|
76
|
+
assert 'manifest key field is not allowed' in source
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_action_reuses_observed_item_before_creating():
|
|
80
|
+
source = ACTION.read_text(encoding="utf-8")
|
|
81
|
+
assert "dashboard_candidates" in source
|
|
82
|
+
assert "reused_item=True" in source
|
|
83
|
+
assert "exact_version_verified=True" in source
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: microsoft-teams-platform
|
|
3
|
+
description: Microsoft Teams browser mechanics and verified meeting flows.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microsoft Teams
|
|
7
|
+
|
|
8
|
+
This platform defines site behavior only. Execution lifecycle and isolation are external to platform code.
|
|
9
|
+
|
|
10
|
+
## Canonical instant-meeting path
|
|
11
|
+
|
|
12
|
+
For automated meeting E2E, use exactly:
|
|
13
|
+
|
|
14
|
+
`left sidebar Calendar -> Meet now -> Start meeting (when shown) -> pre-join -> Join now -> in-call`
|
|
15
|
+
|
|
16
|
+
Do not substitute a Join button from an existing chat or calendar event when the caller asks to create an instant meeting.
|
|
17
|
+
|
|
18
|
+
## Actions
|
|
19
|
+
|
|
20
|
+
Canonical action source: `actions/_meeting.py`.
|
|
21
|
+
|
|
22
|
+
Supported config actions:
|
|
23
|
+
|
|
24
|
+
- `start-instant-meeting`: open Teams, follow the canonical path, and verify in-call state.
|
|
25
|
+
- `status`: report whether the active Teams page is in pre-join or in-call.
|
|
26
|
+
- `leave`: leave the active meeting when one is in progress.
|
|
27
|
+
|
|
28
|
+
If Teams reports that the account is already in another call and the requested instant meeting cannot reach pre-join, fail with evidence rather than manipulating unrelated tabs.
|
|
29
|
+
|
|
30
|
+
## Verification
|
|
31
|
+
|
|
32
|
+
See `manifest.yaml`. The instant-meeting path, in-call detection, and exit were live-verified on 2026-10-01.
|