From 7e9600ffc88dd85f5361ac156c1a8cbd7b9ec99a Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 19 Apr 2026 19:08:01 +0800 Subject: [PATCH] feat(security): expose security status on /health for shield icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /health endpoint now returns a `security` field with the classifier status, suitable for driving the sidepanel shield icon: { status: 'protected' | 'degraded' | 'inactive', layers: { testsavant, transcript, canary }, lastUpdated: ISO8601 } Backend plumbing: * server.ts imports getStatus from security.ts (pure-string, safe in compiled binary) and includes it in the /health response. * sidebar-agent.ts writes ~/.gstack/security/session-state.json when the classifier warmup completes (success OR failure). This is the cross- process handoff — server.ts reads the state file via getStatus() to surface the result to the sidepanel. The sidepanel rendering (SVG shield icon + color states + tooltip) is a follow-up commit in the extension/ code. Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/src/server.ts | 7 ++++++- browse/src/sidebar-agent.ts | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/browse/src/server.ts b/browse/src/server.ts index e3f24fa0..c417b831 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -25,7 +25,7 @@ import { runContentFilters, type ContentFilterResult, markHiddenElements, getCleanTextWithStripping, cleanupHiddenMarkers, } from './content-security'; -import { generateCanary, injectCanary } from './security'; +import { generateCanary, injectCanary, getStatus as getSecurityStatus } from './security'; import { handleSnapshot, SNAPSHOT_FLAGS } from './snapshot'; import { initRegistry, validateToken as validateScopedToken, checkScope, checkDomain, @@ -1447,6 +1447,11 @@ async function start() { queueLength: messageQueue.length, }, session: sidebarSession ? { id: sidebarSession.id, name: sidebarSession.name } : null, + // Security module status — drives the shield icon in the sidepanel. + // Returns {status: 'protected'|'degraded'|'inactive', layers: {...}}. + // Source of truth is ~/.gstack/security/session-state.json, written + // by sidebar-agent as the classifier warms up. + security: getSecurityStatus(), }), { status: 200, headers: { 'Content-Type': 'application/json' }, diff --git a/browse/src/sidebar-agent.ts b/browse/src/sidebar-agent.ts index 8b4dbce9..4111fa02 100644 --- a/browse/src/sidebar-agent.ts +++ b/browse/src/sidebar-agent.ts @@ -15,7 +15,7 @@ import * as path from 'path'; import { safeUnlink } from './error-handling'; import { checkCanaryInStructure, logAttempt, hashPayload, extractDomain, - combineVerdict, type LayerSignal, + combineVerdict, writeSessionState, readSessionState, type LayerSignal, } from './security'; import { loadTestsavant, scanPageContent, checkTranscript, @@ -696,10 +696,22 @@ async function main() { // Warm up the ML classifier in the background. First call triggers a 112MB // download (~30s on average broadband). Non-blocking — the sidebar stays // functional on cold start; classifier just reports 'off' until warmed. + // + // On warmup completion (success or failure), write the classifier status to + // ~/.gstack/security/session-state.json so server.ts's /health endpoint can + // report it to the sidepanel for shield icon rendering. loadTestsavant((msg) => console.log(`[security-classifier] ${msg}`)) .then(() => { const s = getClassifierStatus(); console.log(`[sidebar-agent] Classifier warmup complete: ${JSON.stringify(s)}`); + const existing = readSessionState(); + writeSessionState({ + sessionId: existing?.sessionId ?? String(process.pid), + canary: existing?.canary ?? '', + warnedDomains: existing?.warnedDomains ?? [], + classifierStatus: s, + lastUpdated: new Date().toISOString(), + }); }) .catch((err) => console.warn('[sidebar-agent] Classifier warmup failed (degraded mode):', err?.message));