From d21f11af5d76a0422a64434cbbdb5d5bb7d8a9b9 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 19:12:01 -0700 Subject: [PATCH] docs: scrub the sidebar-agent ghost from comments and CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 20+ comments across 10 files still described the deleted sidebar-agent.ts as a live process — including load-bearing architecture claims ('IMPORTED ONLY BY sidebar-agent.ts', 'sidebar-agent fills this in on first prompt-injection load', 'kill sidebar-agent' in shutdown docs) and ~60 lines of tombstone blocks in server.ts enumerating deleted identifiers by name (a false grep surface: searching processAgentEvent hit server.ts and looked live). CLAUDE.md's security-stack section now documents the LIVE architecture: L1-L3 content filters + testsavant via the security sidecar subprocess; the L4b/ensemble rows, the GSTACK_SECURITY_ENSEMBLE knob, and the 721MB DeBERTa download are gone (deleted as dead code this wave) with an explicit do-not-re-document note; attempts.jsonl is correctly attributed to tunnel-denial-log.ts; the no-live-writer status of classifierStatus is stated. Comments that survive now describe what IS, not what WAS: the promotion gate in domain-skills.ts explains why classifier_score>0 is load-bearing given no L4 load-time scan exists; file-permissions.ts names real sensitive files. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 58 +++++++++++++---------------- browse/src/browser-manager.ts | 4 +- browse/src/cdp-commands.ts | 4 +- browse/src/cli.ts | 10 ++--- browse/src/domain-skill-commands.ts | 15 ++++---- browse/src/domain-skills.ts | 12 +++--- browse/src/file-permissions.ts | 4 +- browse/src/server.ts | 35 ++--------------- browse/src/terminal-agent.ts | 4 +- extension/sidepanel.js | 9 +---- 10 files changed, 55 insertions(+), 100 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 461914a1d..1a6ff73c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -391,47 +391,39 @@ every `git pull`. | Layer | Module | Lives in | |-------|--------|----------| -| L1-L3 | `content-security.ts` | both server and agent — datamarking, hidden element strip, ARIA regex, URL blocklist, envelope wrapping | -| L4 | `security-classifier.ts` (TestSavantAI ONNX) | **sidebar-agent only** | -| L4b | `security-classifier.ts` (Claude Haiku transcript) | **sidebar-agent only** | -| L5 | `security.ts` (canary) | both — inject in compiled, check in agent | -| L6 | `security.ts` (combineVerdict ensemble) | both | +| L1-L3 | `content-security.ts` | server + read path — datamarking, hidden element strip, ARIA regex, URL blocklist, envelope wrapping | +| L4 | `security-classifier.ts` (TestSavantAI ONNX) | **security sidecar subprocess only** (`security-sidecar-entry.ts`, driven by `security-sidecar-client.ts` from server.ts) | +| Canary | `security.ts` (generate/inject/detect) | pure utilities — no production injector today (the chat prompt-builder that injected them was ripped) | +| Combiner | `security.ts` (combineVerdict + THRESHOLDS) | pure, tested; retains transcript/deberta vote handling for LayerSignal inputs no live layer produces anymore | + +History note: an L4b Haiku transcript classifier and an opt-in DeBERTa ensemble +(`GSTACK_SECURITY_ENSEMBLE=deberta`) existed until the chat-path agent that +invoked them was ripped; both were deleted as dead code (zero production +callers). Do not re-document them as live. **Critical constraint:** `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. Only `security.ts` -(pure-string operations — canary, verdict combiner, attack log, status) is safe -for `server.ts`. See `~/.gstack/projects/garrytan-gstack/ceo-plans/2026-04-19-prompt-injection-guard.md` -§"Pre-Impl Gate 1 Outcome" for full architectural decision. +which fails to `dlopen` from Bun compile's temp extract dir — hence the sidecar +subprocess. Only `security.ts` (pure-string operations — canary utilities, +verdict combiner, status) is safe for `server.ts`. See +`~/.gstack/projects/garrytan-gstack/ceo-plans/2026-04-19-prompt-injection-guard.md` +§"Pre-Impl Gate 1 Outcome" for the original architectural decision. -**Thresholds** (in `security.ts`): -- `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 - (testsavant, deberta). Intentionally higher than `BLOCK` because these layers can't - distinguish "this is an injection" from "this looks like phishing aimed at the user." - The transcript classifier keeps a separate, label-gated solo path at `BLOCK` (0.85). - -**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). +**Thresholds** (in `security.ts`): `BLOCK: 0.85`, `WARN: 0.75`, `LOG_ONLY: 0.40`, +`SOLO_CONTENT_BLOCK: 0.92` (label-less content classifiers can't distinguish +"injection" from "phishing aimed at the user", so their solo bar is higher). +The live L4 path applies these in server.ts's sidecar-scan handling; canary +leak always BLOCKs (deterministic). **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 for cross-model - agreement. 721MB first-run download. With ensemble enabled, BLOCK requires - 2-of-3 ML classifiers agreeing at >= WARN (testsavant, deberta, transcript). - Without ensemble (default), BLOCK requires testsavant + transcript at >= WARN. + warmed; the L1-L3 filters keep running. - Classifier model cache: `~/.gstack/models/testsavant-small/` (112MB, first run only) - plus `~/.gstack/models/deberta-v3-injection/` (721MB, only when ensemble enabled) -- Attack log: `~/.gstack/security/attempts.jsonl` (salted sha256 + 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) +- Attack log: `~/.gstack/security/attempts.jsonl` — written by + `tunnel-denial-log.ts` (tunnel-surface rejections; rotates at 10MB, 5 generations) +- Session state: `~/.gstack/security/session-state.json` (cross-process, atomic; + NOTE: classifierStatus currently has no live writer — shield status derives + from what's on disk) ## Dev symlink awareness diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 4b378cc4f..130296935 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -244,7 +244,7 @@ export class BrowserManager { // Called when the headed browser disconnects without intentional teardown // (user closed the window). Wired up by server.ts to run full cleanup - // (sidebar-agent, state file, profile locks) before exiting with code 2. + // (terminal agent, state file, profile locks) before exiting with code 2. // Returns void or a Promise; rejections are caught and fall back to exit(2). // `exitCode` is the resolved process exit code from the disconnect cause: // 0 on clean user-initiated quit (e.g., Cmd+Q on headed Chromium), 2 on @@ -680,7 +680,7 @@ export class BrowserManager { // restart loop. Crash → process.exit(2) preserves the legacy headed // semantics that's distinct from launch()'s code 1. // Always calls onDisconnect() first to trigger full shutdown (kill - // sidebar-agent, save session, clean profile locks + state file) so + // terminal agent, save session, clean profile locks + state file) so // crashes don't strand resources either. if (this.browser) { this.browser.on('disconnected', () => { diff --git a/browse/src/cdp-commands.ts b/browse/src/cdp-commands.ts index 1f29a6ed8..1a71f80db 100644 --- a/browse/src/cdp-commands.ts +++ b/browse/src/cdp-commands.ts @@ -3,8 +3,8 @@ * * Output for trusted methods is a plain JSON pretty-print. * Output for untrusted methods is wrapped with the centralized UNTRUSTED EXTERNAL - * CONTENT envelope so the sidebar-agent classifier sees it (matches the pattern - * used by other untrusted-content commands in commands.ts). + * CONTENT envelope so downstream consumers treat it as data, not instructions + * (matches the pattern used by other untrusted-content commands in commands.ts). */ import type { BrowserManager } from './browser-manager'; diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 2ef5be3f5..e319282ec 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -442,8 +442,8 @@ async function ensureServer(flags?: GlobalFlags): Promise { return state; } - // BROWSE_NO_AUTOSTART: sidebar agent sets this so the child claude never - // spawns an invisible headless browser. If the headed server is down, + // BROWSE_NO_AUTOSTART: agent-spawned children (e.g. the terminal-agent PTY + // claude) set this so a child never spawns an invisible headless browser. If the headed server is down, // fail fast with a clear error instead of silently starting a new one. if (process.env.BROWSE_NO_AUTOSTART === '1') { console.error('[browse] Server not available and BROWSE_NO_AUTOSTART is set.'); @@ -527,7 +527,7 @@ export function extractTabId(args: string[]): { tabId: number | undefined; args: async function sendCommand(state: ServerState, command: string, args: string[], retries = 0): Promise { // Precedence: CLI --tab-id flag > BROWSE_TAB env var. // make-pdf always passes --tab-id; human users typically rely on BROWSE_TAB - // (set by sidebar-agent per-tab) or the active tab. + // or the active tab. const extracted = extractTabId(args); args = extracted.args; const envTab = process.env.BROWSE_TAB; @@ -1116,10 +1116,6 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: console.log('(If you still don\'t see it, check Mission Control / other Spaces.)'); } - // sidebar-agent.ts spawn was here. Ripped alongside the chat queue — - // the Terminal pane runs an interactive PTY now, no more one-shot - // claude -p subprocesses to multiplex. - // Auto-start terminal agent (non-compiled bun process). Owns the PTY // WebSocket for the sidebar Terminal pane. Routes through the shared // spawnTerminalAgent helper so the CLI cold-start path and the diff --git a/browse/src/domain-skill-commands.ts b/browse/src/domain-skill-commands.ts index f3fa5d992..504dd06f8 100644 --- a/browse/src/domain-skill-commands.ts +++ b/browse/src/domain-skill-commands.ts @@ -14,8 +14,9 @@ * - host is ALWAYS derived from the active tab's top-level origin (T3 * confused-deputy fix). Never accepted as an arg. * - Save-time security uses content-security.ts L1-L3 filters (importable - * from the compiled binary, unlike the L4 ML classifier). The full L4 - * scan happens in sidebar-agent.ts when the skill is loaded into a prompt. + * from the compiled binary, unlike the L4 ML classifier). There is NO + * load-time L4 scan today — it died with the chat path; the + * classifier_score>0 promotion gate in domain-skills.ts compensates. * - Output is structured: every success/error includes problem + cause + * suggested-action. Matches the gstack house style. * @@ -117,8 +118,8 @@ async function handleSave(args: string[], bm: BrowserManager): Promise { ); } // L1-L3 content filters (datamarking, hidden-element strip, ARIA regex, - // URL blocklist). The full L4 ML classifier runs at sidebar-agent prompt - // injection time, not here (CLAUDE.md: classifier can't import in compiled binary). + // URL blocklist). No L4 ML scan here — the classifier can't import in the + // compiled binary, and the load-time scan path no longer exists. const filterResult = runContentFilters(body, page.url(), 'domain-skill-save'); if (filterResult.blocked) { logTelemetry({ event: 'domain_skill_save_blocked', host, reason: filterResult.message }); @@ -128,9 +129,9 @@ async function handleSave(args: string[], bm: BrowserManager): Promise { 'Action: review the body for suspicious instruction-like content; rewrite and retry.' ); } - // L1-L3 score is binary (passed or not). For the L4 score field we leave 0 - // (meaning "not yet scanned by ML classifier") — sidebar-agent fills this - // in on first prompt-injection load. + // L1-L3 score is binary (passed or not). The L4 score field stays 0 + // ("never ML-scanned") — nothing fills it in today, which is exactly why + // the promotion gate in domain-skills.ts requires classifier_score > 0. const slug = getCurrentProjectSlug(); const row = await writeSkill({ host, diff --git a/browse/src/domain-skills.ts b/browse/src/domain-skills.ts index 011059b27..92258fee3 100644 --- a/browse/src/domain-skills.ts +++ b/browse/src/domain-skills.ts @@ -287,7 +287,8 @@ export async function writeSkill(input: WriteSkillInput): Promise 0 gate is load-bearing: handleSave currently writes - * classifier_score=0 with the comment "L4 deferred to load-time / sidebar-agent - * fills this in on first prompt-injection load," but sidebar-agent was ripped - * (CLAUDE.md "Sidebar architecture") and nothing else updates the score, so - * skills authored via the production path never had their body scanned by L4. + * The classifier_score > 0 gate is load-bearing: handleSave writes + * classifier_score=0 (meaning "never ML-scanned"), and NOTHING updates the + * score today — the load-time L4 scan died with the chat path, so skills + * authored via the production path never had their body scanned by L4. * Without this gate, three benign uses promote any quarantined skill — including * one written under the influence of a poisoned page — into the prompt context * for every subsequent visit. The gate re-opens automatically the day L4 is diff --git a/browse/src/file-permissions.ts b/browse/src/file-permissions.ts index d3d404acd..445831ead 100644 --- a/browse/src/file-permissions.ts +++ b/browse/src/file-permissions.ts @@ -4,8 +4,8 @@ * Why this exists * ---------------- * POSIX mode bits (`0o600` for files, `0o700` for dirs) are how gstack marks - * sensitive state files — auth tokens, canary tokens, chat history, agent - * queue, device salt, per-tab security decisions. On Linux and macOS, + * sensitive state files — auth tokens, PTY session state, tab context. On + * Linux and macOS, * `fs.chmodSync(path, 0o600)` and `fs.writeFileSync(path, data, { mode: 0o600 })` * do exactly what you'd hope: the file ends up readable and writable only * by the owning user, no access for group / other. diff --git a/browse/src/server.ts b/browse/src/server.ts index 57b989cdf..77c3d1b1d 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -496,10 +496,6 @@ function isRootRequest(req: Request): boolean { return token !== null && isRootToken(token); } -// Sidebar model router was here (sonnet vs opus by message intent). Ripped -// alongside the chat queue; the interactive PTY just runs whatever model -// the user's `claude` CLI is configured with. - // ─── Help text (auto-generated from COMMAND_DESCRIPTIONS) ──────── function generateHelpText(): string { // Group commands by category @@ -572,15 +568,6 @@ function tmpStatePath(): string { // ─── Sidebar agent / chat state ripped ────────────────────────────── -// ChatEntry, SidebarSession, TabAgentState interfaces; chatBuffer, -// chatBuffers, sidebarSession, agentProcess, agentStatus, agentStartTime, -// agentTabId, messageQueue, currentMessage, tabAgents; addChatEntry, -// loadSession, createSession, persistSession, processAgentEvent, -// killAgent, listSessions, getTabAgent, getTabAgentStatus, and the -// agentHealthInterval all lived here. Replaced by the live PTY in -// terminal-agent.ts; chat queue + per-tab agent multiplexing are no -// longer needed. - let lastConsoleFlushed = 0; let lastNetworkFlushed = 0; let lastDialogFlushed = 0; @@ -779,7 +766,7 @@ const browserManager = new BrowserManager(); // short-circuits idle-shutdown. let activeBrowserManager: BrowserManager = browserManager; // When the user closes the headed browser window, run full cleanup -// (kill sidebar-agent, save session, remove profile locks, delete state file) +// (kill terminal agent, save session, remove profile locks, delete state file) // before exiting. Exit code 0 means user-initiated clean quit (Cmd+Q on // macOS) so process supervisors like gbrowser's gbd skip the restart loop; // 2 means a real crash that should respawn. The fallback `?? 2` preserves @@ -1031,7 +1018,7 @@ async function handleCommandInternalImpl( if (!opts?.skipRateCheck && tokenInfo.token) recordCommand(tokenInfo.token); } - // Pin to a specific tab if requested (set by BROWSE_TAB env var in sidebar agents). + // Pin to a specific tab if requested (set by BROWSE_TAB env var, e.g. per-tab agent contexts). // This prevents parallel agents from interfering with each other's tab context. // Safe because Bun's event loop is single-threaded — no concurrent handleCommand. let savedTabId: number | null = null; @@ -1833,9 +1820,7 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { tabs: browserManager.getTabCount(), // Security module status — drives the shield icon in the sidepanel. // Returns {status: 'protected'|'degraded'|'inactive', layers: {...}}. - // The chat-path classifier no longer feeds this since - // sidebar-agent.ts was ripped; only the page-content side - // (canary, content-security) keeps reporting in. + // Fed by the page-content side (testsavant sidecar, canary state). security: getSecurityStatus(), // Terminal-agent discovery. ONLY a port number — never a token. // Tokens flow via the /pty-session HttpOnly cookie path. See @@ -2559,15 +2544,6 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { } - // ─── Sidebar chat endpoints ripped ────────────────────────────── - // /sidebar-tabs, /sidebar-tabs/switch, /sidebar-chat[/clear], - // /sidebar-command, /sidebar-agent/{event,kill,stop}, - // /sidebar-queue/dismiss, /sidebar-session{,/new,/list} all lived - // here. They drove the one-shot claude -p chat queue. Replaced by - // the interactive PTY in terminal-agent.ts; the queue + browser-tab - // multiplexing are no longer needed. - - // ─── Batch endpoint — N commands, 1 HTTP round-trip ───────────── // Accepts both root AND scoped tokens (same as /command). // Executes commands sequentially through the full security pipeline. @@ -3110,11 +3086,6 @@ export async function start() { console.log(`[browse] State file: ${config.stateFile}`); console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1000}s`); - // initSidebarSession() ripped alongside the chat queue (it loaded - // chat.jsonl into memory and started the agent-health watchdog — - // both functions are gone). The Terminal pane manages its own state - // directly via terminal-agent.ts. - // ─── Tunnel startup (optional) ──────────────────────────────── // Start ngrok tunnel if BROWSE_TUNNEL=1 is set. Uses the dual-listener // pattern: bind a dedicated tunnel listener on an ephemeral port and diff --git a/browse/src/terminal-agent.ts b/browse/src/terminal-agent.ts index 2e39d99e4..b0862b209 100644 --- a/browse/src/terminal-agent.ts +++ b/browse/src/terminal-agent.ts @@ -3,8 +3,8 @@ * sidebar. Translates the phoenix gbrowser PTY (cmd/gbd/terminal.go) into * Bun, with a few changes informed by codex's outside-voice review: * - * - Lives in a separate non-compiled bun process from sidebar-agent.ts so - * a bug in WS framing or PTY cleanup can't take down the chat path. + * - Lives in a separate non-compiled bun process from the browse daemon so + * a bug in WS framing or PTY cleanup can't take down the command surface. * - Binds 127.0.0.1 only — never on the dual-listener tunnel surface. * - Origin validation on the WS upgrade is REQUIRED (not defense-in-depth) * because a localhost shell WS is a real cross-site WebSocket-hijacking diff --git a/extension/sidepanel.js b/extension/sidepanel.js index 9490e3786..98c851334 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -2,8 +2,7 @@ * gstack browse — Side Panel * * Terminal pane (default): live claude PTY via xterm.js, driven by - * sidepanel-terminal.js. The chat queue + sidebar-agent.ts were ripped - * in favor of the interactive REPL — no more one-shot claude -p. + * sidepanel-terminal.js. * * Debug tabs (behind the `debug` toggle): activity feed (SSE) + refs + * inspector. Quick-actions toolbar (Cleanup / Screenshot / Cookies) @@ -994,8 +993,7 @@ inspectorSendBtn.addEventListener('click', async () => { } // Inject into the running claude PTY so the user can ask claude to act - // on the inspector data. Replaces the old `sidebar-command` route which - // spawned a one-shot claude -p (sidebar-agent.ts is gone). + // on the inspector data. // // Pre-scan via /pty-inject-scan before injection (D6, closes #1370). // gstackScanForPTYInject is async; gstackInjectToTerminal stays sync. @@ -1022,9 +1020,6 @@ inspectorSendBtn.addEventListener('click', async () => { * "Cleanup" injects a prompt into the running claude PTY. claude takes the * prompt, snapshots the page, hides ads/banners/popups, leaves article * content. The user watches it happen in the Terminal pane. - * - * Replaced the old chat-queue path (sidebar-agent.ts spawning a one-shot - * claude -p) — we have a live REPL now, so route through that instead. */ async function runCleanup(...buttons) { buttons.forEach(b => b?.classList.add('loading'));