mirror of https://github.com/garrytan/gstack.git
docs: scrub the sidebar-agent ghost from comments and CLAUDE.md
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 <noreply@anthropic.com>
This commit is contained in:
parent
4328748136
commit
d21f11af5d
58
CLAUDE.md
58
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -442,8 +442,8 @@ async function ensureServer(flags?: GlobalFlags): Promise<ServerState> {
|
|||
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<void> {
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
|||
);
|
||||
}
|
||||
// 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<string> {
|
|||
'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,
|
||||
|
|
|
|||
|
|
@ -287,7 +287,8 @@ export async function writeSkill(input: WriteSkillInput): Promise<DomainSkillRow
|
|||
|
||||
/**
|
||||
* Promote a quarantined skill to active in its project after N=3 uses without
|
||||
* classifier flagging. Called by sidebar-agent on successful skill use.
|
||||
* classifier flagging. No production caller today — the chat-path agent that
|
||||
* invoked it on successful skill use was ripped with the chat queue.
|
||||
*
|
||||
* Auto-promote logic:
|
||||
* - increment use_count
|
||||
|
|
@ -296,11 +297,10 @@ export async function writeSkill(input: WriteSkillInput): Promise<DomainSkillRow
|
|||
* - else stay quarantined with updated counter; user must run
|
||||
* `domain-skill promote-to-global` manually
|
||||
*
|
||||
* The classifier_score > 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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue