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.
Files changed (117) hide show
  1. package/README.md +16 -2
  2. package/SKILL.md +60 -37
  3. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  4. package/bin/browser-workspace +174 -13
  5. package/package.json +1 -1
  6. package/platform_runner.py +23 -6
  7. package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
  8. package/platforms/chatgpt/tests/test_contract.py +7 -0
  9. package/platforms/wechat-channels/actions/_common.py +1 -1
  10. package/platforms/wechat-channels/actions/_post.py +37 -38
  11. package/platforms/wechat-channels/manifest.yaml +19 -13
  12. package/platforms/xhs/actions/_post.py +109 -20
  13. package/platforms/xhs/manifest.yaml +14 -5
  14. package/platforms/xhs/tests/test_contract.py +17 -0
  15. package/platforms/youtube/actions/_post.py +7 -4
  16. package/pyproject.toml +6 -2
  17. package/session_client.py +21 -11
  18. package/session_daemon.py +115 -9
  19. package/src/browser_workspace.egg-info/PKG-INFO +2 -1
  20. package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
  21. package/src/browser_workspace.egg-info/entry_points.txt +2 -0
  22. package/src/browser_workspace.egg-info/requires.txt +1 -0
  23. package/src/browser_workspace.egg-info/top_level.txt +1 -1
  24. package/src/browser_workspace_app/__init__.py +1 -0
  25. package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
  26. package/src/browser_workspace_app/cli.py +236 -0
  27. package/src/browser_workspace_app/extension/manifest.json +26 -0
  28. package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
  29. package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
  30. package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
  31. package/src/browser_workspace_app/extension/workspace.html +8 -0
  32. package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
  33. package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
  34. package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
  35. package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
  36. package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
  37. package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
  38. package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
  39. package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
  40. package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
  41. package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
  42. package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
  43. package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
  44. package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
  45. package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
  46. package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
  47. package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
  48. package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
  49. package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
  50. package/src/browser_workspace_app/managed_chrome.py +244 -0
  51. package/src/browser_workspace_app/node/admin.mjs +44 -0
  52. package/src/browser_workspace_app/package.json +36 -0
  53. package/src/browser_workspace_app/platform_runner.py +204 -0
  54. package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
  55. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
  56. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
  57. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
  58. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
  59. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
  60. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
  61. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
  62. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
  63. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
  64. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
  65. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
  66. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
  67. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
  68. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
  69. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
  70. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
  71. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
  72. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
  73. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
  74. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
  75. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
  76. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
  77. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
  78. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
  79. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
  80. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
  81. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
  82. package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
  83. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
  84. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
  85. package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
  86. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
  87. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
  88. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
  89. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
  90. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
  91. package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
  92. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
  93. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
  94. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
  95. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
  96. package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
  97. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
  98. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
  99. package/src/browser_workspace_app/pyproject.toml +33 -0
  100. package/src/browser_workspace_app/session_client.py +107 -0
  101. package/src/browser_workspace_app/session_daemon.py +431 -0
  102. package/src/browser_harness/__init__.py +0 -2
  103. package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
  104. package/src/browser_harness/_ipc.py +0 -201
  105. package/src/browser_harness/admin.py +0 -1588
  106. package/src/browser_harness/auth.py +0 -546
  107. package/src/browser_harness/daemon.py +0 -894
  108. package/src/browser_harness/helpers.py +0 -668
  109. package/src/browser_harness/macos.py +0 -131
  110. package/src/browser_harness/mcp_cli.py +0 -16
  111. package/src/browser_harness/paths.py +0 -49
  112. package/src/browser_harness/recorder.py +0 -343
  113. package/src/browser_harness/run.py +0 -413
  114. package/src/browser_harness/telemetry.py +0 -308
  115. package/src/browser_harness/video-template.html +0 -970
  116. package/src/browser_harness/video.py +0 -749
  117. package/src/browser_harness/video_render.py +0 -524
package/README.md CHANGED
@@ -20,13 +20,13 @@ browser-workspace session stop SESSION_ID
20
20
 
21
21
  `--workspace` is hidden from normal CLI help. Normal sessions use `Harness`; product integrations may set an explicit workspace internally when they require isolation.
22
22
 
