diff --git a/src/lib/config/runtimeSettings.ts b/src/lib/config/runtimeSettings.ts index bf615749ba..0cb93c8fee 100644 --- a/src/lib/config/runtimeSettings.ts +++ b/src/lib/config/runtimeSettings.ts @@ -21,6 +21,7 @@ export type RuntimeReloadSection = | "corsOrigins" | "ccBridgeTransforms" | "systemTransforms" + | "systemPrompt" | "authzBypass" | "bannedSignals"; @@ -48,6 +49,7 @@ interface RuntimeSettingsSnapshot { corsOrigins: string; ccBridgeTransforms: unknown; systemTransforms: unknown; + systemPrompt: unknown; authzBypass: AuthzBypassSnapshot; customBannedSignals: string[]; providerErrorRules: Record | null; @@ -75,6 +77,7 @@ const DEFAULT_RUNTIME_SETTINGS_SNAPSHOT: RuntimeSettingsSnapshot = { corsOrigins: "", ccBridgeTransforms: null, systemTransforms: null, + systemPrompt: null, authzBypass: DEFAULT_AUTHZ_BYPASS_SNAPSHOT, customBannedSignals: [], providerErrorRules: null, @@ -93,7 +96,6 @@ function isTruthyEnvFlag(value: string | undefined): boolean { return new Set(["1", "true", "yes", "on"]).has(value.trim().toLowerCase()); } - function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; } @@ -276,6 +278,9 @@ export function buildRuntimeSettingsSnapshot( corsOrigins: typeof settings.corsOrigins === "string" ? settings.corsOrigins : "", ccBridgeTransforms: parseStoredJson(settings.ccBridgeTransforms, "ccBridgeTransforms"), systemTransforms: parseStoredJson(settings.systemTransforms, "systemTransforms"), + systemPrompt: settings.systemPrompt + ? parseStoredJson(settings.systemPrompt, "systemPrompt") + : null, authzBypass: normalizeAuthzBypass(settings), customBannedSignals: normalizeStringArray(settings.customBannedSignals), providerErrorRules: normalizeOperatorProviderErrorRules(settings.providerErrorRules), @@ -404,6 +409,21 @@ async function applySystemTransformsSection(systemTransforms: unknown) { setSystemTransformsConfig(systemTransforms); } +async function applySystemPromptSection(systemPrompt: unknown) { + const { setSystemPromptConfig } = await import("@omniroute/open-sse/services/systemPrompt.ts"); + + if (systemPrompt && typeof systemPrompt === "object") { + setSystemPromptConfig(systemPrompt as Record); + } else { + setSystemPromptConfig({ + enabled: false, + prefixPrompt: "", + suffixPrompt: "", + prompt: "", + }); + } +} + async function applyModelsDevSyncSection( previousSnapshot: RuntimeSettingsSnapshot, currentSnapshot: RuntimeSettingsSnapshot, @@ -562,6 +582,11 @@ export async function applyRuntimeSettings( markChanged("systemTransforms"); } + if (force || hasChanged(currentSnapshot.systemPrompt, previousSnapshot.systemPrompt)) { + await applySystemPromptSection(currentSnapshot.systemPrompt); + markChanged("systemPrompt"); + } + if (force || hasChanged(currentSnapshot.authzBypass, previousSnapshot.authzBypass)) { applyAuthzBypassSection(currentSnapshot.authzBypass); markChanged("authzBypass"); diff --git a/tests/unit/system-prompt-runtime-hotreload.test.ts b/tests/unit/system-prompt-runtime-hotreload.test.ts new file mode 100644 index 0000000000..e0a8708922 --- /dev/null +++ b/tests/unit/system-prompt-runtime-hotreload.test.ts @@ -0,0 +1,46 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + applyRuntimeSettings, + resetRuntimeSettingsStateForTests, +} from "../../src/lib/config/runtimeSettings"; +import { getSystemPromptConfig } from "../../open-sse/services/systemPrompt.ts"; + +test.afterEach(() => { + resetRuntimeSettingsStateForTests(); +}); + +test("systemPrompt settings hot-reload updates globalThis in-memory config", async () => { + const newSettings = { + systemPrompt: { + enabled: true, + prefixPrompt: "Prefix rules for agent", + suffixPrompt: "Suffix rules for agent", + }, + }; + + const changes = await applyRuntimeSettings(newSettings, { force: true, source: "test" }); + assert.ok( + changes.some((c) => c.section === "systemPrompt"), + "systemPrompt section must be reported as reloaded" + ); + + const cfg = getSystemPromptConfig(); + assert.equal(cfg.enabled, true, "systemPrompt enabled flag must be updated"); + assert.equal( + cfg.prefixPrompt, + "Prefix rules for agent", + "prefixPrompt must match updated settings" + ); + assert.equal( + cfg.suffixPrompt, + "Suffix rules for agent", + "suffixPrompt must match updated settings" + ); + + // Hot-reload clearing systemPrompt + await applyRuntimeSettings({ systemPrompt: null }, { force: true, source: "test" }); + const clearedCfg = getSystemPromptConfig(); + assert.equal(clearedCfg.enabled, false, "systemPrompt must be disabled when set to null"); + assert.equal(clearedCfg.prefixPrompt, "", "prefixPrompt must be cleared when set to null"); +});