ADR-015 — Extension UX modes & lexicon refactor
1. Context
Two coupled problems with current extension UX:
Problem A — Wrong terminology: 163 source files reference "PII" in labels, popup HTML, badge tooltips, onboarding copy, i18n keys, CSS classnames. But Noxys actually detects far more: secrets (API keys, JWT, tokens), proprietary code/IP, business confidentials (M&A, layoff plans via intent classifier), multi-modal risks (images via OCR per ADR-011), shadow AI usage signals, and EU AI Act compliance events. "PII" understates the product by ~70%.
Problem B — Single-mode UX: current UX assumes a self-service consumer install — visible onboarding, rich popup, settings panel, easy disable button. Inappropriate for enterprise deployments where IT pushes the extension via policy and end-users must not be able to disable, configure, or even fully see what the extension is doing. We also need automatic enrollment so end-users never type a tenant ID.
The 3 deployment methods rule (memory feedback_three_deployment_methods.md) keeps customer-facing surface at 3. This ADR adds modes within the existing extension surface — not a 4th method.
2. Decision summary
- Two operational modes: Self-Service (Mode A, default) and Managed (Mode B). Detected at boot, not user-toggled.
- Mode B auto-provisioning: tenant_id + enrollment JWT pushed via
chrome.storage.managed(Chrome ADMX / Edge / Firefoxpolicies.json/ macOS configuration profile). Extension auto-enrolls on first run, no user interaction. - Mode B tamper resistance: heartbeat to
/api/v1/extension/heartbeatevery 5 min; integrity hash check at boot; console alerts admin on silence >30 min or hash mismatch; OS-level disable lock viaExtensionInstallForcelist. - Lexicon migration: "PII" demoted from top-level term to one sub-category. Top-level becomes "Risk Signals" with categories
pii / secret / code / ip / confidential / shadow-ai. - Collection invariant, display variable: Mode A and Mode B capture identical data; only the UI surface differs. Privacy notice + DPIA cover this.
- Tier gating: Mode B only available to Pro+ (Free tier locked to self-service to prevent local-bypass to Pro features). Enterprise = managed mandatory.
3. Mode detection at boot
// src/background/mode-detect.ts (new)
export async function detectMode(): Promise<"managed" | "self-service"> {
const managed = await chrome.storage.managed.get(["tenant_id", "enrollment_jwt"]);
if (managed.tenant_id && managed.enrollment_jwt) return "managed";
return "self-service";
}
chrome.storage.managed is read-only and signed by the OS policy mechanism — cannot be spoofed by user-land code.
4. UX matrix
| Surface | Mode A (self-service) | Mode B (managed) |
|---|---|---|
| Onboarding | 5-step wizard | None — silent enroll |
| Popup body | Stats + detection feed + tips | Logo + green dot + 1 line "Protected by Noxys" |
| Settings page | Full | Returns 404 / hidden route |
| Disable toggle | Visible | Removed; OS lock via ExtensionInstallForcelist |
| Coaching overlay | Verbose | Silent (admin-toggleable) |
| Badge counter | Visible | Hidden (counter-productive — "I did something wrong" feel) |
| Devtools warning | None | Logs devtools_open event (non-blocking) |
| Telemetry payload | Identical | Identical |
Principle: collection invariant, display variable.
5. Auto-enrollment flow (Mode B)
- IT pushes managed config via OS policy:
{
"tenant_id": "acme-corp",
"enrollment_jwt": "<short-lived JWT signed by noxys-api>",
"api_url": "https://api.noxys.cloud",
"channel": "stable"
} - Extension reads via
chrome.storage.managed.getat boot. - POST
/api/v1/extension/enrollwithenrollment_jwt→ API validates tier (must be Pro+), returns long-livedextension_token. - Stored in
chrome.storage.local, wrapped via WebCrypto + (when available) DPAPI/Keychain via native messaging. - Heartbeat starts; first sync of policies + recognizers.
If enrollment_jwt rejected (tier mismatch, expired, revoked): extension stays in dormant Mode B — UI invisible, no detection, retries every 24h. Admin sees "Enrollment failed" in Console.
6. Tamper resistance (Mode B only)
| Mechanism | Implementation |
|---|---|
| Disable lock | ExtensionInstallForcelist (Chrome/Edge enterprise built-in) |
| Heartbeat | POST /api/v1/extension/heartbeat every 5 min; payload includes version, channel, build, last_event_ts |
| Silence alert | Console raises P2 alert if no heartbeat for >30 min on any enrolled user |
| Integrity check | At boot, hash CRX manifest + key files (background/index.js, content-scripts/prompt-interceptor.js); compare to expected hash from /api/v1/extension/integrity?version=X.Y.Z; mismatch → log + dormant |
| Devtools detection | Content-script monitors window.__noxys_devtools_open heuristic; logs event (non-blocking) |
| Incognito coverage | Mode B sets incognito: split — events captured even in incognito (Mode A respects incognito boundary) |
7. Lexicon migration plan
Replacement table (binding for all UI surfaces, i18n keys, code identifiers):
| Old | New |
|---|---|
PII detected | Risk signal blocked |
PII alerts | Risk events |
PII this session | Events this session |
Auto-mask PII before sending | Auto-redact sensitive content before sending |
pii.alert.label (i18n key) | risk.event.label |
.pii-banner (CSS) | .risk-banner |
.pii-item (CSS) | .risk-item |
pii_count (storage key) | risk_event_count (with migration shim for 1 minor version) |
Categories visible in Pro+ UI:
pii— names, emails, phone, ID numbers (existing Presidio recognizers)secret— API keys, JWT, tokens, certscode— proprietary code snippets, repo pathsip— design docs, internal architecture, patents-in-progressconfidential— financials, M&A, layoff plans (via intent classifier per AI-PLACE-4)shadow-ai— usage of unsanctioned LLM service
Storage migration: extension v0.x.0+1 reads pii_count, writes risk_event_count, keeps both for one minor; v0.x.0+2 drops pii_count.
8. Tier matrix
| Mode | Free | Pro | Enterprise |
|---|---|---|---|
| A self-service | yes (default) | yes | — |
| B managed | — | optional | mandatory |
Free tier locked to Mode A — prevents local-machine bypass to unlock Pro-only AI capabilities (per ADR-014).
9. Sprint plan
8 sprints, 17 days total. Distributed W23-24 → W27-28.
| ID | Title | Repo | Window | Effort | Priority |
|---|---|---|---|---|---|
| EXT-UX-1 | Lexicon refactor — PII → Risk Signals (163 files) | noxys-extension | W23-24 | 2d | P1 |
| EXT-UX-2 | Mode detection at boot (managed vs self-service) | noxys-extension | W23-24 | 1d | P0 |
| EXT-UX-3 | Managed-mode UI — minimal popup, hidden options route | noxys-extension | W23-24 | 2d | P1 |
| EXT-UX-4 | Auto-enrollment — chrome.storage.managed + JWT exchange | noxys-extension + noxys-api | W23-24 | 3d | P0 |
| EXT-UX-5 | ADMX / policies.json / macOS config profile templates | noxys-infra | W25-26 | 2d | P1 |
| EXT-UX-6 | Heartbeat + integrity hash + console silence alerts | noxys-extension + noxys-api | W25-26 | 3d | P1 |
| EXT-UX-7 | Risk Signal categorization UI (multi-cat replaces mono-PII) | noxys-extension | W25-26 | 2d | P2 |
| EXT-UX-8 | Console — managed deployment dashboard (heartbeat status, tamper alerts) | noxys-console | W27-28 | 2d | P2 |
Dependencies:
- EXT-UX-3 → EXT-UX-2
- EXT-UX-4 → EXT-UX-2; depends on
/api/v1/extension/enroll(already in ENROLL-1) - EXT-UX-6 → EXT-UX-4
- EXT-UX-8 → EXT-UX-6
10. Privacy & legal
Mode B = workplace monitoring tool. Legal binding requirements:
- CNIL Art 88 RGPD: employer must declare to works council (CSE) before deployment in France. Provide template notice (FR/EN/DE) in
noxys-console/Compliancepage (covered by AI-PLACE-6). - Extension store listings: "Used as managed enterprise extension" disclosure in Chrome Web Store + AMO descriptions.
- DPIA template: AI-PLACE-6 generates per-tenant DPIA referencing Mode B specifically.
- Privacy notice: Mode B end-users see one-time toast on first detection: "This device is protected by Noxys, deployed by your organization. [Learn more]" — link to tenant-customizable notice page.
11. Non-goals
- Native Windows app / driver-level capture (out of scope; that's the endpoint-agent surface).
- Mode B for self-installed users (would defeat tier gating).
- Dynamic mode switching mid-session (mode = boot-locked).
- Forcing Mode B onto consumer Chrome without enterprise policy (impossible by design + would violate store policies).
12. Open questions
- Firefox managed install maturity: confirm
3rdparty.<extension-id>policy carries enrollment payload reliably across Firefox 115+ ESR. Mitigation: degrade to in-extension first-run pairing screen if managed storage unavailable. - Integrity hash registry: per-version hash served by API needs CDN cache + stale-while-revalidate to avoid false-positive on rolling auto-update windows.
- macOS native messaging for keychain wrap: optional or required for v1? Default to optional (WebCrypto-only) — keychain via separate sprint if needed.
13. Refs
- ADR-013 Extension versioning (channels + telemetry headers reused for heartbeat)
- ADR-014 AI placement (Mode B forces SLM ON without toggle)
- Memory:
feedback_three_deployment_methods.md - Memory:
project_pricing_tiers.md - Existing: ENROLL-1 (extension enrollment endpoint backend)
- Existing: 163 PII references inventoried via
grep -r "PII\|pii" src/