23
- `session stop` closes all tabs created/owned by that session while leaving unrelated browser tabs untouched.
23
+ `session stop` closes all tabs created/owned by that session while leaving unrelated browser tabs untouched. Raw session CDP cannot create targets: `Target.createTarget` is rejected and callers must use `new_tab()` so workspace/session ownership is preserved.
24
24
 
25
25
  ## Included layers
26
26
 
27
27
  ```text
28
28
  browser-workspace
29
- ├── src/browser_harness/ low-level browser/CDP runtime (forked internally)
29
+ ├── pyproject.toml pins the external browser-harness runtime dependency
30
30
  ├── agent-workspace/ Browser Workspace helper layer
31
31
  ├── extension/ Chrome workspace/tab manager
32
32
  ├── session_daemon.py persistent Python session service
@@ -136,3 +136,17 @@ Each platform manifest records explicit verification status and evidence. `verif
136
136
  ## npx skills installation
137
137
 
138
138
  After `npx skills add ...`, invoke `~/.agents/skills/browser-workspace/bin/browser-workspace` directly. The first session start bootstraps the skill-owned Python virtual environment automatically; running `scripts/install.sh` is optional.
139
+
140
+ ### Python distribution (PyPI)
141
+
142
+ Browser Workspace is also packaged as a Python CLI. Once its PyPI trusted publisher is configured and a `pypi-v*` release tag is published, invoke it with:
143
+
144
+ ```bash
145
+ uvx browser-workspace status
146
+ uvx browser-workspace session list
147
+ uvx browser-workspace platform profile wechat-channels
148
+ ```
149
+
150
+ The package entry point is `browser_workspace_app.cli:main`, and Python dependencies are managed by `pyproject.toml`. This uses the same existing Chrome profile and Session Daemon socket as the previous npm CLI; it does **not** start a second browser or daemon when the existing one is healthy.
151
+
152
+ Before building, run `python tools/sync-pypi-package.py` to copy canonical Python and packaged platform assets to the wheel source. Run `uv build`, then validate with `uvx --from dist/browser_workspace-<version>-py3-none-any.whl browser-workspace status`. The PyPI publishing workflow uses GitHub OIDC trusted publishing; configure `lalalic/browser-workspace` with the `pypi` environment as the publisher on PyPI before the first tag release.
package/SKILL.md CHANGED
@@ -9,31 +9,33 @@ Browser Workspace is the **only browser skill agents should load**. It includes
9
9
 
10
10
  ## Agent contract
11
11
 
12
- When installed with `npx skills`, use the CLI from the skill itself; no separate install step is required:
12
+ Install the Skill instructions through your preferred skill manager, but run the published Python CLI through **PyPI/uvx**. The Skill directory is documentation, not the executable source of truth:
13
13
 
14
14
  ```bash
15
- BW_CLI="$HOME/.agents/skills/browser-workspace/bin/browser-workspace"
16
- $BW_CLI status
15
+ bw() { uvx browser-workspace "$@"; }
16
+ bw status
17
17
  ```
18
18
 
19
- On first use, Browser Workspace bootstraps its own `.venv` automatically.
19
+ On first use, `uvx` installs Browser Workspace and Python dependencies from PyPI into its managed cache. No local source checkout, `.venv`, `.env`, or `npx` CLI is required. Use `uvx browser-workspace ...` consistently, including in NeoY/Agents Relay workers.
20
20
 
21
- Use one of two execution paths:
21
+ Use one session-owned execution path for orchestrated browser work:
22
22
 
23
23
  ```text
24
- known platform flow
25
- -> $BW_CLI platform run <platform> <action> [--config FILE]
26
- -> runner creates the browser session
27
- -> runner executes the platform action
28
- -> runner always stops and cleans up
29
-
30
- custom browser workflow
31
- -> $BW_CLI session start
32
- -> $BW_CLI session exec <session_id>
33
- -> $BW_CLI session stop <session_id>
24
+ start one Browser Workspace session
25
+ -> retain SESSION_ID for the whole workflow
26
+ -> platform run <platform> <action> --session-id SESSION_ID
27
+ -> inspect each result
28
+ -> on recoverable failure, diagnose and self-heal in the SAME session
29
+ -> write every successful site-specific repair back to the platform code
30
+ -> continue until the external workflow reaches a terminal outcome
31
+ -> verify the terminal external state
32
+ finally
33
+ -> stop SESSION_ID
34
34
  ```
35
35
 
