moonlighting 0.1.0__tar.gz

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 (33) hide show
  1. moonlighting-0.1.0/LICENSE +21 -0
  2. moonlighting-0.1.0/PKG-INFO +240 -0
  3. moonlighting-0.1.0/README.md +219 -0
  4. moonlighting-0.1.0/pyproject.toml +62 -0
  5. moonlighting-0.1.0/scripts/auth.py +65 -0
  6. moonlighting-0.1.0/scripts/browser_daemon.py +213 -0
  7. moonlighting-0.1.0/scripts/cdp_click.py +160 -0
  8. moonlighting-0.1.0/scripts/cred_dialog.py +265 -0
  9. moonlighting-0.1.0/scripts/cred_form.html +113 -0
  10. moonlighting-0.1.0/scripts/desk_browser.py +198 -0
  11. moonlighting-0.1.0/scripts/desk_chrome.py +258 -0
  12. moonlighting-0.1.0/scripts/desk_restore.py +84 -0
  13. moonlighting-0.1.0/scripts/desk_stream.py +648 -0
  14. moonlighting-0.1.0/scripts/ext_click.py +152 -0
  15. moonlighting-0.1.0/scripts/login_flow.py +122 -0
  16. moonlighting-0.1.0/scripts/mcp_server.py +250 -0
  17. moonlighting-0.1.0/scripts/moonlighting.egg-info/PKG-INFO +240 -0
  18. moonlighting-0.1.0/scripts/moonlighting.egg-info/SOURCES.txt +31 -0
  19. moonlighting-0.1.0/scripts/moonlighting.egg-info/dependency_links.txt +1 -0
  20. moonlighting-0.1.0/scripts/moonlighting.egg-info/entry_points.txt +5 -0
  21. moonlighting-0.1.0/scripts/moonlighting.egg-info/requires.txt +2 -0
  22. moonlighting-0.1.0/scripts/moonlighting.egg-info/top_level.txt +19 -0
  23. moonlighting-0.1.0/scripts/ports.py +169 -0
  24. moonlighting-0.1.0/scripts/private_desktop.py +212 -0
  25. moonlighting-0.1.0/scripts/qclick.py +132 -0
  26. moonlighting-0.1.0/scripts/session.py +176 -0
  27. moonlighting-0.1.0/scripts/setup.py +154 -0
  28. moonlighting-0.1.0/scripts/svc.py +153 -0
  29. moonlighting-0.1.0/scripts/templates/desk_ctl.html +106 -0
  30. moonlighting-0.1.0/scripts/templates/desk_view.html +77 -0
  31. moonlighting-0.1.0/scripts/vault_broker.py +663 -0
  32. moonlighting-0.1.0/scripts/wclick.py +131 -0
  33. moonlighting-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 junjunni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: moonlighting
