browserwright 0.7.3__tar.gz → 0.7.4__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 (94) hide show
  1. {browserwright-0.7.3 → browserwright-0.7.4}/PKG-INFO +1 -1
  2. {browserwright-0.7.3 → browserwright-0.7.4}/README.md +10 -0
  3. {browserwright-0.7.3 → browserwright-0.7.4}/pyproject.toml +1 -1
  4. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/cli.py +32 -2
  5. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/config.py +22 -0
  6. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/facade.py +39 -9
  7. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/listener.py +6 -1
  8. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright.egg-info/PKG-INFO +1 -1
  9. {browserwright-0.7.3 → browserwright-0.7.4}/setup.cfg +0 -0
  10. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/__init__.py +0 -0
  11. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/__main__.py +0 -0
  12. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/_executor/__init__.py +0 -0
  13. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/_executor/__main__.py +0 -0
  14. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/_executor/client.py +0 -0
  15. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/_executor/process.py +0 -0
  16. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/_executor/protocol.py +0 -0
  17. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/api.py +0 -0
  18. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/cdp.py +0 -0
  19. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/cli.py +0 -0
  20. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/__init__.py +0 -0
  21. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/_ipc.py +0 -0
  22. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/_stale.py +0 -0
  23. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/backends/__init__.py +0 -0
  24. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/backends/base.py +0 -0
  25. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/backends/cdp.py +0 -0
  26. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/backends/extension.py +0 -0
  27. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/doctor.py +0 -0
  28. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/errors.py +0 -0
  29. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/launch_chrome.py +0 -0
  30. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/observability.py +0 -0
  31. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/platforms.py +0 -0
  32. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/resolver.py +0 -0
  33. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/__init__.py +0 -0
  34. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/daemon.py +0 -0
  35. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/executor_registry.py +0 -0
  36. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/extension_upstream.py +0 -0
  37. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/facade_extension.py +0 -0
  38. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/proxy.py +0 -0
  39. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/relay.py +0 -0
  40. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/state.py +0 -0
  41. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/upstream.py +0 -0
  42. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/server/verbs.py +0 -0
  43. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/daemon/userscripts.py +0 -0
  44. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/discovery.py +0 -0
  45. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/errors.py +0 -0
  46. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/health.py +0 -0
  47. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/install.py +0 -0
  48. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/memory/__init__.py +0 -0
  49. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/memory/_lock.py +0 -0
  50. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/memory/_md.py +0 -0
  51. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/memory/_yaml.py +0 -0
  52. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/memory/global_mem.py +0 -0
  53. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/memory/site_mem.py +0 -0
  54. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/mode_b_client.py +0 -0
  55. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/output_schema.py +0 -0
  56. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/primitives/__init__.py +0 -0
  57. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/primitives/discovery_api.py +0 -0
  58. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/primitives/http.py +0 -0
  59. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/primitives/site.py +0 -0
  60. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/repl/__init__.py +0 -0
  61. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/repl/_namespace.py +0 -0
  62. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/repl/_smart_goto.py +0 -0
  63. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/repl/inline.py +0 -0
  64. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/repl/playwright_handle.py +0 -0
  65. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/repl/snapshot.py +0 -0
  66. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/session.py +0 -0
  67. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/session_create.py +0 -0
  68. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/session_ctx.py +0 -0
  69. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/session_registry.py +0 -0
  70. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/session_runtime.py +0 -0
  71. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
  72. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
  73. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
  74. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
  75. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
  76. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
  77. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
  78. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
  79. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
  80. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
  81. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
  82. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
  83. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
  84. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
  85. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
  86. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/skill_doc.py +0 -0
  87. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/skill_runtime.md +0 -0
  88. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/task_runner.py +0 -0
  89. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright/version.py +0 -0
  90. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright.egg-info/SOURCES.txt +0 -0
  91. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright.egg-info/dependency_links.txt +0 -0
  92. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright.egg-info/entry_points.txt +0 -0
  93. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright.egg-info/requires.txt +0 -0
  94. {browserwright-0.7.3 → browserwright-0.7.4}/src/browserwright.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browserwright
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: websockets==15.0.1
@@ -179,6 +179,16 @@ external profiles with N isolated daemons — each with its own `XDG_RUNTIME_DIR
179
179
  (distinct socket), `--facade-port`, and `BD_CDP_WS`, one `env` session apiece.
180
180
  See [docs/session-workspaces.md](docs/session-workspaces.md) §"Env Backend".
181
181
 
182
+ **Reaching the facade from another machine (Tailscale/LAN):** the Playwright
183
+ facade binds `127.0.0.1` by default and is *never* exposed off-box unless you
184
+ opt in. Pass `--facade-host <tailnet-ip>` (or `BD_FACADE_HOST` / `facade_host`
185
+ in config.toml; `0.0.0.0` to bind all interfaces) and a remote client can
186
+ `connect_over_cdp("http://<tailnet-ip>:19990/cdp")` — the facade's
187
+ `/json/version` bootstrap rewrites the advertised `webSocketDebuggerUrl` from
188
+ the request's `Host` header, so the ws URL points back at the address the
189
+ client actually used. No auth is added, so only bind an interface you trust
190
+ (a Tailscale IP is private to your tailnet).
191
+
182
192
  ### Userscripts
183
193
 
184
194
  Author Tampermonkey-style scripts the `extension` backend injects on matching sites:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "browserwright"
7
- version = "0.7.3"
7
+ version = "0.7.4"
8
8
  description = "Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends)."
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -108,6 +108,16 @@ def _build_parser() -> argparse.ArgumentParser:
108
108
  "(default: ON at 19990; pass 0 to disable). Lets a real "
109
109
  "Playwright client `connect_over_cdp(ws://127.0.0.1:N/cdp)` and "
110
110
  "the skill heredoc `page`/`context` drive the resolved Chrome."))
