libre-webui 0.15.1 → 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/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 +658 -170
- 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/workAgentService.d.ts +5 -2
- package/backend/dist/services/workAgentService.d.ts.map +1 -1
- package/backend/dist/services/workAgentService.js +315 -44
- package/backend/dist/services/workAgentService.js.map +1 -1
- package/backend/dist/services/workModelProviderService.d.ts +6 -3
- package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
- package/backend/dist/services/workModelProviderService.js +243 -19
- 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/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 +20 -2
- package/backend/dist/utils/pluginValidation.d.ts.map +1 -1
- package/backend/dist/utils/pluginValidation.js +187 -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 +3 -3
- package/frontend/dist/js/{ArtifactContainer-BHSBrlQB.js → ArtifactContainer-B0uVR8Z9.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-BHGJmJAT.js → ArtifactDemoPage-DBVPHFxk.js} +1 -1
- package/frontend/dist/js/{ArtifactSlideOutPanel-s8vcE5Ea.js → ArtifactSlideOutPanel-Cg5kAwv9.js} +1 -1
- package/frontend/dist/js/ChatPage-CPHlIZqU.js +16 -0
- package/frontend/dist/js/{FirstTimeSetup-BYWAVAdE.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/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-BdU5vXz9.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/{estree-Bu_-Wi6O.js → estree-PDLT672k.js} +1 -1
- package/frontend/dist/js/{html-Ca764Rus.js → html-CkhF9wDQ.js} +1 -1
- package/frontend/dist/js/{markdown-DaiQGPEs.js → markdown-B1iNUbse.js} +1 -1
- package/frontend/dist/js/{standalone-DWWnsbIr.js → standalone-CCd6rkf-.js} +1 -1
- package/frontend/dist/js/{typescript-CatjJeHm.js → typescript-DKJ1uM5L.js} +1 -1
- package/frontend/dist/js/useBaseQuery-MuQj7vA5.js +1 -0
- package/frontend/dist/js/useQuery-CYGPkxWR.js +1 -0
- package/package.json +2 -2
- package/plugins/github.json +1 -2
- package/plugins/huggingface.json +30 -30
- package/plugins/openai.json +53 -22
- package/frontend/dist/assets/index-DkShhNBf.js +0 -5
- package/frontend/dist/assets/index-bdSWBsAM.css +0 -1
- package/frontend/dist/js/ChatPage-B6Hm_pdu.js +0 -16
- package/frontend/dist/js/GalleryPage-CH4f65q8.js +0 -1
- package/frontend/dist/js/LibreClawPage-CcH7M3mJ.js +0 -1
- package/frontend/dist/js/ModelSelector-T1KZtIde.js +0 -1
- package/frontend/dist/js/ModelsPage-C1bM_Yoc.js +0 -2
- package/frontend/dist/js/PersonasPage-T_PrhnPG.js +0 -13
- package/frontend/dist/js/SettingsModal-BVj-D1L4.js +0 -1
- package/frontend/dist/js/UserManagementPage-Pix71oRW.js +0 -1
- package/frontend/dist/js/WorkPage-DdOVGyj_.js +0 -2
- package/frontend/dist/js/api-Do6u0tMM.js +0 -19
- package/frontend/dist/js/useBaseQuery-CVNuhuID.js +0 -1
- package/frontend/dist/js/useQuery-CYhC_da4.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, pluginRequiresApiKey, 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,38 +669,52 @@ 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
|
|
337
716
|
// leaving both auth fields empty.
|
|
717
|
+
this.resolveOperationEndpoint(plugin, userId);
|
|
338
718
|
const apiKey = this.getApiKey(plugin, userId);
|
|
339
719
|
if (pluginRequiresApiKey(plugin) && !apiKey) {
|
|
340
720
|
continue;
|
|
@@ -345,52 +725,57 @@ class PluginService {
|
|
|
345
725
|
return null;
|
|
346
726
|
}
|
|
347
727
|
// Get all currently active plugins
|
|
348
|
-
getActivePlugins() {
|
|
349
|
-
const allPlugins = this.getAllPlugins();
|
|
350
|
-
const activePlugins = allPlugins.filter(plugin =>
|
|
728
|
+
getActivePlugins(userId) {
|
|
729
|
+
const allPlugins = this.getAllPlugins(userId);
|
|
730
|
+
const activePlugins = allPlugins.filter(plugin => plugin.active);
|
|
351
731
|
return activePlugins;
|
|
352
732
|
}
|
|
353
733
|
// Legacy method for backward compatibility - returns first active plugin
|
|
354
|
-
getActivePlugin() {
|
|
355
|
-
const activePlugins = this.getActivePlugins();
|
|
734
|
+
getActivePlugin(userId) {
|
|
735
|
+
const activePlugins = this.getActivePlugins(userId);
|
|
356
736
|
return activePlugins.length > 0 ? activePlugins[0] : null;
|
|
357
737
|
}
|
|
358
738
|
// Get plugin status
|
|
359
|
-
getPluginStatus() {
|
|
360
|
-
const plugins = this.getAllPlugins();
|
|
739
|
+
getPluginStatus(userId) {
|
|
740
|
+
const plugins = this.getAllPlugins(userId);
|
|
361
741
|
return plugins.map(plugin => ({
|
|
362
742
|
id: plugin.id,
|
|
363
743
|
active: plugin.active || false,
|
|
364
|
-
available:
|
|
744
|
+
available: !pluginRequiresApiKey(plugin) ||
|
|
745
|
+
this.getApiKey(plugin, userId) !== null,
|
|
365
746
|
}));
|
|
366
747
|
}
|
|
367
748
|
// Execute a chat request through the active plugin
|
|
368
|
-
async executePluginRequest(model, messages, options = {}, userId) {
|
|
749
|
+
async executePluginRequest(model, messages, options = {}, userId, pluginId) {
|
|
369
750
|
validatePluginModel(model);
|
|
370
|
-
const activePlugin = this.getActivePluginForModel(model, userId);
|
|
751
|
+
const activePlugin = this.getActivePluginForModel(model, userId, pluginId);
|
|
371
752
|
if (!activePlugin) {
|
|
372
753
|
throw new Error(`No active plugin found for model: ${model}`);
|
|
373
754
|
}
|
|
374
755
|
if (!activePlugin.model_map.includes(model)) {
|
|
375
756
|
throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
|
|
376
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');
|
|
377
762
|
const apiKey = this.getApiKey(activePlugin, userId);
|
|
378
763
|
if (pluginRequiresApiKey(activePlugin) && !apiKey) {
|
|
379
|
-
throw new Error(`API key not found for plugin ${activePlugin.id} (
|
|
764
|
+
throw new Error(`API key not found for plugin ${activePlugin.id} (save a provider credential in Settings)`);
|
|
380
765
|
}
|
|
381
|
-
const
|
|
382
|
-
|
|
766
|
+
const providerStateScope = apiMode === 'responses'
|
|
767
|
+
? createOpenAIResponsesStateScope(activePlugin.id, model, processedEndpoint, createPluginCredentialFingerprint(apiKey))
|
|
768
|
+
: undefined;
|
|
383
769
|
const headers = buildPluginAuthHeaders(activePlugin, apiKey);
|
|
384
|
-
const { payload, headers: payloadHeaders } = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, false);
|
|
770
|
+
const { payload, headers: payloadHeaders } = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, false, apiMode, providerStateScope);
|
|
385
771
|
Object.assign(headers, payloadHeaders);
|
|
386
|
-
const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
|
|
387
|
-
assertSafePluginEndpoint(processedEndpoint, 'endpoint URL constructed');
|
|
388
772
|
try {
|
|
389
773
|
const response = await axios.post(processedEndpoint, payload, {
|
|
390
774
|
headers,
|
|
391
775
|
timeout: 60000, // 60 second timeout
|
|
776
|
+
maxRedirects: 0,
|
|
392
777
|
});
|
|
393
|
-
return convertProviderResponse(activePlugin, response.data, model);
|
|
778
|
+
return convertProviderResponse(activePlugin, response.data, model, apiMode, providerStateScope);
|
|
394
779
|
}
|
|
395
780
|
catch (error) {
|
|
396
781
|
logger.error(`Plugin request failed for ${activePlugin.id}:`, error);
|
|
@@ -412,27 +797,32 @@ class PluginService {
|
|
|
412
797
|
* Returns an async generator that yields SSE delta content strings.
|
|
413
798
|
* Also collects tool_calls from the stream.
|
|
414
799
|
*/
|
|
415
|
-
async *executePluginStreamRequest(model, messages, options = {}, userId) {
|
|
800
|
+
async *executePluginStreamRequest(model, messages, options = {}, userId, pluginId) {
|
|
416
801
|
validatePluginModel(model);
|
|
417
|
-
const activePlugin = this.getActivePluginForModel(model, userId);
|
|
802
|
+
const activePlugin = this.getActivePluginForModel(model, userId, pluginId);
|
|
418
803
|
if (!activePlugin) {
|
|
419
804
|
throw new Error(`No active plugin found for model: ${model}`);
|
|
420
805
|
}
|
|
421
806
|
if (!activePlugin.model_map.includes(model)) {
|
|
422
807
|
throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
|
|
423
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);
|
|
424
813
|
const apiKey = this.getApiKey(activePlugin, userId);
|
|
425
814
|
if (pluginRequiresApiKey(activePlugin) && !apiKey) {
|
|
426
|
-
throw new Error(`API key not found for plugin ${activePlugin.id} (
|
|
815
|
+
throw new Error(`API key not found for plugin ${activePlugin.id} (save a provider credential in Settings)`);
|
|
427
816
|
}
|
|
428
|
-
const
|
|
429
|
-
|
|
817
|
+
const providerStateScope = apiMode === 'responses'
|
|
818
|
+
? createOpenAIResponsesStateScope(activePlugin.id, model, processedEndpoint, createPluginCredentialFingerprint(apiKey))
|
|
819
|
+
: undefined;
|
|
430
820
|
const headers = buildPluginAuthHeaders(activePlugin, apiKey);
|
|
431
821
|
let payload;
|
|
432
|
-
if (activePlugin.id === 'anthropic') {
|
|
433
|
-
const
|
|
434
|
-
payload =
|
|
435
|
-
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);
|
|
436
826
|
}
|
|
437
827
|
else {
|
|
438
828
|
const params = resolvePluginChatParameters(options, pluginVars);
|
|
@@ -445,54 +835,152 @@ class PluginService {
|
|
|
445
835
|
stream: true,
|
|
446
836
|
};
|
|
447
837
|
}
|
|
448
|
-
const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
|
|
449
|
-
assertSafePluginEndpoint(processedEndpoint);
|
|
450
838
|
const response = await fetch(processedEndpoint, {
|
|
451
839
|
method: 'POST',
|
|
452
840
|
headers,
|
|
453
841
|
body: JSON.stringify(payload),
|
|
842
|
+
redirect: 'error',
|
|
454
843
|
});
|
|
455
844
|
if (activePlugin.id === 'anthropic') {
|
|
456
845
|
yield* streamAnthropicResponse(response);
|
|
457
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
|
+
}
|
|
458
907
|
else {
|
|
459
908
|
yield* streamOpenAICompatibleResponse(response);
|
|
460
909
|
}
|
|
461
910
|
}
|
|
462
911
|
// Validate plugin structure
|
|
463
912
|
validatePlugin(plugin) {
|
|
464
|
-
|
|
465
|
-
plugin
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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);
|
|
479
952
|
}
|
|
480
953
|
// Export plugin to JSON
|
|
481
|
-
exportPlugin(id) {
|
|
482
|
-
return this.getPlugin(id);
|
|
954
|
+
exportPlugin(id, userId) {
|
|
955
|
+
return this.getPlugin(id, userId);
|
|
483
956
|
}
|
|
484
957
|
// Import plugin from JSON data
|
|
485
|
-
importPlugin(pluginData) {
|
|
958
|
+
importPlugin(pluginData, approvedByUserId) {
|
|
486
959
|
// Validate and clean the plugin data
|
|
487
960
|
if (!this.validatePlugin(pluginData)) {
|
|
488
961
|
throw new Error('Invalid plugin data');
|
|
489
962
|
}
|
|
490
963
|
// Check if plugin already exists
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
493
|
-
|
|
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
|
+
}
|
|
494
982
|
}
|
|
495
|
-
return this.installPlugin(pluginData);
|
|
983
|
+
return this.installPlugin(pluginData, approvedByUserId);
|
|
496
984
|
}
|
|
497
985
|
// ============================================
|
|
498
986
|
// Capability Methods
|
|
@@ -515,20 +1003,20 @@ class PluginService {
|
|
|
515
1003
|
async executeTTSRequest(model, input, options = {}) {
|
|
516
1004
|
return this.ttsService.executeTTSRequest(model, input, options);
|
|
517
1005
|
}
|
|
518
|
-
getTTSConfig(pluginId) {
|
|
519
|
-
return this.ttsService.getTTSConfig(pluginId);
|
|
1006
|
+
getTTSConfig(pluginId, userId) {
|
|
1007
|
+
return this.ttsService.getTTSConfig(pluginId, userId);
|
|
520
1008
|
}
|
|
521
|
-
getPluginForImageGen(model) {
|
|
522
|
-
return this.imageGenerationService.getPluginForImageGen(model);
|
|
1009
|
+
getPluginForImageGen(model, pluginId, userId) {
|
|
1010
|
+
return this.imageGenerationService.getPluginForImageGen(model, pluginId, userId);
|
|
523
1011
|
}
|
|
524
|
-
getAvailableImageGenModels() {
|
|
525
|
-
return this.imageGenerationService.getAvailableImageGenModels();
|
|
1012
|
+
getAvailableImageGenModels(userId) {
|
|
1013
|
+
return this.imageGenerationService.getAvailableImageGenModels(userId);
|
|
526
1014
|
}
|
|
527
|
-
async executeImageGenRequest(model, prompt, options
|
|
1015
|
+
async executeImageGenRequest(model, prompt, options) {
|
|
528
1016
|
return this.imageGenerationService.executeImageGenRequest(model, prompt, options);
|
|
529
1017
|
}
|
|
530
|
-
getImageGenConfig(pluginId) {
|
|
531
|
-
return this.imageGenerationService.getImageGenConfig(pluginId);
|
|
1018
|
+
getImageGenConfig(pluginId, userId) {
|
|
1019
|
+
return this.imageGenerationService.getImageGenConfig(pluginId, userId);
|
|
532
1020
|
}
|
|
533
1021
|
getPluginsByCapability(capabilityType, userId) {
|
|
534
1022
|
return this.capabilityRegistryService.getPluginsByCapability(capabilityType, userId);
|