36
- Do not manually create a session around a platform action unless product code intentionally owns a longer-lived browser context. There are no separate top-level `open`, `tabs`, `status`, or `screenshot` commands.
36
+ `platform run` therefore requires `--session-id` by default. If omitted, the CLI returns a structured error that recommends the `session start -> platform run --session-id -> self-heal -> session stop` workflow. `--auto-session` is an explicit one-shot escape hatch: it creates and cleans up a temporary session automatically, but **does not provide orchestrator self-heal** after failure.
37
+
38
+ There are no separate top-level `open`, `tabs`, `status`, or `screenshot` commands.
37
39
 
38
40
  Every logical browser workflow must follow:
39
41
 
@@ -59,7 +61,7 @@ Rules:
59
61
  Default workspace is `Harness`:
60
62
 
61
63
  ```bash
62
- $BW_CLI session start
64
+ bw session start
63
65
  ```
64
66
 
65
67
  Normal agents do not choose or manage workspaces. `session start` defaults to `Harness`; the `--workspace` implementation option is hidden from normal CLI help. Workspace lifecycle is not exposed through the agent CLI. Product runtimes that need persistent isolation discover the product-only Node admin helper with `browser-workspace status`, then call `ensureWorkspace` / `deleteWorkspace` through that helper.
@@ -67,7 +69,7 @@ Normal agents do not choose or manage workspaces. `session start` defaults to `H
67
69
  Start directly at a URL when known:
68
70
 
69
71
  ```bash
70
- $BW_CLI session start --url https://example.com
72
+ bw session start --url https://example.com
71
73
  ```
72
74
 
73
75
  The result is JSON containing `session_id`, `workspace`, `target_id`, the starting `url`, and `workspace_supported`.
@@ -79,7 +81,7 @@ Browser Workspace self-manages exactly one machine-wide persistent, visible Chro
79
81
  `session exec` reads Python from stdin. Helpers are already imported; do not import another browser package.
80
82
 
81
83
  ```bash
82
- $BW_CLI session exec <session_id> <<'PY'
84
+ bw session exec <session_id> <<'PY'
83
85
  print(page_info())
84
86
  print(snapshot())
85
87
  PY
@@ -88,7 +90,7 @@ PY
88
90
  Python state persists across later calls in the same session:
89
91
 
90
92
  ```bash
91
- $BW_CLI session exec <session_id> <<'PY'
93
+ bw session exec <session_id> <<'PY'
92
94
  meeting_name = "Demo"
93
95
  print(current_tab())
94
96
  PY
@@ -97,7 +99,7 @@ PY
97
99
  Then later:
98
100
 
99
101
  ```bash
100
- $BW_CLI session exec <session_id> <<'PY'
102
+ bw session exec <session_id> <<'PY'
101
103
  print(meeting_name)
102
104
  PY
103
105
  ```
@@ -107,19 +109,19 @@ A Python file may also be used with `--code-file`.
107
109
  ## Stop
108
110
 
109
111
  ```bash
110
- $BW_CLI session stop <session_id>
112
+ bw session stop <session_id>
111
113
  ```
112
114
 
113
- Stopping closes every tab owned or opened by that session—including tabs created with `new_tab()`, `bh.new_tab()`, direct `Target.createTarget`, and detected child/popup tabs—then destroys the session's Python namespace. It does not close unrelated browser tabs. Do not reuse the ID afterward.
115
+ Stopping closes every tab owned or opened by that session—including tabs created with `new_tab()`, `bh.new_tab()`, and detected child/popup tabs—then destroys the session's Python namespace. Raw `cdp("Target.createTarget", ...)` is rejected; use `new_tab()` so Browser Workspace can own and group the tab. It does not close unrelated browser tabs. Do not reuse the ID afterward.
114
116
 
115
117
  A safe shell structure is:
116
118
 
117
119
  ```bash
118
- SESSION_JSON=$($BW_CLI session start)
120
+ SESSION_JSON=$(bw session start)
119
121
  SESSION_ID=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["session_id"])' "$SESSION_JSON")
120
- trap '$BW_CLI session stop "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
122
+ trap 'bw session stop "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
121
123
 
122
- $BW_CLI session exec "$SESSION_ID" <<'PY'
124
+ bw session exec "$SESSION_ID" <<'PY'
123
125
  print(snapshot())
