feat: ship the AI assistant as a first-party in-process plugin

Moves src/backend/ai and its routes into plugins/ai, mounted through a
small /ai dispatcher instead of its own port. Moves the ai-enabled and
ai-private-endpoints settings routes out of user-settings-routes.ts
into the plugin too, so it has no leftover core dependents.
This commit is contained in:
LukeGus committed 2026-09-21 15:20:05 -05:00
1 parent c58ccbfc3f
commit 504710b065
43 files changed
+568 -262

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
import type { NextFunction, Response } from "express";
import type { AuthenticatedRequest } from "../../../src/types/index.js";
import {
createCurrentSettingsRepository,
createCurrentUserPreferenceRepository,
} from "../../../src/backend/database/repositories/factory.js";
/**
* Three gates, all checked on the server.
*
* The admin global is a hard kill switch: when it is off the feature does not
* exist for anyone, regardless of what any user has enabled. It defaults to
* false so upgrading an existing install turns nothing on by surprise.
*
* Mirrors the shape of isSharingEnabledForHost in the session-sharing routes,
* where the global also wins over the per-entity setting.
*/
export const AI_GLOBAL_ENABLED_KEY = "ai_globally_enabled";
export async function isAiGloballyEnabled(): Promise<boolean> {
return createCurrentSettingsRepository().getBoolean(
AI_GLOBAL_ENABLED_KEY,
false,
);
}
export interface AiAccess {
enabled: boolean;
allowReadOnlyCommands: boolean;
}
export async function resolveAiAccess(userId: string): Promise<AiAccess> {
const globalEnabled = await isAiGloballyEnabled();
if (!globalEnabled) {
return { enabled: false, allowReadOnlyCommands: false };
}
const preferences =
await createCurrentUserPreferenceRepository().findByUserId(userId);
return {
// Null means the user was never asked, which is not consent.
enabled: preferences?.aiAssistantEnabled === true,
allowReadOnlyCommands: preferences?.aiReadOnlyCommands === true,
};
}
/** Rejects any AI request unless both gates are open. */
export function createAiGate() {
return async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
): Promise<void> => {
if (!req.userId) {
res.status(401).json({ error: "Authentication required" });
return;
}
const access = await resolveAiAccess(req.userId);
if (!access.enabled) {
res.status(403).json({ error: "The AI assistant is not enabled" });
return;
}
(req as AuthenticatedRequest & { aiAccess?: AiAccess }).aiAccess = access;
next();
};
}