-
Notifications
You must be signed in to change notification settings - Fork 0
Trim GStack 2: remove sidebar/terminal/classifier, add anti-slop contribution bar, pair-agent opt-in #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Trim GStack 2: remove sidebar/terminal/classifier, add anti-slop contribution bar, pair-agent opt-in #15
Changes from all commits
ab4e9ae
d596232
b6a007a
438e843
d57b179
adb3bce
d977070
7baffc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <!-- | ||
| gstack is AI-coded and proud of it. The bar is EVIDENCE OF REAL USE, not lines | ||
| of code. A PR with no proof behind it gets closed, no matter how clean it looks. | ||
| Fill every section below. See CONTRIBUTING.md → "The evidence bar". | ||
| --> | ||
|
|
||
| ## Why (in your own words) | ||
|
|
||
| <!-- One paragraph: what breaks for a user today, and what this change does about | ||
| it. Not a restatement of the diff. --> | ||
|
|
||
| ## Live evidence | ||
|
|
||
| <!-- REQUIRED. Paste the command(s) you ran and their real output — before and | ||
| after. For a bug: the reproduction, failing then fixed. For a skill change: the | ||
| actual transcript / `claude -p` output. For anything visual: before/after | ||
| screenshots. "bun test passes" alone is not enough — show the behavior you | ||
| changed. --> | ||
|
|
||
| ``` | ||
| # what you ran + what it produced | ||
| ``` | ||
|
|
||
| ## Scope | ||
|
|
||
| - **Changed:** | ||
| - **Verified live by:** | ||
| - **Did NOT test:** | ||
|
|
||
| ## Liveness proof (required) | ||
|
|
||
| <!-- Attach a screenshot of your own machine with the text `GSTACK PR` typed LIVE | ||
| into a real surface — terminal prompt, a shell command, your browser | ||
| address/search bar, an editor buffer. It must be TYPED INTO A LIVE UI, not drawn, | ||
| overlaid, or edited onto the image. A painted-on `GSTACK PR` is an automatic | ||
| close. This confirms a human opened this PR. --> | ||
|
|
||
| ## Checklist | ||
|
|
||
| - [ ] Liveness screenshot attached: `GSTACK PR` typed live into a real surface (not edited onto the image) | ||
| - [ ] Commits are signed off (`git commit -s`) — DCO | ||
| - [ ] This is not a generated-file-only diff (I edited the source/template and regenerated) | ||
| - [ ] No ETHOS.md edits, and no changes to voice / founder perspective / YC references | ||
| - [ ] New public command / external service / host adapter has an accepted issue linked (or N/A) | ||
| - [ ] Linked issue or reproduction: # | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The "Linked issue or reproduction: #" checklist item mixes a fill-in field with a checkbox, creating ambiguity. The contributor must replace Prompt for AI agents |
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Root-token page-content reads do not receive the documented L1-L3 stack, and there is no remaining sidebar tool-output path. Please scope this guarantee to paired/scoped-token commands so the authoritative security model does not overstate protection. Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| 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 | ||||||
|
|
||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The liveness proof is replayable across PRs, so it does not establish that a human exercised this submission. Including a per-change value such as the current short commit SHA would tie the screenshot to the PR's actual revision.
Prompt for AI agents