返回 last30days-skill
box_chrome_login.py
根目录 / skills / last30days / scripts / box_chrome_login.py
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
210 lines PYTHON