From 7ed3b12854b4474c1c3966b448d497374f8f7056 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 4 Apr 2026 23:59:39 -0700 Subject: [PATCH] feat: smart ngrok detection + auto-tunnel in pair-agent The pair-agent command now checks ngrok's native config (not just ~/.gstack/ngrok.env) and auto-starts the tunnel when ngrok is available. The skill template walks users through ngrok install and auth if not set up, instead of just printing a dead localhost URL. Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/cli.ts | 114 +++++++++++++++++++++++++++++++++++---- pair-agent/SKILL.md | 69 +++++++++++++++--------- pair-agent/SKILL.md.tmpl | 69 +++++++++++++++--------- 3 files changed, 191 insertions(+), 61 deletions(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index ffd1d330..4d1ff86d 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -447,6 +447,33 @@ async function sendCommand(state: ServerState, command: string, args: string[], } } +// ─── Ngrok Detection ─────────────────────────────────────────── + +/** Check if ngrok is installed and authenticated (native config or gstack env). */ +function isNgrokAvailable(): boolean { + // Check gstack's own ngrok env + const ngrokEnvPath = path.join(process.env.HOME || '/tmp', '.gstack', 'ngrok.env'); + if (fs.existsSync(ngrokEnvPath)) return true; + + // Check NGROK_AUTHTOKEN env var + if (process.env.NGROK_AUTHTOKEN) return true; + + // Check ngrok's native config (macOS + Linux) + const ngrokConfigs = [ + path.join(process.env.HOME || '/tmp', 'Library', 'Application Support', 'ngrok', 'ngrok.yml'), + path.join(process.env.HOME || '/tmp', '.config', 'ngrok', 'ngrok.yml'), + path.join(process.env.HOME || '/tmp', '.ngrok2', 'ngrok.yml'), + ]; + for (const conf of ngrokConfigs) { + try { + const content = fs.readFileSync(conf, 'utf-8'); + if (content.includes('authtoken:')) return true; + } catch {} + } + + return false; +} + // ─── Pair-Agent DX ───────────────────────────────────────────── interface InstructionBlockOptions { @@ -586,16 +613,85 @@ async function handlePairAgent(state: ServerState, args: string[]): Promise null); + // Wait for server to come back, then restart with tunnel + await Bun.sleep(1000); + } catch {} + // Restart the server process with BROWSE_TUNNEL=1 + console.log('[browse] Restarting server with tunnel...'); + const serverScript = resolveServerScript(); + const proc = Bun.spawn(['bun', 'run', serverScript], { + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_TUNNEL: '1' }, + }); + proc.unref(); + // Wait for server to come back with tunnel + const deadline = Date.now() + 15000; + let tunnelUrl: string | null = null; + while (Date.now() < deadline) { + await Bun.sleep(500); + const newState = readState(); + if (newState && await isServerHealthy(newState.port)) { + try { + const healthResp = await fetch(`http://127.0.0.1:${newState.port}/health`, { + signal: AbortSignal.timeout(2000), + }); + const health = await healthResp.json() as any; + if (health.tunnel?.url) { + tunnelUrl = health.tunnel.url; + // Update state for the rest of the function + state.port = newState.port; + state.token = newState.token; + break; + } + } catch {} + } + } + if (tunnelUrl) { + console.log(`[browse] Tunnel active: ${tunnelUrl}\n`); + serverUrl = tunnelUrl; + // Re-create setup key with the new server (old one used old root token) + const newPairResp = await fetch(`http://127.0.0.1:${state.port}/pair`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${state.token}`, + }, + body: JSON.stringify({ clientId: clientName, admin }), + signal: AbortSignal.timeout(5000), + }); + if (newPairResp.ok) { + const newData = await newPairResp.json() as typeof pairData; + pairData.setup_key = newData.setup_key; + pairData.expires_at = newData.expires_at; + pairData.scopes = newData.scopes; + } + } else { + console.warn('[browse] Failed to start tunnel. Using localhost (same-machine only).\n'); + serverUrl = pairData.server_url; + } + } else { + console.warn('[browse] No tunnel active and ngrok is not installed/configured.'); + console.warn('[browse] Instructions will use localhost (same-machine only).'); + console.warn('[browse] For remote agents: install ngrok (https://ngrok.com) and run `ngrok config add-authtoken `\n'); + serverUrl = pairData.server_url; } + } else { serverUrl = pairData.server_url; } diff --git a/pair-agent/SKILL.md b/pair-agent/SKILL.md index 17d4fec8..6f7105b2 100644 --- a/pair-agent/SKILL.md +++ b/pair-agent/SKILL.md @@ -606,9 +606,8 @@ Use AskUserQuestion: > **Same machine** skips the copy-paste ceremony. Credentials are written directly to > the agent's config directory. No tunnel needed. > -> **Different machine** requires an ngrok tunnel so the remote agent can reach your -> browser over the internet. A setup key and instruction block are generated for -> copy-paste. +> **Different machine** generates a setup key and instruction block. If ngrok is +> installed, the tunnel starts automatically. If not, I'll walk you through setup. > > RECOMMENDATION: Choose A if the agent is local. It's instant, no copy-paste needed. @@ -637,45 +636,63 @@ using the generic remote flow instead. ### If different machine (option B): -Check if a tunnel is running: +First, detect ngrok status: ```bash -$B pair-agent +which ngrok 2>/dev/null && echo "NGROK_INSTALLED" || echo "NGROK_NOT_INSTALLED" +ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED" ``` -If the output shows "No tunnel active" and mentions ngrok: +**If ngrok is installed and authed:** Just run the command. The CLI will auto-detect +ngrok, start the tunnel, and print the instruction block with the tunnel URL: -Tell the user: -"Your browser server is localhost-only. For a remote agent to connect, you need -an ngrok tunnel. Here's how to set one up: +```bash +$B pair-agent --client TARGET_HOST +``` -1. Sign up at ngrok.com (free tier works) -2. Copy your auth token -3. Save it: `echo 'NGROK_AUTHTOKEN=your_token_here' > ~/.gstack/ngrok.env` -4. Restart the server with tunnel: `BROWSE_TUNNEL=1 $B restart` -5. Run `/pair-agent` again +If the user also needs admin access (JS execution, cookies, storage): -If you just want to test locally, choose 'Same machine' instead." - -STOP here. Wait for the user to set up ngrok and re-invoke. - -If the tunnel IS active (or if the user is OK with localhost-only for same-network use), -the pair-agent command will print the instruction block. Show it to the user and tell them: +```bash +$B pair-agent --admin --client TARGET_HOST +``` +Show the output to the user: "Copy everything between the ═══ lines and paste it into your other agent's chat. The agent will follow the instructions to connect. The setup key expires in 5 minutes." -### Admin access +**If ngrok is installed but NOT authed:** Walk the user through authentication: -If the user mentions needing JavaScript execution, cookie access, or storage access: +Tell the user: +"ngrok is installed but not logged in. Let's fix that: +1. Go to https://dashboard.ngrok.com/get-started/your-authtoken +2. Copy your auth token +3. Come back here and I'll run the auth command for you." + +STOP here and wait for the user to provide their auth token. + +When they provide it, run: ```bash -$B pair-agent --admin +ngrok config add-authtoken THEIR_TOKEN ``` -Tell the user: "This gives the remote agent full admin access including JS execution, -cookie reading, and storage access. Only do this if you trust the agent and need -these capabilities." +Then retry `$B pair-agent --client TARGET_HOST`. + +**If ngrok is NOT installed:** Walk the user through installation: + +Tell the user: +"To connect a remote agent, we need ngrok (a tunnel that exposes your local +browser to the internet securely). + +1. Go to https://ngrok.com and sign up (free tier works) +2. Install ngrok: + - macOS: `brew install ngrok` + - Linux: `snap install ngrok` or download from ngrok.com/download +3. Auth it: `ngrok config add-authtoken YOUR_TOKEN` + (get your token from https://dashboard.ngrok.com/get-started/your-authtoken) +4. Come back here and run `/pair-agent` again." + +STOP here. Wait for the user to install ngrok and re-invoke. ## Step 5: Verify connection diff --git a/pair-agent/SKILL.md.tmpl b/pair-agent/SKILL.md.tmpl index 59c3e9cc..a8969fc0 100644 --- a/pair-agent/SKILL.md.tmpl +++ b/pair-agent/SKILL.md.tmpl @@ -93,9 +93,8 @@ Use AskUserQuestion: > **Same machine** skips the copy-paste ceremony. Credentials are written directly to > the agent's config directory. No tunnel needed. > -> **Different machine** requires an ngrok tunnel so the remote agent can reach your -> browser over the internet. A setup key and instruction block are generated for -> copy-paste. +> **Different machine** generates a setup key and instruction block. If ngrok is +> installed, the tunnel starts automatically. If not, I'll walk you through setup. > > RECOMMENDATION: Choose A if the agent is local. It's instant, no copy-paste needed. @@ -124,45 +123,63 @@ using the generic remote flow instead. ### If different machine (option B): -Check if a tunnel is running: +First, detect ngrok status: ```bash -$B pair-agent +which ngrok 2>/dev/null && echo "NGROK_INSTALLED" || echo "NGROK_NOT_INSTALLED" +ngrok config check 2>/dev/null && echo "NGROK_AUTHED" || echo "NGROK_NOT_AUTHED" ``` -If the output shows "No tunnel active" and mentions ngrok: +**If ngrok is installed and authed:** Just run the command. The CLI will auto-detect +ngrok, start the tunnel, and print the instruction block with the tunnel URL: -Tell the user: -"Your browser server is localhost-only. For a remote agent to connect, you need -an ngrok tunnel. Here's how to set one up: +```bash +$B pair-agent --client TARGET_HOST +``` -1. Sign up at ngrok.com (free tier works) -2. Copy your auth token -3. Save it: `echo 'NGROK_AUTHTOKEN=your_token_here' > ~/.gstack/ngrok.env` -4. Restart the server with tunnel: `BROWSE_TUNNEL=1 $B restart` -5. Run `/pair-agent` again +If the user also needs admin access (JS execution, cookies, storage): -If you just want to test locally, choose 'Same machine' instead." - -STOP here. Wait for the user to set up ngrok and re-invoke. - -If the tunnel IS active (or if the user is OK with localhost-only for same-network use), -the pair-agent command will print the instruction block. Show it to the user and tell them: +```bash +$B pair-agent --admin --client TARGET_HOST +``` +Show the output to the user: "Copy everything between the ═══ lines and paste it into your other agent's chat. The agent will follow the instructions to connect. The setup key expires in 5 minutes." -### Admin access +**If ngrok is installed but NOT authed:** Walk the user through authentication: -If the user mentions needing JavaScript execution, cookie access, or storage access: +Tell the user: +"ngrok is installed but not logged in. Let's fix that: +1. Go to https://dashboard.ngrok.com/get-started/your-authtoken +2. Copy your auth token +3. Come back here and I'll run the auth command for you." + +STOP here and wait for the user to provide their auth token. + +When they provide it, run: ```bash -$B pair-agent --admin +ngrok config add-authtoken THEIR_TOKEN ``` -Tell the user: "This gives the remote agent full admin access including JS execution, -cookie reading, and storage access. Only do this if you trust the agent and need -these capabilities." +Then retry `$B pair-agent --client TARGET_HOST`. + +**If ngrok is NOT installed:** Walk the user through installation: + +Tell the user: +"To connect a remote agent, we need ngrok (a tunnel that exposes your local +browser to the internet securely). + +1. Go to https://ngrok.com and sign up (free tier works) +2. Install ngrok: + - macOS: `brew install ngrok` + - Linux: `snap install ngrok` or download from ngrok.com/download +3. Auth it: `ngrok config add-authtoken YOUR_TOKEN` + (get your token from https://dashboard.ngrok.com/get-started/your-authtoken) +4. Come back here and run `/pair-agent` again." + +STOP here. Wait for the user to install ngrok and re-invoke. ## Step 5: Verify connection