124
126
  PY
125
127
  ```
@@ -216,21 +218,41 @@ platforms/<name>/
216
218
 
217
219
  Platform rules:
218
220
 
219
- - Platform actions may use browser helpers such as `new_tab()`, `current_tab()`, `snapshot()`, `js()`, `cdp()`, uploads, downloads, and screenshots.
221
+ - Platform actions may use semantic browser helpers such as `new_tab()`, `current_tab()`, `snapshot()`, `click(selector)`, `fill_input(selector, text)`, `js()`, `cdp()`, uploads, downloads, and screenshots. `cdp()` must not create targets; `Target.createTarget` is blocked and tab creation must use `new_tab()`.
222
+ - Platform interaction code must be **semantic and element-based**. It must not use screen coordinates, viewport coordinates, `x/y` click targets, `click_at_xy()`, or coordinate calculations to interact with the page. Browser size, zoom, display resolution, and viewport geometry must not affect correctness.
220
223
  - Platform actions must **not** create/stop Browser Workspace sessions, choose a workspace, call workspace-management APIs, invoke the Browser Workspace CLI, or import session/workspace infrastructure.
224
+ - The orchestrator owns the session lifecycle for normal platform execution: start once, retain the same session across execution and recovery, and stop only after terminal success, terminal failure, or an explicit human blocker.
225
+ - On any recoverable failure, the orchestrator must inspect the live page in the same session, diagnose the failure, repair/recover, and resume from the safest point rather than restarting the whole workflow.
226
+ - **Self-heal writeback is mandatory:** if recovery discovers a site-specific selector, flow, wait condition, validation rule, or other adapter fix that makes the action work, write that repair back to `platforms/<name>/actions/` (and update platform docs/verification metadata when applicable) before declaring the workflow complete. A repair that exists only in an ad-hoc session snippet is not complete.
227
+ - After a possible external side effect (publish, send, payment, delete, submit, etc.), recovery must be verification-first. Never repeat the side effect until read-only evidence proves the previous attempt did not take effect.
221
228
  - Platform `README.md` and flow docs should describe site behavior, preconditions, side effects, and verification evidence—not Browser Workspace lifecycle mechanics.
222
- - Platform actions must remain usable in either a generic runner-owned session or a product-owned browser context.
223
- - The generic runner owns the normal lifecycle: start a default Harness session, inject browser helpers/config, execute the action, then always stop the session and close its owned tabs.
224
- - Products that require a dedicated or persistent browser context (for example Family Tutor or Agents Relay) may execute the same platform action inside product-owned infrastructure. The platform code itself stays unchanged.
225
229
  - Do not rediscover a known site's mechanics when a verified platform flow/action already exists.
226
230
 
227
- Run a platform action through the generic runner:
231
+ Normal orchestrator workflow:
232
+
233
+ ```bash
234
+ SESSION_JSON=$(bw session start --url https://example.com)
235
+ SESSION_ID=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["session_id"])' "$SESSION_JSON")
236
+
237
+ bw platform run microsoft-teams meeting \
238
+ --session-id "$SESSION_ID" \
239
+ --config /path/to/config.json
240
+
241
+ # If it fails: inspect/self-heal using the same SESSION_ID, write the repair
242
+ # back to the platform implementation, then resume safely.
243
+
244
+ bw session stop "$SESSION_ID"
245
+ ```
246
+
247
+ One-shot mode is explicit and is not self-healing:
228
248
 
229
249
  ```bash
230
- $BW_CLI platform run microsoft-teams meeting --config /path/to/config.json
250
+ bw platform run microsoft-teams meeting \
251
+ --auto-session \
252
+ --config /path/to/config.json
231
253
  ```
232
254
 
233
- The runner output hides the raw session ID. It returns the platform/action result, optional workspace-extension warning, and cleanup evidence.
255
+ Use `--auto-session` only when losing the browser state after a failure is acceptable.
234
256
 
235
257
  ### Platform verification contract
236
258
 
@@ -274,13 +296,14 @@ Read the matching platform `README.md` and `manifest.yaml` before invoking or mo
274
296
 
275
297
  ## Installation
276
298
 
277
- Run the bundled installer from the skill directory:
299
+ Use the published PyPI command (requires `uv`/`uvx`):
278
300
 
279
301
  ```bash
