# Remote Browser Access — How to Pair With a GStack Browser A GStack Browser server can be shared with any AI agent that can make HTTP requests. The agent gets scoped access to a real Chromium browser: navigate pages, read content, click elements, fill forms, take screenshots. Each agent gets its own tab. This document is the reference for remote agents. The quick-start instructions are generated by `$B pair-agent` with the actual credentials baked in. ## Architecture ``` Your Machine Remote Agent ───────────── ──────────── GStack Browser Server Any AI agent ├── Chromium (Playwright) (OpenClaw, Hermes, Codex, etc.) ├── HTTP API on localhost:PORT │ ├── ngrok tunnel (optional) │ │ https://xxx.ngrok.dev ─────────────┘ └── Token Registry ├── Root token (local only) ├── Setup keys (5 min, one-time) └── Session tokens (24h, scoped) ``` ## Connection Flow 1. **User runs** `$B pair-agent` (or `/pair-agent` in Claude Code) 2. **Server creates** a one-time setup key (expires in 5 minutes) 3. **User copies** the instruction block into the other agent's chat 4. **Remote agent runs** `POST /connect` with the setup key 5. **Server returns** a scoped session token (24h default) 6. **Remote agent creates** its own tab via `POST /command` with `newtab` 7. **Remote agent browses** using `POST /command` with its session token + tabId ## API Reference ### Authentication All endpoints except `/connect` and `/health` require a Bearer token: ``` Authorization: Bearer gsk_sess_... ``` ### Endpoints #### POST /connect Exchange a setup key for a session token. No auth required. Rate-limited to 3/minute. ```json Request: {"setup_key": "gsk_setup_..."} Response: {"token": "gsk_sess_...", "expires": "ISO8601", "scopes": ["read","write"], "agent": "agent-name"} ``` #### POST /command Send a browser command. Requires Bearer auth. ```json Request: {"command": "goto", "args": ["https://example.com"], "tabId": 1} Response: (plain text result of the command) ``` #### GET /health Server status. No auth required. Returns status, tabs, mode, uptime. ### Commands #### Navigation | Command | Args | Description | |---------|------|-------------| | `goto` | `["URL"]` | Navigate to a URL | | `back` | `[]` | Go back | | `forward` | `[]` | Go forward | | `reload` | `[]` | Reload page | #### Reading Content | Command | Args | Description | |---------|------|-------------| | `snapshot` | `["-i"]` | Interactive snapshot with @ref labels (most useful) | | `text` | `[]` | Full page text | | `html` | `["selector?"]` | HTML of element or full page | | `links` | `[]` | All links on page | | `screenshot` | `["/tmp/s.png"]` | Take a screenshot | | `url` | `[]` | Current URL | #### Interaction | Command | Args | Description | |---------|------|-------------| | `click` | `["@e3"]` | Click an element (use @ref from snapshot) | | `fill` | `["@e5", "text"]` | Fill a form field | | `select` | `["@e7", "option"]` | Select dropdown value | | `type` | `["text"]` | Type text (keyboard) | | `press` | `["Enter"]` | Press a key | | `scroll` | `["down"]` | Scroll the page | #### Tabs | Command | Args | Description | |---------|------|-------------| | `newtab` | `["URL?"]` | Create a new tab (required before writing) | | `tabs` | `[]` | List all tabs | | `closetab` | `["id?"]` | Close a tab | ## The Snapshot → @ref Pattern This is the most powerful browsing pattern. Instead of writing CSS selectors: 1. Run `snapshot -i` to get an interactive snapshot with labeled elements 2. The snapshot returns text like: ``` [Page Title] @e1 [link] "Home" @e2 [button] "Sign In" @e3 [input] "Search..." ``` 3. Use the `@e` refs directly in commands: `click @e2`, `fill @e3 "search query"` This is how the snapshot system works, and it's much more reliable than guessing CSS selectors. Always `snapshot -i` first, then use the refs. ## Scopes | Scope | What it allows | |-------|---------------| | `read` | snapshot, text, html, links, screenshot, url, tabs, console, etc. | | `write` | goto, click, fill, scroll, newtab, closetab, etc. | | `admin` | eval, js, cookies, storage, cookie-import, useragent, etc. | | `meta` | tab, diff, frame, responsive, watch | Default tokens get `read` + `write`. Admin requires `--admin` flag when pairing. ## Tab Isolation Each agent owns the tabs it creates. Rules: - **Read:** Any agent can read any tab (snapshot, text, screenshot) - **Write:** Only the tab owner can write (click, fill, goto, etc.) - **Unowned tabs:** Pre-existing tabs are root-only for writes - **First step:** Always `newtab` before trying to interact ## Error Codes | Code | Meaning | What to do | |------|---------|------------| | 401 | Token invalid, expired, or revoked | Ask user to run /pair-agent again | | 403 | Command not in scope, or tab not yours | Use newtab, or ask for --admin | | 429 | Rate limit exceeded (>10 req/s) | Wait for Retry-After header | ## Security Model - Setup keys expire in 5 minutes and can only be used once - Session tokens expire in 24 hours (configurable) - The root token never appears in instruction blocks or connection strings - Admin scope (JS execution, cookie access) is denied by default - Tokens can be revoked instantly: `$B tunnel revoke agent-name` - All agent activity is logged with attribution (clientId) ## Same-Machine Shortcut If both agents are on the same machine, skip the copy-paste: ```bash $B pair-agent --local openclaw # writes to ~/.openclaw/skills/gstack/browse-remote.json $B pair-agent --local codex # writes to ~/.codex/skills/gstack/browse-remote.json $B pair-agent --local cursor # writes to ~/.cursor/skills/gstack/browse-remote.json ``` No tunnel needed. Uses localhost directly. ## ngrok Tunnel Setup For remote agents on different machines: 1. Sign up at [ngrok.com](https://ngrok.com) (free tier works) 2. Copy your auth token from the dashboard 3. Save it: `echo 'NGROK_AUTHTOKEN=your_token' > ~/.gstack/ngrok.env` 4. Optionally claim a stable domain: `echo 'NGROK_DOMAIN=your-name.ngrok-free.dev' >> ~/.gstack/ngrok.env` 5. Start with tunnel: `BROWSE_TUNNEL=1 $B restart` 6. Run `$B pair-agent` — it will use the tunnel URL automatically