111
+ # Facade bind host. Defaults to loopback (127.0.0.1) — the facade is NEVER
112
+ # reachable off-box unless you opt in here. Set a Tailscale/LAN IP (or
113
+ # 0.0.0.0) to let a remote Playwright client connect_over_cdp. Equivalent to
114
+ # BD_FACADE_HOST env or `facade_host` in config.toml.
115
+ p_serve.add_argument(
116
+ "--facade-host", type=str, default=None, metavar="HOST",
117
+ help=("bind the Playwright-facing CDP facade on this host "
118
+ "(default: 127.0.0.1 / loopback). Set a Tailscale/LAN IP or "
119
+ "0.0.0.0 to reach it from another machine. Equivalent to "
120
+ "BD_FACADE_HOST env or `facade_host` in config.toml."))
111
121
 
112
122
  # stop (v0.2)
113
123
  p_stop = sub.add_parser("stop", help="stop the running daemon")
@@ -283,6 +293,13 @@ def _build_parser() -> argparse.ArgumentParser:
283
293
  help=argparse.SUPPRESS)
284
294
  p_inst.add_argument("--extension-port", type=int, default=None, metavar="N",
285
295
  help="override the relay ws port (default 19989)")
296
+ p_inst.add_argument("--facade-port", type=int, default=None, metavar="N",
297
+ help="override the Playwright facade port (default 19990)")
298
+ p_inst.add_argument(
299
+ "--facade-host", type=str, default=None, metavar="HOST",
300
+ help=("bind the Playwright facade on this host (default 127.0.0.1 / "
301
+ "loopback). Set a Tailscale/LAN IP or 0.0.0.0 to reach the "
302
+ "installed daemon's facade from another machine."))
286
303
  p_inst.add_argument("--force", action="store_true",
287
304
  help="replace an existing LaunchAgent with the same name")
288
305
 
@@ -329,6 +346,7 @@ def _cfg_from_args(args) -> Config:
329
346
  # per subcommand, so getattr-with-default keeps non-serve calls clean.
330
347
  cli_extension_port=getattr(args, "extension_port", None),
331
348
  cli_facade_port=getattr(args, "facade_port", None),
349
+ cli_facade_host=getattr(args, "facade_host", None),
332
350
  )
333
351
 
334
352
 