280
- ./scripts/install.sh
302
+ uvx browser-workspace status
303
+ uvx browser-workspace session list
281
304
  ```
282
305
 
283
- It creates a Browser Workspace-owned `.venv`, installs the vendored browser runtime and dependencies, writes Browser Workspace environment configuration to `agent-workspace/.env`, and links `browser-workspace` into `~/.local/bin` by default.
306
+ `uvx` resolves the published Python wheel and installs the dependencies declared in `pyproject.toml`. Do not run the legacy bundled installer or point agents at a checked-out `bin/browser-workspace` script. The installed CLI reuses the same Browser Workspace Chrome profile and Session Daemon socket.
284
307
 
285
308
  There is no required separate Browser Harness installation.
286
309
 
@@ -298,4 +321,4 @@ kgbghhigmbpefppgkocgjgnnnbhjchic
298
321
 
299
322
  ## Internal provenance
300
323
 
301
- The low-level browser runtime is forked from `browser-use/browser-harness` and kept internally under `src/browser_harness/`. Browser Workspace is the public product/skill/CLI surface. Upstream licensing is preserved under `licenses/`.
324
+ The low-level browser runtime is the separately maintained `browser-harness` Python dependency. Browser Workspace owns the session/workspace/platform product layer and pins the exact Harness runtime revision it was tested with; Harness source is not vendored into this repository. Upstream licensing remains documented under `licenses/`.
@@ -30,18 +30,165 @@ def status():
30
30
  }
31
31
 
32
32
  def main():
33
- p=argparse.ArgumentParser(prog='browser-workspace')
34
- sp=p.add_subparsers(dest='group',required=True)
35
- sp.add_parser('status')
36
- session=sp.add_parser('session'); ssp=session.add_subparsers(dest='cmd',required=True)
37
- start=ssp.add_parser('start'); start.add_argument('--workspace', help=argparse.SUPPRESS); start.add_argument('--url')
38
- ssp.add_parser('list')
39
- ex=ssp.add_parser('exec'); ex.add_argument('session_id'); ex.add_argument('--code-file')
40
- stop=ssp.add_parser('stop'); stop.add_argument('session_id', nargs='?'); stop.add_argument('--all', action='store_true', dest='stop_all')
41
- platform=sp.add_parser('platform'); psp=platform.add_subparsers(dest='pcmd',required=True)
42
- profile=psp.add_parser('profile'); profile.add_argument('platform')
43
- source=psp.add_parser('source'); source.add_argument('platform'); source.add_argument('action'); source.add_argument('--config')
44
- run=psp.add_parser('run'); run.add_argument('platform'); run.add_argument('action'); run.add_argument('--config'); run.add_argument('--url'); run.add_argument('--session-id')
33
+ fmt=argparse.RawDescriptionHelpFormatter
34
+ p=argparse.ArgumentParser(
35
+ prog='browser-workspace',
36
+ description='Control the shared Browser Workspace Chrome, browser sessions, and platform automations.',
37
+ epilog='''Examples:
38
+ browser-workspace status
39
+ browser-workspace session start --url https://example.com
40
+ browser-workspace session list
41
+ browser-workspace session exec SESSION_ID --code-file script.py
42
+ browser-workspace session stop SESSION_ID
43
+ browser-workspace session stop --all
44
+ browser-workspace platform profile wechat-channels
45
+ browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
46
+ browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
47
+ browser-workspace platform run wechat-channels manage --auto-session --config status.json
48
+ ''',
49
+ formatter_class=fmt,
50
+ )
51
+ sp=p.add_subparsers(dest='group',required=True,title='commands',metavar='COMMAND')
52
+
53
+ sp.add_parser(
54
+ 'status',
55
+ help='Show Browser Workspace runtime, managed Chrome, daemon, and active-session status.',
56
+ description='Show Browser Workspace runtime, managed Chrome, daemon, and active-session status.',
57
+ epilog='Example:\n browser-workspace status',
58
+ formatter_class=fmt,
59
+ )
60
+
61
+ session=sp.add_parser(
62
+ 'session',
63
+ help='Create, inspect, execute code in, and stop browser sessions.',
64
+ description='Manage Browser Workspace sessions.',
65
+ epilog='''Examples:
66
+ browser-workspace session start --url https://example.com
67
+ browser-workspace session list
68
+ browser-workspace session exec SESSION_ID --code-file script.py
69
+ cat script.py | browser-workspace session exec SESSION_ID
70
+ browser-workspace session stop SESSION_ID
71
+ browser-workspace session stop --all
72
+ ''',
73
+ formatter_class=fmt,
74
+ )
75
+ ssp=session.add_subparsers(dest='cmd',required=True,title='session commands',metavar='COMMAND')
76
+
77
+ start=ssp.add_parser(
78
+ 'start',
79
+ help='Start a browser session, optionally opening a URL.',
80
+ description='Start a browser session. The shared managed Chrome is reused; this does not launch a separate browser per session.',
81
+ epilog='''Examples:
82
+ browser-workspace session start
83
+ browser-workspace session start --url https://example.com
84
+ ''',
85
+ formatter_class=fmt,
86
+ )
87
+ start.add_argument('--workspace', help=argparse.SUPPRESS)
88
+ start.add_argument('--url', metavar='URL', help='URL to open when the session starts.')
89
+
90
+ ssp.add_parser(
91
+ 'list',
92
+ help='List currently active Browser Workspace sessions.',
93
+ description='List currently active Browser Workspace sessions and their target tabs.',
94
+ epilog='Example:\n browser-workspace session list',
95
+ formatter_class=fmt,
96
+ )
97
+
98
+ ex=ssp.add_parser(
99
+ 'exec',
100
+ help='Execute Browser Workspace Python helpers inside an existing session.',
101
+ description='Execute Python using Browser Workspace helpers in an existing session. Read code from --code-file or stdin.',
102
+ epilog='''Examples:
103
+ browser-workspace session exec SESSION_ID --code-file script.py
104
+
105
+ browser-workspace session exec SESSION_ID <<'PY'
106
+ print(page_info())
107
+ print(snapshot(interactive_only=False))
108
+ PY
109
+ ''',
110
+ formatter_class=fmt,
111
+ )
112
+ ex.add_argument('session_id', metavar='SESSION_ID', help='Session ID returned by "session start" or "session list".')
113
+ ex.add_argument('--code-file', metavar='PATH', help='Read Python code from this file instead of stdin.')
114
+
115
+ stop=ssp.add_parser(
116
+ 'stop',
117
+ help='Stop one session or all sessions and release their Browser Workspace tabs.',
118
+ description='Stop one Browser Workspace session, or all sessions with --all.',
119
+ epilog='''Examples:
120
+ browser-workspace session stop SESSION_ID
121
+ browser-workspace session stop --all
122
+ ''',
123
+ formatter_class=fmt,
124
+ )
125
+ stop.add_argument('session_id', nargs='?', metavar='SESSION_ID', help='Session ID to stop.')
126
+ stop.add_argument('--all', action='store_true', dest='stop_all', help='Stop all active Browser Workspace sessions.')
127
+
128
+ platform=sp.add_parser(
129
+ 'platform',
130
+ help='Inspect and run reusable website-specific platform automations.',
131
+ description='Inspect and run reusable Browser Workspace platform automations.',
132
+ epilog='''Examples:
133
+ browser-workspace platform profile wechat-channels
134
+ browser-workspace platform source wechat-channels post --config post.json
135
+ browser-workspace session start --url https://channels.weixin.qq.com/platform/post/list
136
+ browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
137
+ browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
138
+ browser-workspace session stop SESSION_ID
139
+
140
+ One-shot mode (no orchestrator self-heal):
141
+ browser-workspace platform run wechat-channels manage --auto-session --config status.json
142
+ ''',
143
+ formatter_class=fmt,
144
+ )
145
+ psp=platform.add_subparsers(dest='pcmd',required=True,title='platform commands',metavar='COMMAND')
146
+
147
+ profile=psp.add_parser(
148
+ 'profile',
149
+ help='Show a platform manifest/profile: fields, media rules, and capabilities.',
150
+ description='Show the declared Browser Workspace profile for a platform.',
151
+ epilog='Example:\n browser-workspace platform profile wechat-channels',
152
+ formatter_class=fmt,
153
+ )
154
+ profile.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
155
+
156
+ source=psp.add_parser(
157
+ 'source',
158
+ help='Print the fully prepared Python source for one platform action without running it.',
159
+ description='Render the action source after injecting the optional JSON config path. No browser action is executed.',
160
+ epilog='''Examples:
161
+ browser-workspace platform source wechat-channels post --config post.json
162
+ browser-workspace platform source wechat-channels manage --config status.json
163
+ ''',
164
+ formatter_class=fmt,
165
+ )
166
+ source.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
167
+ source.add_argument('action', metavar='ACTION', help='Declared action name, for example "post" or "manage".')
168
+ source.add_argument('--config', metavar='PATH', help='JSON configuration file used to prepare the action.')
169
+
170
+ run=psp.add_parser(
171
+ 'run',
172
+ help='Run one platform action in Browser Workspace.',
173
+ description='Run a declared platform action. A caller-owned --session-id is required by default so an orchestrator can inspect failures, self-heal in the same browser state, write repairs back to platform code, and close the session only after completion. --auto-session is an explicit one-shot mode without orchestrator self-heal.',
174
+ epilog='''Examples:
175
+ browser-workspace session start --url https://channels.weixin.qq.com/platform/post/list
176
+ browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
177
+ browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
178
+ browser-workspace session stop SESSION_ID
179
+
180
+ One-shot mode (no orchestrator self-heal):
181
+ browser-workspace platform run wechat-channels manage --auto-session --config status.json
182
+ ''',
183
+ formatter_class=fmt,
184
+ )
185
+ run.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
186
+ run.add_argument('action', metavar='ACTION', help='Declared action name, for example "post" or "manage".')
187
+ run.add_argument('--config', metavar='PATH', help='JSON configuration file for the platform action.')
188
+ run.add_argument('--url', metavar='URL', help='Optional initial URL for a temporary session.')
189
+ session_mode = run.add_mutually_exclusive_group()
190
+ session_mode.add_argument('--session-id', metavar='SESSION_ID', help='Required by default. Reuse a caller-owned session so failures can be inspected and self-healed before the session is closed.')
191
+ session_mode.add_argument('--auto-session', action='store_true', help='Explicit one-shot mode: create and clean up a temporary session automatically. No orchestrator self-heal is available after failure.')
45
192
  args=p.parse_args()
46
193
  if args.group=='status': emit(status()); return
47
194
  if args.group=='session' and args.cmd=='list':
@@ -71,5 +218,19 @@ def main():
71
218
  from platform_runner import platform_profile
72
219
  emit(platform_profile(args.platform)); return
73
220
  if args.group=='platform' and args.pcmd=='run':
74
- r=run_platform_action(args.platform,args.action,config_path=args.config,url=args.url,session_id=args.session_id); emit(r); raise SystemExit(0 if (r.get('result') or {}).get('ok',False) else 1)
221
+ if not args.session_id and not args.auto_session:
222
+ emit({
223
+ 'ok': False,
224
+ 'error': 'session_id_required',
225
+ 'message': 'platform run requires --session-id by default so the orchestrator can self-heal failures in the same browser session.',
226
+ 'suggested_workflow': [
227
+ 'browser-workspace session start --url <URL>',
228
+ 'browser-workspace platform run <PLATFORM> <ACTION> --session-id SESSION_ID [--config FILE]',
229
+ 'inspect/self-heal failures in the same SESSION_ID and write successful repairs back to platform code',
230
+ 'browser-workspace session stop SESSION_ID',
231
+ ],
232
+ 'auto_session': 'Use --auto-session only for a one-shot run. It creates and closes its own session and does not provide orchestrator self-heal.',
233
+ })
234
+ raise SystemExit(2)
235
+ r=run_platform_action(args.platform,args.action,config_path=args.config,url=args.url,session_id=args.session_id,auto_session=args.auto_session); emit(r); raise SystemExit(0 if (r.get('result') or {}).get('ok',False) else 1)
75
236
  if __name__=='__main__': main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-workspace",
3
- "version": "0.4.1",
3
+ "version": "0.4.5",
4
4
  "description": "Agent browser workspace with persistent sessions, tab leasing, snapshots, and platform workflows.",
5
5
  "bin": {
6
6
  "browser-workspace": "bin/browser-workspace"
@@ -102,11 +102,16 @@ def declared_actions(platform: str) -> dict[str, Path]:
102
102
  raise ValueError(f"{platform}/manifest.yaml has no actions block")
103
103
 
104
104
  actions: dict[str, Path] = {}
105
- for name, relative in re.findall(
106
- r'^\s{2}([\w-]+):\s*["\']([^"\']+)["\']\s*$',
107
- match.group("body"),
108
- flags=re.MULTILINE,
109
- ):
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
110
115
  path = PLATFORMS / platform / relative
111
116
  if not path.is_file():
112
117
  raise FileNotFoundError(f"declared platform action missing: {platform}/{name} -> {relative}")
@@ -146,6 +151,7 @@ def run_platform_action(
146
151
  config_path: str | None = None,
147
152
  url: str | None = None,
148
153
  session_id: str | None = None,
154
+ auto_session: bool = False,
149
155
  ) -> dict:
150
156
  path = action_path(platform, action)
151
157
  config_file = Path(config_path) if config_path else None
@@ -156,7 +162,16 @@ def run_platform_action(
156
162
  validate_platform_config(platform, action, config)
157
163
  code = prepare_action(path, config_file)
158
164
 
159
- owned_session = not bool(session_id)
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)
160
175
  started = request({"op": "start", "workspace": None, "url": url}) if owned_session else {"session_id": session_id, "workspace_supported": None}
161
176
  sid = started["session_id"]
162
177
  result = None
@@ -168,6 +183,8 @@ def run_platform_action(
168
183
  "platform": platform,
169
184
  "action": action,
170
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,
171
188
  "result": result,
172
189
  }
173
190
  if started.get("warning"):
@@ -439,7 +439,14 @@ def _fill_prompt_if_needed(prompt, mode_label, timeout=30):
439
439
  deadline = time.time() + timeout
440
440
  last_error = None
441
441
  while time.time() < deadline:
442
- selector = _wait_for_composer(timeout=min(5, max(1, deadline - time.time())))
442
+ try:
443
+ selector = _wait_for_composer(timeout=min(5, max(1, deadline - time.time())))
444
+ except RuntimeError as exc:
445
+ # Hydration and navigation can temporarily remove the composer.
446
+ # Continue the outer readiness window rather than aborting on the first poll.
447
+ last_error = exc
448
+ time.sleep(.25)
449
+ continue
443
450
  if prompt_text_matches(_composer_text(selector), prompt):
444
451
  return selector
445
452
 
@@ -567,5 +574,18 @@ while time.time() < deadline:
567
574
  else:
568
575
  raise RuntimeError("Temporary Chat activation was not observed")
569
576
 
570
- result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
577
+ try:
578
+ result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
579
+ except RuntimeError as exc:
580
+ # A URL-prefilled Temporary Chat can retain the pre-hydration home input
581
+ # without ever mounting the active composer. No send occurred, so retry
582
+ # once on a clean Temporary Chat page without URL prompt prefill.
583
+ if "composer readiness was not observed ready" not in str(exc):
584
+ raise
585
+ goto_url(temporary_chat_entry_url())
586
+ wait_for_load()
587
+ _click_temporary_chat_toggle()
588
+ if not _temporary_chat_enabled():
589
+ raise RuntimeError("Temporary Chat mode lost during composer recovery") from exc
590
+ result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
571
591
  print(json.dumps(result, ensure_ascii=False), flush=True)
@@ -174,3 +174,10 @@ def test_new_turn_project_id_is_optional():
174
174
  assert 'if not thread_id: raise RuntimeError("thread_id is required")' in turn
175
175
  assert 'https://chatgpt.com/c/{quote(thread_id' in turn
176
176
  assert 'https://chatgpt.com/g/{quote(project_id' in turn
177
+
178
+
179
+ def test_temporary_submit_recovers_only_before_send_from_prefill_hydration():
180
+ code=(ACTIONS/"_temporary_submit.py").read_text()
181
+ assert 'goto_url(temporary_chat_entry_url())' in code
182
+ assert '"composer readiness was not observed ready" not in str(exc)' in code
183
+ assert 'if not _temporary_chat_enabled():' in code
@@ -6,7 +6,7 @@ import re
6
6
  from typing import Any, Optional
7
7
  from urllib.parse import parse_qs, urlparse
8
8
 
9
- MANAGER_URL = "https://channels.weixin.qq.com/platform/post"
9
+ MANAGER_URL = "https://channels.weixin.qq.com/platform/post/list"
10
10
 
11
11
  STATUS_LABELS: tuple[tuple[str, str], ...] = (
12
12
  ("审核中", "reviewing"),