diff --git a/extension/sidepanel.js b/extension/sidepanel.js
index 089f1ccd..9a1c1585 100644
--- a/extension/sidepanel.js
+++ b/extension/sidepanel.js
@@ -107,6 +107,102 @@ let agentText = ''; // Accumulated text
// repeat rendering on reconnect or tab switch (server replays from disk)
const renderedEntryIds = new Set();
+// Security banner (variant A from /plan-design-review 2026-04-19).
+// Renders on security_event — canary leaks, ML classifier BLOCK verdicts.
+// Defense-in-depth trust UX — user sees WHICH layer fired at WHAT confidence.
+const SECURITY_LAYER_LABELS = {
+ testsavant_content: 'Content ML',
+ transcript_classifier: 'Transcript ML',
+ aria_regex: 'ARIA pattern',
+ canary: 'Canary leak',
+};
+
+function showSecurityBanner(event) {
+ const banner = document.getElementById('security-banner');
+ if (!banner) return;
+
+ const title = document.getElementById('security-banner-title');
+ const subtitle = document.getElementById('security-banner-subtitle');
+ const layersEl = document.getElementById('security-banner-layers');
+ const expandBtn = document.getElementById('security-banner-expand');
+ const details = document.getElementById('security-banner-details');
+ const chevron = banner.querySelector('.security-banner-chevron');
+
+ // Title + subtitle
+ if (title) title.textContent = 'Session terminated';
+ if (subtitle) {
+ const fromDomain = event.domain ? ` from ${event.domain}` : '';
+ subtitle.textContent = `— prompt injection detected${fromDomain}`;
+ }
+
+ // Layer signals list (mono scores)
+ if (layersEl) {
+ layersEl.innerHTML = '';
+ const rows = [];
+ // If we got a primary layer + confidence, show that first
+ if (event.layer) {
+ rows.push({ layer: event.layer, confidence: event.confidence ?? 1.0 });
+ }
+ // Any additional signals the agent sent
+ if (Array.isArray(event.signals)) {
+ for (const s of event.signals) {
+ if (s.layer && !rows.some(r => r.layer === s.layer)) {
+ rows.push({ layer: s.layer, confidence: s.confidence ?? 0 });
+ }
+ }
+ }
+ for (const row of rows) {
+ const label = SECURITY_LAYER_LABELS[row.layer] || row.layer;
+ const score = Number(row.confidence).toFixed(2);
+ const div = document.createElement('div');
+ div.className = 'security-banner-layer';
+ div.innerHTML = `${label}${score}`;
+ layersEl.appendChild(div);
+ }
+ }
+
+ // Reset expand state on each render
+ if (expandBtn && details) {
+ expandBtn.setAttribute('aria-expanded', 'false');
+ details.hidden = true;
+ if (chevron) chevron.style.transform = 'rotate(0deg)';
+ }
+
+ banner.style.display = 'block';
+}
+
+function hideSecurityBanner() {
+ const banner = document.getElementById('security-banner');
+ if (banner) banner.style.display = 'none';
+}
+
+// Wire up banner interactivity once on load
+document.addEventListener('DOMContentLoaded', () => {
+ const closeBtn = document.getElementById('security-banner-close');
+ const expandBtn = document.getElementById('security-banner-expand');
+ const banner = document.getElementById('security-banner');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', hideSecurityBanner);
+ }
+ if (expandBtn) {
+ expandBtn.addEventListener('click', () => {
+ const details = document.getElementById('security-banner-details');
+ const chevron = banner && banner.querySelector('.security-banner-chevron');
+ if (!details) return;
+ const open = !details.hidden;
+ details.hidden = open;
+ expandBtn.setAttribute('aria-expanded', String(!open));
+ if (chevron) chevron.style.transform = open ? 'rotate(0deg)' : 'rotate(180deg)';
+ });
+ }
+ // Escape dismisses the banner (a11y)
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && banner && banner.style.display !== 'none') {
+ hideSecurityBanner();
+ }
+ });
+});
+
function addChatEntry(entry) {
// Dedup by entry ID — prevent repeat rendering on reconnect/replay
if (entry.id !== undefined) {
@@ -228,6 +324,11 @@ function handleAgentEvent(entry) {
return;
}
+ if (entry.type === 'security_event') {
+ showSecurityBanner(entry);
+ return;
+ }
+
if (entry.type === 'agent_error') {
// Suppress timeout errors that fire after agent_done (cleanup noise)
if (entry.error && entry.error.includes('Timed out') && !agentContainer) {