libre-webui 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/backend/dist/db.d.ts.map +1 -1
- package/backend/dist/db.js +63 -0
- package/backend/dist/db.js.map +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +4 -4
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/chat.d.ts.map +1 -1
- package/backend/dist/routes/chat.js +93 -10
- package/backend/dist/routes/chat.js.map +1 -1
- package/backend/dist/routes/imageGen.d.ts.map +1 -1
- package/backend/dist/routes/imageGen.js +52 -29
- package/backend/dist/routes/imageGen.js.map +1 -1
- package/backend/dist/routes/plugins.d.ts.map +1 -1
- package/backend/dist/routes/plugins.js +218 -41
- package/backend/dist/routes/plugins.js.map +1 -1
- package/backend/dist/routes/preferences.d.ts.map +1 -1
- package/backend/dist/routes/preferences.js +5 -4
- package/backend/dist/routes/preferences.js.map +1 -1
- package/backend/dist/routes/tts.js +2 -2
- package/backend/dist/routes/tts.js.map +1 -1
- package/backend/dist/services/assistantCompletionService.d.ts +2 -1
- package/backend/dist/services/assistantCompletionService.d.ts.map +1 -1
- package/backend/dist/services/assistantCompletionService.js +3 -1
- package/backend/dist/services/assistantCompletionService.js.map +1 -1
- package/backend/dist/services/chatGenerationService.d.ts +5 -3
- package/backend/dist/services/chatGenerationService.d.ts.map +1 -1
- package/backend/dist/services/chatGenerationService.js +18 -6
- package/backend/dist/services/chatGenerationService.js.map +1 -1
- package/backend/dist/services/chatRequestService.d.ts +6 -4
- package/backend/dist/services/chatRequestService.d.ts.map +1 -1
- package/backend/dist/services/chatRequestService.js +10 -2
- package/backend/dist/services/chatRequestService.js.map +1 -1
- package/backend/dist/services/chatService.d.ts +2 -2
- package/backend/dist/services/chatService.d.ts.map +1 -1
- package/backend/dist/services/chatService.js +29 -17
- package/backend/dist/services/chatService.js.map +1 -1
- package/backend/dist/services/embeddingService.js +1 -1
- package/backend/dist/services/embeddingService.js.map +1 -1
- package/backend/dist/services/pluginActivationService.d.ts +11 -0
- package/backend/dist/services/pluginActivationService.d.ts.map +1 -0
- package/backend/dist/services/pluginActivationService.js +163 -0
- package/backend/dist/services/pluginActivationService.js.map +1 -0
- package/backend/dist/services/pluginCapabilityRegistryService.d.ts +1 -1
- package/backend/dist/services/pluginCapabilityRegistryService.d.ts.map +1 -1
- package/backend/dist/services/pluginCapabilityRegistryService.js +1 -1
- package/backend/dist/services/pluginCapabilityRegistryService.js.map +1 -1
- package/backend/dist/services/pluginCredentialsService.d.ts +16 -8
- package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -1
- package/backend/dist/services/pluginCredentialsService.js +47 -14
- package/backend/dist/services/pluginCredentialsService.js.map +1 -1
- package/backend/dist/services/pluginEmbeddingService.d.ts +2 -2
- package/backend/dist/services/pluginEmbeddingService.d.ts.map +1 -1
- package/backend/dist/services/pluginEmbeddingService.js +33 -13
- package/backend/dist/services/pluginEmbeddingService.js.map +1 -1
- package/backend/dist/services/pluginImageGenerationService.d.ts +11 -7
- package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -1
- package/backend/dist/services/pluginImageGenerationService.js +265 -81
- package/backend/dist/services/pluginImageGenerationService.js.map +1 -1
- package/backend/dist/services/pluginService.d.ts +55 -25
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +663 -174
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/services/pluginTTSService.d.ts +4 -4
- package/backend/dist/services/pluginTTSService.d.ts.map +1 -1
- package/backend/dist/services/pluginTTSService.js +24 -28
- package/backend/dist/services/pluginTTSService.js.map +1 -1
- package/backend/dist/services/pluginVariablesService.d.ts +6 -1
- package/backend/dist/services/pluginVariablesService.d.ts.map +1 -1
- package/backend/dist/services/pluginVariablesService.js +59 -1
- package/backend/dist/services/pluginVariablesService.js.map +1 -1
- package/backend/dist/services/preferencesService.d.ts +2 -2
- package/backend/dist/services/preferencesService.d.ts.map +1 -1
- package/backend/dist/services/preferencesService.js +78 -10
- package/backend/dist/services/preferencesService.js.map +1 -1
- package/backend/dist/services/titleGenerationService.d.ts +6 -4
- package/backend/dist/services/titleGenerationService.d.ts.map +1 -1
- package/backend/dist/services/titleGenerationService.js +9 -5
- package/backend/dist/services/titleGenerationService.js.map +1 -1
- package/backend/dist/services/workAgentGuidance.d.ts +1 -0
- package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
- package/backend/dist/services/workAgentGuidance.js +2 -0
- package/backend/dist/services/workAgentGuidance.js.map +1 -1
- package/backend/dist/services/workAgentService.d.ts +5 -2
- package/backend/dist/services/workAgentService.d.ts.map +1 -1
- package/backend/dist/services/workAgentService.js +379 -53
- package/backend/dist/services/workAgentService.js.map +1 -1
- package/backend/dist/services/workModelProviderService.d.ts +8 -3
- package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
- package/backend/dist/services/workModelProviderService.js +293 -33
- package/backend/dist/services/workModelProviderService.js.map +1 -1
- package/backend/dist/services/workTaskService.d.ts +2 -0
- package/backend/dist/services/workTaskService.d.ts.map +1 -1
- package/backend/dist/services/workTaskService.js +89 -14
- package/backend/dist/services/workTaskService.js.map +1 -1
- package/backend/dist/storage.d.ts.map +1 -1
- package/backend/dist/storage.js +9 -6
- package/backend/dist/storage.js.map +1 -1
- package/backend/dist/storageMappers.d.ts +3 -0
- package/backend/dist/storageMappers.d.ts.map +1 -1
- package/backend/dist/storageMappers.js +3 -0
- package/backend/dist/storageMappers.js.map +1 -1
- package/backend/dist/types/index.d.ts +54 -1
- package/backend/dist/types/index.d.ts.map +1 -1
- package/backend/dist/types/index.js.map +1 -1
- package/backend/dist/types/work.d.ts +1 -1
- package/backend/dist/types/work.d.ts.map +1 -1
- package/backend/dist/utils/chatContext.d.ts +13 -1
- package/backend/dist/utils/chatContext.d.ts.map +1 -1
- package/backend/dist/utils/chatContext.js +52 -0
- package/backend/dist/utils/chatContext.js.map +1 -1
- package/backend/dist/utils/chatProviderSelection.d.ts +15 -0
- package/backend/dist/utils/chatProviderSelection.d.ts.map +1 -0
- package/backend/dist/utils/chatProviderSelection.js +59 -0
- package/backend/dist/utils/chatProviderSelection.js.map +1 -0
- package/backend/dist/utils/imageGenerationValidation.d.ts +3 -0
- package/backend/dist/utils/imageGenerationValidation.d.ts.map +1 -0
- package/backend/dist/utils/imageGenerationValidation.js +55 -0
- package/backend/dist/utils/imageGenerationValidation.js.map +1 -0
- package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -1
- package/backend/dist/utils/ollamaLibrary.js +81 -8
- package/backend/dist/utils/ollamaLibrary.js.map +1 -1
- package/backend/dist/utils/openAIResponsesAdapter.d.ts +88 -0
- package/backend/dist/utils/openAIResponsesAdapter.d.ts.map +1 -0
- package/backend/dist/utils/openAIResponsesAdapter.js +521 -0
- package/backend/dist/utils/openAIResponsesAdapter.js.map +1 -0
- package/backend/dist/utils/pluginChatAdapter.d.ts +7 -4
- package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.js +33 -5
- package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
- package/backend/dist/utils/pluginChatContext.d.ts.map +1 -1
- package/backend/dist/utils/pluginChatContext.js +5 -1
- package/backend/dist/utils/pluginChatContext.js.map +1 -1
- package/backend/dist/utils/pluginConnectionVariables.d.ts +6 -0
- package/backend/dist/utils/pluginConnectionVariables.d.ts.map +1 -0
- package/backend/dist/utils/pluginConnectionVariables.js +52 -0
- package/backend/dist/utils/pluginConnectionVariables.js.map +1 -0
- package/backend/dist/utils/pluginDefinitionTrust.d.ts +9 -0
- package/backend/dist/utils/pluginDefinitionTrust.d.ts.map +1 -0
- package/backend/dist/utils/pluginDefinitionTrust.js +71 -0
- package/backend/dist/utils/pluginDefinitionTrust.js.map +1 -0
- package/backend/dist/utils/pluginStreamAdapter.d.ts +3 -0
- package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.js +410 -0
- package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
- package/backend/dist/utils/pluginStreaming.d.ts +7 -1
- package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreaming.js +12 -1
- package/backend/dist/utils/pluginStreaming.js.map +1 -1
- package/backend/dist/utils/pluginValidation.d.ts +21 -2
- package/backend/dist/utils/pluginValidation.d.ts.map +1 -1
- package/backend/dist/utils/pluginValidation.js +190 -17
- package/backend/dist/utils/pluginValidation.js.map +1 -1
- package/backend/dist/utils/pluginVariableValidation.d.ts +7 -0
- package/backend/dist/utils/pluginVariableValidation.d.ts.map +1 -1
- package/backend/dist/utils/pluginVariableValidation.js +79 -4
- package/backend/dist/utils/pluginVariableValidation.js.map +1 -1
- package/backend/dist/websocketServer.d.ts.map +1 -1
- package/backend/dist/websocketServer.js +16 -3
- package/backend/dist/websocketServer.js.map +1 -1
- package/backend/package.json +1 -1
- package/frontend/dist/assets/index-CKV9Lnj8.js +5 -0
- package/frontend/dist/assets/index-Dme2Ozol.css +1 -0
- package/frontend/dist/index.html +4 -4
- package/frontend/dist/js/{ArtifactContainer-CXfmVx8T.js → ArtifactContainer-B0uVR8Z9.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-B1_AuMd2.js → ArtifactDemoPage-DBVPHFxk.js} +1 -1
- package/frontend/dist/js/{ArtifactSlideOutPanel-C0eTguNL.js → ArtifactSlideOutPanel-Cg5kAwv9.js} +1 -1
- package/frontend/dist/js/ChatPage-CPHlIZqU.js +16 -0
- package/frontend/dist/js/{FirstTimeSetup-DOoZUaRs.js → FirstTimeSetup-DksL5arK.js} +1 -1
- package/frontend/dist/js/GalleryPage-1mdUGo_N.js +1 -0
- package/frontend/dist/js/LibreClawPage-qGr1Jy8p.js +1 -0
- package/frontend/dist/js/{ModelManagerModals-DneqFQJC.js → ModelManagerModals-DltRprHU.js} +1 -1
- package/frontend/dist/js/ModelSelector-CUnwRmNU.js +1 -0
- package/frontend/dist/js/ModelsPage-DMYVg3LT.js +2 -0
- package/frontend/dist/js/PersonasPage-Ct-KKW-j.js +13 -0
- package/frontend/dist/js/{RichMessageContent-WQLdJ5jq.js → RichMessageContent-CGBAcUES.js} +1 -1
- package/frontend/dist/js/SettingsModal-BVj3gFCS.js +1 -0
- package/frontend/dist/js/UserManagementPage-D3tLGtTu.js +1 -0
- package/frontend/dist/js/WorkPage-DhTjy-kf.js +2 -0
- package/frontend/dist/js/api-YWoSobKv.js +19 -0
- package/frontend/dist/js/{ar-CCVY0-7X.js → ar-KceI0PfT.js} +1 -1
- package/frontend/dist/js/{bn-CoU5CSph.js → bn-DsKo2dzS.js} +1 -1
- package/frontend/dist/js/{cs-CR6pjIbG.js → cs-6UKhKj0A.js} +1 -1
- package/frontend/dist/js/{da-CkyYITRz.js → da-CJgZ3YuL.js} +1 -1
- package/frontend/dist/js/{de-DwNANzB-.js → de-BZAmOuCT.js} +1 -1
- package/frontend/dist/js/{es-TxygiaVQ.js → es-CD4u7no9.js} +1 -1
- package/frontend/dist/js/{estree-DPzMpjFm.js → estree-PDLT672k.js} +1 -1
- package/frontend/dist/js/{fr-DvEqVpZr.js → fr-Cou2jOS3.js} +1 -1
- package/frontend/dist/js/{hi-CyrWkyh7.js → hi-ZxkbNCyB.js} +1 -1
- package/frontend/dist/js/{html-R50jmBYq.js → html-CkhF9wDQ.js} +1 -1
- package/frontend/dist/js/{id-PJjIGHGM.js → id-DT9CqaWQ.js} +1 -1
- package/frontend/dist/js/{is-ChXcN1tI.js → is-BWSbAIoq.js} +1 -1
- package/frontend/dist/js/{it-DsgM8-MJ.js → it-CdNoTXm3.js} +1 -1
- package/frontend/dist/js/{ja-ivtt80N2.js → ja-CIuQg0xv.js} +1 -1
- package/frontend/dist/js/{ko-B8l_Ice1.js → ko-BWRUjDOH.js} +1 -1
- package/frontend/dist/js/{markdown-C08x2H-K.js → markdown-B1iNUbse.js} +1 -1
- package/frontend/dist/js/{ms-DoUBI6Qi.js → ms-gJstIj2a.js} +1 -1
- package/frontend/dist/js/{nl-DsX5Hj8m.js → nl-CydjVNUw.js} +1 -1
- package/frontend/dist/js/{pl-Db_NKL4x.js → pl-DPN9mAa5.js} +1 -1
- package/frontend/dist/js/{pt-CpzBhb5J.js → pt-MRXGqIjQ.js} +1 -1
- package/frontend/dist/js/{ru-DCXvJyXJ.js → ru-CvQ9GkJF.js} +1 -1
- package/frontend/dist/js/{standalone-DpMZUvjd.js → standalone-CCd6rkf-.js} +1 -1
- package/frontend/dist/js/{sv-DcVoybc7.js → sv-BxS2fvhU.js} +1 -1
- package/frontend/dist/js/{th-D0G7HyQn.js → th-DMxyn6e4.js} +1 -1
- package/frontend/dist/js/{tr-CMDOh4Aq.js → tr-BTPldUaA.js} +1 -1
- package/frontend/dist/js/{typescript-B6y_hxBm.js → typescript-DKJ1uM5L.js} +1 -1
- package/frontend/dist/js/ui-vendor-CAFEDSaO.js +177 -0
- package/frontend/dist/js/{uk-D2MMa6LP.js → uk-BGxZOKgp.js} +1 -1
- package/frontend/dist/js/useBaseQuery-MuQj7vA5.js +1 -0
- package/frontend/dist/js/useQuery-CYGPkxWR.js +1 -0
- package/frontend/dist/js/{vi-B8uHT8yh.js → vi-82JkwFsQ.js} +1 -1
- package/frontend/dist/js/{zh-CNqa_15K.js → zh-Bxxfwci3.js} +1 -1
- package/package.json +3 -2
- package/plugins/github.json +1 -2
- package/plugins/huggingface.json +30 -30
- package/plugins/mlx-lm.json +55 -0
- package/plugins/openai.json +53 -22
- package/frontend/dist/assets/index-Bv-twCuY.css +0 -1
- package/frontend/dist/assets/index-uCHBH_PM.js +0 -5
- package/frontend/dist/js/ChatPage-1bK0gDBy.js +0 -16
- package/frontend/dist/js/GalleryPage-6WwyH3kt.js +0 -1
- package/frontend/dist/js/LibreClawPage-ChHVfPLb.js +0 -1
- package/frontend/dist/js/ModelsPage-Dw6wdiMt.js +0 -2
- package/frontend/dist/js/PersonasPage-BxdI2e1-.js +0 -13
- package/frontend/dist/js/SettingsModal-C-cK6TNb.js +0 -1
- package/frontend/dist/js/UserManagementPage-DqjNXPkf.js +0 -1
- package/frontend/dist/js/WorkPage-MGR3odo6.js +0 -2
- package/frontend/dist/js/api-DD5qhhLf.js +0 -19
- package/frontend/dist/js/ui-vendor-DUqvZsfs.js +0 -177
- package/frontend/dist/js/useBaseQuery-Duml3DYp.js +0 -1
- package/frontend/dist/js/useQuery-C0lZwNoc.js +0 -1
|
@@ -16,23 +16,78 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
import path from 'path';
|
|
19
|
+
import { createHash } from 'crypto';
|
|
19
20
|
import sanitize from 'sanitize-filename';
|
|
20
21
|
import axios from 'axios';
|
|
21
22
|
import pluginCredentialsService from './pluginCredentialsService.js';
|
|
22
23
|
import pluginVariablesService from './pluginVariablesService.js';
|
|
24
|
+
import pluginActivationService from './pluginActivationService.js';
|
|
23
25
|
import { PluginCapabilityRegistryService } from './pluginCapabilityRegistryService.js';
|
|
24
26
|
import { PluginEmbeddingService } from './pluginEmbeddingService.js';
|
|
25
27
|
import { PluginImageGenerationService } from './pluginImageGenerationService.js';
|
|
26
28
|
import { PluginTTSService } from './pluginTTSService.js';
|
|
27
29
|
import { applyPluginDefinitionPolicy, buildPluginChatPayload, convertProviderResponse, getOpenAICompatibleSamplingParameters, resolvePluginChatParameters, toOpenAICompatibleMessages, } from '../utils/pluginChatAdapter.js';
|
|
28
|
-
import { streamAnthropicResponse, streamOpenAICompatibleResponse, } from '../utils/pluginStreamAdapter.js';
|
|
29
|
-
import { applyModelEndpointTemplate, assertSafePluginEndpoint, buildPluginAuthHeaders, buildPluginModelDiscoveryHeaders, resolvePluginEndpoint, resolvePluginModelsEndpoint,
|
|
30
|
+
import { streamAnthropicResponse, streamOpenAICompatibleResponse, streamOpenAIResponsesResponse, } from '../utils/pluginStreamAdapter.js';
|
|
31
|
+
import { applyModelEndpointTemplate, assertSafePluginEndpoint, buildPluginAuthHeaders, buildPluginModelDiscoveryHeaders, pluginRequiresApiKey, resolvePluginApiConfig, resolvePluginEndpoint, resolvePluginModelsEndpoint, validatePluginModel, } from '../utils/pluginValidation.js';
|
|
30
32
|
import { createLogger } from '../utils/logger.js';
|
|
31
33
|
import { resolveBundledPluginsDir } from '../utils/packagePaths.js';
|
|
34
|
+
import { getDatabaseSafe } from '../db.js';
|
|
35
|
+
import { createOpenAIResponsesStateScope, createPluginCredentialFingerprint, OPENAI_RESPONSES_INCOMPLETE_REASON_METADATA_KEY, } from '../utils/openAIResponsesAdapter.js';
|
|
36
|
+
import { getPluginConnectionVariableNames } from '../utils/pluginConnectionVariables.js';
|
|
37
|
+
import { getPluginDefinitionFingerprint, matchesBundledPluginTrustAnchor, } from '../utils/pluginDefinitionTrust.js';
|
|
32
38
|
const logger = createLogger('plugins');
|
|
33
|
-
|
|
39
|
+
function getPluginRoutingAuthProjection(plugin) {
|
|
40
|
+
const capabilityEndpoints = Object.entries((plugin.capabilities || {}))
|
|
41
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
42
|
+
.map(([name, value]) => {
|
|
43
|
+
const capability = value && typeof value === 'object'
|
|
44
|
+
? value
|
|
45
|
+
: {};
|
|
46
|
+
const config = capability.config && typeof capability.config === 'object'
|
|
47
|
+
? capability.config
|
|
48
|
+
: {};
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
endpoint: capability.endpoint ?? null,
|
|
52
|
+
endpoint_variable: config.endpoint_variable ?? capability.endpoint_variable ?? null,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const definitions = new Map((plugin.variables || []).map(definition => [definition.name, definition]));
|
|
56
|
+
const connectionVariables = Array.from(getPluginConnectionVariableNames(plugin), name => {
|
|
57
|
+
const definition = definitions.get(name);
|
|
58
|
+
if (!definition)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
name: definition.name,
|
|
62
|
+
type: definition.type,
|
|
63
|
+
label: definition.label,
|
|
64
|
+
description: definition.description ?? null,
|
|
65
|
+
default: definition.default ?? null,
|
|
66
|
+
required: definition.required ?? null,
|
|
67
|
+
sensitive: definition.sensitive ?? null,
|
|
68
|
+
options: definition.options ?? null,
|
|
69
|
+
min: definition.min ?? null,
|
|
70
|
+
max: definition.max ?? null,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
endpoint: plugin.endpoint,
|
|
75
|
+
base_url: plugin.base_url ?? null,
|
|
76
|
+
api_path: plugin.api_path ?? null,
|
|
77
|
+
api_mode: plugin.api_mode ?? null,
|
|
78
|
+
auth: {
|
|
79
|
+
header: plugin.auth.header,
|
|
80
|
+
prefix: plugin.auth.prefix ?? '',
|
|
81
|
+
key_env: plugin.auth.key_env,
|
|
82
|
+
},
|
|
83
|
+
capabilityEndpoints,
|
|
84
|
+
connectionVariables,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export class PluginService {
|
|
34
88
|
constructor() {
|
|
35
|
-
this.
|
|
89
|
+
this.discoveredModelsCache = new Map();
|
|
90
|
+
this.bundledRoutingProjectionCache = new Map();
|
|
36
91
|
this.bundledPluginsDir = resolveBundledPluginsDir(import.meta.url);
|
|
37
92
|
this.legacyPluginsDir = path.join(process.cwd(), 'plugins');
|
|
38
93
|
this.pluginsDir =
|
|
@@ -42,67 +97,280 @@ class PluginService {
|
|
|
42
97
|
: this.legacyPluginsDir);
|
|
43
98
|
this.pluginReadDirs = Array.from(new Set([this.bundledPluginsDir, this.legacyPluginsDir, this.pluginsDir]));
|
|
44
99
|
this.ensurePluginsDirectory();
|
|
45
|
-
this.
|
|
100
|
+
pluginActivationService.migrateLegacyStatus([this.pluginsDir, this.legacyPluginsDir], pluginId => this.canMigrateLegacyActivation(pluginId));
|
|
46
101
|
this.embeddingService = new PluginEmbeddingService({
|
|
47
|
-
getAllPlugins:
|
|
102
|
+
getAllPlugins: userId => this.getActivePlugins(userId),
|
|
48
103
|
getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
|
|
49
104
|
getPluginVariables: (plugin, userId) => this.getPluginVariables(plugin, userId),
|
|
50
105
|
validateEndpointUrl: endpoint => this.validateEndpointUrl(endpoint),
|
|
51
106
|
});
|
|
52
107
|
this.ttsService = new PluginTTSService({
|
|
53
|
-
getAllPlugins:
|
|
54
|
-
getPlugin: id => this.getPlugin(id),
|
|
108
|
+
getAllPlugins: userId => this.getActivePlugins(userId),
|
|
109
|
+
getPlugin: (id, userId) => this.getPlugin(id, userId),
|
|
55
110
|
getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
|
|
56
111
|
getPluginVariables: (plugin, userId) => this.getPluginVariables(plugin, userId),
|
|
57
112
|
validateEndpointUrl: endpoint => this.validateEndpointUrl(endpoint),
|
|
58
113
|
});
|
|
59
114
|
this.imageGenerationService = new PluginImageGenerationService({
|
|
60
|
-
getAllPlugins:
|
|
61
|
-
getPlugin: id => this.getPlugin(id),
|
|
115
|
+
getAllPlugins: userId => this.getActivePlugins(userId),
|
|
116
|
+
getPlugin: (id, userId) => this.getPlugin(id, userId),
|
|
62
117
|
getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
|
|
63
118
|
getPluginVariables: (plugin, userId) => this.getPluginVariables(plugin, userId),
|
|
64
119
|
validateEndpointUrl: endpoint => this.validateEndpointUrl(endpoint),
|
|
65
120
|
});
|
|
66
121
|
this.capabilityRegistryService = new PluginCapabilityRegistryService({
|
|
67
|
-
getAllPlugins:
|
|
122
|
+
getAllPlugins: userId => this.getActivePlugins(userId),
|
|
68
123
|
getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
|
|
69
124
|
});
|
|
70
125
|
}
|
|
126
|
+
discoveredModelsCacheKey(pluginId, userId) {
|
|
127
|
+
return `${userId}:${pluginId}`;
|
|
128
|
+
}
|
|
129
|
+
getDiscoveredModels(pluginId, userId) {
|
|
130
|
+
const effectiveUserId = userId || 'default';
|
|
131
|
+
const cacheKey = this.discoveredModelsCacheKey(pluginId, effectiveUserId);
|
|
132
|
+
if (this.discoveredModelsCache.has(cacheKey)) {
|
|
133
|
+
return this.discoveredModelsCache.get(cacheKey) || undefined;
|
|
134
|
+
}
|
|
135
|
+
const db = getDatabaseSafe();
|
|
136
|
+
if (!db) {
|
|
137
|
+
this.discoveredModelsCache.set(cacheKey, null);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const row = db
|
|
142
|
+
.prepare('SELECT models_json FROM plugin_discovered_models WHERE user_id = ? AND plugin_id = ?')
|
|
143
|
+
.get(effectiveUserId, pluginId);
|
|
144
|
+
if (!row) {
|
|
145
|
+
this.discoveredModelsCache.set(cacheKey, null);
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const parsed = JSON.parse(row.models_json);
|
|
149
|
+
const models = Array.isArray(parsed)
|
|
150
|
+
? Array.from(new Set(parsed.filter((model) => typeof model === 'string' && model.length > 0)))
|
|
151
|
+
: [];
|
|
152
|
+
if (models.length === 0) {
|
|
153
|
+
this.discoveredModelsCache.set(cacheKey, null);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
this.discoveredModelsCache.set(cacheKey, models);
|
|
157
|
+
return models;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger.warn('Failed to read discovered models for plugin %s:', pluginId, error);
|
|
161
|
+
this.discoveredModelsCache.set(cacheKey, null);
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
storeDiscoveredModels(pluginId, models, userId) {
|
|
166
|
+
const effectiveUserId = userId || 'default';
|
|
167
|
+
const uniqueModels = Array.from(new Set(models));
|
|
168
|
+
const cacheKey = this.discoveredModelsCacheKey(pluginId, effectiveUserId);
|
|
169
|
+
this.discoveredModelsCache.set(cacheKey, uniqueModels);
|
|
170
|
+
const db = getDatabaseSafe();
|
|
171
|
+
if (!db)
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
db.prepare(`INSERT INTO plugin_discovered_models
|
|
175
|
+
(user_id, plugin_id, models_json, updated_at)
|
|
176
|
+
VALUES (?, ?, ?, ?)
|
|
177
|
+
ON CONFLICT(user_id, plugin_id) DO UPDATE SET
|
|
178
|
+
models_json = excluded.models_json,
|
|
179
|
+
updated_at = excluded.updated_at`).run(effectiveUserId, pluginId, JSON.stringify(uniqueModels), Date.now());
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
logger.warn('Failed to persist discovered models for plugin %s:', pluginId, error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
clearDiscoveredModels(pluginId, userId) {
|
|
186
|
+
const db = getDatabaseSafe();
|
|
187
|
+
if (userId) {
|
|
188
|
+
this.discoveredModelsCache.set(this.discoveredModelsCacheKey(pluginId, userId), null);
|
|
189
|
+
if (!db)
|
|
190
|
+
return;
|
|
191
|
+
try {
|
|
192
|
+
db.prepare('DELETE FROM plugin_discovered_models WHERE user_id = ? AND plugin_id = ?').run(userId, pluginId);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
logger.warn('Failed to clear discovered models for plugin %s:', pluginId, error);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const key of this.discoveredModelsCache.keys()) {
|
|
200
|
+
if (key.endsWith(`:${pluginId}`)) {
|
|
201
|
+
this.discoveredModelsCache.delete(key);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!db)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
db.prepare('DELETE FROM plugin_discovered_models WHERE plugin_id = ?').run(pluginId);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
logger.warn('Failed to clear discovered models for plugin %s:', pluginId, error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
applyDiscoveredModels(plugin, userId) {
|
|
214
|
+
if (!this.canUseStoredConnectionOverrides(userId) &&
|
|
215
|
+
pluginVariablesService.hasStoredConnectionOverride(plugin.id, userId, plugin.variables, getPluginConnectionVariableNames(plugin))) {
|
|
216
|
+
return plugin;
|
|
217
|
+
}
|
|
218
|
+
const models = this.getDiscoveredModels(plugin.id, userId);
|
|
219
|
+
return models ? { ...plugin, model_map: [...models] } : plugin;
|
|
220
|
+
}
|
|
71
221
|
ensurePluginsDirectory() {
|
|
72
222
|
if (!fs.existsSync(this.pluginsDir)) {
|
|
73
223
|
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
|
74
224
|
}
|
|
75
225
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
226
|
+
bundledPluginPath(id) {
|
|
227
|
+
return path.resolve(this.bundledPluginsDir, `${sanitize(id)}.json`);
|
|
228
|
+
}
|
|
229
|
+
isAnchoredBundledDefinition(plugin, filePath) {
|
|
230
|
+
return (path.resolve(filePath) === this.bundledPluginPath(plugin.id) &&
|
|
231
|
+
matchesBundledPluginTrustAnchor(plugin));
|
|
232
|
+
}
|
|
233
|
+
isPluginDefinitionApproved(plugin, filePath) {
|
|
234
|
+
if (this.isAnchoredBundledDefinition(plugin, filePath)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
const db = getDatabaseSafe();
|
|
238
|
+
if (!db)
|
|
239
|
+
return false;
|
|
240
|
+
try {
|
|
241
|
+
const row = db
|
|
242
|
+
.prepare(`SELECT definition_fingerprint, source_path
|
|
243
|
+
FROM plugin_definition_approvals
|
|
244
|
+
WHERE plugin_id = ?`)
|
|
245
|
+
.get(plugin.id);
|
|
246
|
+
return (row?.definition_fingerprint ===
|
|
247
|
+
getPluginDefinitionFingerprint(plugin) &&
|
|
248
|
+
row.source_path === path.resolve(filePath));
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
logger.warn('Failed to inspect definition approval for plugin %s:', plugin.id, error);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
approvePluginDefinition(plugin, filePath, userId) {
|
|
256
|
+
if (!this.canUseStoredConnectionOverrides(userId)) {
|
|
257
|
+
throw new Error('Administrator approval is required');
|
|
258
|
+
}
|
|
259
|
+
const db = getDatabaseSafe();
|
|
260
|
+
if (!db) {
|
|
261
|
+
throw new Error('Database not available for plugin approval');
|
|
262
|
+
}
|
|
263
|
+
db.prepare(`INSERT INTO plugin_definition_approvals
|
|
264
|
+
(plugin_id, definition_fingerprint, source_path,
|
|
265
|
+
approved_by_user_id, approved_at)
|
|
266
|
+
VALUES (?, ?, ?, ?, ?)
|
|
267
|
+
ON CONFLICT(plugin_id) DO UPDATE SET
|
|
268
|
+
definition_fingerprint = excluded.definition_fingerprint,
|
|
269
|
+
source_path = excluded.source_path,
|
|
270
|
+
approved_by_user_id = excluded.approved_by_user_id,
|
|
271
|
+
approved_at = excluded.approved_at`).run(plugin.id, getPluginDefinitionFingerprint(plugin), path.resolve(filePath), userId, Date.now());
|
|
272
|
+
}
|
|
273
|
+
removePluginDefinitionApproval(pluginId) {
|
|
274
|
+
const db = getDatabaseSafe();
|
|
275
|
+
if (!db)
|
|
276
|
+
return;
|
|
277
|
+
db.prepare('DELETE FROM plugin_definition_approvals WHERE plugin_id = ?').run(pluginId);
|
|
278
|
+
}
|
|
279
|
+
revokePluginDefinitionConsent(pluginId) {
|
|
280
|
+
const db = getDatabaseSafe();
|
|
281
|
+
if (!db) {
|
|
282
|
+
throw new Error('Database not available for plugin approval');
|
|
283
|
+
}
|
|
284
|
+
db.transaction(() => {
|
|
285
|
+
db.prepare('DELETE FROM plugin_activations WHERE plugin_id = ?').run(pluginId);
|
|
286
|
+
db.prepare('DELETE FROM plugin_definition_approvals WHERE plugin_id = ?').run(pluginId);
|
|
287
|
+
})();
|
|
288
|
+
}
|
|
289
|
+
canMigrateLegacyActivation(pluginId) {
|
|
290
|
+
const effectivePath = this.resolveEffectivePluginFilePath(pluginId);
|
|
291
|
+
if (!effectivePath)
|
|
292
|
+
return false;
|
|
293
|
+
try {
|
|
294
|
+
const parsedPlugin = JSON.parse(fs.readFileSync(effectivePath, 'utf8'));
|
|
295
|
+
return (this.validatePlugin(parsedPlugin) &&
|
|
296
|
+
parsedPlugin.id === pluginId &&
|
|
297
|
+
this.isAnchoredBundledDefinition(parsedPlugin, effectivePath));
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
resolveEffectivePluginFilePath(id) {
|
|
304
|
+
const sanitizedId = sanitize(id);
|
|
305
|
+
if (!sanitizedId || sanitizedId !== id)
|
|
306
|
+
return null;
|
|
307
|
+
for (const pluginsDir of [...this.pluginReadDirs].reverse()) {
|
|
308
|
+
const resolvedDirectory = path.resolve(pluginsDir);
|
|
309
|
+
const candidate = path.resolve(pluginsDir, `${sanitizedId}.json`);
|
|
310
|
+
if (candidate.startsWith(resolvedDirectory) && fs.existsSync(candidate)) {
|
|
311
|
+
return candidate;
|
|
93
312
|
}
|
|
94
|
-
|
|
95
|
-
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
getBundledRoutingProjection(id) {
|
|
317
|
+
if (this.bundledRoutingProjectionCache.has(id)) {
|
|
318
|
+
return this.bundledRoutingProjectionCache.get(id) ?? null;
|
|
319
|
+
}
|
|
320
|
+
const sanitizedId = sanitize(id);
|
|
321
|
+
if (!sanitizedId || sanitizedId !== id)
|
|
322
|
+
return null;
|
|
323
|
+
const bundledDirectory = path.resolve(this.bundledPluginsDir);
|
|
324
|
+
const bundledPath = this.bundledPluginPath(sanitizedId);
|
|
325
|
+
if (!bundledPath.startsWith(bundledDirectory) ||
|
|
326
|
+
!fs.existsSync(bundledPath)) {
|
|
327
|
+
this.bundledRoutingProjectionCache.set(id, null);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const parsedPlugin = JSON.parse(fs.readFileSync(bundledPath, 'utf8'));
|
|
332
|
+
if (!this.validatePlugin(parsedPlugin) ||
|
|
333
|
+
parsedPlugin.id !== sanitizedId ||
|
|
334
|
+
!matchesBundledPluginTrustAnchor(parsedPlugin)) {
|
|
335
|
+
this.bundledRoutingProjectionCache.set(id, null);
|
|
336
|
+
return null;
|
|
96
337
|
}
|
|
338
|
+
const projection = getPluginRoutingAuthProjection(applyPluginDefinitionPolicy(parsedPlugin));
|
|
339
|
+
this.bundledRoutingProjectionCache.set(id, projection);
|
|
340
|
+
return projection;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
logger.warn('Failed to inspect bundled routing for plugin %s:', id, error);
|
|
344
|
+
this.bundledRoutingProjectionCache.set(id, null);
|
|
345
|
+
return null;
|
|
97
346
|
}
|
|
98
347
|
}
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
348
|
+
usesTrustedBundledRouting(plugin) {
|
|
349
|
+
const effectivePath = this.resolveEffectivePluginFilePath(plugin.id);
|
|
350
|
+
const bundledPath = this.bundledPluginPath(plugin.id);
|
|
351
|
+
if (!effectivePath || effectivePath !== bundledPath)
|
|
352
|
+
return false;
|
|
353
|
+
const bundledProjection = this.getBundledRoutingProjection(plugin.id);
|
|
354
|
+
return (bundledProjection !== null &&
|
|
355
|
+
getPluginRoutingAuthProjection(plugin) === bundledProjection);
|
|
356
|
+
}
|
|
357
|
+
isPluginActive(pluginId, userId) {
|
|
358
|
+
return pluginActivationService.getActivePluginIds(userId).has(pluginId);
|
|
359
|
+
}
|
|
360
|
+
canUseStoredConnectionOverrides(userId) {
|
|
361
|
+
const db = getDatabaseSafe();
|
|
362
|
+
if (!db)
|
|
363
|
+
return false;
|
|
364
|
+
try {
|
|
365
|
+
const row = db
|
|
366
|
+
.prepare('SELECT role FROM users WHERE id = ?')
|
|
367
|
+
.get(userId || 'default');
|
|
368
|
+
return row?.role === 'admin';
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
logger.warn('Failed to resolve plugin routing permission:', error);
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
106
374
|
}
|
|
107
375
|
/**
|
|
108
376
|
* Get API key for a plugin from database (per-user) or environment variable (fallback)
|
|
@@ -111,7 +379,57 @@ class PluginService {
|
|
|
111
379
|
* @returns The API key or null if not found
|
|
112
380
|
*/
|
|
113
381
|
getApiKey(plugin, userId) {
|
|
114
|
-
|
|
382
|
+
const hasHonoredConnectionOverride = this.canUseStoredConnectionOverrides(userId) &&
|
|
383
|
+
pluginVariablesService.hasStoredConnectionOverride(plugin.id, userId, plugin.variables, getPluginConnectionVariableNames(plugin));
|
|
384
|
+
const usesTrustedBundledRouting = this.usesTrustedBundledRouting(plugin);
|
|
385
|
+
const allowTrustedFallback = usesTrustedBundledRouting && !hasHonoredConnectionOverride;
|
|
386
|
+
return pluginCredentialsService.getApiKey(plugin.id, plugin.auth.key_env, userId, {
|
|
387
|
+
allowEnvironmentFallback: allowTrustedFallback,
|
|
388
|
+
expectedRoutingAuthFingerprint: this.getCredentialRoutingAuthFingerprint(plugin, userId),
|
|
389
|
+
allowLegacyUnboundCredential: allowTrustedFallback,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Bind a user credential to the exact routing/authentication contract in
|
|
394
|
+
* effect when they save it. Generation controls are intentionally excluded.
|
|
395
|
+
*/
|
|
396
|
+
getCredentialRoutingAuthFingerprint(plugin, userId) {
|
|
397
|
+
const variables = this.getPluginVariables(plugin, userId);
|
|
398
|
+
const effectiveConnectionValues = Array.from(getPluginConnectionVariableNames(plugin), name => {
|
|
399
|
+
const definition = plugin.variables?.find(candidate => candidate.name === name);
|
|
400
|
+
if (!definition)
|
|
401
|
+
return null;
|
|
402
|
+
return {
|
|
403
|
+
name,
|
|
404
|
+
value: variables[name] ?? definition.default ?? '',
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
const effectivePath = this.resolveEffectivePluginFilePath(plugin.id);
|
|
408
|
+
let effectiveDefinitionFingerprint = getPluginDefinitionFingerprint(plugin);
|
|
409
|
+
if (effectivePath) {
|
|
410
|
+
try {
|
|
411
|
+
const effectiveDefinition = JSON.parse(fs.readFileSync(effectivePath, 'utf8'));
|
|
412
|
+
if (this.validatePlugin(effectiveDefinition) &&
|
|
413
|
+
effectiveDefinition.id === plugin.id) {
|
|
414
|
+
effectiveDefinitionFingerprint =
|
|
415
|
+
getPluginDefinitionFingerprint(effectiveDefinition);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Keep the in-memory fingerprint. A missing/invalid source is already
|
|
420
|
+
// excluded from normal plugin loading and cannot gain trust here.
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const fingerprintInput = JSON.stringify({
|
|
424
|
+
plugin_id: plugin.id,
|
|
425
|
+
plugin_type: plugin.type,
|
|
426
|
+
trusted_bundled_source: this.usesTrustedBundledRouting(plugin),
|
|
427
|
+
effective_source_path: effectivePath ? path.resolve(effectivePath) : null,
|
|
428
|
+
effective_definition_fingerprint: effectiveDefinitionFingerprint,
|
|
429
|
+
routing_auth_projection: getPluginRoutingAuthProjection(plugin),
|
|
430
|
+
effective_connection_values: effectiveConnectionValues,
|
|
431
|
+
});
|
|
432
|
+
return createHash('sha256').update(fingerprintInput).digest('hex');
|
|
115
433
|
}
|
|
116
434
|
/**
|
|
117
435
|
* Get resolved variable values for a plugin (decrypted, typed).
|
|
@@ -120,34 +438,58 @@ class PluginService {
|
|
|
120
438
|
if (!plugin.variables || plugin.variables.length === 0) {
|
|
121
439
|
return {};
|
|
122
440
|
}
|
|
123
|
-
|
|
441
|
+
const variables = pluginVariablesService.getResolvedVariables(plugin.id, plugin.variables, userId);
|
|
442
|
+
if (this.canUseStoredConnectionOverrides(userId))
|
|
443
|
+
return variables;
|
|
444
|
+
return Object.fromEntries(Object.entries(variables).map(([name, value]) => {
|
|
445
|
+
if (!getPluginConnectionVariableNames(plugin).has(name)) {
|
|
446
|
+
return [name, value];
|
|
447
|
+
}
|
|
448
|
+
const definition = plugin.variables?.find(candidate => candidate.name === name);
|
|
449
|
+
return [name, definition?.default ?? ''];
|
|
450
|
+
}));
|
|
124
451
|
}
|
|
125
452
|
/**
|
|
126
|
-
* Validate an endpoint
|
|
127
|
-
* Returns the URL string if valid
|
|
453
|
+
* Validate an endpoint override as an absolute HTTP or HTTPS URL.
|
|
454
|
+
* Returns the URL string if valid and throws for an invalid explicit value.
|
|
128
455
|
*/
|
|
129
456
|
validateEndpointUrl(endpoint) {
|
|
130
|
-
return
|
|
457
|
+
return resolvePluginEndpoint('', endpoint);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Resolve and validate routing before credential lookup. The credential
|
|
461
|
+
* service's custom-route policy is integrated separately; keeping this
|
|
462
|
+
* boundary ordered prevents a rejected override from ever selecting an
|
|
463
|
+
* environment credential.
|
|
464
|
+
*/
|
|
465
|
+
resolveOperationEndpoint(plugin, userId) {
|
|
466
|
+
return resolvePluginApiConfig(plugin, this.getPluginVariables(plugin, userId)).endpoint;
|
|
131
467
|
}
|
|
132
468
|
/**
|
|
133
|
-
* Attempt to auto-discover available models from a plugin's
|
|
469
|
+
* Attempt to auto-discover available models from a plugin's full API endpoint.
|
|
134
470
|
* Resolves the provider's model-list endpoint and updates the plugin's model_map.
|
|
135
471
|
* Falls back silently to the existing model_map if the endpoint is unavailable.
|
|
136
472
|
*/
|
|
137
473
|
async discoverModels(pluginId, userId) {
|
|
138
|
-
const plugin = this.getPlugin(pluginId);
|
|
474
|
+
const plugin = this.getPlugin(pluginId, userId);
|
|
139
475
|
if (!plugin)
|
|
140
476
|
return [];
|
|
141
477
|
const pluginVars = this.getPluginVariables(plugin, userId);
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
478
|
+
const { endpoint: effectiveEndpoint } = resolvePluginApiConfig(plugin, pluginVars);
|
|
479
|
+
const modelsEndpoint = resolvePluginModelsEndpoint(effectiveEndpoint, typeof pluginVars.models_endpoint === 'string'
|
|
480
|
+
? pluginVars.models_endpoint
|
|
481
|
+
: undefined);
|
|
482
|
+
assertSafePluginEndpoint(modelsEndpoint, 'model discovery endpoint');
|
|
145
483
|
const apiKey = this.getApiKey(plugin, userId);
|
|
484
|
+
if (pluginRequiresApiKey(plugin) && !apiKey) {
|
|
485
|
+
return plugin.model_map;
|
|
486
|
+
}
|
|
146
487
|
const headers = buildPluginModelDiscoveryHeaders(plugin, apiKey);
|
|
147
488
|
try {
|
|
148
|
-
const response = await axios.get(
|
|
489
|
+
const response = await axios.get(modelsEndpoint, {
|
|
149
490
|
headers,
|
|
150
491
|
timeout: 5000,
|
|
492
|
+
maxRedirects: 0,
|
|
151
493
|
});
|
|
152
494
|
if (response.data?.data && Array.isArray(response.data.data)) {
|
|
153
495
|
const models = response.data.data
|
|
@@ -155,21 +497,7 @@ class PluginService {
|
|
|
155
497
|
.filter((id) => typeof id === 'string');
|
|
156
498
|
if (models.length > 0) {
|
|
157
499
|
logger.debug('[Plugin] Auto-discovered %d models for %s:', models.length, pluginId, models);
|
|
158
|
-
|
|
159
|
-
plugin.model_map = models;
|
|
160
|
-
const safeId = pluginId.replace(/[^a-zA-Z0-9_.-]/g, '');
|
|
161
|
-
if (safeId !== pluginId)
|
|
162
|
-
throw new Error('Invalid plugin ID');
|
|
163
|
-
const filePath = path.resolve(this.pluginsDir, `${safeId}.json`);
|
|
164
|
-
if (!filePath.startsWith(path.resolve(this.pluginsDir))) {
|
|
165
|
-
throw new Error('Path traversal detected');
|
|
166
|
-
}
|
|
167
|
-
const pluginData = fs.existsSync(filePath)
|
|
168
|
-
? JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
169
|
-
: plugin;
|
|
170
|
-
pluginData.model_map = models;
|
|
171
|
-
pluginData.updated_at = Date.now();
|
|
172
|
-
fs.writeFileSync(filePath, JSON.stringify(pluginData, null, 2));
|
|
500
|
+
this.storeDiscoveredModels(pluginId, models, userId);
|
|
173
501
|
return models;
|
|
174
502
|
}
|
|
175
503
|
}
|
|
@@ -180,8 +508,9 @@ class PluginService {
|
|
|
180
508
|
return plugin.model_map;
|
|
181
509
|
}
|
|
182
510
|
// List all installed plugins
|
|
183
|
-
getAllPlugins() {
|
|
511
|
+
getAllPlugins(userId) {
|
|
184
512
|
const plugins = new Map();
|
|
513
|
+
const activePluginIds = pluginActivationService.getActivePluginIds(userId);
|
|
185
514
|
for (const pluginsDir of this.pluginReadDirs) {
|
|
186
515
|
if (!fs.existsSync(pluginsDir)) {
|
|
187
516
|
continue;
|
|
@@ -190,18 +519,36 @@ class PluginService {
|
|
|
190
519
|
const files = fs.readdirSync(pluginsDir);
|
|
191
520
|
for (const file of files) {
|
|
192
521
|
if (file.endsWith('.json') && !file.startsWith('.')) {
|
|
522
|
+
const filenameId = path.basename(file, '.json');
|
|
193
523
|
try {
|
|
194
524
|
const filePath = path.join(pluginsDir, file);
|
|
195
525
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
196
526
|
const parsedPlugin = JSON.parse(content);
|
|
197
527
|
// Validate plugin structure
|
|
198
|
-
if (this.validatePlugin(parsedPlugin)
|
|
528
|
+
if (this.validatePlugin(parsedPlugin) &&
|
|
529
|
+
parsedPlugin.id === filenameId) {
|
|
530
|
+
if (!this.isPluginDefinitionApproved(parsedPlugin, filePath)) {
|
|
531
|
+
// A later writable definition shadows an earlier bundled
|
|
532
|
+
// definition even while quarantined.
|
|
533
|
+
plugins.delete(parsedPlugin.id);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
199
536
|
const plugin = applyPluginDefinitionPolicy(parsedPlugin);
|
|
200
|
-
plugin.active =
|
|
201
|
-
plugins.set(plugin.id, plugin);
|
|
537
|
+
plugin.active = activePluginIds.has(plugin.id);
|
|
538
|
+
plugins.set(plugin.id, this.applyDiscoveredModels(plugin, userId));
|
|
539
|
+
}
|
|
540
|
+
else if (parsedPlugin.id !== filenameId) {
|
|
541
|
+
plugins.delete(filenameId);
|
|
542
|
+
logger.warn('Ignoring plugin %s because its declared ID does not match its filename', file);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
plugins.delete(filenameId);
|
|
202
546
|
}
|
|
203
547
|
}
|
|
204
548
|
catch (error) {
|
|
549
|
+
// Invalid JSON in an effective same-ID writable file must fail
|
|
550
|
+
// closed instead of revealing an earlier bundled definition.
|
|
551
|
+
plugins.delete(filenameId);
|
|
205
552
|
logger.error(`Failed to load plugin ${file}:`, error);
|
|
206
553
|
}
|
|
207
554
|
}
|
|
@@ -214,39 +561,40 @@ class PluginService {
|
|
|
214
561
|
return Array.from(plugins.values());
|
|
215
562
|
}
|
|
216
563
|
// Get a specific plugin by ID
|
|
217
|
-
getPlugin(id) {
|
|
564
|
+
getPlugin(id, userId) {
|
|
218
565
|
// Sanitize the ID to prevent path traversal
|
|
219
566
|
const sanitizedId = sanitize(id);
|
|
220
567
|
if (!sanitizedId || sanitizedId !== id) {
|
|
221
568
|
logger.error('Invalid plugin ID provided:', id);
|
|
222
569
|
return null;
|
|
223
570
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return plugin;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
logger.error('Failed to load plugin %s:', sanitizedId, error);
|
|
571
|
+
const filePath = this.resolveEffectivePluginFilePath(sanitizedId);
|
|
572
|
+
if (!filePath)
|
|
573
|
+
return null;
|
|
574
|
+
try {
|
|
575
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
576
|
+
const parsedPlugin = JSON.parse(content);
|
|
577
|
+
if (this.validatePlugin(parsedPlugin) &&
|
|
578
|
+
parsedPlugin.id === sanitizedId &&
|
|
579
|
+
this.isPluginDefinitionApproved(parsedPlugin, filePath)) {
|
|
580
|
+
const plugin = applyPluginDefinitionPolicy(parsedPlugin);
|
|
581
|
+
plugin.active = this.isPluginActive(plugin.id, userId);
|
|
582
|
+
return this.applyDiscoveredModels(plugin, userId);
|
|
241
583
|
}
|
|
242
584
|
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
logger.error('Failed to load plugin %s:', sanitizedId, error);
|
|
587
|
+
}
|
|
243
588
|
return null;
|
|
244
589
|
}
|
|
245
590
|
// Install or update a plugin
|
|
246
|
-
installPlugin(pluginData) {
|
|
591
|
+
installPlugin(pluginData, approvedByUserId) {
|
|
247
592
|
if (!this.validatePlugin(pluginData)) {
|
|
248
593
|
throw new Error('Invalid plugin structure');
|
|
249
594
|
}
|
|
595
|
+
if (!this.canUseStoredConnectionOverrides(approvedByUserId)) {
|
|
596
|
+
throw new Error('Administrator approval is required');
|
|
597
|
+
}
|
|
250
598
|
const now = Date.now();
|
|
251
599
|
const plugin = {
|
|
252
600
|
...pluginData,
|
|
@@ -261,7 +609,25 @@ class PluginService {
|
|
|
261
609
|
if (!filePath.startsWith(path.resolve(this.pluginsDir))) {
|
|
262
610
|
throw new Error('Path traversal detected');
|
|
263
611
|
}
|
|
264
|
-
|
|
612
|
+
// Revoke definition approval and every user's activation before replacing
|
|
613
|
+
// bytes on disk. Any crash or write failure therefore leaves the provider
|
|
614
|
+
// quarantined and inactive.
|
|
615
|
+
this.revokePluginDefinitionConsent(plugin.id);
|
|
616
|
+
this.clearDiscoveredModels(plugin.id);
|
|
617
|
+
const temporaryPath = path.resolve(this.pluginsDir, `.${safeId}.${process.pid}.${Date.now()}.tmp`);
|
|
618
|
+
try {
|
|
619
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(plugin, null, 2), {
|
|
620
|
+
flag: 'wx',
|
|
621
|
+
});
|
|
622
|
+
fs.renameSync(temporaryPath, filePath);
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
if (fs.existsSync(temporaryPath)) {
|
|
626
|
+
fs.unlinkSync(temporaryPath);
|
|
627
|
+
}
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
this.approvePluginDefinition(plugin, filePath, approvedByUserId);
|
|
265
631
|
return plugin;
|
|
266
632
|
}
|
|
267
633
|
// Delete a plugin
|
|
@@ -278,7 +644,8 @@ class PluginService {
|
|
|
278
644
|
logger.error('Plugin ID failed sanitization:', id);
|
|
279
645
|
return false;
|
|
280
646
|
}
|
|
281
|
-
const
|
|
647
|
+
const bundledDirectory = path.resolve(this.bundledPluginsDir);
|
|
648
|
+
const writablePluginDirs = Array.from(new Set([this.pluginsDir, this.legacyPluginsDir])).filter(pluginsDir => path.resolve(pluginsDir) !== bundledDirectory);
|
|
282
649
|
const filePath = writablePluginDirs
|
|
283
650
|
.map(pluginsDir => path.resolve(pluginsDir, `${sanitizedId}.json`))
|
|
284
651
|
.find(candidate => fs.existsSync(candidate));
|
|
@@ -288,13 +655,12 @@ class PluginService {
|
|
|
288
655
|
}
|
|
289
656
|
try {
|
|
290
657
|
fs.unlinkSync(filePath);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.activePluginIds.delete(id);
|
|
294
|
-
this.saveActivePlugins();
|
|
295
|
-
}
|
|
658
|
+
pluginActivationService.deletePlugin(id);
|
|
659
|
+
this.removePluginDefinitionApproval(id);
|
|
296
660
|
// Clean up stored variables
|
|
297
661
|
pluginVariablesService.deletePluginVariables(id);
|
|
662
|
+
pluginCredentialsService.deleteAllPluginCredentials(id);
|
|
663
|
+
this.clearDiscoveredModels(id);
|
|
298
664
|
return true;
|
|
299
665
|
}
|
|
300
666
|
catch (error) {
|
|
@@ -303,39 +669,54 @@ class PluginService {
|
|
|
303
669
|
}
|
|
304
670
|
}
|
|
305
671
|
// Activate a plugin
|
|
306
|
-
activatePlugin(id) {
|
|
307
|
-
const plugin = this.getPlugin(id);
|
|
672
|
+
async activatePlugin(id, userId) {
|
|
673
|
+
const plugin = this.getPlugin(id, userId);
|
|
308
674
|
if (!plugin) {
|
|
309
675
|
throw new Error('Plugin not found');
|
|
310
676
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
677
|
+
if (!pluginActivationService.activate(id, userId)) {
|
|
678
|
+
throw new Error('Failed to persist plugin activation');
|
|
679
|
+
}
|
|
680
|
+
// Wait for discovery so the activation response and the UI's first reload
|
|
681
|
+
// observe the same user-scoped model catalog.
|
|
682
|
+
await this.discoverModels(id, userId).catch(() => { });
|
|
315
683
|
return true;
|
|
316
684
|
}
|
|
317
685
|
// Deactivate a specific plugin
|
|
318
|
-
deactivatePlugin(id) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
// Legacy: deactivate all plugins
|
|
324
|
-
this.activePluginIds.clear();
|
|
325
|
-
}
|
|
326
|
-
this.saveActivePlugins();
|
|
327
|
-
return true;
|
|
686
|
+
deactivatePlugin(id, userId) {
|
|
687
|
+
// The legacy no-ID route now deactivates all plugins only for this user.
|
|
688
|
+
return pluginActivationService.deactivate(id, userId);
|
|
328
689
|
}
|
|
329
690
|
// Get the active plugin for a specific model
|
|
330
|
-
getActivePluginForModel(model, userId) {
|
|
691
|
+
getActivePluginForModel(model, userId, pluginId) {
|
|
692
|
+
if (pluginId) {
|
|
693
|
+
const plugin = this.getPlugin(pluginId, userId);
|
|
694
|
+
if (!plugin) {
|
|
695
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
696
|
+
}
|
|
697
|
+
if (!this.isPluginActive(pluginId, userId)) {
|
|
698
|
+
throw new Error(`Plugin is not active: ${pluginId}`);
|
|
699
|
+
}
|
|
700
|
+
if (!plugin.model_map.includes(model)) {
|
|
701
|
+
throw new Error(`Model ${model} is not supported by plugin ${pluginId}`);
|
|
702
|
+
}
|
|
703
|
+
this.resolveOperationEndpoint(plugin, userId);
|
|
704
|
+
const apiKey = this.getApiKey(plugin, userId);
|
|
705
|
+
if (pluginRequiresApiKey(plugin) && !apiKey) {
|
|
706
|
+
throw new Error(`API key not found for plugin ${pluginId} (save a provider credential in Settings)`);
|
|
707
|
+
}
|
|
708
|
+
return plugin;
|
|
709
|
+
}
|
|
331
710
|
// Only route through plugins the user explicitly activated.
|
|
332
|
-
const activePlugins = this.getActivePlugins();
|
|
711
|
+
const activePlugins = this.getActivePlugins(userId);
|
|
333
712
|
// Find the active plugin that supports this model
|
|
334
713
|
for (const plugin of activePlugins) {
|
|
335
714
|
if (plugin.model_map.includes(model)) {
|
|
336
|
-
//
|
|
715
|
+
// Local OpenAI-compatible servers can explicitly opt out of auth by
|
|
716
|
+
// leaving both auth fields empty.
|
|
717
|
+
this.resolveOperationEndpoint(plugin, userId);
|
|
337
718
|
const apiKey = this.getApiKey(plugin, userId);
|
|
338
|
-
if (!apiKey) {
|
|
719
|
+
if (pluginRequiresApiKey(plugin) && !apiKey) {
|
|
339
720
|
continue;
|
|
340
721
|
}
|
|
341
722
|
return plugin;
|
|
@@ -344,52 +725,57 @@ class PluginService {
|
|
|
344
725
|
return null;
|
|
345
726
|
}
|
|
346
727
|
// Get all currently active plugins
|
|
347
|
-
getActivePlugins() {
|
|
348
|
-
const allPlugins = this.getAllPlugins();
|
|
349
|
-
const activePlugins = allPlugins.filter(plugin =>
|
|
728
|
+
getActivePlugins(userId) {
|
|
729
|
+
const allPlugins = this.getAllPlugins(userId);
|
|
730
|
+
const activePlugins = allPlugins.filter(plugin => plugin.active);
|
|
350
731
|
return activePlugins;
|
|
351
732
|
}
|
|
352
733
|
// Legacy method for backward compatibility - returns first active plugin
|
|
353
|
-
getActivePlugin() {
|
|
354
|
-
const activePlugins = this.getActivePlugins();
|
|
734
|
+
getActivePlugin(userId) {
|
|
735
|
+
const activePlugins = this.getActivePlugins(userId);
|
|
355
736
|
return activePlugins.length > 0 ? activePlugins[0] : null;
|
|
356
737
|
}
|
|
357
738
|
// Get plugin status
|
|
358
|
-
getPluginStatus() {
|
|
359
|
-
const plugins = this.getAllPlugins();
|
|
739
|
+
getPluginStatus(userId) {
|
|
740
|
+
const plugins = this.getAllPlugins(userId);
|
|
360
741
|
return plugins.map(plugin => ({
|
|
361
742
|
id: plugin.id,
|
|
362
743
|
active: plugin.active || false,
|
|
363
|
-
available:
|
|
744
|
+
available: !pluginRequiresApiKey(plugin) ||
|
|
745
|
+
this.getApiKey(plugin, userId) !== null,
|
|
364
746
|
}));
|
|
365
747
|
}
|
|
366
748
|
// Execute a chat request through the active plugin
|
|
367
|
-
async executePluginRequest(model, messages, options = {}, userId) {
|
|
749
|
+
async executePluginRequest(model, messages, options = {}, userId, pluginId) {
|
|
368
750
|
validatePluginModel(model);
|
|
369
|
-
const activePlugin = this.getActivePluginForModel(model, userId);
|
|
751
|
+
const activePlugin = this.getActivePluginForModel(model, userId, pluginId);
|
|
370
752
|
if (!activePlugin) {
|
|
371
753
|
throw new Error(`No active plugin found for model: ${model}`);
|
|
372
754
|
}
|
|
373
755
|
if (!activePlugin.model_map.includes(model)) {
|
|
374
756
|
throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
|
|
375
757
|
}
|
|
758
|
+
const pluginVars = this.getPluginVariables(activePlugin, userId);
|
|
759
|
+
const { apiMode, endpoint: effectiveEndpoint } = resolvePluginApiConfig(activePlugin, pluginVars);
|
|
760
|
+
const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
|
|
761
|
+
assertSafePluginEndpoint(processedEndpoint, 'endpoint URL constructed');
|
|
376
762
|
const apiKey = this.getApiKey(activePlugin, userId);
|
|
377
|
-
if (!apiKey) {
|
|
378
|
-
throw new Error(`API key not found for plugin ${activePlugin.id} (
|
|
763
|
+
if (pluginRequiresApiKey(activePlugin) && !apiKey) {
|
|
764
|
+
throw new Error(`API key not found for plugin ${activePlugin.id} (save a provider credential in Settings)`);
|
|
379
765
|
}
|
|
380
|
-
const
|
|
381
|
-
|
|
766
|
+
const providerStateScope = apiMode === 'responses'
|
|
767
|
+
? createOpenAIResponsesStateScope(activePlugin.id, model, processedEndpoint, createPluginCredentialFingerprint(apiKey))
|
|
768
|
+
: undefined;
|
|
382
769
|
const headers = buildPluginAuthHeaders(activePlugin, apiKey);
|
|
383
|
-
const { payload, headers: payloadHeaders } = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars);
|
|
770
|
+
const { payload, headers: payloadHeaders } = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, false, apiMode, providerStateScope);
|
|
384
771
|
Object.assign(headers, payloadHeaders);
|
|
385
|
-
const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
|
|
386
|
-
assertSafePluginEndpoint(processedEndpoint, 'endpoint URL constructed');
|
|
387
772
|
try {
|
|
388
773
|
const response = await axios.post(processedEndpoint, payload, {
|
|
389
774
|
headers,
|
|
390
775
|
timeout: 60000, // 60 second timeout
|
|
776
|
+
maxRedirects: 0,
|
|
391
777
|
});
|
|
392
|
-
return convertProviderResponse(activePlugin, response.data, model);
|
|
778
|
+
return convertProviderResponse(activePlugin, response.data, model, apiMode, providerStateScope);
|
|
393
779
|
}
|
|
394
780
|
catch (error) {
|
|
395
781
|
logger.error(`Plugin request failed for ${activePlugin.id}:`, error);
|
|
@@ -411,27 +797,32 @@ class PluginService {
|
|
|
411
797
|
* Returns an async generator that yields SSE delta content strings.
|
|
412
798
|
* Also collects tool_calls from the stream.
|
|
413
799
|
*/
|
|
414
|
-
async *executePluginStreamRequest(model, messages, options = {}, userId) {
|
|
800
|
+
async *executePluginStreamRequest(model, messages, options = {}, userId, pluginId) {
|
|
415
801
|
validatePluginModel(model);
|
|
416
|
-
const activePlugin = this.getActivePluginForModel(model, userId);
|
|
802
|
+
const activePlugin = this.getActivePluginForModel(model, userId, pluginId);
|
|
417
803
|
if (!activePlugin) {
|
|
418
804
|
throw new Error(`No active plugin found for model: ${model}`);
|
|
419
805
|
}
|
|
420
806
|
if (!activePlugin.model_map.includes(model)) {
|
|
421
807
|
throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
|
|
422
808
|
}
|
|
809
|
+
const pluginVars = this.getPluginVariables(activePlugin, userId);
|
|
810
|
+
const { apiMode, endpoint: effectiveEndpoint } = resolvePluginApiConfig(activePlugin, pluginVars);
|
|
811
|
+
const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
|
|
812
|
+
assertSafePluginEndpoint(processedEndpoint);
|
|
423
813
|
const apiKey = this.getApiKey(activePlugin, userId);
|
|
424
|
-
if (!apiKey) {
|
|
425
|
-
throw new Error(`API key not found for plugin ${activePlugin.id} (
|
|
814
|
+
if (pluginRequiresApiKey(activePlugin) && !apiKey) {
|
|
815
|
+
throw new Error(`API key not found for plugin ${activePlugin.id} (save a provider credential in Settings)`);
|
|
426
816
|
}
|
|
427
|
-
const
|
|
428
|
-
|
|
817
|
+
const providerStateScope = apiMode === 'responses'
|
|
818
|
+
? createOpenAIResponsesStateScope(activePlugin.id, model, processedEndpoint, createPluginCredentialFingerprint(apiKey))
|
|
819
|
+
: undefined;
|
|
429
820
|
const headers = buildPluginAuthHeaders(activePlugin, apiKey);
|
|
430
821
|
let payload;
|
|
431
|
-
if (activePlugin.id === 'anthropic') {
|
|
432
|
-
const
|
|
433
|
-
payload =
|
|
434
|
-
Object.assign(headers,
|
|
822
|
+
if (activePlugin.id === 'anthropic' || apiMode === 'responses') {
|
|
823
|
+
const pluginRequest = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, true, apiMode, providerStateScope);
|
|
824
|
+
payload = pluginRequest.payload;
|
|
825
|
+
Object.assign(headers, pluginRequest.headers);
|
|
435
826
|
}
|
|
436
827
|
else {
|
|
437
828
|
const params = resolvePluginChatParameters(options, pluginVars);
|
|
@@ -444,54 +835,152 @@ class PluginService {
|
|
|
444
835
|
stream: true,
|
|
445
836
|
};
|
|
446
837
|
}
|
|
447
|
-
const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
|
|
448
|
-
assertSafePluginEndpoint(processedEndpoint);
|
|
449
838
|
const response = await fetch(processedEndpoint, {
|
|
450
839
|
method: 'POST',
|
|
451
840
|
headers,
|
|
452
841
|
body: JSON.stringify(payload),
|
|
842
|
+
redirect: 'error',
|
|
453
843
|
});
|
|
454
844
|
if (activePlugin.id === 'anthropic') {
|
|
455
845
|
yield* streamAnthropicResponse(response);
|
|
456
846
|
}
|
|
847
|
+
else if (apiMode === 'responses') {
|
|
848
|
+
const contentType = response.headers.get('content-type') || '';
|
|
849
|
+
if (!contentType.includes('text/event-stream')) {
|
|
850
|
+
if (!response.ok) {
|
|
851
|
+
const errorText = await response.text();
|
|
852
|
+
throw new Error(`Plugin API error: ${response.status} - ${errorText.slice(0, 200)}`);
|
|
853
|
+
}
|
|
854
|
+
const normalized = convertProviderResponse(activePlugin, (await response.json()), model, apiMode, providerStateScope);
|
|
855
|
+
const choice = normalized.choices[0];
|
|
856
|
+
const message = choice?.message;
|
|
857
|
+
if (typeof message?.content === 'string' && message.content) {
|
|
858
|
+
yield { type: 'content', content: message.content };
|
|
859
|
+
}
|
|
860
|
+
if (typeof message?.reasoning_content === 'string' &&
|
|
861
|
+
message.reasoning_content) {
|
|
862
|
+
yield {
|
|
863
|
+
type: 'reasoning',
|
|
864
|
+
content: message.reasoning_content,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
for (const call of message?.tool_calls || []) {
|
|
868
|
+
yield {
|
|
869
|
+
type: 'tool_call',
|
|
870
|
+
toolCall: {
|
|
871
|
+
id: call.id,
|
|
872
|
+
name: call.function.name,
|
|
873
|
+
arguments: call.function.arguments,
|
|
874
|
+
...(call.providerMetadata
|
|
875
|
+
? { providerMetadata: call.providerMetadata }
|
|
876
|
+
: {}),
|
|
877
|
+
},
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
if (normalized.usage) {
|
|
881
|
+
yield {
|
|
882
|
+
type: 'usage',
|
|
883
|
+
usage: {
|
|
884
|
+
promptTokens: normalized.usage.prompt_tokens,
|
|
885
|
+
completionTokens: normalized.usage.completion_tokens,
|
|
886
|
+
totalTokens: normalized.usage.total_tokens,
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const incompleteReason = typeof normalized.providerMetadata?.[OPENAI_RESPONSES_INCOMPLETE_REASON_METADATA_KEY] === 'string'
|
|
891
|
+
? normalized.providerMetadata[OPENAI_RESPONSES_INCOMPLETE_REASON_METADATA_KEY]
|
|
892
|
+
: undefined;
|
|
893
|
+
yield {
|
|
894
|
+
type: 'done',
|
|
895
|
+
...(incompleteReason
|
|
896
|
+
? { doneReason: `incomplete:${incompleteReason}` }
|
|
897
|
+
: {}),
|
|
898
|
+
...(normalized.providerMetadata
|
|
899
|
+
? { providerMetadata: normalized.providerMetadata }
|
|
900
|
+
: {}),
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
yield* streamOpenAIResponsesResponse(response, providerStateScope);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
457
907
|
else {
|
|
458
908
|
yield* streamOpenAICompatibleResponse(response);
|
|
459
909
|
}
|
|
460
910
|
}
|
|
461
911
|
// Validate plugin structure
|
|
462
912
|
validatePlugin(plugin) {
|
|
463
|
-
|
|
464
|
-
plugin
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
913
|
+
const pluginRecord = typeof plugin === 'object' && plugin !== null
|
|
914
|
+
? plugin
|
|
915
|
+
: null;
|
|
916
|
+
const variables = pluginRecord?.variables;
|
|
917
|
+
const variableNames = variables === undefined
|
|
918
|
+
? []
|
|
919
|
+
: Array.isArray(variables)
|
|
920
|
+
? variables.map(variable => typeof variable === 'object' &&
|
|
921
|
+
variable !== null &&
|
|
922
|
+
typeof variable.name === 'string'
|
|
923
|
+
? variable.name
|
|
924
|
+
: null)
|
|
925
|
+
: [null];
|
|
926
|
+
const hasUniqueVariableNames = !variableNames.includes(null) &&
|
|
927
|
+
new Set(variableNames).size === variableNames.length;
|
|
928
|
+
return (pluginRecord !== null &&
|
|
929
|
+
hasUniqueVariableNames &&
|
|
930
|
+
typeof pluginRecord.id === 'string' &&
|
|
931
|
+
typeof pluginRecord.name === 'string' &&
|
|
932
|
+
typeof pluginRecord.type === 'string' &&
|
|
933
|
+
typeof pluginRecord.endpoint === 'string' &&
|
|
934
|
+
(pluginRecord.api_mode === undefined ||
|
|
935
|
+
pluginRecord.api_mode === 'chat_completions' ||
|
|
936
|
+
pluginRecord.api_mode === 'responses') &&
|
|
937
|
+
(pluginRecord.base_url === undefined ||
|
|
938
|
+
typeof pluginRecord.base_url === 'string') &&
|
|
939
|
+
(pluginRecord.api_path === undefined ||
|
|
940
|
+
typeof pluginRecord.api_path === 'string') &&
|
|
941
|
+
typeof pluginRecord.auth === 'object' &&
|
|
942
|
+
pluginRecord.auth !== null &&
|
|
943
|
+
typeof pluginRecord.auth.header ===
|
|
944
|
+
'string' &&
|
|
945
|
+
typeof pluginRecord.auth.key_env ===
|
|
946
|
+
'string' &&
|
|
947
|
+
(pluginRecord.auth.prefix === undefined ||
|
|
948
|
+
typeof pluginRecord.auth.prefix ===
|
|
949
|
+
'string') &&
|
|
950
|
+
Array.isArray(pluginRecord.model_map) &&
|
|
951
|
+
pluginRecord.model_map.length > 0);
|
|
478
952
|
}
|
|
479
953
|
// Export plugin to JSON
|
|
480
|
-
exportPlugin(id) {
|
|
481
|
-
return this.getPlugin(id);
|
|
954
|
+
exportPlugin(id, userId) {
|
|
955
|
+
return this.getPlugin(id, userId);
|
|
482
956
|
}
|
|
483
957
|
// Import plugin from JSON data
|
|
484
|
-
importPlugin(pluginData) {
|
|
958
|
+
importPlugin(pluginData, approvedByUserId) {
|
|
485
959
|
// Validate and clean the plugin data
|
|
486
960
|
if (!this.validatePlugin(pluginData)) {
|
|
487
961
|
throw new Error('Invalid plugin data');
|
|
488
962
|
}
|
|
489
963
|
// Check if plugin already exists
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
|
|
964
|
+
const existingPluginPath = this.resolveEffectivePluginFilePath(pluginData.id);
|
|
965
|
+
if (existingPluginPath) {
|
|
966
|
+
try {
|
|
967
|
+
const existingPlugin = JSON.parse(fs.readFileSync(existingPluginPath, 'utf8'));
|
|
968
|
+
if (this.validatePlugin(existingPlugin) &&
|
|
969
|
+
existingPlugin.id === pluginData.id &&
|
|
970
|
+
this.isPluginDefinitionApproved(existingPlugin, existingPluginPath)) {
|
|
971
|
+
throw new Error(`Plugin with ID ${pluginData.id} already exists`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
if (error instanceof Error &&
|
|
976
|
+
error.message.includes('already exists')) {
|
|
977
|
+
throw error;
|
|
978
|
+
}
|
|
979
|
+
// Invalid and unapproved legacy definitions may be replaced by this
|
|
980
|
+
// authenticated administrator import.
|
|
981
|
+
}
|
|
493
982
|
}
|
|
494
|
-
return this.installPlugin(pluginData);
|
|
983
|
+
return this.installPlugin(pluginData, approvedByUserId);
|
|
495
984
|
}
|
|
496
985
|
// ============================================
|
|
497
986
|
// Capability Methods
|
|
@@ -514,20 +1003,20 @@ class PluginService {
|
|
|
514
1003
|
async executeTTSRequest(model, input, options = {}) {
|
|
515
1004
|
return this.ttsService.executeTTSRequest(model, input, options);
|
|
516
1005
|
}
|
|
517
|
-
getTTSConfig(pluginId) {
|
|
518
|
-
return this.ttsService.getTTSConfig(pluginId);
|
|
1006
|
+
getTTSConfig(pluginId, userId) {
|
|
1007
|
+
return this.ttsService.getTTSConfig(pluginId, userId);
|
|
519
1008
|
}
|
|
520
|
-
getPluginForImageGen(model) {
|
|
521
|
-
return this.imageGenerationService.getPluginForImageGen(model);
|
|
1009
|
+
getPluginForImageGen(model, pluginId, userId) {
|
|
1010
|
+
return this.imageGenerationService.getPluginForImageGen(model, pluginId, userId);
|
|
522
1011
|
}
|
|
523
|
-
getAvailableImageGenModels() {
|
|
524
|
-
return this.imageGenerationService.getAvailableImageGenModels();
|
|
1012
|
+
getAvailableImageGenModels(userId) {
|
|
1013
|
+
return this.imageGenerationService.getAvailableImageGenModels(userId);
|
|
525
1014
|
}
|
|
526
|
-
async executeImageGenRequest(model, prompt, options
|
|
1015
|
+
async executeImageGenRequest(model, prompt, options) {
|
|
527
1016
|
return this.imageGenerationService.executeImageGenRequest(model, prompt, options);
|
|
528
1017
|
}
|
|
529
|
-
getImageGenConfig(pluginId) {
|
|
530
|
-
return this.imageGenerationService.getImageGenConfig(pluginId);
|
|
1018
|
+
getImageGenConfig(pluginId, userId) {
|
|
1019
|
+
return this.imageGenerationService.getImageGenConfig(pluginId, userId);
|
|
531
1020
|
}
|
|
532
1021
|
getPluginsByCapability(capabilityType, userId) {
|
|
533
1022
|
return this.capabilityRegistryService.getPluginsByCapability(capabilityType, userId);
|