From d977070f28c256db862a2f1c79ba325ea0ba1aa5 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 17:24:47 -0700 Subject: [PATCH] docs: align ARCHITECTURE/README/BROWSER with sidebar removal Co-Authored-By: Claude Opus 4.8 (1M context) --- ARCHITECTURE.md | 37 +++----- BROWSER.md | 241 ++++++++++++++---------------------------------- README.md | 12 +-- 3 files changed, 88 insertions(+), 202 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b58953b7c..a2f2eac27 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -167,8 +167,8 @@ When a user runs `pair-agent --client`, the daemon starts an ngrok tunnel so a r The fix is **two HTTP listeners**, not one: -- **Local listener** (`127.0.0.1:LOCAL_PORT`) — always bound. Serves bootstrap (`/health` with token delivery), `/cookie-picker`, `/inspector/*`, `/welcome`, `/refs`, the sidebar-agent API, and the full command surface. Never forwarded. -- **Tunnel listener** (`127.0.0.1:TUNNEL_PORT`) — bound lazily on `/tunnel/start`, torn down on `/tunnel/stop`. Serves a locked allowlist: `/connect` (pairing ceremony, unauth + rate-limited), `/command` (scoped tokens only, further restricted to a browser-driving command allowlist), and `/sidebar-chat`. Everything else 404s. +- **Local listener** (`127.0.0.1:LOCAL_PORT`) — always bound. Serves bootstrap (`/health` with token delivery), `/cookie-picker`, `/inspector/*`, `/welcome`, `/refs`, and the full command surface. Never forwarded. +- **Tunnel listener** (`127.0.0.1:TUNNEL_PORT`) — bound lazily on `/tunnel/start`, torn down on `/tunnel/stop`. Serves a locked allowlist: `/connect` (pairing ceremony, unauth + rate-limited) and `/command` (scoped tokens only, further restricted to a browser-driving command allowlist). Everything else 404s. ngrok forwards only the tunnel port. The security property comes from **physical port separation**: a tunnel caller cannot reach `/health` or `/cookie-picker` because those paths don't exist on that TCP socket. Header inference (check `x-forwarded-for`, check origin) is unreliable (ngrok header behavior changes; local proxies can add these headers); socket separation isn't. @@ -178,7 +178,6 @@ ngrok forwards only the tunnel port. The security property comes from **physical | `GET /connect` | public (`{alive:true}`) | public (`{alive:true}`) | Probe path for tunnel liveness | | `POST /connect` | public (rate-limited 300/min) | public (rate-limited) | Setup-key exchange for pair-agent | | `POST /command` | auth (Bearer root OR scoped) | auth (scoped only, allowlisted commands) | Root token on tunnel = 403 | -| `POST /sidebar-chat` | auth | auth | Lets remote agent post into local sidebar | | `POST /pair` | root-only | 404 | Pairing mint — local operator action | | `POST /tunnel/{start,stop}` | root-only | 404 | Daemon configuration | | `POST /token`, `DELETE /token/:id` | root-only | 404 | Scoped token mint/revoke | @@ -190,7 +189,7 @@ ngrok forwards only the tunnel port. The security property comes from **physical | `GET /inspector/events` | Bearer OR HttpOnly `gstack_sse` cookie | 404 | SSE. Same cookie as /activity/stream | | `POST /sse-session` | auth (Bearer) | 404 | Mints the view-only 30-min SSE session cookie | -**Tunnel surface denial logs.** Every rejection on the tunnel listener (`path_not_on_tunnel`, `root_token_on_tunnel`, `missing_scoped_token`, `disallowed_command:*`) is recorded asynchronously to `~/.gstack/security/attempts.jsonl` with timestamp, source IP (from `x-forwarded-for`), path, and method. Rate-capped at 60 writes/min globally to prevent log-flood DoS. Shares the attempt log with the prompt-injection scanner. +**Tunnel surface denial logs.** Every rejection on the tunnel listener (`path_not_on_tunnel`, `root_token_on_tunnel`, `missing_scoped_token`, `disallowed_command:*`) is recorded asynchronously to `~/.gstack/security/attempts.jsonl` with timestamp, source IP (from `x-forwarded-for`), path, and method. Rate-capped at 60 writes/min globally to prevent log-flood DoS. Shares the attempt log with the page-content security layers (canary detection). **SSE session cookies.** EventSource can't send Authorization headers, so the extension POSTs `/sse-session` once at bootstrap with the root Bearer and receives a 30-minute view-only cookie (`gstack_sse`, HttpOnly, SameSite=Strict). The cookie is valid ONLY for `/activity/stream` and `/inspector/events` — it is NOT a scoped token and cannot be used on `/command`. Scope isolation is enforced by the module boundary: `sse-session-cookie.ts` has no imports from `token-registry.ts`. @@ -235,31 +234,25 @@ Page content harvested by CDP can contain lone UTF-16 surrogate halves (orphaned **Architectural invariant.** Every new SSE/WebSocket writer or HTTP response that ships page-content-derived strings MUST go through one of two paths: `JSON.stringify(payload, sanitizeReplacer)` for object payloads, or `sanitizeLoneSurrogates(body)` for text bodies. New surfaces that bypass both will desync the system. Inline comments at both SSE producers in `server.ts` say so; `browse/test/server-sanitize-surrogates.test.ts` pins wiring with bug-repro + invariant tests (`handleCommandInternalImpl` rename, central sanitization line, replacer existence, SSE producers stringify with replacer). -### Prompt injection defense (sidebar agent) +### Page-content security layers -The Chrome sidebar agent has tools (Bash, Read, Glob, Grep, WebFetch) and reads hostile web pages, so it's the part of gstack most exposed to prompt injection. Defense is layered, not single-point. +The browser reads hostile web pages, so page content is the part of gstack most exposed to prompt injection. Defense is layered, not single-point, and every retained layer is pure-string — no ML model, no native runtime. -> **GStack 2 production boundary:** the ML sidecar described below is retained -> as 1.x source and test material only. The managed runtime does not build or -> copy the sidecar, keeps `@huggingface/transformers` development-only, installs -> no ONNX runtime or model weights, and reports L4 unavailable. The generator -> preserves this historical judgment without promoting it into 2.0 setup. +| Layer | Module | Notes | +|-------|--------|-------| +| L1-L3 | `content-security.ts` | datamarking, hidden element strip, ARIA regex, URL blocklist, envelope wrapping | +| L5 | `security.ts` (canary) | inject + check | +| L6 | `security.ts` (combineVerdict) | threshold aggregation | -1. **L1-L3 content security (`browse/src/content-security.ts`).** Runs on every page-content command and every tool output: datamarking, hidden-element strip, ARIA regex, URL blocklist, and a trust-boundary envelope wrapper. Applied at both the server and the agent. +1. **L1-L3 content security (`browse/src/content-security.ts`).** Runs on every page-content command and every tool output: datamarking, hidden-element strip, ARIA regex, URL blocklist, and a trust-boundary envelope wrapper. -2. **L4 ML classifier — TestSavantAI (`browse/src/security-classifier.ts`).** A 22MB BERT-small ONNX model (int8 quantized) bundled with the agent. Runs locally, no network. Scans every user message and every Read/Glob/Grep/WebFetch tool output before Claude sees it. Opt-in 721MB DeBERTa-v3 ensemble via `GSTACK_SECURITY_ENSEMBLE=deberta`. +2. **L5 canary token (`browse/src/security.ts`).** A random token injected into the system prompt at session start. Rolling-buffer detection across `text_delta` and `input_json_delta` streams catches the token if it shows up anywhere in Claude's output, tool arguments, URLs, or file writes. Deterministic BLOCK — if the token leaks, the attacker convinced Claude to reveal the system prompt, and the session ends. -3. **L4b transcript classifier.** A Claude Haiku pass that looks at the full conversation shape (user message, tool calls, tool output), not just text. Gated by `LOG_ONLY: 0.40` so most clean traffic skips the paid call. +3. **L6 verdict combiner (`combineVerdict`).** Aggregates per-layer verdicts against the thresholds in `security.ts`. Canary leak always BLOCKs. -4. **L5 canary token (`browse/src/security.ts`).** A random token injected into the system prompt at session start. Rolling-buffer detection across `text_delta` and `input_json_delta` streams catches the token if it shows up anywhere in Claude's output, tool arguments, URLs, or file writes. Deterministic BLOCK — if the token leaks, the attacker convinced Claude to reveal the system prompt, and the session ends. +`security.ts` is pure-string (canary, verdict combiner, attack log, status) and safe to import from the compiled `browse/dist/browse` binary — it loads no native modules. The prompt-injection ML classifier (TestSavantAI/DeBERTa ONNX, `security-classifier.ts`) and its in-browser sidebar/terminal caller were removed; there is no L4 layer. -5. **L6 ensemble combiner (`combineVerdict`).** BLOCK requires agreement from two ML classifiers at >= `WARN` (0.75), not a single confident hit. This is the Stack Overflow instruction-writing false-positive mitigation. On tool-output scans, single-layer high confidence BLOCKs directly — the content wasn't user-authored, so the FP concern doesn't apply. - -**Critical constraint:** `security-classifier.ts` runs only in the sidebar-agent process, never in the compiled browse binary. `@huggingface/transformers` v4 requires `onnxruntime-node`, which fails `dlopen` from Bun compile's temp extract directory. Only the pure-string pieces (canary inject/check, verdict combiner, attack log, status) are in `security.ts`, which is safe to import from `server.ts`. - -**Env knobs:** `GSTACK_SECURITY_OFF=1` is a real kill switch (skips ML scan, canary still injects). Model cache at `~/.gstack/models/testsavant-small/` (112MB, first run) and `~/.gstack/models/deberta-v3-injection/` (721MB, opt-in only). Attack log at `~/.gstack/security/attempts.jsonl` (salted sha256 + domain, rotates at 10MB, 5 generations). Per-device salt at `~/.gstack/security/device-salt` (0600), cached in-process to survive FS-unwritable environments. - -**Visibility.** The sidebar header shows a shield icon (green/amber/red) polled via `/sidebar-chat`. A centered banner appears on canary leak or BLOCK verdict with the exact layer scores. `bin/gstack-security-dashboard` aggregates local attempts; `supabase/functions/community-pulse` aggregates opt-in community telemetry across users. +**Env knobs:** `GSTACK_SECURITY_OFF=1` is a kill switch for the content-security scan path (the canary is still injected regardless). Attack log at `~/.gstack/security/attempts.jsonl` (salted sha256 + domain, rotates at 10MB, 5 generations). Per-device salt at `~/.gstack/security/device-salt` (0600), cached in-process to survive FS-unwritable environments. `bin/gstack-security-dashboard` aggregates local attempts. ## The ref system diff --git a/BROWSER.md b/BROWSER.md index affa0447d..d0894ce90 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -2,9 +2,9 @@ gstack's browser surface in one document. Headless Chromium daemon, ~70+ commands, ref-based element selection, codifiable browser-skills, real-browser -mode with a Chrome side panel, an in-sidebar Claude PTY, an ngrok pair-agent -flow, and a layered prompt-injection defense — all behind a compiled CLI that -prints plain text to stdout. ~100-200ms per call. Zero context-token overhead. +mode, an ngrok pair-agent flow, and a layered page-content prompt-injection +defense — all behind a compiled CLI that prints plain text to stdout. +~100-200ms per call. Zero context-token overhead. If you've used gstack in the last release or two, the productivity loop is the new headline: `/scrape ` drives a page once, `/skillify` codifies the @@ -35,7 +35,7 @@ $B screenshot /tmp/hn.png /scrape hacker news front page # second call: 200ms via the codified skill # Watch Claude work in real time -$B connect # headed Chromium + Side Panel extension +$B connect # headed GStack Browser (anti-bot stealth) ``` --- @@ -50,24 +50,23 @@ $B connect # headed Chromium + Side Panel extension 6. [Browser-skills runtime](#browser-skills-runtime) 7. [Domain-skills (per-site agent notes)](#domain-skills) 8. [Real-browser mode (`$B connect`)](#real-browser-mode) — including [`--headed` + `--proxy` + `--navigate` (v1.28.0.0)](#headed-mode--proxy--browser-native-downloads-v12800) -9. [Side Panel + sidebar agent](#side-panel--sidebar-agent) -10. [Pair-agent — remote agents over an ngrok tunnel](#pair-agent) -11. [Authentication + tokens](#authentication) -12. [Prompt-injection security stack (L1–L6)](#security-stack) -13. [Screenshots, PDFs, visual inspection](#screenshots-pdfs-visual) -14. [Local HTML — `goto file://` vs `load-html`](#local-html) -15. [Batch endpoint](#batch-endpoint) -16. [Console, network, dialog capture](#capture) -17. [JS execution — `js` + `eval`](#js-execution) -18. [Tabs, frames, state, watch, inbox](#tabs-frames-state) -19. [CDP escape hatch + CSS inspector](#cdp) -20. [Performance + scale](#performance) -21. [Multi-workspace isolation](#multi-workspace) -22. [Environment variables](#environment-variables) -23. [Source map](#source-map) -24. [Development + testing](#development) -25. [Cross-references](#cross-references) -26. [Acknowledgments](#acknowledgments) +9. [Pair-agent — remote agents over an ngrok tunnel](#pair-agent) +10. [Authentication + tokens](#authentication) +11. [Page-content security stack (L1–L3, L5–L6)](#security-stack) +12. [Screenshots, PDFs, visual inspection](#screenshots-pdfs-visual) +13. [Local HTML — `goto file://` vs `load-html`](#local-html) +14. [Batch endpoint](#batch-endpoint) +15. [Console, network, dialog capture](#capture) +16. [JS execution — `js` + `eval`](#js-execution) +17. [Tabs, frames, state, watch, inbox](#tabs-frames-state) +18. [CDP escape hatch + CSS inspector](#cdp) +19. [Performance + scale](#performance) +20. [Multi-workspace isolation](#multi-workspace) +21. [Environment variables](#environment-variables) +22. [Source map](#source-map) +23. [Development + testing](#development) +24. [Cross-references](#cross-references) +25. [Acknowledgments](#acknowledgments) --- @@ -88,8 +87,8 @@ Three escalating modes: cheapest, what skills like `/qa`, `/design-review`, `/benchmark` use by default. - **Headed via `$B connect`**. Same daemon, but Chromium is visible (rebranded - as "GStack Browser") with the Side Panel extension auto-loaded. You watch - every command tick through in real time. + as "GStack Browser") with anti-bot stealth. You watch every command tick + through in real time. - **Pair-agent over a tunnel**. Daemon binds a second listener that ngrok forwards. A remote agent (Codex, OpenClaw, Hermes, anything that can speak HTTP) drives your local browser through a 26-command allowlist with a @@ -313,7 +312,7 @@ from `snapshot`, or `@c` refs from `snapshot -C`. Full table: | `status` | Daemon health + mode (headless / headed / cdp) | | `stop` | Shut down daemon | | `restart` | Restart daemon | -| `connect` | Launch headed GStack Browser with Side Panel extension | +| `connect` | Launch headed GStack Browser (anti-bot stealth) | | `disconnect` | Close headed Chrome, return to headless | | `focus [@ref]` | Bring headed Chrome to foreground (macOS); `@ref` also scrolls into view | | `state save\|load ` | Save or load browser state (cookies + URLs) | @@ -331,7 +330,7 @@ from `snapshot`, or `@c` refs from `snapshot -C`. Full table: | Command | Description | |---------|-------------| | `chain` (JSON via stdin) | Run a sequence of commands. Pipe `[["cmd","arg1",...],...]` to `$B chain`. Stops at first error. | -| `inbox [--clear]` | List messages from sidebar scout inbox | +| `inbox [--clear]` | List queued inbox messages (`.gstack/browser-scout.jsonl`); `--clear` empties after reading | | `watch [stop]` | Passive observation — periodic snapshots while user browses; `stop` returns summary | ### Browser-skills runtime @@ -348,7 +347,7 @@ from `snapshot`, or `@c` refs from `snapshot -C`. Full table: | Command | Description | |---------|-------------| -| `domain-skill save\|list\|show\|edit\|promote-to-global\|rollback\|rm ` | Per-site agent notes (host derived from active tab). Lifecycle: quarantined → active (after N=3 successful uses without classifier flag) → global (explicit promote) | +| `domain-skill save\|list\|show\|edit\|promote-to-global\|rollback\|rm ` | Per-site agent notes (host derived from active tab). Lifecycle: quarantined → active (after N=3 successful uses without a security flag) → global (explicit promote) | Aliases: `setcontent`, `set-content`, `setContent` → `load-html` (canonicalized before scope checks, so a read-scoped token can't use the alias to run a @@ -486,14 +485,14 @@ site (not deterministic scripts). One per hostname. Lifecycle: 1. `domain-skill save ` — agent writes a note about the site (e.g., "GitHub: PR creation needs `--draft` flag for non-staff", "X.com: timeline uses cursor pagination, not page numbers"). Default state: **quarantined**. -2. After **N=3** successful uses without the L4 prompt-injection classifier +2. After **N=3** successful uses without the page-content security scan flagging the note, it auto-promotes to **active**. 3. `domain-skill promote-to-global ` lifts it to the global tier (machine-wide, all projects). 4. `domain-skill rollback ` demotes; `domain-skill rm ` tombstones. -The classifier flag is set automatically by the L4 prompt-injection scan; -agents do not set it manually. +The security flag is set automatically by the page-content security scan +(canary + content-security layers); agents do not set it manually. Storage: - Per-project: `/.gstack/domain-skills/.md` @@ -506,9 +505,8 @@ Source: `browse/src/domain-skills.ts`, `domain-skill-commands.ts`. ## Real-browser mode `$B connect` launches **GStack Browser** — a rebranded Chromium controlled by -Playwright with the Side Panel extension auto-loaded and anti-bot stealth -patches applied. You watch every command tick through a visible window in -real time. +Playwright with anti-bot stealth patches applied. You watch every command tick +through a visible window in real time. ```bash $B connect # launches GStack Browser, headed @@ -530,9 +528,8 @@ Not your daily Chrome — a Playwright-managed Chromium with custom branding in the Dock and menu bar (the `.app` name, Dock icon, and tray, NOT the UA string), always-on Layer C anti-bot stealth (most JS-observable automation tells are masked, so many anti-bot-protected sites load cleanly), a -stock-Chrome user agent that reports the underlying Chromium version, and the -gstack extension pre-loaded via `launchPersistentContext`. The UA no longer -carries a `GStackBrowser` suffix — that branding string was itself a +stock-Chrome user agent that reports the underlying Chromium version. The UA +no longer carries a `GStackBrowser` suffix — that branding string was itself a high-entropy tell, so the browser now reports a plain `Chrome/` UA. Deepest-layer CDP-protocol detection still gets through (Google can still trigger captchas; see the CDP-patch item in `TODOS.md`). Your regular Chrome @@ -640,73 +637,6 @@ transport retries that could corrupt browser traffic. --- -## Side Panel + sidebar agent - -The Chrome extension that ships baked into GStack Browser shows a live -activity feed of every browse command in a Side Panel, plus `@ref` overlays -on the page, plus an interactive Claude PTY inside the sidebar. - -### The Terminal pane (the headline) - -The Side Panel's primary surface is the **Terminal pane** — a live `claude -p` -PTY you can type into directly from the sidebar. Activity / Refs / Inspector -are debug overlays behind the footer's `debug` toggle. WebSocket auth uses -`Sec-WebSocket-Protocol` (browsers can't set `Authorization` on a WebSocket -upgrade), and the PTY session token is a 30-minute HttpOnly cookie minted -via `POST /pty-session`. - -The toolbar's Cleanup button and the Inspector's "Send to Code" action both -pipe text into the live Claude PTY via `window.gstackInjectToTerminal(text)`, -exposed by `sidepanel-terminal.js`. There's no separate `/sidebar-command` -POST — the live REPL is the only execution surface. - -### Activity feed - -A scrolling feed of every browse command — name, args, duration, status, -errors. Shows up in real time as Claude works. Backed by SSE (`/activity/stream`) -that accepts the Bearer token OR the HttpOnly `gstack_sse` session cookie -(30-minute stream-scope cookie minted via `POST /sse-session`). - -### Refs tab - -After `$B snapshot`, shows the current `@ref` list (role + name) so you can -see what Claude is targeting. - -### CSS Inspector - -Powered by `$B inspect` (CDP-based). Click any element on the page to see the -full CSS rule cascade, computed styles, box model, and modification history. -The "Send to Code" button injects a description into the Claude PTY. - -### Sidebar architecture - -| Component | Where it lives | Notes | -|-----------|----------------|-------| -| Side Panel UI | `extension/sidepanel.js`, `sidepanel-terminal.js` | Chrome extension surface | -| Background SW | `extension/background.js` | Manages tab events, port management | -| Content script | `extension/content.js` | Page overlays, `gstack` pill | -| Terminal agent | `browse/src/terminal-agent.ts` | PTY spawn, lifecycle, auth | -| Sidebar utilities | `browse/src/sidebar-utils.ts` | URL sanitization, helpers | - -Before modifying any of these, read the comment block in `CLAUDE.md` under -"Sidebar architecture" — silent failures here usually trace to not understanding -the cross-component flow. - -### Manual install (for your regular Chrome) - -If you want the extension in your everyday Chrome (not the Playwright-controlled -one): - -```bash -bin/gstack-extension # opens chrome://extensions, copies path to clipboard -``` - -Or do it manually: `chrome://extensions` → toggle Developer mode → Load -unpacked → navigate to `~/.claude/skills/gstack/extension` → pin the -extension → enter the port from `$B status`. - ---- - ## Pair-agent Remote AI agents (Codex, OpenClaw, Hermes, anything that speaks HTTP) can @@ -728,11 +658,10 @@ by a 26-command allowlist, scoped tokens, and a denial log. When `pair-agent` activates, the daemon binds **two HTTP listeners**: - **Local listener** (`127.0.0.1:LOCAL_PORT`). Full command surface. Never - forwarded by ngrok. Used by your Claude Code, the Side Panel, anything - on your machine. + forwarded by ngrok. Used by your Claude Code and anything on your machine. - **Tunnel listener** (`127.0.0.1:TUNNEL_PORT`). Locked allowlist — `/connect`, `/command` (scoped tokens + 26-command browser-driving - allowlist), `/sidebar-chat`. ngrok forwards only this port. + allowlist). ngrok forwards only this port. Root tokens sent over the tunnel return 403. SSE endpoints use a 30-minute HttpOnly `gstack_sse` cookie (never valid against `/command`). @@ -790,15 +719,8 @@ Every command that mutates browser state must include SSE endpoints (`/activity/stream`, `/inspector/events`) accept the Bearer token OR a 30-minute HttpOnly `gstack_sse` cookie minted via `POST /sse-session`. The `?token=` query-param auth is no longer -supported. This is what lets the Chrome extension subscribe to the activity -feed without putting the root token in extension storage. - -### PTY session cookie - -The Terminal pane uses a separate session cookie, `gstack_pty`, minted via -`POST /pty-session`. Different scope — can spawn / drive the live `claude` -PTY, can't dispatch arbitrary `/command` calls. `/health` endpoint MUST NOT -surface this token. +supported. This lets a browser-based SSE subscriber follow the activity or +inspector stream without putting the root token in client storage. ### Token registry @@ -811,58 +733,43 @@ startup. ## Security stack -Layered defense against prompt injection. Every layer runs synchronously on -every user message and every tool output that could carry untrusted content -(Read, Glob, Grep, WebFetch, page text from `$B`). +Layered defense against prompt injection in the page content the browser +reads. Every layer runs synchronously on every page-content command and every +tool output that could carry untrusted content. All of it is pure-string — +no ML model, no native runtime. -| Layer | Module | Lives in | -|-------|--------|----------| -| **L1** Datamarking | `content-security.ts` | both server + sidebar agent | -| **L2** Hidden-element strip | `content-security.ts` | both | -| **L3** ARIA + URL blocklist + envelope wrapping | `content-security.ts` | both | -| **L4** TestSavantAI ML classifier (22MB ONNX) | `security-classifier.ts` | sidebar-agent only* | -| **L4b** Claude Haiku transcript check | `security-classifier.ts` | sidebar-agent only | -| **L5** Canary token (session-exfil detection) | `security.ts` | both — inject in compiled, check in agent | -| **L6** `combineVerdict` ensemble | `security.ts` | both | +| Layer | Module | Notes | +|-------|--------|-------| +| **L1** Datamarking | `content-security.ts` | datamark page content before Claude sees it | +| **L2** Hidden-element strip | `content-security.ts` | remove hidden / off-screen injected text | +| **L3** ARIA regex + URL blocklist + envelope wrapping | `content-security.ts` | trust-boundary framing | +| **L5** Canary token (session-exfil detection) | `security.ts` | inject + check | +| **L6** `combineVerdict` | `security.ts` | threshold aggregation | -\* `security-classifier.ts` cannot be imported from the compiled browse -binary — `@huggingface/transformers` v4 requires `onnxruntime-node` which -fails to `dlopen` from Bun compile's temp extract dir. The compiled binary -runs L1–L3, L5, L6 only. +`security.ts` is pure-string (canary, verdict combiner, attack log, status) +and safe to import from the compiled `browse/dist/browse` binary — it loads no +native modules. There is no L4 layer: the prompt-injection ML classifier +(TestSavantAI/DeBERTa ONNX, `security-classifier.ts`) and its in-browser +sidebar/terminal caller were removed. -### Thresholds +### Verdict rule -- `BLOCK: 0.85` — single-layer score that would cause BLOCK if cross-confirmed -- `WARN: 0.75` — cross-confirm threshold. When L4 AND L4b both >= 0.75 → BLOCK -- `LOG_ONLY: 0.40` — gates transcript classifier (skip Haiku when all layers < 0.40) -- `SOLO_CONTENT_BLOCK: 0.92` — single-layer threshold for label-less content classifiers - -### Ensemble rule - -BLOCK only when the ML content classifier AND the transcript classifier both -report >= WARN. Single-layer high confidence degrades to WARN — this is the -Stack Overflow instruction-writing FP mitigation. **Canary leak always -BLOCKs (deterministic).** +L6 aggregates per-layer verdicts against the thresholds in `security.ts`. +**Canary leak always BLOCKs (deterministic)** — if the token shows up in +Claude's output, tool arguments, URLs, or file writes, the attacker convinced +Claude to reveal the system prompt and the session ends. ### Env knobs -- `GSTACK_SECURITY_OFF=1` — emergency kill switch. Classifier stays off - even if warmed. Canary is still injected; just the ML scan is skipped. -- `GSTACK_SECURITY_ENSEMBLE=deberta` — opt-in DeBERTa-v3 ensemble. Adds - ProtectAI DeBERTa-v3-base-injection-onnx as L4c classifier. 721MB - first-run download. With ensemble enabled, BLOCK requires 2-of-3 ML - classifiers agreeing at >= WARN. -- Classifier model cache: `~/.gstack/models/testsavant-small/` (112MB, first - run only) plus `~/.gstack/models/deberta-v3-injection/` (721MB, only when - ensemble enabled). +- `GSTACK_SECURITY_OFF=1` — kill switch for the content-security scan path. + The canary is still injected regardless; only the scan is skipped. - Attack log: `~/.gstack/security/attempts.jsonl` (salted SHA-256 + domain only, rotates at 10MB, 5 generations). - Per-device salt: `~/.gstack/security/device-salt` (0600). - Session state: `~/.gstack/security/session-state.json` (cross-process, atomic). -A shield icon in the sidebar header shows the live status. See -ARCHITECTURE.md § "Prompt injection defense" for the full threat model. +See ARCHITECTURE.md § "Page-content security layers" for the full threat model. --- @@ -1082,13 +989,12 @@ you did at the end without spamming `snapshot` calls. ### Inbox ```bash -$B inbox # list messages from sidebar scout +$B inbox # list queued inbox messages $B inbox --clear # clear after reading ``` -The sidebar scout (a background process the Chrome extension can spawn) drops -notes for Claude when the user surfaces something they want noticed. Stored -in `.gstack/browser-scout.jsonl`. +A queue of notes for Claude, stored in `.gstack/browser-scout.jsonl`. Anything +that appends to that file surfaces here on the next `$B inbox`. --- @@ -1197,8 +1103,7 @@ the global `~/.gstack/browser-skills/foo/` only inside project-a. | `BROWSE_TUNNEL` | 0 | Activate the dual-listener tunnel architecture (requires `NGROK_AUTHTOKEN`) | | `BROWSE_TUNNEL_LOCAL_ONLY` | 0 | Test-only — bind both listeners locally without ngrok | | `GSTACK_BROWSE_MAX_HTML_BYTES` | 52428800 (50MB) | `load-html` size cap | -| `GSTACK_SECURITY_OFF` | unset | Emergency kill switch — disable ML classifier | -| `GSTACK_SECURITY_ENSEMBLE` | unset | Set to `deberta` for 3-classifier ensemble (721MB download) | +| `GSTACK_SECURITY_OFF` | unset | Kill switch for the content-security scan path (canary still injects) | | `GSTACK_STEALTH` | unset | Set to `extended` (also accepts `1`/`true`) to layer six aggressive patches (WebGL spoof, faked plugins, mediaDevices) on top of Layer C. Actively lies; can break sites. | | `GSTACK_CDP_STEALTH` | unset | Set to `on`/`1`/`true` to emit `--gstack-suppress-prepare-stack-trace` (gbrowser Pack 2 / B11 C++ patch only; no-op on stock Chromium) | | `GSTACK_GPU_VENDOR`, `GSTACK_GPU_RENDERER`, `GSTACK_GPU_CHIPSET` | unset | Per-install GPU spoof fed to the Pack 1 WebGL/UA-CH C++ patches. Set by gbd from the host profile; emitted as `--gstack-gpu-vendor` / `--gstack-gpu-renderer` / `--gstack-ua-model` cmdline switches only when present. | @@ -1240,15 +1145,11 @@ browse/ │ ├── tab-session.ts # Per-tab session state (load-html replay, ref map scope) │ ├── token-registry.ts # Mint/validate/revoke for root + setup keys + scoped tokens │ ├── sse-session-cookie.ts # 30-min HttpOnly cookie for /activity/stream + /inspector/events -│ ├── pty-session-cookie.ts # Separate scope: live Claude PTY auth │ ├── tunnel-denial-log.ts # ~/.gstack/security/attempts.jsonl writer (salted) │ ├── path-security.ts # validateOutputPath / validateReadPath / validateTempPath │ ├── url-validation.ts # URL safety checks for goto │ ├── content-security.ts # L1-L3: datamarking, hidden strip, ARIA, URL blocklist, envelopes -│ ├── security.ts # L5 canary + L6 verdict combiner + thresholds -│ ├── security-classifier.ts # L4 ML classifier (TestSavant + optional DeBERTa ensemble) -│ ├── terminal-agent.ts # Side Panel Claude PTY manager (auth + lifecycle) -│ ├── sidebar-utils.ts # Sidebar URL sanitization + helpers +│ ├── security.ts # L5 canary + L6 verdict combiner + thresholds (pure-string, compiled-safe) │ ├── cookie-import-browser.ts # Decrypt + import cookies from real Chromium browsers │ ├── cookie-picker-routes.ts # HTTP routes for /cookie-picker/* │ ├── cookie-picker-ui.ts # Self-contained HTML/CSS/JS for cookie picker @@ -1371,8 +1272,8 @@ cp browse/dist/browse ~/.claude/skills/gstack/browse/dist/browse ## Cross-references -- [`ARCHITECTURE.md`](ARCHITECTURE.md) — system-level architecture, dual-listener tunnel design, prompt-injection defense threat model -- [`CLAUDE.md`](CLAUDE.md) — project-level instructions, sidebar architecture notes, security-stack constraints +- [`ARCHITECTURE.md`](ARCHITECTURE.md) — system-level architecture, dual-listener tunnel design, page-content security threat model +- [`CLAUDE.md`](CLAUDE.md) — project-level instructions, security-stack constraints - [`docs/REMOTE_BROWSER_ACCESS.md`](docs/REMOTE_BROWSER_ACCESS.md) — operator guide for `/pair-agent` (setup keys, scoped tokens, denial log) - [`docs/designs/BROWSER_SKILLS_V1.md`](docs/designs/BROWSER_SKILLS_V1.md) — design doc for browser-skills runtime (Phase 1 + 2a + roadmap) - [`scrape/SKILL.md`](scrape/SKILL.md) — `/scrape` skill: match-or-prototype data extraction @@ -1391,12 +1292,6 @@ them back to Playwright Locators — is built entirely on top of Playwright's primitives. Thank you to the Playwright team for building such a solid foundation. -The prompt-injection L4 layer uses -[TestSavantAI/distilbert-v1.1-32](https://huggingface.co/TestSavantAI/distilbert-v1.1-32) -(112MB ONNX), and the optional ensemble layer uses -[ProtectAI/deberta-v3-base-prompt-injection-v2](https://huggingface.co/protectai/deberta-v3-base-prompt-injection-v2) -(721MB ONNX) — both run locally via `@huggingface/transformers`. - The CDP escape hatch is gated by an allowlist directly inspired by Codex's T2 outside-voice review during the v1.4 design pass: deny-default with an explicit allowlist, not allow-default with a denylist. diff --git a/README.md b/README.md index 07793085c..f88a5a341 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan- | `/document-release` | **Technical Writer** | Update all project docs to match what you just shipped. Catches stale READMEs automatically. Builds a Diataxis coverage map (reference / how-to / tutorial / explanation) so gaps are visible in the PR body. | | `/document-generate` | **Documentation Author** | Generate missing docs from scratch using the Diataxis framework. Researches the codebase first, then writes reference / how-to / tutorial / explanation docs that actually match the code. Invokable standalone or chained from `/document-release` when the coverage map finds gaps. Learn more: [tutorial](docs/tutorial-document-generate.md) • [how-to](docs/howto-document-a-shipped-feature.md) • [why Diataxis](docs/explanation-diataxis-in-gstack.md). | | `/retro` | **Eng Manager** | Team-aware weekly retro. Per-person breakdowns, shipping streaks, test health trends, growth opportunities. `/retro global` runs across all your projects and AI tools (Claude Code, Codex, Gemini). | -| `/browse` | **QA Engineer** | Give the agent eyes. Real Chromium browser, real clicks, real screenshots. ~100ms per command. `/open-gstack-browser` launches GStack Browser with sidebar, anti-bot stealth, and auto model routing. | +| `/browse` | **QA Engineer** | Give the agent eyes. Real Chromium browser, real clicks, real screenshots. ~100ms per command. `/open-gstack-browser` launches GStack Browser with anti-bot stealth and one-click cookie import. | | `/setup-browser-cookies` | **Session Manager** | Import cookies from your real browser (Chrome, Arc, Brave, Edge) into the headless session. Test authenticated pages. | | `/autoplan` | **Review Pipeline** | One command, fully reviewed plan. Runs CEO → design → eng review automatically with encoded decision principles. Surfaces only taste decisions for your approval. | | `/spec` | **Spec Author** | Turn vague intent into a precise, executable spec in five phases (why, scope, technical with mandatory code-reading, draft, file). Codex quality gate before file (blocks below 7/10), fail-closed secret redaction, dedupe against existing issues, archive to `$GSTACK_STATE_ROOT/projects/$SLUG/specs/` for team-corpus recall. `--execute` spawns `claude -p` in a fresh worktree; `/ship` auto-closes the source issue on merge. Plan-mode aware. | @@ -351,7 +351,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan- | `/freeze` | **Edit Lock** — restrict file edits to one directory. Prevents accidental changes outside scope while debugging. | | `/guard` | **Full Safety** — `/careful` + `/freeze` in one command. Maximum safety for prod work. | | `/unfreeze` | **Unlock** — remove the `/freeze` boundary. | -| `/open-gstack-browser` | **GStack Browser** — launch GStack Browser with sidebar, anti-bot stealth, auto model routing (Sonnet for actions, Opus for analysis), one-click cookie import, and Claude Code integration. Clean up pages, take smart screenshots, edit CSS, and pass info back to your terminal. | +| `/open-gstack-browser` | **GStack Browser** — launch an AI-controlled Chromium with anti-bot stealth and one-click cookie import from your real Chrome. Drive it from your terminal: clean up pages, take smart screenshots, edit CSS. | | `/setup-deploy` | **Deploy Configurator** — one-time setup for `/land-and-deploy`. Detects your platform, production URL, and deploy commands. | | `/setup-gbrain` | **GBrain Onboarding** — from zero to running gbrain in under 5 minutes. PGLite local, Supabase existing URL, or auto-provision a new Supabase project via Management API. MCP registration for Claude Code + per-repo trust triad (read-write/read-only/deny). [Full guide](USING_GBRAIN_WITH_GSTACK.md). | | `/sync-gbrain` | **Keep Brain Current** — re-index this repo's code into gbrain via `gbrain sources add` + `gbrain sync --strategy code`, refresh the `## GBrain Search Guidance` block in CLAUDE.md, and auto-remove guidance when the capability check fails. `--incremental` (default), `--full`, `--dry-run`. Idempotent; safe to re-run. | @@ -407,13 +407,11 @@ gstack works well with one sprint. It gets interesting with ten running at once. **`/document-release` is the engineer you never had.** It reads every doc file in your project, cross-references the diff, and updates everything that drifted. README, ARCHITECTURE, CONTRIBUTING, CLAUDE.md, TODOS — all kept current automatically. And now `/ship` auto-invokes it — docs stay current without an extra command. -**Real browser mode.** `/open-gstack-browser` launches GStack Browser, an AI-controlled Chromium with anti-bot stealth, custom branding, and the sidebar extension baked in. Sites like Google and NYTimes work without captchas. The menu bar says "GStack Browser" instead of "Chrome for Testing." Your regular Chrome stays untouched. All existing browse commands work unchanged. `$B disconnect` returns to headless. The browser stays alive as long as the window is open... no idle timeout killing it while you're working. +**Real browser mode.** `/open-gstack-browser` launches GStack Browser, an AI-controlled Chromium with anti-bot stealth and custom branding. Sites like Google and NYTimes work without captchas. The menu bar says "GStack Browser" instead of "Chrome for Testing." Your regular Chrome stays untouched. All existing browse commands work unchanged. `$B disconnect` returns to headless. The browser stays alive as long as the window is open... no idle timeout killing it while you're working. -**Sidebar agent — your AI browser assistant.** Type natural language in the Chrome side panel and a child Claude instance executes it. "Navigate to the settings page and screenshot it." "Fill out this form with test data." "Go through every item in this list and extract the prices." The sidebar auto-routes to the right model: Sonnet for fast actions (click, navigate, screenshot) and Opus for reading and analysis. Each task gets up to 5 minutes. The sidebar agent runs in an isolated session, so it won't interfere with your main Claude Code window. One-click cookie import right from the sidebar footer. +**Personal automation.** The AI-controlled browser isn't just for dev workflows. Example: "Browse my kid's school parent portal and add all the other parents' names, phone numbers, and photos to my Google Contacts." Two ways to get authenticated: (1) log in once in the headed browser, your session persists, or (2) one-click cookie import from your real Chrome. Once authenticated, Claude navigates the directory, extracts the data, and creates the contacts. -**Personal automation.** The sidebar agent isn't just for dev workflows. Example: "Browse my kid's school parent portal and add all the other parents' names, phone numbers, and photos to my Google Contacts." Two ways to get authenticated: (1) log in once in the headed browser, your session persists, or (2) click the "cookies" button in the sidebar footer to import cookies from your real Chrome. Once authenticated, Claude navigates the directory, extracts the data, and creates the contacts. - -**Prompt injection defense.** Hostile web pages try to hijack your sidebar agent. Deterministic content stripping, datamarking, URL checks, canary exfiltration detection, and scoped tool boundaries remain active. The retained 1.x source also documents an ML sidecar and optional ensemble, but the GStack 2 production setup does not build or install that sidecar, Hugging Face/ONNX, or model weights; it reports L4 unavailable. See [ARCHITECTURE.md](ARCHITECTURE.md#prompt-injection-defense-sidebar-agent) for the retained stack and the GStack 2 boundary. +**Prompt injection defense.** Hostile web pages try to hijack the agent through the content it reads. Deterministic content stripping, datamarking, URL checks, canary exfiltration detection, and scoped tool boundaries stay active on every page-content command. All of it is pure-string — no ML model, no native runtime to install. See [ARCHITECTURE.md](ARCHITECTURE.md#page-content-security-layers) for the full layer stack. **Browser handoff when the AI gets stuck.** Hit a CAPTCHA, auth wall, or MFA prompt? `$B handoff` opens a visible Chrome at the exact same page with all your cookies and tabs intact. Solve the problem, tell Claude you're done, `$B resume` picks up right where it left off. The agent even suggests it automatically after 3 consecutive failures.