feat(security): expose security status on /health for shield icon

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) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-19 19:08:01 +08:00
parent 1a1a182251
commit 7e9600ffc8
2 changed files with 19 additions and 2 deletions
+6 -1
View File
@@ -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' },
+13 -1
View File
@@ -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));