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.
- moonlighting-0.1.0/LICENSE +21 -0
- moonlighting-0.1.0/PKG-INFO +240 -0
- moonlighting-0.1.0/README.md +219 -0
- moonlighting-0.1.0/pyproject.toml +62 -0
- moonlighting-0.1.0/scripts/auth.py +65 -0
- moonlighting-0.1.0/scripts/browser_daemon.py +213 -0
- moonlighting-0.1.0/scripts/cdp_click.py +160 -0
- moonlighting-0.1.0/scripts/cred_dialog.py +265 -0
- moonlighting-0.1.0/scripts/cred_form.html +113 -0
- moonlighting-0.1.0/scripts/desk_browser.py +198 -0
- moonlighting-0.1.0/scripts/desk_chrome.py +258 -0
- moonlighting-0.1.0/scripts/desk_restore.py +84 -0
- moonlighting-0.1.0/scripts/desk_stream.py +648 -0
- moonlighting-0.1.0/scripts/ext_click.py +152 -0
- moonlighting-0.1.0/scripts/login_flow.py +122 -0
- moonlighting-0.1.0/scripts/mcp_server.py +250 -0
- moonlighting-0.1.0/scripts/moonlighting.egg-info/PKG-INFO +240 -0
- moonlighting-0.1.0/scripts/moonlighting.egg-info/SOURCES.txt +31 -0
- moonlighting-0.1.0/scripts/moonlighting.egg-info/dependency_links.txt +1 -0
- moonlighting-0.1.0/scripts/moonlighting.egg-info/entry_points.txt +5 -0
- moonlighting-0.1.0/scripts/moonlighting.egg-info/requires.txt +2 -0
- moonlighting-0.1.0/scripts/moonlighting.egg-info/top_level.txt +19 -0
- moonlighting-0.1.0/scripts/ports.py +169 -0
- moonlighting-0.1.0/scripts/private_desktop.py +212 -0
- moonlighting-0.1.0/scripts/qclick.py +132 -0
- moonlighting-0.1.0/scripts/session.py +176 -0
- moonlighting-0.1.0/scripts/setup.py +154 -0
- moonlighting-0.1.0/scripts/svc.py +153 -0
- moonlighting-0.1.0/scripts/templates/desk_ctl.html +106 -0
- moonlighting-0.1.0/scripts/templates/desk_view.html +77 -0
- moonlighting-0.1.0/scripts/vault_broker.py +663 -0
- moonlighting-0.1.0/scripts/wclick.py +131 -0
- 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] + "…(已写入文件,不完整打印)")
|