maestro-browser-bridge 1.18.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.
- maestro_browser_bridge-1.18.0/PKG-INFO +118 -0
- maestro_browser_bridge-1.18.0/README.md +82 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/__init__.py +12 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/cli.py +269 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/config.py +83 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/daemon.py +356 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/handlers.py +330 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/hmac_sig.py +67 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/logging.py +88 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/playwright_session.py +262 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/protocol.py +148 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge/security.py +186 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge.egg-info/PKG-INFO +118 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge.egg-info/SOURCES.txt +23 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge.egg-info/dependency_links.txt +1 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge.egg-info/entry_points.txt +2 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge.egg-info/requires.txt +10 -0
- maestro_browser_bridge-1.18.0/maestro_browser_bridge.egg-info/top_level.txt +1 -0
- maestro_browser_bridge-1.18.0/pyproject.toml +60 -0
- maestro_browser_bridge-1.18.0/setup.cfg +4 -0
- maestro_browser_bridge-1.18.0/tests/test_cli.py +143 -0
- maestro_browser_bridge-1.18.0/tests/test_daemon_local_server.py +135 -0
- maestro_browser_bridge-1.18.0/tests/test_handlers.py +312 -0
- maestro_browser_bridge-1.18.0/tests/test_protocol.py +152 -0
- maestro_browser_bridge-1.18.0/tests/test_security.py +158 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maestro-browser-bridge
|
|
3
|
+
Version: 1.18.0
|
|
4
|
+
Summary: Local browser bridge for Remote AI Maestro — runs a Playwright-managed Chromium and exposes BROWSER_* commands over WebSocket.
|
|
5
|
+
Author-email: Shaun <scwj1210@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shaunchew/remote-ai-maestro
|
|
8
|
+
Project-URL: Repository, https://github.com/shaunchew/remote-ai-maestro
|
|
9
|
+
Project-URL: Documentation, https://github.com/shaunchew/remote-ai-maestro#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/shaunchew/remote-ai-maestro/issues
|
|
11
|
+
Keywords: remote,ai,browser,playwright,chromium,automation,maestro
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
Requires-Dist: websockets>=13.0
|
|
28
|
+
Requires-Dist: click>=8.0
|
|
29
|
+
Requires-Dist: pydantic>=2
|
|
30
|
+
Requires-Dist: structlog>=24
|
|
31
|
+
Requires-Dist: playwright>=1.45
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-mock>=3.12; extra == "test"
|
|
36
|
+
|
|
37
|
+
# maestro-browser-bridge
|
|
38
|
+
|
|
39
|
+
Local browser bridge for [Remote AI Maestro](https://remote-ai-maestro.thesavvydeveloper.com).
|
|
40
|
+
|
|
41
|
+
The bridge is the companion app you install on the same laptop where you keep
|
|
42
|
+
your real browser. It launches a Playwright-managed Chromium in an *isolated*
|
|
43
|
+
profile and exposes it over a WebSocket so an AI agent running inside a Maestro
|
|
44
|
+
sandbox can drive it — navigate, click, type, screenshot, read DOM, capture
|
|
45
|
+
console logs.
|
|
46
|
+
|
|
47
|
+
This is the **Phase 2** counterpart to the simpler Phase 1 `maestro browser
|
|
48
|
+
open <url>` command, which only pops URLs in your already-open browser via the
|
|
49
|
+
frontend's `window.open`. The bridge gives Claude (or any agent) real browser
|
|
50
|
+
control without ever touching your logged-in Chrome profile.
|
|
51
|
+
|
|
52
|
+
## Why a separate process
|
|
53
|
+
|
|
54
|
+
The AI agent runs on a remote machine (or an EC2 sandbox) but the browser to
|
|
55
|
+
be controlled lives on your local desktop. There is no other process that runs
|
|
56
|
+
on the user's actual laptop in a known-good location — the agent daemon runs
|
|
57
|
+
on whichever machine the sandbox is bound to, which may be EC2. So we ship a
|
|
58
|
+
dedicated bridge process that runs *on the user's laptop*, maintains an
|
|
59
|
+
outbound WebSocket to the relay, and translates `BROWSER_*` messages into
|
|
60
|
+
Playwright calls.
|
|
61
|
+
|
|
62
|
+
The bridge is per-user (one bridge per laptop) — distinct from the agent
|
|
63
|
+
daemon, which is per-machine. Relay routing maps `user_id → bridge`.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install maestro-browser-bridge
|
|
69
|
+
|
|
70
|
+
# Pre-download the Playwright Chromium binary (~200MB)
|
|
71
|
+
playwright install chromium
|
|
72
|
+
|
|
73
|
+
# Configure once
|
|
74
|
+
maestro-browser-bridge configure \
|
|
75
|
+
--relay-url wss://relay.thesavvydeveloper.com \
|
|
76
|
+
--user-id YOUR_USER_UUID \
|
|
77
|
+
--user-token YOUR_BRIDGE_TOKEN
|
|
78
|
+
|
|
79
|
+
# Start
|
|
80
|
+
maestro-browser-bridge start
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--detach` is wired but not yet implemented; for now run the bridge in the
|
|
84
|
+
foreground (or wrap it in your own service manager — `launchd` / `systemd`
|
|
85
|
+
unit shipping is planned).
|
|
86
|
+
|
|
87
|
+
## Status
|
|
88
|
+
|
|
89
|
+
This package is the **scaffold** for the Phase 2 work tracked in
|
|
90
|
+
`_reference/11_BROWSER_CONTROL.md` and `implementation_plan.md`'s
|
|
91
|
+
"BC — Browser Control (Phase 2)" section. The protocol models, dispatch
|
|
92
|
+
shell, and CLI surface are in place; the actual Playwright handlers raise
|
|
93
|
+
`NotImplementedError` and will be wired in subsequent commits (BC.1.2 in the
|
|
94
|
+
plan).
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Editable install with test deps (from the repo root)
|
|
100
|
+
pip install -e ./bridge[test]
|
|
101
|
+
|
|
102
|
+
# Run tests
|
|
103
|
+
cd bridge && pytest
|
|
104
|
+
|
|
105
|
+
# Smoke check the CLI
|
|
106
|
+
maestro-browser-bridge --version
|
|
107
|
+
maestro-browser-bridge --help
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## References
|
|
111
|
+
|
|
112
|
+
- Architecture & wire protocol: `_reference/11_BROWSER_CONTROL.md`
|
|
113
|
+
- Milestone plan: `_reference/12_BROWSER_PHASE2_PLAN.md`
|
|
114
|
+
- Pip package contract: `implementation_plan.md` § BC.1
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# maestro-browser-bridge
|
|
2
|
+
|
|
3
|
+
Local browser bridge for [Remote AI Maestro](https://remote-ai-maestro.thesavvydeveloper.com).
|
|
4
|
+
|
|
5
|
+
The bridge is the companion app you install on the same laptop where you keep
|
|
6
|
+
your real browser. It launches a Playwright-managed Chromium in an *isolated*
|
|
7
|
+
profile and exposes it over a WebSocket so an AI agent running inside a Maestro
|
|
8
|
+
sandbox can drive it — navigate, click, type, screenshot, read DOM, capture
|
|
9
|
+
console logs.
|
|
10
|
+
|
|
11
|
+
This is the **Phase 2** counterpart to the simpler Phase 1 `maestro browser
|
|
12
|
+
open <url>` command, which only pops URLs in your already-open browser via the
|
|
13
|
+
frontend's `window.open`. The bridge gives Claude (or any agent) real browser
|
|
14
|
+
control without ever touching your logged-in Chrome profile.
|
|
15
|
+
|
|
16
|
+
## Why a separate process
|
|
17
|
+
|
|
18
|
+
The AI agent runs on a remote machine (or an EC2 sandbox) but the browser to
|
|
19
|
+
be controlled lives on your local desktop. There is no other process that runs
|
|
20
|
+
on the user's actual laptop in a known-good location — the agent daemon runs
|
|
21
|
+
on whichever machine the sandbox is bound to, which may be EC2. So we ship a
|
|
22
|
+
dedicated bridge process that runs *on the user's laptop*, maintains an
|
|
23
|
+
outbound WebSocket to the relay, and translates `BROWSER_*` messages into
|
|
24
|
+
Playwright calls.
|
|
25
|
+
|
|
26
|
+
The bridge is per-user (one bridge per laptop) — distinct from the agent
|
|
27
|
+
daemon, which is per-machine. Relay routing maps `user_id → bridge`.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install maestro-browser-bridge
|
|
33
|
+
|
|
34
|
+
# Pre-download the Playwright Chromium binary (~200MB)
|
|
35
|
+
playwright install chromium
|
|
36
|
+
|
|
37
|
+
# Configure once
|
|
38
|
+
maestro-browser-bridge configure \
|
|
39
|
+
--relay-url wss://relay.thesavvydeveloper.com \
|
|
40
|
+
--user-id YOUR_USER_UUID \
|
|
41
|
+
--user-token YOUR_BRIDGE_TOKEN
|
|
42
|
+
|
|
43
|
+
# Start
|
|
44
|
+
maestro-browser-bridge start
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--detach` is wired but not yet implemented; for now run the bridge in the
|
|
48
|
+
foreground (or wrap it in your own service manager — `launchd` / `systemd`
|
|
49
|
+
unit shipping is planned).
|
|
50
|
+
|
|
51
|
+
## Status
|
|
52
|
+
|
|
53
|
+
This package is the **scaffold** for the Phase 2 work tracked in
|
|
54
|
+
`_reference/11_BROWSER_CONTROL.md` and `implementation_plan.md`'s
|
|
55
|
+
"BC — Browser Control (Phase 2)" section. The protocol models, dispatch
|
|
56
|
+
shell, and CLI surface are in place; the actual Playwright handlers raise
|
|
57
|
+
`NotImplementedError` and will be wired in subsequent commits (BC.1.2 in the
|
|
58
|
+
plan).
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Editable install with test deps (from the repo root)
|
|
64
|
+
pip install -e ./bridge[test]
|
|
65
|
+
|
|
66
|
+
# Run tests
|
|
67
|
+
cd bridge && pytest
|
|
68
|
+
|
|
69
|
+
# Smoke check the CLI
|
|
70
|
+
maestro-browser-bridge --version
|
|
71
|
+
maestro-browser-bridge --help
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
- Architecture & wire protocol: `_reference/11_BROWSER_CONTROL.md`
|
|
77
|
+
- Milestone plan: `_reference/12_BROWSER_PHASE2_PLAN.md`
|
|
78
|
+
- Pip package contract: `implementation_plan.md` § BC.1
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Maestro Browser Bridge — local Playwright-driven Chromium for the
|
|
2
|
+
Remote AI Maestro platform.
|
|
3
|
+
|
|
4
|
+
The bridge runs on the user's *local* laptop (not on a registered Maestro
|
|
5
|
+
"machine") and maintains an outbound WebSocket to the relay so AI agents
|
|
6
|
+
running inside remote sandboxes can drive a real browser the user can see
|
|
7
|
+
and intervene in.
|
|
8
|
+
|
|
9
|
+
See ``_reference/11_BROWSER_CONTROL.md`` for the design spec.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Click CLI for the ``maestro-browser-bridge`` package.
|
|
2
|
+
|
|
3
|
+
Subcommands mirror the shape of ``remotedev-agent``'s CLI so the install
|
|
4
|
+
experience is familiar:
|
|
5
|
+
|
|
6
|
+
- ``configure`` — write config to ``~/.maestro/bridge-config.json``.
|
|
7
|
+
- ``start`` — load config and run the daemon (``--detach`` stubbed).
|
|
8
|
+
- ``stop`` — placeholder.
|
|
9
|
+
- ``status`` — print version + config path.
|
|
10
|
+
- ``install-service`` — placeholder (launchd/systemd installer lands in BC.1.4).
|
|
11
|
+
- ``--version`` — read from package metadata.
|
|
12
|
+
|
|
13
|
+
Only the bits required by the acceptance criteria are wired today; the
|
|
14
|
+
``daemon.py`` skeleton already documents the rest as TODOs.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from .config import CONFIG_FILE, DEFAULT_LOCAL_PORT, BridgeConfig
|
|
25
|
+
from .logging import configure_logging
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _package_version() -> str:
|
|
29
|
+
"""Resolve the installed package version, falling back to ``__init__.__version__``.
|
|
30
|
+
|
|
31
|
+
Mirrors the pattern in ``agent/remotedev_agent/cli.py`` so source-checkout
|
|
32
|
+
runs (without dist-info) still produce something useful.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return _pkg_version("maestro-browser-bridge")
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
from . import __version__
|
|
38
|
+
return __version__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group()
|
|
42
|
+
@click.version_option(version=_package_version(), prog_name="maestro-browser-bridge")
|
|
43
|
+
def cli() -> None:
|
|
44
|
+
"""Maestro Browser Bridge — local Playwright Chromium controller."""
|
|
45
|
+
# Configure structlog once per CLI invocation. Cheap (idempotent), and
|
|
46
|
+
# ensures even short-lived commands like ``status`` emit JSON in prod.
|
|
47
|
+
configure_logging()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# configure
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cli.command()
|
|
56
|
+
@click.option(
|
|
57
|
+
"--relay-url",
|
|
58
|
+
required=True,
|
|
59
|
+
help="WebSocket URL of the Maestro relay (e.g. wss://relay.thesavvydeveloper.com).",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--user-id",
|
|
63
|
+
required=True,
|
|
64
|
+
help="Maestro user UUID — determines relay routing (user_id → bridge).",
|
|
65
|
+
)
|
|
66
|
+
@click.option(
|
|
67
|
+
"--user-token",
|
|
68
|
+
required=True,
|
|
69
|
+
help="Per-user bridge token issued by the backend.",
|
|
70
|
+
)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--port",
|
|
73
|
+
type=int,
|
|
74
|
+
default=DEFAULT_LOCAL_PORT,
|
|
75
|
+
show_default=True,
|
|
76
|
+
help="Local loopback port for the dev WS server.",
|
|
77
|
+
)
|
|
78
|
+
def configure(relay_url: str, user_id: str, user_token: str, port: int) -> None:
|
|
79
|
+
"""Persist bridge configuration to ``~/.maestro/bridge-config.json``.
|
|
80
|
+
|
|
81
|
+
The file is written 0600 inside a 0700 directory — equivalent posture
|
|
82
|
+
to the agent's config, since the bridge token is approximately as
|
|
83
|
+
sensitive as a Maestro JWT.
|
|
84
|
+
"""
|
|
85
|
+
config = BridgeConfig(
|
|
86
|
+
relay_url=relay_url,
|
|
87
|
+
user_id=user_id,
|
|
88
|
+
user_token=user_token,
|
|
89
|
+
port=port,
|
|
90
|
+
)
|
|
91
|
+
config.save()
|
|
92
|
+
click.echo(f"Config saved to {CONFIG_FILE}.")
|
|
93
|
+
click.echo("Start the bridge with: maestro-browser-bridge start")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# start
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@cli.command()
|
|
102
|
+
@click.option(
|
|
103
|
+
"--detach",
|
|
104
|
+
"-d",
|
|
105
|
+
is_flag=True,
|
|
106
|
+
help="Run in the background (not yet implemented — exits non-zero).",
|
|
107
|
+
)
|
|
108
|
+
def start(detach: bool) -> None:
|
|
109
|
+
"""Launch the bridge daemon in the foreground.
|
|
110
|
+
|
|
111
|
+
``--detach`` is wired so the flag is visible in ``--help`` but the
|
|
112
|
+
background-launch path (PID file, log rotation, daemonization) lands
|
|
113
|
+
in a later milestone — see ``agent/remotedev_agent/process_manager.py``
|
|
114
|
+
for the pattern we'll mirror.
|
|
115
|
+
"""
|
|
116
|
+
if detach:
|
|
117
|
+
click.echo("detach not yet implemented", err=True)
|
|
118
|
+
raise SystemExit(1)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
config = BridgeConfig.load()
|
|
122
|
+
except FileNotFoundError as e:
|
|
123
|
+
click.echo(str(e), err=True)
|
|
124
|
+
raise SystemExit(1)
|
|
125
|
+
|
|
126
|
+
# Import inside the command so test collection (which doesn't run
|
|
127
|
+
# ``start``) doesn't pay the cost of pulling in websockets / playwright
|
|
128
|
+
# just to introspect ``--help``.
|
|
129
|
+
from .daemon import BridgeDaemon
|
|
130
|
+
|
|
131
|
+
click.echo(f"Starting bridge for user {config.user_id}...")
|
|
132
|
+
daemon = BridgeDaemon(config)
|
|
133
|
+
try:
|
|
134
|
+
asyncio.run(daemon.run())
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
# Ctrl-C is a normal exit path during dev; don't print a stack
|
|
137
|
+
# trace at the user.
|
|
138
|
+
click.echo("Bridge stopped.")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# stop
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@cli.command("stop")
|
|
147
|
+
def cmd_stop() -> None:
|
|
148
|
+
"""Stop a detached bridge daemon (placeholder).
|
|
149
|
+
|
|
150
|
+
Implementation lands alongside ``--detach`` in BC.1.4 — same
|
|
151
|
+
process-manager surface as the agent.
|
|
152
|
+
"""
|
|
153
|
+
click.echo("stop not yet implemented — run the bridge in the foreground for now.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# status
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@cli.command("status")
|
|
162
|
+
def cmd_status() -> None:
|
|
163
|
+
"""Print version + config path. Loads the saved config when available."""
|
|
164
|
+
click.echo(f"maestro-browser-bridge {_package_version()}")
|
|
165
|
+
click.echo(f"Config file: {CONFIG_FILE}")
|
|
166
|
+
try:
|
|
167
|
+
config = BridgeConfig.load()
|
|
168
|
+
except FileNotFoundError:
|
|
169
|
+
click.echo("Config: NOT CONFIGURED (run 'maestro-browser-bridge configure ...').")
|
|
170
|
+
return
|
|
171
|
+
click.echo(f"Relay URL: {config.relay_url}")
|
|
172
|
+
click.echo(f"User ID: {config.user_id}")
|
|
173
|
+
click.echo(f"Local port: {config.port}")
|
|
174
|
+
# NB — never print user_token; it's roughly as sensitive as a session JWT.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# install-service
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@cli.command("install-service")
|
|
183
|
+
def install_service() -> None:
|
|
184
|
+
"""Install as a background service (placeholder).
|
|
185
|
+
|
|
186
|
+
Plan: mirror ``agent/remotedev_agent/service/`` — launchd on macOS,
|
|
187
|
+
systemd on Linux. Tracked in BC.1.4.
|
|
188
|
+
"""
|
|
189
|
+
click.echo("install-service not yet implemented — see BC.1.4 in implementation_plan.md.")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Kill switch — pause / resume (BC.6.1)
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _send_bridge_control(action: str) -> dict:
|
|
198
|
+
"""Send a BRIDGE_CONTROL command to the running bridge via 127.0.0.1.
|
|
199
|
+
|
|
200
|
+
Synchronous — uses raw asyncio.run + websockets.connect so the CLI
|
|
201
|
+
doesn't need to manage an event loop itself. Returns the parsed
|
|
202
|
+
reply dict, raises ``ClickException`` on connect / I/O failure.
|
|
203
|
+
"""
|
|
204
|
+
import asyncio
|
|
205
|
+
import json as _json
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
config = BridgeConfig.load()
|
|
209
|
+
except FileNotFoundError as e:
|
|
210
|
+
raise click.ClickException(str(e))
|
|
211
|
+
|
|
212
|
+
async def _go() -> dict:
|
|
213
|
+
# Import lazily so ``--help`` / ``status`` don't pay the websockets cost.
|
|
214
|
+
import websockets
|
|
215
|
+
|
|
216
|
+
url = f"ws://127.0.0.1:{config.port}"
|
|
217
|
+
async with websockets.connect(url, open_timeout=2.0) as ws:
|
|
218
|
+
await ws.send(_json.dumps({"type": "BRIDGE_CONTROL", "action": action}))
|
|
219
|
+
reply_raw = await ws.recv()
|
|
220
|
+
return _json.loads(reply_raw if isinstance(reply_raw, str) else reply_raw.decode("utf-8"))
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
return asyncio.run(_go())
|
|
224
|
+
except (ConnectionRefusedError, OSError) as e:
|
|
225
|
+
raise click.ClickException(
|
|
226
|
+
f"Could not reach bridge on 127.0.0.1:{config.port} ({e}). "
|
|
227
|
+
"Is 'maestro-browser-bridge start' running?"
|
|
228
|
+
)
|
|
229
|
+
except Exception as e: # noqa: BLE001
|
|
230
|
+
raise click.ClickException(f"Bridge control failed: {e}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@cli.command("pause")
|
|
234
|
+
def cmd_pause() -> None:
|
|
235
|
+
"""Pause the bridge — every BROWSER_COMMAND will fail until ``resume``.
|
|
236
|
+
|
|
237
|
+
Use this as a kill switch if Claude is doing something you don't
|
|
238
|
+
want it to. State is in-memory only, so a daemon restart implicitly
|
|
239
|
+
resumes — that's intentional, so the recovery path is just
|
|
240
|
+
``maestro-browser-bridge start``.
|
|
241
|
+
"""
|
|
242
|
+
reply = _send_bridge_control("pause")
|
|
243
|
+
if not reply.get("ok"):
|
|
244
|
+
raise click.ClickException(reply.get("error") or "Unknown error")
|
|
245
|
+
click.echo("Bridge paused. Resume with 'maestro-browser-bridge resume'.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@cli.command("resume")
|
|
249
|
+
def cmd_resume() -> None:
|
|
250
|
+
"""Resume the bridge after a ``pause``."""
|
|
251
|
+
reply = _send_bridge_control("resume")
|
|
252
|
+
if not reply.get("ok"):
|
|
253
|
+
raise click.ClickException(reply.get("error") or "Unknown error")
|
|
254
|
+
click.echo("Bridge resumed.")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@cli.command("bridge-status")
|
|
258
|
+
def cmd_bridge_status() -> None:
|
|
259
|
+
"""Print the running bridge's runtime state (paused / allowlist / rate)."""
|
|
260
|
+
reply = _send_bridge_control("status")
|
|
261
|
+
if not reply.get("ok"):
|
|
262
|
+
raise click.ClickException(reply.get("error") or "Unknown error")
|
|
263
|
+
click.echo(f"paused: {reply.get('paused')}")
|
|
264
|
+
click.echo(f"allowlist hosts: {reply.get('allowlist_size')}")
|
|
265
|
+
click.echo(f"rate limit / min: {reply.get('rate_limit_per_min')}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
cli()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Bridge configuration — persisted as JSON in the user's home dir.
|
|
2
|
+
|
|
3
|
+
Mirrors ``agent/remotedev_agent/config.py`` so the file-on-disk semantics
|
|
4
|
+
(0600 mode, parent dir 0700) are uniform across both daemons. We store the
|
|
5
|
+
config in ``~/.maestro/`` rather than ``~/.remotedev/`` because the bridge
|
|
6
|
+
is a *user-scoped* service (one per laptop, not per machine) while the agent
|
|
7
|
+
is *machine-scoped*. Sharing a dir would conflate two different identities.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# ``~/.maestro`` is the per-user config root for the bridge. The agent uses
|
|
18
|
+
# ``~/.remotedev`` instead — intentionally a different namespace so that an
|
|
19
|
+
# uninstall of one doesn't blow away the other's state.
|
|
20
|
+
CONFIG_DIR = Path.home() / ".maestro"
|
|
21
|
+
CONFIG_FILE = CONFIG_DIR / "bridge-config.json"
|
|
22
|
+
|
|
23
|
+
# Default port for the local WS server. ``127.0.0.1:7654`` is mentioned in
|
|
24
|
+
# the spec; bound to loopback only — never exposed on a public interface
|
|
25
|
+
# because the bridge can issue privileged Playwright commands.
|
|
26
|
+
DEFAULT_LOCAL_PORT = 7654
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class BridgeConfig:
|
|
31
|
+
"""Configuration for the local browser bridge.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
relay_url: WebSocket URL of the Maestro relay (e.g.
|
|
35
|
+
``wss://relay.thesavvydeveloper.com``). The bridge will dial
|
|
36
|
+
``<relay_url>/ws/browser-bridge/<user_id>`` on start.
|
|
37
|
+
user_id: UUID of the Maestro user this bridge belongs to. Determines
|
|
38
|
+
relay routing — one bridge per user (vs. agents which are
|
|
39
|
+
per-machine).
|
|
40
|
+
user_token: Bridge token issued by the backend (single-use issuance,
|
|
41
|
+
see ``POST /browser-bridges`` in the BC.2 plan). Used both as the
|
|
42
|
+
``X-Bridge-Token`` header on connect and as the HMAC signing key
|
|
43
|
+
for every WS message.
|
|
44
|
+
port: Local loopback port for the in-process WS server. Defaults to
|
|
45
|
+
7654. Bound to ``127.0.0.1`` — never a public interface.
|
|
46
|
+
allowlist_extra: User-added hosts the bridge allows for navigate.
|
|
47
|
+
Merged with the default allowlist in ``security.py``.
|
|
48
|
+
rate_limit_per_min: Per-minute BROWSER_COMMAND budget. Defaults to
|
|
49
|
+
60 — enough for an interactive user, low enough to bound a
|
|
50
|
+
runaway script.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
relay_url: str
|
|
54
|
+
user_id: str
|
|
55
|
+
user_token: str
|
|
56
|
+
port: int = DEFAULT_LOCAL_PORT
|
|
57
|
+
allowlist_extra: list[str] = field(default_factory=list)
|
|
58
|
+
rate_limit_per_min: int = 60
|
|
59
|
+
|
|
60
|
+
def save(self) -> None:
|
|
61
|
+
"""Persist config to ``~/.maestro/bridge-config.json`` with 0600 mode.
|
|
62
|
+
|
|
63
|
+
The 0600 / 0700 combo prevents other users on a shared machine from
|
|
64
|
+
reading the bridge token, which is equivalent in blast radius to the
|
|
65
|
+
user's Maestro JWT.
|
|
66
|
+
"""
|
|
67
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
os.chmod(CONFIG_DIR, 0o700)
|
|
69
|
+
CONFIG_FILE.write_text(json.dumps(asdict(self), indent=2))
|
|
70
|
+
os.chmod(CONFIG_FILE, 0o600)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def load(cls) -> "BridgeConfig":
|
|
74
|
+
"""Read the saved config, or raise ``FileNotFoundError`` with a hint."""
|
|
75
|
+
if not CONFIG_FILE.exists():
|
|
76
|
+
raise FileNotFoundError(
|
|
77
|
+
f"Config not found at {CONFIG_FILE}. "
|
|
78
|
+
"Run 'maestro-browser-bridge configure --relay-url ... --user-id ... --user-token ...' first."
|
|
79
|
+
)
|
|
80
|
+
data = json.loads(CONFIG_FILE.read_text())
|
|
81
|
+
# Tolerate older configs that don't carry ``port`` by letting the
|
|
82
|
+
# dataclass default kick in.
|
|
83
|
+
return cls(**data)
|