diff --git a/extension/sidepanel.css b/extension/sidepanel.css
index 0939a350..c490f4d2 100644
--- a/extension/sidepanel.css
+++ b/extension/sidepanel.css
@@ -47,6 +47,39 @@
--radius-full: 9999px;
}
+/* ─── Security Shield ───────────────────────────────────────────── */
+/* 3 states — green=protected, amber=degraded, red=inactive.
+ Custom SVG outline + "SEC" label in JetBrains Mono to match the
+ industrial/CLI aesthetic (design review Pass 7 decision). */
+
+.security-shield {
+ position: absolute;
+ top: 6px;
+ right: 8px;
+ z-index: 10;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 6px;
+ border-radius: var(--radius-sm, 4px);
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ background: rgba(255, 255, 255, 0.02);
+ transition: color 200ms ease-out, background 200ms ease-out;
+ cursor: default;
+}
+.security-shield[data-status="protected"] {
+ color: var(--success, #22C55E);
+}
+.security-shield[data-status="degraded"] {
+ color: var(--amber-400, #FBBF24);
+}
+.security-shield[data-status="inactive"] {
+ color: var(--error, #EF4444);
+}
+
/* ─── Connection Banner ─────────────────────────────────────────── */
.conn-banner {
diff --git a/extension/sidepanel.html b/extension/sidepanel.html
index 1824cda4..fb5d79e6 100644
--- a/extension/sidepanel.html
+++ b/extension/sidepanel.html
@@ -5,6 +5,16 @@
+
+
+
Reconnecting...
diff --git a/extension/sidepanel.js b/extension/sidepanel.js
index 9a1c1585..28821bc6 100644
--- a/extension/sidepanel.js
+++ b/extension/sidepanel.js
@@ -176,6 +176,35 @@ function hideSecurityBanner() {
if (banner) banner.style.display = 'none';
}
+// Shield icon state update — consumes /health.security.status.
+// status ∈ { 'protected', 'degraded', 'inactive' }.
+// 'protected' = all layers ok. 'degraded' = at least one ML layer off or failed
+// (sidebar still defended by canary + architectural controls).
+// 'inactive' = security module crashed — only architectural controls active.
+const SHIELD_LABELS = {
+ protected: { label: 'SEC', aria: 'Security status: protected' },
+ degraded: { label: 'SEC', aria: 'Security status: degraded (some layers offline)' },
+ inactive: { label: 'SEC', aria: 'Security status: inactive (architectural controls only)' },
+};
+function updateSecurityShield(securityState) {
+ const shield = document.getElementById('security-shield');
+ const labelEl = document.getElementById('security-shield-label');
+ if (!shield || !securityState) return;
+ const status = securityState.status || 'inactive';
+ const info = SHIELD_LABELS[status] || SHIELD_LABELS.inactive;
+ shield.setAttribute('data-status', status);
+ shield.setAttribute('aria-label', info.aria);
+ shield.style.display = 'inline-flex';
+ if (labelEl) labelEl.textContent = info.label;
+ // Hover tooltip gives layer-level detail for debugging.
+ if (securityState.layers) {
+ const parts = Object.entries(securityState.layers).map(([k, v]) => `${k}:${v}`);
+ shield.setAttribute('title', `Security — ${status}\n${parts.join('\n')}`);
+ } else {
+ shield.setAttribute('title', `Security — ${status}`);
+ }
+}
+
// Wire up banner interactivity once on load
document.addEventListener('DOMContentLoaded', () => {
const closeBtn = document.getElementById('security-banner-close');
@@ -1662,6 +1691,8 @@ async function tryConnect() {
`token: yes (from /health)\nStarting SSE + chat polling...`
);
updateConnection(`http://127.0.0.1:${port}`, data.token);
+ // Shield state arrives on /health alongside the auth token.
+ if (data.security) updateSecurityShield(data.security);
return;
}
setLoadingStatus(