fix(csp): allow loopback HTTP for the local AI provider; enable AI Assistant by default

Real end-to-end verification (Playwright-driven real Electron app on this
Mac, against the actual local Ollama instance, not a mock) found the actual
blocker: production's connect-src CSP (`'self' https: wss:`) rejects plain
http:// entirely, so lib/ai/local-client.ts's loopback fetch to Ollama never
even attempted the network in a production build - Electron or browser
alike. This is almost certainly what looked like a browser-sandbox network
issue in the earlier (non-Electron) QA pass tonight too.

Fix is narrow, not a blanket http: relaxation: connect-src now additionally
allows `http://127.0.0.1:*` and `http://localhost:*` specifically. Loopback
has no network hop, so it doesn't reopen the mixed-content-style downgrade
risk the existing https-only production policy guards against - unlike
dev's blanket `http:` allowance, which stays dev-only.

Confirmed fixed: rebuilt (build:standalone + build:electron), launched the
real Electron app via Playwright's _electron, and got a genuine answer back
from the real local Ollama - "Test connection" showed Reachable (the real
success state, not the CORS-diagnostic fallback text), and asking "Reply
with exactly the words: LOCAL AI WORKS" returned exactly that, with the
correct "no local mail index in this session" banner alongside it (accurate
for a fresh Electron session with nothing synced yet).

Also flips FeatureGates.aiAssistantEnabled's default false->true: local now
genuinely works and ships free/unmetered (see lib/ai/types.ts), so there is
a real feature behind the tab, not an empty preview - matches tonight's
explicit "I want AI visible" instruction. An admin can still turn it off.

Verified: typecheck clean, lint clean (0 errors, pre-existing warnings
only), translations pass (48/48), full production build succeeds.
This commit is contained in:
Bernd Rodler
2026-08-05 22:50:57 +02:00
parent 5d7ae230ce
commit 147660aef3
3 changed files with 22 additions and 7 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
crossStarredViewEnabled: { label: 'Unified Mailbox: Starred', description: 'Allow a "Starred" entry in the Unified Mailbox section that lists flagged/starred mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
crossAllViewEnabled: { label: 'Unified Mailbox: All Mail', description: 'Allow an "All mail" entry in the Unified Mailbox section that lists all mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
unifiedCrossAccountEnabled: { label: 'Unified Mailbox: Cross-account', description: 'Allow users to expand the Unified Mailbox beyond the active account boundary so its lists merge across every logged-in account. When off, the Unified Mailbox stays within the active account and its shared folders.' },
aiAssistantEnabled: { label: 'AI Assistant (preview)', description: 'Show the AI Assistant settings tab. No provider is wired up yet (see docs/AI-ASSISTANT-CONCEPT.md) — enabling this only proves the settings pane and policy fetch, it does not grant access to any model.' },
aiAssistantEnabled: { label: 'AI Assistant (preview)', description: 'Show the AI Assistant settings tab. Local (Ollama on the user\'s own machine or this desktop app) is free and unmetered; public (bring-your-own-key) is available too but not yet monitored or metered — see docs/AI-ASSISTANT-CONCEPT.md.' },
};
const RESTRICTABLE_SETTINGS = [
+5 -5
View File
@@ -68,10 +68,10 @@ export interface FeatureGates {
unifiedCrossAccountEnabled: boolean;
/**
* Master admin switch for the AI Assistant tab (docs/AI-ASSISTANT-CONCEPT.md).
* Defaults false, like pluginsEnabled — unlike every other gate, this one
* fronts a feature with no licensed provider class behind it yet (P1/P2 of
* that doc's phased rollout), so an admin opts in explicitly rather than
* every existing install suddenly showing a tab that does nothing.
* Defaults true as of the 2026-08-05 evening decision to make local AI
* visible by default (see lib/ai/types.ts) — local ships free with no
* entitlement gate, so there's a real, working feature behind this tab
* now, not an empty preview. An admin can still turn it off per-tenant.
*/
aiAssistantEnabled: boolean;
}
@@ -100,7 +100,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
crossStarredViewEnabled: false,
crossAllViewEnabled: false,
unifiedCrossAccountEnabled: false,
aiAssistantEnabled: false,
aiAssistantEnabled: true,
};
export interface ThemePolicy {
+16 -1
View File
@@ -109,7 +109,22 @@ export async function proxy(request: NextRequest) {
// https-served production app already gets unencrypted connections
// blocked as mixed content by the browser itself, so allowing bare `ws:`
// here would add no capability, only a false sense of one.
const connectSrc = isDev ? `'self' http: https: ws: wss:` : `'self' https: wss:`;
// `http://127.0.0.1:*`/`http://localhost:*` in production alongside
// `https:`: the AI Assistant's `local` provider class (lib/ai/local-client.ts)
// talks directly to a loopback Ollama-compatible runtime, over plain HTTP -
// Ollama has no built-in TLS story, and there's no realistic MITM risk to
// guard against on loopback (no network hop ever occurs). This is
// deliberately NOT the same relaxation as blanket `http:` in dev: a
// narrow, loopback-only allowance doesn't reopen the mixed-content-style
// downgrade risk documented below for `wss:`. Confirmed as a real gap, not
// theoretical: before this fix, a production build's own Electron shell
// blocked `fetch('http://127.0.0.1:11434/...')` before any network
// attempt happened at all (a CSP violation, connect-src as the violated
// directive) - `local` was entirely inert in a production build,
// Electron or browser alike.
const connectSrc = isDev
? `'self' http: https: ws: wss:`
: `'self' https: wss: http://127.0.0.1:* http://localhost:*`;
const frameAncestors = isSandboxPath
? `'self'`