← Về thư mục
📄 / / root / .hermes / skills / autonomous-ai-agents / browser-auth-on-headless-vps / SKILL.md

name: browser-auth-on-headless-vps description: Use when a browser-based login must be completed on a headless Ubuntu VPS but the user can only interact from a separate personal machine. Sets up a real browser on the VPS, exposes it via remote debugging, and keeps the steps clearly split between VPS-side and local-machine-side actions. version: 1.0.0 author: Antigravity license: MIT metadata: hermes: tags: [browser, auth, vps, ubuntu, remote-debugging, chrome, headless] related_skills: [hermes-agent, handoff-task]


Browser Auth on a Headless VPS

Overview

Use this workflow when you need to authenticate a browser-backed service on an Ubuntu VPS that has no usable local display, while the user is operating from a different computer. The goal is to avoid brittle cookie-only setups by using a real Chrome/Chromium session on the VPS, then exposing that session through remote debugging so the user can complete login from their own machine.

Keep the execution split explicit: - VPS-side work: install browser, start the browser, start remote debugging, verify the port is alive. - Local-machine work: open SSH tunnel, attach Chrome DevTools to localhost:9222, complete login.

When to Use

Browser-login workflow on a remote VPS

  1. Install the CLI and confirm the binary is present on the VPS.
  2. Check the CLI auth status before changing any auth files.
  3. If the CLI already shows a valid API key, report that clearly and ask whether to keep it or switch to browser login.
  4. If browser login is requested, keep the VPS and local-machine steps separate and pause for the user's interactive browser action when needed.
  5. After login, re-check auth status and verify the persisted credential file.

Known-good config shape for 9router

When wiring Codex to a 9router endpoint, keep the config in ~/.codex/ on the target machine. Use a config.toml that sets model_provider = "9router" and a base_url pointing at the remote router URL, plus an auth.json file for the credential mode expected by the CLI.

  1. Install a real browser on the VPS. Prefer Google Chrome Stable or Chromium from the distro repos. Verify the browser binary exists before continuing.

  2. Start Chrome with a dedicated profile. Use a separate user-data directory for the service so future auth is isolated from other logins.

  3. Expose Chrome DevTools on loopback only. Use --remote-debugging-port=9222, --remote-debugging-address=127.0.0.1, and --remote-allow-origins=* (to prevent HTTP 403 Forbidden errors when connecting via WebSocket). If the VPS has no display, wrap the launch in xvfb-run or use --headless=new with --no-sandbox if running as root.

  4. Verify the debugging endpoint from the VPS. Check http://127.0.0.1:9222/json/version before asking the user to do anything. Completion criterion: the endpoint returns a JSON payload with the browser version and websocket URL.

  5. Have the user open an SSH tunnel from their local machine. The user runs ssh -N -L 9222:127.0.0.1:9222 root@<vps-ip> from their own computer.

  6. Have the user inspect the browser session locally. The user opens chrome://inspect, adds localhost:9222, and attaches to the target tab.

  7. Let the user finish login. The user signs in on their machine while the real browser session is actually running on the VPS.

  8. Re-check auth from the VPS. After login, run the service CLI's auth check or profile list to confirm a persistent profile was saved.

Resuming NotebookLM (Specific Case)

Refer to references/auth-architecture.md for details on the cookie hierarchy and internal auth mechanics.

⚠️ CRITICAL: Don't Trust check_auth() Alone

The CLI's check_auth() and nlm login --check can report false negatives — the user's Google session in Chrome may be perfectly valid while the CLI's cached auth files (cookies.json, auth.json) are stale. Before attempting any login flow:

  1. Check if Chrome is alive (port 9222 or 9223): bash curl -s http://127.0.0.1:9223/json/version
  2. If alive, try extracting cookies from the running instance first — this is the PRIMARY recovery path, not nlm login --force: bash python3 /opt/ai-os/products/ceo/scripts/notebooklm_refresh.py --port 9223
  3. Only if extraction fails (Chrome dead, profile empty) → proceed to revive or interactive login.

Why this order matters: nlm login --force launches its own Chrome instance, which conflicts with any existing Chrome using the same profile (SingletonLock). The user's existing Chrome session with a live Google login is the most reliable source of auth tokens. Never launch Chrome unless the old one is confirmed dead.

Primary Recovery Path (Chrome Alive, Session Exists)

When the CLI reports auth expired but Chrome headless is running with a saved Google session:

# STRAIGHT TO COOKIE EXTRACTION — skip login commands entirely
cd /opt/ai-os/products/ceo/integrations/notebooklm-mcp-cli
.venv/bin/python /opt/ai-os/products/ceo/scripts/notebooklm_refresh.py --port 9223

This connects to Chrome's CDP WebSocket, calls Network.getAllCookies, extracts CSRF/session_id/build_label from the page DOM, and saves to the CLI's profile storage. Verification is automatic.

Do NOT run nlm login --force or nlm login when Chrome is already running. These commands try to start a second Chrome instance, hit SingletonLock, and timeout.

Quick Revive (Chrome Process Dead)

When Chrome headless itself is dead (port unreachable, no process), run:

/opt/ai-os/products/ceo/scripts/revive_notebooklm_auth.sh

This one-shot script: 1. Kills stale Chrome processes on port 9223. 2. Removes SingletonLock if profile is locked. 3. Relaunches Chrome headless with the existing profile. 4. Runs notebooklm_refresh.py to extract fresh cookies via CDP into the CLI profile. 5. Runs check_auth(live=True) to confirm validity.

