refactor(settings): adopt environment-only runtime secret flow
This commit is contained in:
@@ -76,12 +76,14 @@ import { insightsRouter } from "./insights.js";
|
||||
import { scenarioRouter } from "./scenario.js";
|
||||
import { allocationRouter } from "./allocation.js";
|
||||
import { staffingRouter } from "./staffing.js";
|
||||
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
|
||||
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
|
||||
|
||||
export const MUTATION_TOOLS = new Set([
|
||||
"import_csv_data",
|
||||
"update_system_settings",
|
||||
"clear_stored_runtime_secrets",
|
||||
"test_ai_connection",
|
||||
"test_smtp_connection",
|
||||
"test_gemini_connection",
|
||||
@@ -4772,14 +4774,13 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_system_settings",
|
||||
description: "Update system settings through the real settings router. Admin role required. Always confirm first.",
|
||||
description: "Update non-secret system settings through the real settings router. Runtime secrets must be provisioned via deployment environment or secret manager. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
aiProvider: { type: "string", enum: ["openai", "azure"] },
|
||||
azureOpenAiEndpoint: { type: "string" },
|
||||
azureOpenAiDeployment: { type: "string" },
|
||||
azureOpenAiApiKey: { type: "string" },
|
||||
azureApiVersion: { type: "string" },
|
||||
aiMaxCompletionTokens: { type: "integer" },
|
||||
aiTemperature: { type: "number" },
|
||||
@@ -4789,17 +4790,13 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
smtpHost: { type: "string" },
|
||||
smtpPort: { type: "integer" },
|
||||
smtpUser: { type: "string" },
|
||||
smtpPassword: { type: "string" },
|
||||
smtpFrom: { type: "string" },
|
||||
smtpTls: { type: "boolean" },
|
||||
anonymizationEnabled: { type: "boolean" },
|
||||
anonymizationDomain: { type: "string" },
|
||||
anonymizationSeed: { type: "string" },
|
||||
anonymizationMode: { type: "string", enum: ["global"] },
|
||||
azureDalleDeployment: { type: "string" },
|
||||
azureDalleEndpoint: { type: "string" },
|
||||
azureDalleApiKey: { type: "string" },
|
||||
geminiApiKey: { type: "string" },
|
||||
geminiModel: { type: "string" },
|
||||
imageProvider: { type: "string", enum: ["dalle", "gemini"] },
|
||||
vacationDefaultDays: { type: "integer" },
|
||||
@@ -4809,6 +4806,17 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "clear_stored_runtime_secrets",
|
||||
description: "Clear legacy database-stored runtime secrets after they have been migrated to deployment secret management. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_ai_connection",
|
||||
@@ -9306,7 +9314,6 @@ const executors = {
|
||||
aiProvider?: "openai" | "azure";
|
||||
azureOpenAiEndpoint?: string;
|
||||
azureOpenAiDeployment?: string;
|
||||
azureOpenAiApiKey?: string;
|
||||
azureApiVersion?: string;
|
||||
aiMaxCompletionTokens?: number;
|
||||
aiTemperature?: number;
|
||||
@@ -9322,17 +9329,13 @@ const executors = {
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
smtpUser?: string;
|
||||
smtpPassword?: string;
|
||||
smtpFrom?: string;
|
||||
smtpTls?: boolean;
|
||||
anonymizationEnabled?: boolean;
|
||||
anonymizationDomain?: string;
|
||||
anonymizationSeed?: string;
|
||||
anonymizationMode?: "global";
|
||||
azureDalleDeployment?: string;
|
||||
azureDalleEndpoint?: string;
|
||||
azureDalleApiKey?: string;
|
||||
geminiApiKey?: string;
|
||||
geminiModel?: string;
|
||||
imageProvider?: "dalle" | "gemini";
|
||||
vacationDefaultDays?: number;
|
||||
@@ -9342,6 +9345,11 @@ const executors = {
|
||||
return caller.updateSystemSettings(params);
|
||||
},
|
||||
|
||||
async clear_stored_runtime_secrets(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = createSettingsCaller(createScopedCallerContext(ctx));
|
||||
return caller.clearStoredRuntimeSecrets();
|
||||
},
|
||||
|
||||
async test_ai_connection(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = createSettingsCaller(createScopedCallerContext(ctx));
|
||||
return caller.testAiConnection();
|
||||
@@ -9358,7 +9366,7 @@ const executors = {
|
||||
},
|
||||
|
||||
async get_ai_configured(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const settings = await ctx.db.systemSettings.findUnique({
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
aiProvider: true,
|
||||
@@ -9366,7 +9374,7 @@ const executors = {
|
||||
azureOpenAiDeployment: true,
|
||||
azureOpenAiApiKey: true,
|
||||
},
|
||||
});
|
||||
}));
|
||||
return { configured: isAiConfigured(settings) };
|
||||
},
|
||||
|
||||
|
||||
@@ -349,6 +349,7 @@ const ADMIN_ONLY_TOOLS = new Set([
|
||||
"commit_dispo_import_batch",
|
||||
"get_system_settings",
|
||||
"update_system_settings",
|
||||
"clear_stored_runtime_secrets",
|
||||
"get_ai_configured",
|
||||
"test_ai_connection",
|
||||
"test_smtp_connection",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { VALUE_SCORE_WEIGHTS } from "@capakraken/shared";
|
||||
import { testSmtpConnection } from "../lib/email.js";
|
||||
import { createAuditEntry } from "../lib/audit.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
import { getRuntimeSecretStatuses, RUNTIME_SECRET_FIELDS, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
|
||||
/** Fields that must never appear in audit log values */
|
||||
const SENSITIVE_FIELDS = new Set([
|
||||
@@ -23,6 +23,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
where: { id: "singleton" },
|
||||
});
|
||||
const runtimeSettings = resolveSystemSettingsRuntime(settings);
|
||||
const runtimeSecrets = getRuntimeSecretStatuses(settings);
|
||||
const legacyStoredSecretFields = RUNTIME_SECRET_FIELDS.filter(
|
||||
(field) => runtimeSecrets[field].hasStoredValue,
|
||||
);
|
||||
|
||||
const defaultWeights = {
|
||||
skillDepth: VALUE_SCORE_WEIGHTS.SKILL_DEPTH,
|
||||
@@ -42,6 +46,8 @@ export const settingsRouter = createTRPCRouter({
|
||||
aiSummaryPrompt: settings?.aiSummaryPrompt ?? null,
|
||||
defaultSummaryPrompt: DEFAULT_SUMMARY_PROMPT,
|
||||
hasApiKey: !!runtimeSettings.azureOpenAiApiKey,
|
||||
runtimeSecrets,
|
||||
legacyStoredSecretFields,
|
||||
scoreWeights: (settings?.scoreWeights as unknown as typeof defaultWeights) ?? defaultWeights,
|
||||
scoreVisibleRoles: (settings?.scoreVisibleRoles as unknown as string[]) ?? ["ADMIN", "MANAGER"],
|
||||
// SMTP
|
||||
@@ -125,13 +131,14 @@ export const settingsRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const data: Record<string, unknown> = {};
|
||||
const ignoredSecretFields: string[] = [];
|
||||
if (input.aiProvider !== undefined) data.aiProvider = input.aiProvider;
|
||||
if (input.azureOpenAiEndpoint !== undefined)
|
||||
data.azureOpenAiEndpoint = input.azureOpenAiEndpoint || null;
|
||||
if (input.azureOpenAiDeployment !== undefined)
|
||||
data.azureOpenAiDeployment = input.azureOpenAiDeployment || null;
|
||||
if (input.azureOpenAiApiKey !== undefined)
|
||||
data.azureOpenAiApiKey = input.azureOpenAiApiKey || null;
|
||||
ignoredSecretFields.push("azureOpenAiApiKey");
|
||||
if (input.azureApiVersion !== undefined)
|
||||
data.azureApiVersion = input.azureApiVersion || null;
|
||||
if (input.aiMaxCompletionTokens !== undefined)
|
||||
@@ -148,16 +155,13 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (input.smtpHost !== undefined) data.smtpHost = input.smtpHost || null;
|
||||
if (input.smtpPort !== undefined) data.smtpPort = input.smtpPort;
|
||||
if (input.smtpUser !== undefined) data.smtpUser = input.smtpUser || null;
|
||||
if (input.smtpPassword !== undefined) data.smtpPassword = input.smtpPassword || null;
|
||||
if (input.smtpPassword !== undefined) ignoredSecretFields.push("smtpPassword");
|
||||
if (input.smtpFrom !== undefined) data.smtpFrom = input.smtpFrom || null;
|
||||
if (input.smtpTls !== undefined) data.smtpTls = input.smtpTls;
|
||||
// Global anonymization
|
||||
if (input.anonymizationEnabled !== undefined) data.anonymizationEnabled = input.anonymizationEnabled;
|
||||
if (input.anonymizationDomain !== undefined) data.anonymizationDomain = input.anonymizationDomain || "superhartmut.de";
|
||||
if (input.anonymizationSeed !== undefined) {
|
||||
data.anonymizationSeed = input.anonymizationSeed || null;
|
||||
data.anonymizationAliases = null;
|
||||
}
|
||||
if (input.anonymizationSeed !== undefined) ignoredSecretFields.push("anonymizationSeed");
|
||||
if (input.anonymizationMode !== undefined) {
|
||||
data.anonymizationMode = input.anonymizationMode;
|
||||
data.anonymizationAliases = null;
|
||||
@@ -168,10 +172,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (input.azureDalleEndpoint !== undefined)
|
||||
data.azureDalleEndpoint = input.azureDalleEndpoint || null;
|
||||
if (input.azureDalleApiKey !== undefined)
|
||||
data.azureDalleApiKey = input.azureDalleApiKey || null;
|
||||
ignoredSecretFields.push("azureDalleApiKey");
|
||||
// Gemini
|
||||
if (input.geminiApiKey !== undefined)
|
||||
data.geminiApiKey = input.geminiApiKey || null;
|
||||
ignoredSecretFields.push("geminiApiKey");
|
||||
if (input.geminiModel !== undefined)
|
||||
data.geminiModel = input.geminiModel || null;
|
||||
// Image provider
|
||||
@@ -182,6 +186,14 @@ export const settingsRouter = createTRPCRouter({
|
||||
// Timeline
|
||||
if (input.timelineUndoMaxSteps !== undefined) data.timelineUndoMaxSteps = input.timelineUndoMaxSteps;
|
||||
|
||||
if (Object.keys(data).length === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
ignoredSecretFields,
|
||||
secretStorageMode: "environment-only" as const,
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch current settings for before-snapshot
|
||||
const before = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
|
||||
|
||||
@@ -215,9 +227,47 @@ export const settingsRouter = createTRPCRouter({
|
||||
source: "ui",
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
return {
|
||||
ok: true,
|
||||
ignoredSecretFields,
|
||||
secretStorageMode: "environment-only" as const,
|
||||
};
|
||||
}),
|
||||
|
||||
clearStoredRuntimeSecrets: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const existing = await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: Object.fromEntries(
|
||||
RUNTIME_SECRET_FIELDS.map((field) => [field, true]),
|
||||
) as Record<(typeof RUNTIME_SECRET_FIELDS)[number], true>,
|
||||
});
|
||||
|
||||
const clearedFields = RUNTIME_SECRET_FIELDS.filter((field) => !!existing?.[field]);
|
||||
|
||||
if (clearedFields.length === 0) {
|
||||
return { ok: true, clearedFields: [] as string[] };
|
||||
}
|
||||
|
||||
await ctx.db.systemSettings.update({
|
||||
where: { id: "singleton" },
|
||||
data: Object.fromEntries(clearedFields.map((field) => [field, null])),
|
||||
});
|
||||
|
||||
void createAuditEntry({
|
||||
db: ctx.db,
|
||||
entityType: "SystemSettings",
|
||||
entityId: "singleton",
|
||||
entityName: "Runtime Secrets",
|
||||
action: "UPDATE",
|
||||
userId: ctx.dbUser?.id,
|
||||
after: { clearedFields },
|
||||
source: "ui",
|
||||
summary: `Cleared ${clearedFields.length} legacy runtime secret field(s) from database storage`,
|
||||
});
|
||||
|
||||
return { ok: true, clearedFields };
|
||||
}),
|
||||
|
||||
testAiConnection: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
|
||||
Reference in New Issue
Block a user