@@ -949,7 +967,9 @@ def _resolve_browserwright_daemon_bin() -> str:
949
967
  )
950
968
 
951
969
 
952
- def _build_plist(*, extension_port: int | None) -> str:
970
+ def _build_plist(*, extension_port: int | None,
971
+ facade_host: str | None = None,
972
+ facade_port: int | None = None) -> str:
953
973
  """Emit the plist content. Kept inline (no XML lib) — the schema is
954
974
  fixed + tiny, and we avoid a dependency. Every interpolated value passes
955
975
  through ``xml.sax.saxutils.escape``. Pure: no side effects — the caller is
@@ -959,6 +979,10 @@ def _build_plist(*, extension_port: int | None) -> str:
959
979
  args = [bin_path, "serve"]
960
980
  if extension_port is not None:
961
981
  args += ["--extension-port", str(extension_port)]
982
+ if facade_port is not None:
983
+ args += ["--facade-port", str(facade_port)]
984
+ if facade_host is not None:
985
+ args += ["--facade-host", str(facade_host)]
962
986
  log_dir = os.path.expanduser("~/.cache/browserwright-daemon/logs")
963
987
  stdout_path = f"{log_dir}/browserwright-daemon.stdout.log"
964
988
  stderr_path = f"{log_dir}/browserwright-daemon.stderr.log"
@@ -1030,7 +1054,11 @@ def _cmd_install(args, cfg: Config) -> int:
1030
1054
  # `_build_plist` so the generator stays pure / unit-testable (N-1).
1031
1055
  log_dir = os.path.expanduser("~/.cache/browserwright-daemon/logs")
1032
1056
  os.makedirs(log_dir, exist_ok=True)
1033
- content = _build_plist(extension_port=args.extension_port)
1057
+ content = _build_plist(
1058
+ extension_port=args.extension_port,
1059
+ facade_host=getattr(args, "facade_host", None),
1060
+ facade_port=getattr(args, "facade_port", None),
1061
+ )
1034
1062
  # If --force and the plist exists, unload the old one first so launchctl
1035
1063
  # picks up the new ProgramArguments cleanly.
1036
1064
  if plist_path.exists():
@@ -1049,6 +1077,8 @@ def _cmd_install(args, cfg: Config) -> int:
1049
1077
  "label": label,
1050
1078
  "plist": str(plist_path),
1051
1079
  "extension_port": args.extension_port,
1080
+ "facade_host": getattr(args, "facade_host", None),
1081
+ "facade_port": getattr(args, "facade_port", None),
1052
1082
  }, sort_keys=True))
1053
1083
  return 0
1054
1084
 
@@ -29,6 +29,12 @@ _NAME_RE = re.compile(r"\A[A-Za-z0-9_-]{1,64}\Z")
29
29
  # re-exports it for backwards compat.
30
30
  DEFAULT_FACADE_PORT = 19990
31
31
 
32
+ # Default bind host for the Playwright-facing CDP facade. Stays loopback so the
33
+ # facade is NEVER reachable off-box by accident; override via `--facade-host` /
34
+ # `BD_FACADE_HOST` / `facade_host` in config.toml to bind a Tailscale/LAN
35
+ # interface (e.g. `0.0.0.0` or a specific tailnet IP) for remote CDP clients.
36
+ DEFAULT_FACADE_HOST = "127.0.0.1"
37
+
32
38
 
33
39
  def check_name(name: str) -> str:
34
40
  """Path-traversal guard for filesystem-bound names (e.g. `--profile`)."""
@@ -116,6 +122,11 @@ class Config:
116
122
  # - >0 -> explicit override port (CLI/env/toml).
117
123
  # Use `resolved_facade_port()` to collapse this to "bind / don't bind".
118
124
  facade_port: int | None = None
125
+ # Bind host for the Playwright facade. Defaults to loopback (never exposed by
126
+ # accident); set to a Tailscale/LAN IP or `0.0.0.0` to reach it off-box.
127
+ # Precedence mirrors facade_port: CLI `--facade-host` > `BD_FACADE_HOST` env
128
+ # > toml `facade_host` > DEFAULT_FACADE_HOST.
129
+ facade_host: str = DEFAULT_FACADE_HOST
119
130
  backends: BackendsConfig = field(default_factory=BackendsConfig)
120
131
  # Provenance for `env`: which alias key actually fired. Diagnostic-only.
121
132
  cdp_ws_source: str | None = None # "BD_CDP_WS" | "BU_CDP_WS" | None
@@ -156,6 +167,7 @@ def load(
156
167
  cli_config_path: str | None = None,
157
168
  cli_extension_port: int | None = None,
158
169
  cli_facade_port: int | None = None,
170
+ cli_facade_host: str | None = None,
159
171
  env: dict[str, str] | None = None,
160
172
  ) -> Config:
161
173
  """Build a Config from CLI flags + env + optional toml file.