Lưu ý (Trường hợp port 9223): Đôi khi Chrome headless chạy trên port 9223 thay vì 9222 (do config mặc định của company script notebooklm_refresh.py hoặc crontab setup). Hãy luôn check nhanh cả port 9223 bằng notebooklm_refresh.py --port 9223 trước khi kết luận auth hỏng hẳn.

Post-Interactive-Login Sync (Always Required)

After the user completes interactive login via SSH tunnel + chrome://inspect, the Chrome profile gets fresh cookies but the CLI auth files (profiles/default/cookies.json, auth.json) are still stale. Always run the refresh script:

cd /opt/ai-os/products/ceo/integrations/notebooklm-mcp-cli
.venv/bin/python /opt/ai-os/products/ceo/scripts/notebooklm_refresh.py

Without this sync step, the CLI will keep using expired cookies even though Chrome itself is authenticated. Note the script must be run via the virtual environment's python.

Resuming an Existing Session (Process Died)

If the browser process terminates (e.g., after a VPS reboot or timeout), the user's session profile still exists on disk, but the debugging port will refuse connections. Do NOT ask the user to re-authenticate or copy cookies manually. Instead:

  1. Relaunch Chrome pointing to the existing user-data-dir (e.g., ~/.notebooklm-mcp-cli/chrome-profiles/default): bash google-chrome-stable \ --headless=new \ --no-sandbox \ --disable-gpu \ --disable-dev-shm-usage \ --remote-debugging-port=9222 \ --remote-debugging-address=127.0.0.1 \ --remote-allow-origins=* \ --user-data-dir=$HOME/.notebooklm-mcp-cli/chrome-profiles/default
  2. Extract cookies from the revived session into the CLI auth profile: bash python3 /opt/ai-os/products/ceo/scripts/notebooklm_refresh.py --port 9222
  3. Verify with the service CLI (nlm login --check).

Scripted equivalent: /opt/ai-os/products/ceo/scripts/revive_notebooklm_auth.sh handles steps 1–3 automatically using port 9223.

Common Pitfalls

  1. Mixing up VPS actions and local actions. Always label what runs on the VPS versus what the user must do locally. If the user cannot see the VPS, do not instruct them to click there directly.

  2. Trusting check_auth() false-negatives over the live Chrome session. This is the #1 trap. The CLI's check_auth() and nlm login --check can report "expired" while Chrome itself has a perfectly valid Google session. The CLI maintains a separate cookie JSON file that does not auto-sync with Chrome's SQLite cookie store. First-line action should always be: connect to Chrome via CDP, extract cookies with Network.getAllCookies, and save to the CLI profile — not nlm login --force which launches a conflicting Chrome instance.

Detection protocol: Before believing any auth failure, verify Chrome CDP is alive and serving pages: bash curl -s http://127.0.0.1:9223/json/version | grep -q Browser && echo "Chrome alive with session" If Chrome is alive, extract cookies via CDP directly. If the page URL shows notebooklm.google.com/ (not accounts.google.com), the session is valid and only the CLI cache is stale.

  1. Using cookies as the primary plan. Cookies are a fallback, not the preferred path, when the service supports browser/profile auth. Use cookies only when browser auth cannot be completed.

  2. Forgetting --no-sandbox when running Chrome as root. On many VPS setups Chrome will refuse to start without it.

  3. Running Python scripts outside the venv. If a script imports tools from a local repository (like notebooklm_tools), it will fail with ModuleNotFoundError if run with the system python3. Always run them via the project's virtual environment, e.g., .venv/bin/python script.py.

  4. WebSocket 403 Forbidden Handshake Errors. Chrome blocks incoming WebSocket connections from differing origins by default. Always include --remote-allow-origins=* or specify the loopback address origin.

  5. Running nlm login --force when Chrome is already running. This will try to launch a second Chrome instance on the same profile, hit the SingletonLock, and timeout after 120s. If Chrome is alive, use notebooklm_refresh.py instead. If it's dead, use revive_notebooklm_auth.sh.

  6. Skipping the loopback verification. Before involving the user, confirm the remote debugging endpoint is alive on 127.0.0.1:9222 (or the chosen port).

  7. Leaving the tunnel instructions ambiguous. The user needs the real VPS IP from their provider or existing SSH config; do not invent one.

  8. Not syncing CLI profile after interactive login. When the user logs in via SSH tunnel + chrome://inspect, the Chrome profile gets fresh cookies but the CLI auth files stay stale. Always run the refresh script after interactive login.

  9. Stale chrome-port-map.json entries from dead processes. The file ~/.notebooklm-mcp-cli/chrome-port-map.json persists port→profile mappings even if the Chrome process on that port died. A stale entry (e.g., port 9222 pointing to a defunct PID) can mislead the CLI into trying to reuse a dead port. After killing Chrome or on VPS restart, clean up stale port-map entries by hand or run the revive script which handles it.

  10. Assuming a single port; not scanning for alternatives. Chrome headless may be running on a non-default port (e.g., 9223 instead of 9222) if the original port was occupied or configured differently. Always probe a range of ports (9222–9232) for /json/version before concluding Chrome is dead.

  11. Iterative trial-and-error in the main thread on auth issues. When auth troubleshooting requires deep code analysis (reading auth.py, cdp.py, config.py), do not iterate in the main conversation — this burns tokens and frustrates the user. Instead, delegate code research to a subagent via delegate_task() using role=leaf with a clear context, goal, and path references. Let the subagent produce a documented recommendation, then implement it in one shot.

Verification Checklist