browser-workspace 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +15 -1
  2. package/SKILL.md +22 -21
  3. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  4. package/package.json +1 -1
  5. package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
  6. package/platforms/chatgpt/tests/test_contract.py +7 -0
  7. package/pyproject.toml +6 -2
  8. package/session_daemon.py +91 -3
  9. package/src/browser_workspace.egg-info/PKG-INFO +2 -1
  10. package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
  11. package/src/browser_workspace.egg-info/entry_points.txt +2 -0
  12. package/src/browser_workspace.egg-info/requires.txt +1 -0
  13. package/src/browser_workspace.egg-info/top_level.txt +1 -1
  14. package/src/browser_workspace_app/__init__.py +1 -0
  15. package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
  16. package/src/browser_workspace_app/cli.py +236 -0
  17. package/src/browser_workspace_app/extension/manifest.json +26 -0
  18. package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
  19. package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
  20. package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
  21. package/src/browser_workspace_app/extension/workspace.html +8 -0
  22. package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
  23. package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
  24. package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
  25. package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
  26. package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
  27. package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
  28. package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
  29. package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
  30. package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
  31. package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
  32. package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
  33. package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
  34. package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
  35. package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
  36. package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
  37. package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
  38. package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
  39. package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
  40. package/src/browser_workspace_app/managed_chrome.py +244 -0
  41. package/src/browser_workspace_app/node/admin.mjs +44 -0
  42. package/src/browser_workspace_app/package.json +36 -0
  43. package/src/browser_workspace_app/platform_runner.py +204 -0
  44. package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
  45. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
  46. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
  47. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
  48. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
  49. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
  50. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
  51. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
  52. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
  53. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
  54. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
  55. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
  56. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
  57. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
  58. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
  59. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
  60. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
  61. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
  62. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
  63. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
  64. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
  65. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
  66. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
  67. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
  68. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
  69. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
  70. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
  71. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
  72. package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
  73. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
  74. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
  75. package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
  76. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
  77. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
  78. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
  79. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
  80. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
  81. package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
  82. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
  83. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
  84. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
  85. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
  86. package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
  87. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
  88. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
  89. package/src/browser_workspace_app/pyproject.toml +33 -0
  90. package/src/browser_workspace_app/session_client.py +107 -0
  91. package/src/browser_workspace_app/session_daemon.py +431 -0
  92. package/src/browser_harness/__init__.py +0 -2
  93. package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
  94. package/src/browser_harness/_ipc.py +0 -201
  95. package/src/browser_harness/admin.py +0 -1588
  96. package/src/browser_harness/auth.py +0 -546
  97. package/src/browser_harness/daemon.py +0 -894
  98. package/src/browser_harness/helpers.py +0 -668
  99. package/src/browser_harness/macos.py +0 -131
  100. package/src/browser_harness/mcp_cli.py +0 -16
  101. package/src/browser_harness/paths.py +0 -49
  102. package/src/browser_harness/recorder.py +0 -343
  103. package/src/browser_harness/run.py +0 -413
  104. package/src/browser_harness/telemetry.py +0 -308
  105. package/src/browser_harness/video-template.html +0 -970
  106. package/src/browser_harness/video.py +0 -749
  107. package/src/browser_harness/video_render.py +0 -524
@@ -0,0 +1,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.