3
+ Version: 0.1.0
4
+ Summary: Invisible hands for your AI agent: a real Chrome on a hidden desktop, ref-based control, credentials that never reach the model.
5
+ Author: junjunni
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/xinyuzjj/moonlighting
8
+ Project-URL: Issues, https://github.com/xinyuzjj/moonlighting/issues
9
+ Keywords: mcp,browser-automation,ai-agents,playwright,credentials,windows
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: websockets>=12
19
+ Requires-Dist: pillow>=10
20
+ Dynamic: license-file
21
+
22
+ # Moonlighting
23
+
24
+ **Invisible hands for your AI agent.** A real Chrome, on its own hidden Windows desktop,
25
+ driven by element references instead of coordinates — with credentials that never reach
26
+ the model.
27
+
28
+ **English** · [中文](README.zh-CN.md)
29
+
30
+ ```
31
+ ┌─ your desktop ─────────────────────────────┐
32
+ │ Hermes / Claude / Codex / any MCP client │
33
+ └───────────────┬────────────────────────────┘
34
+ │ MCP (stdio) or local HTTP
35
+ ┌───────────────▼────────────────────────────┐
36
+ │ hidden desktop "HermesDesk" │
37
+ │ ┌──────────────────────────────────────┐ │
38
+ │ │ real Chrome + persistent profile │ │
39
+ │ │ (logins, wallet, extensions) │ │
40
+ │ └──────────────────────────────────────┘ │
41
+ │ credential broker (DPAPI, origin-bound) │
42
+ └─────────────────────────────────────────────┘
43
+ ```
44
+
45
+ You never see a window flash. The agent never sees a password.
46
+
47
+ ## Why
48
+
49
+ Driving a browser as an agent usually goes wrong in three places:
50
+
51
+ | Problem | What most setups do | What this does |
52
+ |---|---|---|
53
+ | **Finding elements** | screenshot + guess coordinates | one accessibility snapshot, act on `[ref=e42]` |
54
+ | **Losing state** | fresh profile every run | persistent profile on a private desktop; reopen and the session is still there |
55
+ | **Handling passwords** | paste into the prompt | user types into a masked popup → DPAPI-encrypted locally → injected by the broker, never returned to the model |
56
+
57
+ ## Install
58
+
59
+ Requires Windows 10/11, Python 3.10+, Node 18+ (for the Playwright MCP child), Chrome.
60
+
61
+ ### Option A — pip (no paths to edit)
62
+
63
+ ```bash
64
+ pip install moonlighting
65
+ moonlighting-setup --options # where should browser data live?
66
+ moonlighting-setup --home "D:\moonlighting"
67
+ moonlighting # starts stream + broker + driver
68
+ ```
69
+
70
+ Then wire it into your MCP client with **no absolute paths at all**:
71
+
72
+ ```json
73
+ { "mcpServers": { "browser": { "command": "moonlighting-mcp" } } }
74
+ ```
75
+
76
+ `moonlighting-mcp`, `moonlighting-setup`, `moonlighting-session` and `moonlighting`
77
+ are console scripts installed by the package.
78
+
79
+ ### Option B — clone
80
+
81
+ ```bash
82
+ git clone https://github.com/xinyuzjj/moonlighting.git
83
+ cd moonlighting
84
+ pip install -r requirements.txt
85
+
86
+ python scripts/setup.py --options # where should browser data live?
87
+ python scripts/setup.py --home "D:\moonlighting"
88
+ python scripts/svc.py # starts stream + broker + driver
89
+ ```
90
+
91
+ Either way, `install_mcp.py` writes the right config for you — it detects whether
92
+ `moonlighting-mcp` is installed and falls back to absolute script paths if not:
93
+
94
+ ```bash
95
+ python scripts/install_mcp.py # print configs with real paths
96
+ python scripts/install_mcp.py --write claude # write it (backs up the original)
97
+ python scripts/install_mcp.py --write codex
98
+ python scripts/install_mcp.py --write all
99
+ ```
100
+
101
+ `svc.py` / `moonlighting` prints a preview URL. Open it in the Hermes preview pane (or any
102
+ browser) to watch and even click into the hidden desktop:
103
+
104
+ ```
105
+ preview: http://127.0.0.1:8765/ctl
106
+ ```
107
+
108
+ First run creates `<home>/token.txt` — every state-changing endpoint requires it.
109
+
110
+ ## Use it from an agent
111
+
112
+ ### Any MCP client (Claude Desktop, Claude Code, Codex CLI, Hermes)
113
+
114
+ `scripts/mcp_server.py` is a standard MCP stdio server exposing **27 tools**
115
+ (25 `browser_*` plus `cred_list` / `cred_fill`).
116
+
117
+ If you installed with pip, the whole config is one line — no paths:
118
+
119
+ ```json
120
+ { "mcpServers": { "browser": { "command": "moonlighting-mcp" } } }
121
+ ```
122
+
123
+ Cloned instead? Let `install_mcp.py` fill in the real paths:
124
+
125
+ ```bash
126
+ python scripts/install_mcp.py --write all
127
+ ```
128
+
129
+ Or write it by hand (Claude Desktop / Claude Code, `mcpServers`):
130
+
131
+ ```json
132
+ {
133
+ "mcpServers": {
134
+ "browser": {
135
+ "command": "python",
136
+ "args": ["C:\\path\\to\\moonlighting\\scripts\\mcp_server.py",
137
+ "--cdp-endpoint", "http://127.0.0.1:18763"]
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ Codex CLI (`~/.codex/config.toml` — TOML, snake_case, and give it more startup time):
144
+
145
+ ```toml
146
+ [mcp_servers.browser]
147
+ command = "python"
148
+ args = ["C:\\path\\to\\moonlighting\\scripts\\mcp_server.py", "--cdp-endpoint", "http://127.0.0.1:18763"]
149
+ startup_timeout_sec = 60
150
+ tool_timeout_sec = 120
151
+ ```
152
+
153
+ ### Plain HTTP
154
+
155
+ ```bash
156
+ TOKEN=$(python scripts/auth.py --print | sed 's/^token: //')
157
+
158
+ curl -H "X-Hermes-Token: $TOKEN" http://127.0.0.1:8897/snap # whole page + refs
159
+ curl -H "X-Hermes-Token: $TOKEN" "http://127.0.0.1:8897/click?target=e42&element=Sign%20in"
160
+ curl -H "X-Hermes-Token: $TOKEN" "http://127.0.0.1:8899/list" # saved credentials (no secrets)
161
+ ```
162
+
163
+ ## Multiple agents, no interference
164
+
165
+ Each session gets its own desktop, profile, and ports — two agents cannot fight over tabs
166
+ or invalidate each other's element refs.
167
+
168
+ ```bash
169
+ python scripts/session.py new --name a1 --url https://example.com
170
+ python scripts/session.py list
171
+ python scripts/session.py stop --name a1
172
+ ```
173
+
174
+ ```
175
+ a1 cdp=18764 stream=8766 driver=8898 UP http://127.0.0.1:8766/ctl
176
+ ```
177
+
178
+ ## Security model
179
+
180
+ - **Passwords never enter the model context.** The user types into a masked popup; the
181
+ broker encrypts with Windows DPAPI, binds the secret to the page origin, and injects it.
182
+ Tool results only contain `{ok, verified, submitted, id_len, pwd_len}`.
183
+ - **Origin-bound.** A credential saved for `https://github.com` is refused on any other origin.
184
+ - **Token-gated.** `token.txt` is generated on first run (mode 600). Read-only probes
185
+ (`/health`, `/status`, `/snapshot`) stay open so the preview keeps working.
186
+ - **Audited.** Every injection appends one line to `<home>/audit.jsonl`: timestamp, action,
187
+ origin, handle, result code. No plaintext, ever.
188
+ - **No CAPTCHA evasion.** This drives a real browser; it does not spoof fingerprints,
189
+ solve puzzles, or defeat anti-bot systems.
190
+
191
+ ## Layout
192
+
193
+ ```
194
+ scripts/
195
+ mcp_server.py standard MCP stdio server (browser_* + cred_*)
196
+ browser_daemon.py persistent driver: snapshot / click / type / tabs / eval
197
+ desk_stream.py live view of the hidden desktop (CDP screencast) + click/type back-channel
198
+ vault_broker.py credential broker: DPAPI, origin check, selector fill with read-back verify
199
+ cred_dialog.py masked popup the user types into
200
+ login_flow.py full login loop: popup → fill → verify → submit → retry on failure
201
+ desk_chrome.py launch / close Chrome on a named private desktop
202
+ private_desktop.py Win32 desktop primitives (CreateDesktopW, WM_SETTEXT, PrintWindow)
203
+ session.py per-agent isolation (desktop + profile + ports)
204
+ svc.py start/stop the three services silently (no console windows)
205
+ ports.py port allocation: if the default is taken, pick a free one
206
+ auth.py token file + header helper
207
+ templates/ preview pages (/ctl interactive, /view read-only)
208
+ docs/SKILL.md the long-form playbook: every pitfall we hit, with measurements
209
+ ```
210
+
211
+ ## Platform
212
+
213
+ Windows only, by design: private desktops (`CreateDesktopW`), DPAPI, and silent process
214
+ launch are Win32 primitives. The browser-control layer itself is portable — porting means
215
+ replacing `private_desktop.py` and the DPAPI calls.
216
+
217
+ ## Acknowledgements
218
+
219
+ Standing on these projects:
220
+
221
+ | Project | How it is used |
222
+ |---|---|
223
+ | [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp) | The accessibility-snapshot engine. `mcp_server.py` proxies `browser_*` tools to it; its `[ref=...]` model is what makes coordinate-free clicking possible. (Apache-2.0) |
224
+ | [microsoft/playwright](https://github.com/microsoft/playwright) | The browser driver underneath playwright-mcp. (Apache-2.0) |
225
+ | [python-websockets/websockets](https://github.com/python-websockets/websockets) | CDP transport for the driver, the stream and the broker. |
226
+ | [python-pillow/Pillow](https://github.com/python-pillow/Pillow) | JPEG handling for the live preview and the drawn cursor. |
227
+
228
+ Design ideas borrowed, no code vendored:
229
+
230
+ - [browserbase/stagehand](https://github.com/browserbase/stagehand) — `observe()` returning real
231
+ selectors instead of pixels; that is why this project resolves elements by selector or ref
232
+ and never by coordinates.
233
+ - [browser-use/browser-use](https://github.com/browser-use/browser-use) — the agent loop shape:
234
+ snapshot, act, verify.
235
+ - [web-infra-dev/midscene](https://github.com/web-infra-dev/midscene) — the vision-based fallback
236
+ idea for canvas-only UIs (deliberately not enabled by default; it is slower).
237
+
238
+ ## License
239
+
240
+ MIT. The Apache-2.0 licenses of playwright-mcp and Playwright apply to those components.
@@ -0,0 +1,219 @@
1
+ # Moonlighting
2
+
3
+ **Invisible hands for your AI agent.** A real Chrome, on its own hidden Windows desktop,
4
+ driven by element references instead of coordinates — with credentials that never reach
5
+ the model.
6
+
7
+ **English** · [中文](README.zh-CN.md)
8
+
9
+ ```
10
+ ┌─ your desktop ─────────────────────────────┐
11
+ │ Hermes / Claude / Codex / any MCP client │
12
+ └───────────────┬────────────────────────────┘
13
+ │ MCP (stdio) or local HTTP
14
+ ┌───────────────▼────────────────────────────┐
15
+ │ hidden desktop "HermesDesk" │
16
+ │ ┌──────────────────────────────────────┐ │
17
+ │ │ real Chrome + persistent profile │ │
18
+ │ │ (logins, wallet, extensions) │ │
19
+ │ └──────────────────────────────────────┘ │
20
+ │ credential broker (DPAPI, origin-bound) │
21
+ └─────────────────────────────────────────────┘
22
+ ```
23
+
24
+ You never see a window flash. The agent never sees a password.
25
+
26
+ ## Why
27
+
28
+ Driving a browser as an agent usually goes wrong in three places:
29
+
30
+ | Problem | What most setups do | What this does |
31
+ |---|---|---|
32
+ | **Finding elements** | screenshot + guess coordinates | one accessibility snapshot, act on `[ref=e42]` |
33
+ | **Losing state** | fresh profile every run | persistent profile on a private desktop; reopen and the session is still there |
34
+ | **Handling passwords** | paste into the prompt | user types into a masked popup → DPAPI-encrypted locally → injected by the broker, never returned to the model |
35
+
36
+ ## Install
37
+
38
+ Requires Windows 10/11, Python 3.10+, Node 18+ (for the Playwright MCP child), Chrome.
39
+
40
+ ### Option A — pip (no paths to edit)
41
+
42
+ ```bash
43
+ pip install moonlighting
44
+ moonlighting-setup --options # where should browser data live?
45
+ moonlighting-setup --home "D:\moonlighting"
46
+ moonlighting # starts stream + broker + driver
47
+ ```
48
+
49
+ Then wire it into your MCP client with **no absolute paths at all**:
50
+
51
+ ```json
52
+ { "mcpServers": { "browser": { "command": "moonlighting-mcp" } } }
53
+ ```
54
+
55
+ `moonlighting-mcp`, `moonlighting-setup`, `moonlighting-session` and `moonlighting`
56
+ are console scripts installed by the package.
57
+
58
+ ### Option B — clone
59
+
60
+ ```bash
61
+ git clone https://github.com/xinyuzjj/moonlighting.git
62
+ cd moonlighting
63
+ pip install -r requirements.txt
64
+
65
+ python scripts/setup.py --options # where should browser data live?
66
+ python scripts/setup.py --home "D:\moonlighting"
67
+ python scripts/svc.py # starts stream + broker + driver
68
+ ```
69
+
70
+ Either way, `install_mcp.py` writes the right config for you — it detects whether
71
+ `moonlighting-mcp` is installed and falls back to absolute script paths if not:
72
+
73
+ ```bash
74
+ python scripts/install_mcp.py # print configs with real paths
75
+ python scripts/install_mcp.py --write claude # write it (backs up the original)
76
+ python scripts/install_mcp.py --write codex
77
+ python scripts/install_mcp.py --write all
78
+ ```
79
+
80
+ `svc.py` / `moonlighting` prints a preview URL. Open it in the Hermes preview pane (or any
81
+ browser) to watch and even click into the hidden desktop:
82
+
83
+ ```
84
+ preview: http://127.0.0.1:8765/ctl
85
+ ```
86
+
87
+ First run creates `<home>/token.txt` — every state-changing endpoint requires it.
88
+
89
+ ## Use it from an agent
90
+
91
+ ### Any MCP client (Claude Desktop, Claude Code, Codex CLI, Hermes)
92
+
93
+ `scripts/mcp_server.py` is a standard MCP stdio server exposing **27 tools**
94
+ (25 `browser_*` plus `cred_list` / `cred_fill`).
95
+
96
+ If you installed with pip, the whole config is one line — no paths:
97
+
98
+ ```json
99
+ { "mcpServers": { "browser": { "command": "moonlighting-mcp" } } }
100
+ ```
101
+
102
+ Cloned instead? Let `install_mcp.py` fill in the real paths:
103
+
104
+ ```bash
105
+ python scripts/install_mcp.py --write all
106
+ ```
107
+
108
+ Or write it by hand (Claude Desktop / Claude Code, `mcpServers`):
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "browser": {
114
+ "command": "python",
115
+ "args": ["C:\\path\\to\\moonlighting\\scripts\\mcp_server.py",
116
+ "--cdp-endpoint", "http://127.0.0.1:18763"]
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Codex CLI (`~/.codex/config.toml` — TOML, snake_case, and give it more startup time):
123
+
124
+ ```toml
125
+ [mcp_servers.browser]
126
+ command = "python"
127
+ args = ["C:\\path\\to\\moonlighting\\scripts\\mcp_server.py", "--cdp-endpoint", "http://127.0.0.1:18763"]
128
+ startup_timeout_sec = 60
129
+ tool_timeout_sec = 120
130
+ ```
131
+
132
+ ### Plain HTTP
133
+
134
+ ```bash
135
+ TOKEN=$(python scripts/auth.py --print | sed 's/^token: //')
136
+
137
+ curl -H "X-Hermes-Token: $TOKEN" http://127.0.0.1:8897/snap # whole page + refs
138
+ curl -H "X-Hermes-Token: $TOKEN" "http://127.0.0.1:8897/click?target=e42&element=Sign%20in"
139
+ curl -H "X-Hermes-Token: $TOKEN" "http://127.0.0.1:8899/list" # saved credentials (no secrets)
140
+ ```
141
+
142
+ ## Multiple agents, no interference
143
+
144
+ Each session gets its own desktop, profile, and ports — two agents cannot fight over tabs
145
+ or invalidate each other's element refs.
146
+
147
+ ```bash
148
+ python scripts/session.py new --name a1 --url https://example.com
149
+ python scripts/session.py list
150
+ python scripts/session.py stop --name a1
151
+ ```
152
+
153
+ ```
154
+ a1 cdp=18764 stream=8766 driver=8898 UP http://127.0.0.1:8766/ctl
155
+ ```
156
+
157
+ ## Security model
158
+
159
+ - **Passwords never enter the model context.** The user types into a masked popup; the
160
+ broker encrypts with Windows DPAPI, binds the secret to the page origin, and injects it.
161
+ Tool results only contain `{ok, verified, submitted, id_len, pwd_len}`.
162
+ - **Origin-bound.** A credential saved for `https://github.com` is refused on any other origin.
163
+ - **Token-gated.** `token.txt` is generated on first run (mode 600). Read-only probes
164
+ (`/health`, `/status`, `/snapshot`) stay open so the preview keeps working.
165
+ - **Audited.** Every injection appends one line to `<home>/audit.jsonl`: timestamp, action,
166
+ origin, handle, result code. No plaintext, ever.
167
+ - **No CAPTCHA evasion.** This drives a real browser; it does not spoof fingerprints,
168
+ solve puzzles, or defeat anti-bot systems.
169
+
170
+ ## Layout
171
+
172
+ ```
173
+ scripts/
174
+ mcp_server.py standard MCP stdio server (browser_* + cred_*)
175
+ browser_daemon.py persistent driver: snapshot / click / type / tabs / eval
176
+ desk_stream.py live view of the hidden desktop (CDP screencast) + click/type back-channel
177
+ vault_broker.py credential broker: DPAPI, origin check, selector fill with read-back verify
178
+ cred_dialog.py masked popup the user types into
179
+ login_flow.py full login loop: popup → fill → verify → submit → retry on failure
180
+ desk_chrome.py launch / close Chrome on a named private desktop
181
+ private_desktop.py Win32 desktop primitives (CreateDesktopW, WM_SETTEXT, PrintWindow)
182
+ session.py per-agent isolation (desktop + profile + ports)
183
+ svc.py start/stop the three services silently (no console windows)
184
+ ports.py port allocation: if the default is taken, pick a free one
185
+ auth.py token file + header helper
186
+ templates/ preview pages (/ctl interactive, /view read-only)
187
+ docs/SKILL.md the long-form playbook: every pitfall we hit, with measurements
188
+ ```
189
+
190
+ ## Platform
191
+
192
+ Windows only, by design: private desktops (`CreateDesktopW`), DPAPI, and silent process
193
+ launch are Win32 primitives. The browser-control layer itself is portable — porting means
194
+ replacing `private_desktop.py` and the DPAPI calls.
195
+
196
+ ## Acknowledgements
197
+
198
+ Standing on these projects:
199
+
200
+ | Project | How it is used |
201
+ |---|---|
202
+ | [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp) | The accessibility-snapshot engine. `mcp_server.py` proxies `browser_*` tools to it; its `[ref=...]` model is what makes coordinate-free clicking possible. (Apache-2.0) |
203
+ | [microsoft/playwright](https://github.com/microsoft/playwright) | The browser driver underneath playwright-mcp. (Apache-2.0) |
204
+ | [python-websockets/websockets](https://github.com/python-websockets/websockets) | CDP transport for the driver, the stream and the broker. |
205
+ | [python-pillow/Pillow](https://github.com/python-pillow/Pillow) | JPEG handling for the live preview and the drawn cursor. |
206
+
207
+ Design ideas borrowed, no code vendored:
208
+
209
+ - [browserbase/stagehand](https://github.com/browserbase/stagehand) — `observe()` returning real
210
+ selectors instead of pixels; that is why this project resolves elements by selector or ref
211
+ and never by coordinates.
212
+ - [browser-use/browser-use](https://github.com/browser-use/browser-use) — the agent loop shape:
213
+ snapshot, act, verify.
214
+ - [web-infra-dev/midscene](https://github.com/web-infra-dev/midscene) — the vision-based fallback
215
+ idea for canvas-only UIs (deliberately not enabled by default; it is slower).
216
+
217
+ ## License
218
+
219
+ MIT. The Apache-2.0 licenses of playwright-mcp and Playwright apply to those components.
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "moonlighting"
7
+ version = "0.1.0"
8
+ description = "Invisible hands for your AI agent: a real Chrome on a hidden desktop, ref-based control, credentials that never reach the model."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "junjunni" }]
13
+ keywords = ["mcp", "browser-automation", "ai-agents", "playwright", "credentials", "windows"]
14
+ dependencies = ["websockets>=12", "pillow>=10"]
15
+ classifiers = [
16
+ "Operating System :: Microsoft :: Windows",
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Topic :: Software Development :: Testing",
20
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/xinyuzjj/moonlighting"
25
+ Issues = "https://github.com/xinyuzjj/moonlighting/issues"
26
+
27
+ [project.scripts]
28
+ moonlighting = "svc:cli"
29
+ moonlighting-mcp = "mcp_server:cli"
30
+ moonlighting-setup = "setup:cli"
31
+ moonlighting-session = "session:cli"
32
+
33
+ [tool.setuptools]
34
+ package-dir = { "" = "scripts" }
35
+ py-modules = [
36
+ "auth",
37
+ "browser_daemon",
38
+ "cdp_click",
39
+ "cred_dialog",
40
+ "desk_browser",
41
+ "desk_chrome",
42
+ "desk_restore",
43
+ "desk_stream",
44
+ "ext_click",
45
+ "login_flow",
46
+ "mcp_server",
47
+ "ports",
48
+ "private_desktop",
49
+ "qclick",
50
+ "session",
51
+ "setup",
52
+ "svc",
53
+ "vault_broker",
54
+ "wclick",
55
+ ]
56
+
57
+ [tool.setuptools.data-files]
58
+ "share/moonlighting" = ["scripts/cred_form.html"]
59
+ "share/moonlighting/templates" = [
60
+ "scripts/templates/desk_ctl.html",
61
+ "scripts/templates/desk_view.html",
62
+ ]
@@ -0,0 +1,65 @@
1
+ """本地服务的访问令牌:挡住同机其他进程乱调。
2
+
3
+ 问题:8899 上跑着凭据代理,任何本机进程都能 `GET /inject_sel?handle=...` 让浏览器
4
+ 把密码填进一个 origin 相同的页面(同源伪造),8897/8765 同理能驱动浏览器和注入按键。
5
+
6
+ 做法:
7
+ * 首次启动生成 32 字节随机 token,写到 `<home>/token.txt`
8
+ * 所有**会改变状态**的接口要求 `X-Hermes-Token` 头(或 `?token=`)
9
+ * 只读接口(/snapshot、/status、/list 的元信息)不强制,方便看画面
10
+ * 客户端(cred_dialog、login_flow)自己从同一个文件读,用户无感
11
+
12
+ 环境变量 `HERMES_TOKEN` 优先,便于 CI/临时覆盖。
13
+ """
14
+ import os
15
+ import secrets
16
+
17
+ HOME = os.environ.get("HERMES_BROWSER_HOME") or os.path.join(
18
+ os.environ.get("LOCALAPPDATA") or os.path.expanduser("~"), "hermes-browser")
19
+ try: # 装了 setup.py 就跟着它走
20
+ import ports as _ports
21
+ HOME = _ports.home()
22
+ except Exception: # noqa: BLE001
23
+ pass
24
+ TOKEN_FILE = os.environ.get("HERMES_TOKEN_FILE") or os.path.join(HOME, "token.txt")
25
+
26
+
27
+ def token():
28
+ env = os.environ.get("HERMES_TOKEN")
29
+ if env:
30
+ return env
31
+ try:
32
+ with open(TOKEN_FILE, encoding="utf-8") as f:
33
+ t = f.read().strip()
34
+ if t:
35
+ return t
36
+ except OSError:
37
+ pass
38
+ os.makedirs(os.path.dirname(TOKEN_FILE), exist_ok=True)
39
+ t = secrets.token_urlsafe(24)
40
+ with open(TOKEN_FILE, "w", encoding="utf-8") as f:
41
+ f.write(t)
42
+ try:
43
+ os.chmod(TOKEN_FILE, 0o600)
44
+ except OSError:
45
+ pass
46
+ return t
47
+
48
+
49
+ def check(headers, query):
50
+ """headers: email.Message;query: dict[str, list[str]] 或 dict[str,str]"""
51
+ want = token()
52
+ got = headers.get("X-Hermes-Token") if headers is not None else None
53
+ if not got and query:
54
+ v = query.get("token")
55
+ got = (v[0] if isinstance(v, list) else v) if v else None
56
+ return bool(got) and secrets.compare_digest(str(got), want)
57
+
58
+
59
+ def headers_for_client():
60
+ return {"X-Hermes-Token": token()}
61
+
62
+
63
+ if __name__ == "__main__":
64
+ print("token 文件:", TOKEN_FILE)
65
+ print("token:", token()[:8] + "…(已写入文件,不完整打印)")