@@ -213,6 +225,9 @@ def load(
213
225
  # Playwright facade port (phase A1). toml key `facade_port`.
214
226
  if "facade_port" in toml and isinstance(toml["facade_port"], int):
215
227
  cfg.facade_port = toml["facade_port"]
228
+ # Playwright facade bind host. toml key `facade_host`.
229
+ if "facade_host" in toml and isinstance(toml["facade_host"], str):
230
+ cfg.facade_host = toml["facade_host"]
216
231
 
217
232
  # env level — BD_* wins over BU_*
218
233
  if "BD_TIMEOUT" in e:
@@ -317,6 +332,10 @@ def load(
317
332
  from .errors import UserError
318
333
  raise UserError(
319
334
  f"BD_FACADE_PORT must be an integer, got {e['BD_FACADE_PORT']!r}")
335
+ # Playwright facade bind host via env. Symmetric to BD_FACADE_PORT — set
336
+ # BD_FACADE_HOST=<tailnet-ip>/0.0.0.0 to reach the facade off-box.
337
+ if "BD_FACADE_HOST" in e:
338
+ cfg.facade_host = e["BD_FACADE_HOST"]
320
339
 
321
340
  # CLI level — last word
322
341
  if cli_backend is not None:
@@ -333,5 +352,8 @@ def load(
333
352
  if cli_facade_port is not None:
334
353
  # Phase A1: CLI `--facade-port` tops env / toml.
335
354
  cfg.facade_port = cli_facade_port
355
+ if cli_facade_host is not None:
356
+ # CLI `--facade-host` tops env / toml.
357
+ cfg.facade_host = cli_facade_host
336
358
 
337
359
  return cfg
@@ -158,13 +158,25 @@ class PlaywrightFacade:
158
158
  `webSocketDebuggerUrl: ws://host/cdp`; `connect_over_cdp("ws://...")`
159
159
  also accepts an `http://` URL it resolves via this route. We answer
160
160
  `/json/version`, `/json`, and `/json/list` (the latter two are cheap
161
- and some CDP clients probe them)."""
161
+ and some CDP clients probe them).
162
+
163
+ The advertised `webSocketDebuggerUrl` is derived from the incoming
164
+ request's `Host` header so a client that reached us over Tailscale/LAN
165
+ gets a ws URL that points back at the same authority it just used —
166
+ exactly what CloakBrowser does. We only fall back to the configured
167
+ `host:port` when no Host header is present (e.g. a hand-rolled probe)."""
162
168
  path = (request.path or "/").split("?", 1)[0]
169
+ # Playwright's `connect_over_cdp("http://host:port")` probes
170
+ # `.../json/version/` WITH a trailing slash; normalize it away so the
171
+ # http bootstrap form works, not just the direct `ws://.../cdp` form.
172
+ if len(path) > 1:
173
+ path = path.rstrip("/")
163
174
  session_id = self._session_for_request(request)
175
+ authority = self._authority_from_request(request)
164
176
  if path == "/json/version":
165
- return self._http_json(conn, self._version_payload(session_id))
177
+ return self._http_json(conn, self._version_payload(session_id, authority))
166
178
  if path in ("/json", "/json/list"):
167
- return self._http_json(conn, self._list_payload(session_id))
179
+ return self._http_json(conn, self._list_payload(session_id, authority))
168
180
  # Anything else (e.g. the /cdp ws upgrade) falls through to the ws
169
181
  # handler. Return None to allow the upgrade.
170
182
  return None
@@ -180,22 +192,40 @@ class PlaywrightFacade:
180
192
  qs = parse_qs(urlparse(request.path or "/").query)
181
193
  return (qs.get("session") or [None])[0]
182
194
 
183
- def _ws_url(self, session_id: str | None = None) -> str:
195
+ def _authority_from_request(self, request) -> str | None:
196
+ """Return the `Host` header (``host[:port]``) the client used to reach
197
+ us, or None when absent. Drives the advertised ws so a remote client
198
+ that connected over Tailscale/LAN gets a ws URL that points back at the
199
+ same authority (not a hardcoded loopback it can't reach)."""
200
+ try:
201
+ host = request.headers.get("Host")
202
+ except (AttributeError, KeyError):
203
+ return None
204
+ host = (host or "").strip()
205
+ return host or None
206
+
207
+ def _ws_url(self, session_id: str | None = None,
208
+ authority: str | None = None) -> str:
209
+ # Prefer the incoming request's Host (already `host[:port]`); fall back
210
+ # to the configured bind host:port when no Host header was sent.
211
+ netloc = authority or f"{self._host}:{self._port}"
184
212
  suffix = ""
185
213
  if session_id:
186
214
  from urllib.parse import quote
187
215
  suffix = f"?session={quote(session_id, safe='')}"
188
- return f"ws://{self._host}:{self._port}{FACADE_WS_PATH}{suffix}"
216
+ return f"ws://{netloc}{FACADE_WS_PATH}{suffix}"
189
217
 
190
- def _version_payload(self, session_id: str | None = None) -> dict:
218
+ def _version_payload(self, session_id: str | None = None,
219
+ authority: str | None = None) -> dict:
191
220
  return {
192
221
  "Browser": f"Browserwright/{__version__}",
193
222
  "Protocol-Version": "1.3",
194
223
  "User-Agent": f"Browserwright facade {__version__}",
195
- "webSocketDebuggerUrl": self._ws_url(session_id),
224
+ "webSocketDebuggerUrl": self._ws_url(session_id, authority),
196
225
  }
197
226
 
198
- def _list_payload(self, session_id: str | None = None) -> list:
227
+ def _list_payload(self, session_id: str | None = None,
228
+ authority: str | None = None) -> list:
199
229
  # The browser-level endpoint is what Playwright wants; per-page targets
200
230
  # are discovered via Target.* over the ws once connected. We advertise a
201
231
  # single synthetic "browser" entry pointing at our ws.
@@ -203,7 +233,7 @@ class PlaywrightFacade:
203
233
  "type": "browser",
204
234
  "title": "Browserwright",
205
235
  "url": "",
206
- "webSocketDebuggerUrl": self._ws_url(session_id),
236
+ "webSocketDebuggerUrl": self._ws_url(session_id, authority),
207
237
  }]
208
238
 
209
239
  # ---- ws passthrough --------------------------------------------------
@@ -290,10 +290,15 @@ async def run_serve(cfg: Config) -> int:
290
290
  return shared_context.holder.relay
291
291
 
292
292
  facade = PlaywrightFacade(cfg=cfg, port=facade_port,
293
+ host=cfg.facade_host,
293
294
  relay_getter=_shared_relay,
294
295
  daemon=daemon)
295
296
  bound = await facade.start()
296
- facade_ws = f"ws://127.0.0.1:{bound}/cdp"
297
+ # Advertise the bound host so `status`/skill discovery report a
298
+ # reachable ws (loopback by default; a tailnet/LAN IP when
299
+ # `--facade-host` opts in). Remote clients still get the real
300
+ # incoming Host echoed back via the facade's HTTP bootstrap.
301
+ facade_ws = f"ws://{cfg.facade_host}:{bound}/cdp"
297
302
  # Advertise the bound ws so the skill layer can discover it (Phase C
298
303
  # auto-enable). Best-effort: a write failure must not abort serving.
299
304
  with contextlib.suppress(Exception):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browserwright
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: websockets==15.0.1
File without changes