| 1 | #!/usr/bin/env python3 |
| 2 | """Extras-host X login helper: print (or ``--exec``) the box-chrome launch. |
| 3 | |
| 4 | On extra hosts (Linux, a Darwin Mac mini, a Darwin agentcookie sink, or |
| 5 | ``AGENTCOOKIE=on``) the local Chrome cookie store cannot be decrypted, so the |
| 6 | only way to hand bird a live X session is to launch a throwaway Chrome with a |
| 7 | remote-debugging port, let the human log in, and read the pair over CDP. This |
| 8 | helper prints the exact, host-correct launch command — or launches it with |
| 9 | ``--exec`` — and refuses to do anything on a MacBook (which keeps its Keychain / |
| 10 | Firefox / Safari extract path). |
| 11 | |
| 12 | Contract, matching the rest of the feature: |
| 13 | * **Extras only.** On a MacBook (and any non-extras host) it prints "no launch |
| 14 | needed" and never spawns a browser, even with ``--exec``. An official-only |
| 15 | host (``LAST30DAYS_HOST=grok-bot``, see ``env.x_policy``) gets the same |
| 16 | no-launch recipe with a neutral note, even on Linux or a Mac mini. |
| 17 | * Launch on the last30days extras NUX port ``18800`` |
| 18 | (``SAND_CHROME_REMOTE_DEBUG_PORT=18800``) so ``chrome_cdp`` finds it. This is |
| 19 | NOT box-chrome's built-in default (``9222`` + the display number). |
| 20 | * Uses the host ``box-chrome`` wrapper (which sets ``--class=box-chrome``); |
| 21 | never assembles a raw ``google-chrome-stable`` flag soup and never launches |
| 22 | raw Chrome with a custom ``--class`` (a raw Chrome with ``--class=l30d-…`` |
| 23 | failed live where ``box-chrome`` succeeded). No special user-agent is |
| 24 | required. If ``box-chrome`` is missing it tells the user to sign into a |
| 25 | remote-debugging Chrome and pin ``BROWSER_CDP_URL``. |
| 26 | * Reads NO cookies and prints NO cookie values. It never writes to the ``.env``. |
| 27 | """ |
| 28 | |
| 29 | from __future__ import annotations |
| 30 | |
| 31 | import json |
| 32 | import os |
| 33 | import shutil |
| 34 | import subprocess |
| 35 | import sys |
| 36 | from pathlib import Path |
| 37 | from typing import Any, Dict, List, Optional |
| 38 | |
| 39 | SCRIPT_DIR = Path(__file__).parent.resolve() |
| 40 | sys.path.insert(0, str(SCRIPT_DIR)) |
| 41 | |
| 42 | from lib import chrome_cdp, env # noqa: E402 |
| 43 | |
| 44 | # The last30days extras NUX port — single source of truth is chrome_cdp. |
| 45 | EXTRAS_CDP_PORT = chrome_cdp._BOX_CHROME_PORT |
| 46 | DEFAULT_PROFILE_DIR = "/tmp/last30days-x-chrome" |
| 47 | LOGIN_URL = "https://x.com/login" |
| 48 | BOX_CHROME_BIN = "box-chrome" |
| 49 | # Printed on an official-only host instead of the MacBook note. Names no |
| 50 | # cookie mechanism: this host searches X through official access only. |
| 51 | OFFICIAL_HOST_NOTE = ( |
| 52 | "No launch needed: this host searches X through official access and " |
| 53 | "does not use a browser login window." |
| 54 | ) |
| 55 | |
| 56 | |
| 57 | def build_recipe( |
| 58 | config: Dict[str, Any], |
| 59 | *, |
| 60 | profile_dir: str = DEFAULT_PROFILE_DIR, |
| 61 | url: str = LOGIN_URL, |
| 62 | ) -> Dict[str, Any]: |
| 63 | """Return the gated launch recipe. Never spawns, never reads cookies. |
| 64 | |
| 65 | Keys: ``applies`` (extras host?), ``box_chrome`` (path or None), ``port``, |
| 66 | ``profile_dir``, ``url``, ``env`` (launch env overrides or None), |
| 67 | ``command`` (argv or None), ``note`` (human guidance). |
| 68 | """ |
| 69 | # Official-only host (LAST30DAYS_HOST=grok-bot): the same no-launch |
| 70 | # recipe as a MacBook, checked BEFORE the extras signals so a Linux or |
| 71 | # Mac mini Grok Bot computer never gets a launch command. The note is |
| 72 | # neutral on purpose. A LAST30DAYS_X_BACKEND=bird pin re-enables |
| 73 | # discovery through env.x_policy, and with it this helper. |
| 74 | if not env.x_policy(config).cookie_discovery: |
| 75 | return { |
| 76 | "applies": False, |
| 77 | "box_chrome": None, |
| 78 | "port": EXTRAS_CDP_PORT, |
| 79 | "profile_dir": profile_dir, |
| 80 | "url": url, |
| 81 | "env": None, |
| 82 | "command": None, |
| 83 | "note": OFFICIAL_HOST_NOTE, |
| 84 | } |
| 85 | |
| 86 | if not env.x_extras_enabled(config): |
| 87 | return { |
| 88 | "applies": False, |
| 89 | "box_chrome": None, |
| 90 | "port": EXTRAS_CDP_PORT, |
| 91 | "profile_dir": profile_dir, |
| 92 | "url": url, |
| 93 | "env": None, |
| 94 | "command": None, |
| 95 | "note": ( |
| 96 | "This host uses the standard browser-cookie path (Keychain / " |
| 97 | "Firefox / Safari extract). No box-chrome login is needed; " |
| 98 | "run `setup --allow-browser-cookies` as usual." |
| 99 | ), |
| 100 | } |
| 101 | |
| 102 | box = shutil.which(BOX_CHROME_BIN) |
| 103 | if box: |
| 104 | return { |
| 105 | "applies": True, |
| 106 | "box_chrome": box, |
| 107 | "port": EXTRAS_CDP_PORT, |
| 108 | "profile_dir": profile_dir, |
| 109 | "url": url, |
| 110 | "env": { |
| 111 | "CHROME_USER_DATA_DIR": profile_dir, |
| 112 | "SAND_CHROME_REMOTE_DEBUG_PORT": str(EXTRAS_CDP_PORT), |
| 113 | }, |
| 114 | "command": [box, "--new-window", url], |
| 115 | "note": ( |
| 116 | "Launch this throwaway login Chrome, wait for the x.com login " |
| 117 | "page, then hand the desktop to the human to type. Do NOT drive " |
| 118 | "the page. After they sign in, pin " |
| 119 | f"BROWSER_CDP_URL=http://127.0.0.1:{EXTRAS_CDP_PORT} in .env " |
| 120 | "(never AUTH_TOKEN/CT0) and run `setup --allow-browser-cookies`." |
| 121 | ), |
| 122 | } |
| 123 | |
| 124 | return { |
| 125 | "applies": True, |
| 126 | "box_chrome": None, |
| 127 | "port": EXTRAS_CDP_PORT, |
| 128 | "profile_dir": profile_dir, |
| 129 | "url": url, |
| 130 | "env": None, |
| 131 | "command": None, |
| 132 | "note": ( |
| 133 | "box-chrome is not on PATH. Do not assemble a raw google-chrome " |
| 134 | "command or launch raw Chrome with a custom --class (that is what " |
| 135 | "failed live; box-chrome sets --class=box-chrome). Sign into x.com " |
| 136 | "in a Chrome that already exposes a remote-debugging port, then pin " |
| 137 | "BROWSER_CDP_URL to that endpoint in .env and run " |
| 138 | "`setup --allow-browser-cookies`." |
| 139 | ), |
| 140 | } |
| 141 | |
| 142 | |
| 143 | def render_recipe(recipe: Dict[str, Any]) -> str: |
| 144 | """Human-readable recipe. Contains no cookie values (none are read).""" |
| 145 | lines: List[str] = [] |
| 146 | if not recipe["applies"]: |
| 147 | if recipe["note"] == OFFICIAL_HOST_NOTE: |
| 148 | lines.append("[login helper] No launch needed on this host.") |
| 149 | else: |
| 150 | lines.append("[box-chrome login] Not an extras host.") |
| 151 | lines.append(recipe["note"]) |
| 152 | return "\n".join(lines) |
| 153 | |
| 154 | if recipe["command"] is None: |
| 155 | lines.append("[box-chrome login] Extras host, but box-chrome is unavailable.") |
| 156 | lines.append(recipe["note"]) |
| 157 | return "\n".join(lines) |
| 158 | |
| 159 | env_prefix = " ".join(f"{k}={v}" for k, v in recipe["env"].items()) |
| 160 | cmd = " ".join(recipe["command"]) |
| 161 | lines.append("[box-chrome login] Extras host. Launch the throwaway login Chrome:") |
| 162 | lines.append("") |
| 163 | lines.append(f" mkdir -p {recipe['profile_dir']}") |
| 164 | lines.append(f" {env_prefix} {cmd}") |
| 165 | lines.append("") |
| 166 | lines.append(recipe["note"]) |
| 167 | lines.append( |
| 168 | "For this first-run harvest, set AGENTCOOKIE=off so a sidecar can't mix " |
| 169 | "a different pair; leave AGENTCOOKIE unset again after success." |
| 170 | ) |
| 171 | return "\n".join(lines) |
| 172 | |
| 173 | |
| 174 | def main(argv: Optional[List[str]] = None) -> int: |
| 175 | argv = list(sys.argv[1:] if argv is None else argv) |
| 176 | do_exec = "--exec" in argv |
| 177 | as_json = "--json" in argv |
| 178 | |
| 179 | config = env.get_config() # default policy: no cookie reads, no discovery |
| 180 | recipe = build_recipe(config) |
| 181 | |
| 182 | if as_json: |
| 183 | printable = {k: v for k, v in recipe.items() if k != "box_chrome"} |
| 184 | printable["box_chrome_present"] = recipe["box_chrome"] is not None |
| 185 | print(json.dumps(printable)) |
| 186 | else: |
| 187 | print(render_recipe(recipe)) |
| 188 | |
| 189 | if do_exec: |
| 190 | # Refuse to spawn on a non-extras host, or when box-chrome is missing. |
| 191 | if not recipe["applies"] or not recipe["command"]: |
| 192 | return 0 |
| 193 | try: |
| 194 | os.makedirs(recipe["profile_dir"], exist_ok=True) |
| 195 | except OSError: |
| 196 | pass |
| 197 | launch_env = os.environ.copy() |
| 198 | launch_env.update(recipe["env"]) |
| 199 | try: |
| 200 | subprocess.Popen(recipe["command"], env=launch_env) |
| 201 | except OSError as exc: |
| 202 | print(f"[box-chrome login] failed to launch: {type(exc).__name__}: {exc}", |
| 203 | file=sys.stderr) |
| 204 | return 1 |
| 205 | return 0 |
| 206 | |
| 207 | |
| 208 | if __name__ == "__main__": |
| 209 | raise SystemExit(main()) |
| 210 |