From 9f8ee8c6bcb6bd3e2a4fbead4380ddd2cabc7754 Mon Sep 17 00:00:00 2001 From: Jeff MAURY Date: Thu, 4 Jun 2026 16:00:28 +0200 Subject: [PATCH] feat: create secret information based on new configuration Fixes #2066 Signed-off-by: Jeff MAURY --- .../manager/cursor-inference-manager.spec.ts | 14 +- .../src/manager/cursor-inference-manager.ts | 10 +- .../manager/mistral-inference-manager.spec.ts | 14 +- .../src/manager/mistral-inference-manager.ts | 8 +- .../openai-compatible/src/openAI.spec.ts | 16 +- extensions/openai-compatible/src/openAI.ts | 12 +- .../agent-workspace-manager.spec.ts | 201 +++++++++++++++++- .../agent-workspace-manager.ts | 61 ++++++ packages/main/src/plugin/provider-registry.ts | 21 ++ 9 files changed, 319 insertions(+), 38 deletions(-) diff --git a/extensions/cursor/src/manager/cursor-inference-manager.spec.ts b/extensions/cursor/src/manager/cursor-inference-manager.spec.ts index a074c0c27d..f8dc4c3b23 100644 --- a/extensions/cursor/src/manager/cursor-inference-manager.spec.ts +++ b/extensions/cursor/src/manager/cursor-inference-manager.spec.ts @@ -240,8 +240,8 @@ describe('connection delete lifecycle', () => { expect(SECRET_STORAGE_MOCK.delete).toHaveBeenCalledWith(`${PROVIDER_ID}:fake-uuid-1:token`); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', undefined); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', undefined); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection._type', undefined); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection.token', undefined); expect(disposeMock).toHaveBeenCalledOnce(); }); @@ -270,8 +270,8 @@ describe('workspace configuration', () => { const connection = vi.mocked(PROVIDER_MOCK.registerInferenceProviderConnection).mock.calls[0][0]; expect(configuration.getConfiguration).toHaveBeenCalledWith(undefined, connection); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', PROVIDER_ID); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:fake-uuid-1:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection._type', PROVIDER_ID); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection.token', `${PROVIDER_ID}:fake-uuid-1:token`); }); test('should set workspace configuration for each restored connection', async () => { @@ -287,9 +287,9 @@ describe('workspace configuration', () => { expect(SECRET_STORAGE_MOCK.store).toHaveBeenCalledWith(`${PROVIDER_ID}:id-1:token`, 'key1'); expect(SECRET_STORAGE_MOCK.store).toHaveBeenCalledWith(`${PROVIDER_ID}:id-2:token`, 'key2'); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', PROVIDER_ID); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:id-1:token`); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:id-2:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection._type', PROVIDER_ID); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection.token', `${PROVIDER_ID}:id-1:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('cursor.connection.token', `${PROVIDER_ID}:id-2:token`); }); }); diff --git a/extensions/cursor/src/manager/cursor-inference-manager.ts b/extensions/cursor/src/manager/cursor-inference-manager.ts index bec37f7048..b5ba7f9254 100644 --- a/extensions/cursor/src/manager/cursor-inference-manager.ts +++ b/extensions/cursor/src/manager/cursor-inference-manager.ts @@ -115,17 +115,17 @@ export class CursorInferenceManager { await this.secrets.store(secretName, token); const config = configuration.getConfiguration(undefined, connection); - await config.update('_type', PROVIDER_ID); - await config.update('token', secretName); + await config.update('cursor.connection._type', PROVIDER_ID); + await config.update('cursor.connection.token', secretName); } private async clearConnectionConfiguration(connection: InferenceProviderConnection): Promise { const secretName = this.getSecretName(connection.id); await this.secrets.delete(secretName); - const config = configuration.getConfiguration('cursor.connection', connection); - await config.update('_type', undefined); - await config.update('token', undefined); + const config = configuration.getConfiguration(undefined, connection); + await config.update('cursor.connection._type', undefined); + await config.update('cursor.connection.token', undefined); } private async registerInferenceProviderConnection({ id, token }: { id: string; token: string }): Promise { diff --git a/extensions/mistral/src/manager/mistral-inference-manager.spec.ts b/extensions/mistral/src/manager/mistral-inference-manager.spec.ts index 566b41ae8d..bd74bc9546 100644 --- a/extensions/mistral/src/manager/mistral-inference-manager.spec.ts +++ b/extensions/mistral/src/manager/mistral-inference-manager.spec.ts @@ -275,8 +275,8 @@ describe('connection delete lifecycle', () => { expect(SECRET_STORAGE_MOCK.delete).toHaveBeenCalledWith(`${PROVIDER_ID}:fake-uuid-1:token`); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', undefined); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', undefined); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection._type', undefined); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection.token', undefined); expect(disposeMock).toHaveBeenCalledOnce(); }); @@ -305,8 +305,8 @@ describe('workspace configuration', () => { const connection = vi.mocked(PROVIDER_MOCK.registerInferenceProviderConnection).mock.calls[0][0]; expect(configuration.getConfiguration).toHaveBeenCalledWith(undefined, connection); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', PROVIDER_ID); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:fake-uuid-1:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection._type', PROVIDER_ID); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection.token', `${PROVIDER_ID}:fake-uuid-1:token`); }); test('should set workspace configuration for each restored connection', async () => { @@ -322,9 +322,9 @@ describe('workspace configuration', () => { expect(SECRET_STORAGE_MOCK.store).toHaveBeenCalledWith(`${PROVIDER_ID}:id-1:token`, 'key1'); expect(SECRET_STORAGE_MOCK.store).toHaveBeenCalledWith(`${PROVIDER_ID}:id-2:token`, 'key2'); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', PROVIDER_ID); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:id-1:token`); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:id-2:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection._type', PROVIDER_ID); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection.token', `${PROVIDER_ID}:id-1:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('mistral.connection.token', `${PROVIDER_ID}:id-2:token`); }); }); diff --git a/extensions/mistral/src/manager/mistral-inference-manager.ts b/extensions/mistral/src/manager/mistral-inference-manager.ts index ac2b49be30..f27548b217 100644 --- a/extensions/mistral/src/manager/mistral-inference-manager.ts +++ b/extensions/mistral/src/manager/mistral-inference-manager.ts @@ -106,8 +106,8 @@ export class MistralInferenceManager { await this.secrets.store(secretName, token); const config = configuration.getConfiguration(undefined, connection); - await config.update('_type', PROVIDER_ID); - await config.update('token', secretName); + await config.update('mistral.connection._type', PROVIDER_ID); + await config.update('mistral.connection.token', secretName); } private async clearConnectionConfiguration(connection: InferenceProviderConnection): Promise { @@ -115,8 +115,8 @@ export class MistralInferenceManager { await this.secrets.delete(secretName); const config = configuration.getConfiguration('mistral.connection', connection); - await config.update('_type', undefined); - await config.update('token', undefined); + await config.update('mistral.connection._type', undefined); + await config.update('mistral.connection.token', undefined); } private async registerInferenceProviderConnection({ id, token }: { id: string; token: string }): Promise { diff --git a/extensions/openai-compatible/src/openAI.spec.ts b/extensions/openai-compatible/src/openAI.spec.ts index fbfe743327..9adfc08866 100644 --- a/extensions/openai-compatible/src/openAI.spec.ts +++ b/extensions/openai-compatible/src/openAI.spec.ts @@ -247,8 +247,8 @@ describe('connection delete lifecycle', () => { expect(SECRET_STORAGE_MOCK.delete).toHaveBeenCalledWith(`${PROVIDER_ID}:fake-uuid-1:token`); // configuration values should be cleared - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', undefined); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', undefined); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection._type', undefined); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection.token', undefined); // provider inference connection should be disposed expect(disposeMock).toHaveBeenCalledOnce(); @@ -279,11 +279,11 @@ describe('workspace configuration', () => { // configuration should be scoped to the connection object const connection = vi.mocked(PROVIDER_MOCK.registerInferenceProviderConnection).mock.calls[0][0]; - expect(CONFIGURATION_API_MOCK.getConfiguration).toHaveBeenCalledWith('openai.connection', connection); + expect(CONFIGURATION_API_MOCK.getConfiguration).toHaveBeenCalledWith(undefined, connection); // _type and token should be set - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', PROVIDER_ID); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:fake-uuid-1:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection._type', PROVIDER_ID); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection.token', `${PROVIDER_ID}:fake-uuid-1:token`); }); test('should set workspace configuration for each restored connection', async () => { @@ -301,9 +301,9 @@ describe('workspace configuration', () => { expect(SECRET_STORAGE_MOCK.store).toHaveBeenCalledWith(`${PROVIDER_ID}:id-2:token`, 'key2'); // configuration should be set for each connection - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('_type', PROVIDER_ID); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:id-1:token`); - expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('token', `${PROVIDER_ID}:id-2:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection._type', PROVIDER_ID); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection.token', `${PROVIDER_ID}:id-1:token`); + expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('openai.connection.token', `${PROVIDER_ID}:id-2:token`); }); }); diff --git a/extensions/openai-compatible/src/openAI.ts b/extensions/openai-compatible/src/openAI.ts index 2d5e7718b0..69637b3c82 100644 --- a/extensions/openai-compatible/src/openAI.ts +++ b/extensions/openai-compatible/src/openAI.ts @@ -139,18 +139,18 @@ export class OpenAI implements Disposable { const secretName = this.getSecretName(connection.id); await this.secrets.store(secretName, token); - const config = this.configurationAPI.getConfiguration('openai.connection', connection); - await config.update('_type', PROVIDER_ID); - await config.update('token', secretName); + const config = this.configurationAPI.getConfiguration(undefined, connection); + await config.update('openai.connection._type', PROVIDER_ID); + await config.update('openai.connection.token', secretName); } private async clearConnectionConfiguration(connection: InferenceProviderConnection): Promise { const secretName = this.getSecretName(connection.id); await this.secrets.delete(secretName); - const config = this.configurationAPI.getConfiguration('openai.connection', connection); - await config.update('_type', undefined); - await config.update('token', undefined); + const config = this.configurationAPI.getConfiguration(undefined, connection); + await config.update('openai.connection._type', undefined); + await config.update('openai.connection.token', undefined); } private async registerInferenceProviderConnection({ diff --git a/packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts b/packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts index 0c775cd65d..27e9891353 100644 --- a/packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts +++ b/packages/main/src/plugin/agent-workspace/agent-workspace-manager.spec.ts @@ -19,7 +19,7 @@ import { access, readFile, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import type { FileSystemWatcher } from '@openkaiden/api'; +import type { FileSystemWatcher, InferenceProviderConnection } from '@openkaiden/api'; import type { WebContents } from 'electron'; import type { IPty } from 'node-pty'; import { spawn } from 'node-pty'; @@ -31,6 +31,7 @@ import type { FilesystemMonitoring } from '/@/plugin/filesystem-monitoring.js'; import { KdnCli } from '/@/plugin/kdn-cli/kdn-cli.js'; import { OpenshellCli } from '/@/plugin/openshell-cli/openshell-cli.js'; import type { ProviderRegistry } from '/@/plugin/provider-registry.js'; +import type { SafeStorageRegistry, SecretStorageWrapper } from '/@/plugin/safe-storage/safe-storage-registry.js'; import type { SecretManager } from '/@/plugin/secret-manager/secret-manager.js'; import type { TaskManager } from '/@/plugin/tasks/task-manager.js'; import type { Task } from '/@/plugin/tasks/tasks.js'; @@ -121,10 +122,12 @@ const configurationRegistry = { getConfiguration: vi.fn().mockReturnValue({ get: vi.fn().mockReturnValue(undefined), }), + getConfigurationProperties: vi.fn().mockReturnValue({}), } as unknown as IConfigurationRegistry; const providerRegistry = { getInferenceConnectionCredentials: vi.fn(), + getInferenceConnection: vi.fn(), } as unknown as ProviderRegistry; const secretManager = { @@ -132,6 +135,17 @@ const secretManager = { init: vi.fn(), } as unknown as SecretManager; +const extensionStorageMock = { + get: vi.fn(), + store: vi.fn(), + delete: vi.fn(), + onDidChange: vi.fn(), +} as unknown as SecretStorageWrapper; + +const safeStorageRegistry = { + getExtensionStorage: vi.fn().mockReturnValue(extensionStorageMock), +} as unknown as SafeStorageRegistry; + beforeEach(() => { vi.resetAllMocks(); vi.mocked(taskManager.createTask).mockReturnValue(mockTask); @@ -142,6 +156,8 @@ beforeEach(() => { vi.mocked(configurationRegistry.getConfiguration).mockReturnValue({ get: vi.fn().mockReturnValue(undefined), } as unknown as ReturnType); + vi.mocked(configurationRegistry.getConfigurationProperties).mockReturnValue({}); + vi.mocked(safeStorageRegistry.getExtensionStorage).mockReturnValue(extensionStorageMock); manager = new AgentWorkspaceManager( apiSender, ipcHandle, @@ -153,6 +169,7 @@ beforeEach(() => { providerRegistry, secretManager, openshellCli, + safeStorageRegistry, ); manager.init(); }); @@ -720,6 +737,188 @@ describe('ensureModelSecret', () => { expect(options.secrets).toEqual(['my-workspace-anthropic']); }); + + test('creates secret from connection config when _type and password property exist', async () => { + const mockConnection = { id: 'conn-1', sdk: {}, models: [] } as unknown as InferenceProviderConnection; + vi.mocked(providerRegistry.getInferenceConnection).mockReturnValue({ + connection: mockConnection, + extensionId: 'kaiden.cursor', + }); + vi.mocked(configurationRegistry.getConfiguration).mockReturnValue({ + get: vi.fn().mockImplementation((key: string) => { + if (key === 'cursor.connection._type') return 'cursor'; + if (key === 'cursor.connection.token') return 'cursor:conn-1:token'; + return undefined; + }), + } as unknown as ReturnType); + vi.mocked(configurationRegistry.getConfigurationProperties).mockReturnValue({ + 'cursor.connection._type': { + title: 'Cursor', + parentId: 'cursor', + scope: 'InferenceProviderConnection', + hidden: true, + extension: { + id: 'kaiden.cursor', + }, + }, + 'cursor.connection.token': { + title: 'Cursor', + parentId: 'cursor', + scope: 'InferenceProviderConnection', + format: 'password', + hidden: true, + extension: { + id: 'kaiden.cursor', + }, + }, + }); + vi.mocked(extensionStorageMock.get).mockResolvedValue('actual-api-key'); + vi.mocked(secretManager.create).mockResolvedValue({ name: 'my-workspace-token' }); + + const options = { ...baseOptions, model: 'cursor::gpt-4o::https://api.cursor.com' }; + await manager.ensureModelSecret(options); + + expect(safeStorageRegistry.getExtensionStorage).toHaveBeenCalledWith('kaiden.cursor'); + expect(extensionStorageMock.get).toHaveBeenCalledWith('cursor:conn-1:token'); + expect(secretManager.create).toHaveBeenCalledWith({ + name: 'my-workspace-cursor-token', + type: 'cursor', + value: 'actual-api-key', + }); + expect(options.secrets).toContain('my-workspace-cursor-token'); + expect(providerRegistry.getInferenceConnectionCredentials).not.toHaveBeenCalled(); + }); + + test('falls back to legacy path when getInferenceConnection returns undefined', async () => { + vi.mocked(providerRegistry.getInferenceConnection).mockReturnValue(undefined); + vi.mocked(providerRegistry.getInferenceConnectionCredentials).mockReturnValue({ + credentials: { 'claude:tokens': 'sk-ant-secret' }, + llmMetadataName: 'anthropic', + endpoint: undefined, + }); + vi.mocked(secretManager.create).mockResolvedValue({ name: 'my-workspace-anthropic' }); + + const options = { ...baseOptions, model: 'anthropic::claude-sonnet-4-20250514::' }; + await manager.ensureModelSecret(options); + + expect(secretManager.create).toHaveBeenCalledWith( + expect.objectContaining({ name: 'my-workspace-anthropic', type: 'anthropic' }), + ); + }); + + test('falls back to legacy path when config has no _type', async () => { + const mockConnection = { id: 'conn-1', sdk: {}, models: [] } as unknown as InferenceProviderConnection; + vi.mocked(providerRegistry.getInferenceConnection).mockReturnValue({ + connection: mockConnection, + extensionId: 'kaiden.claude', + }); + vi.mocked(configurationRegistry.getConfiguration).mockReturnValue({ + get: vi.fn().mockReturnValue(undefined), + } as unknown as ReturnType); + vi.mocked(providerRegistry.getInferenceConnectionCredentials).mockReturnValue({ + credentials: { 'claude:tokens': 'sk-ant-secret' }, + llmMetadataName: 'anthropic', + endpoint: undefined, + }); + vi.mocked(secretManager.create).mockResolvedValue({ name: 'my-workspace-anthropic' }); + + const options = { ...baseOptions, model: 'anthropic::claude-sonnet-4-20250514::' }; + await manager.ensureModelSecret(options); + + expect(secretManager.create).toHaveBeenCalledWith( + expect.objectContaining({ name: 'my-workspace-anthropic', type: 'anthropic' }), + ); + }); + + test('does not fall back when _type exists but secret value is not found', async () => { + const mockConnection = { id: 'conn-1', sdk: {}, models: [] } as unknown as InferenceProviderConnection; + vi.mocked(providerRegistry.getInferenceConnection).mockReturnValue({ + connection: mockConnection, + extensionId: 'kaiden.cursor', + }); + vi.mocked(configurationRegistry.getConfiguration).mockReturnValue({ + get: vi.fn().mockImplementation((key: string) => { + if (key === 'cursor.connection._type') return 'cursor'; + if (key === 'cursor.connection.token') return 'cursor:conn-1:token'; + return undefined; + }), + } as unknown as ReturnType); + vi.mocked(configurationRegistry.getConfigurationProperties).mockReturnValue({ + 'cursor.connection._type': { + title: 'Cursor', + parentId: 'cursor', + scope: 'InferenceProviderConnection', + hidden: true, + extension: { + id: 'kaiden.cursor', + }, + }, + 'cursor.connection.token': { + title: 'Cursor', + parentId: 'cursor', + scope: 'InferenceProviderConnection', + format: 'password', + hidden: true, + extension: { + id: 'kaiden.cursor', + }, + }, + }); + vi.mocked(extensionStorageMock.get).mockResolvedValue(undefined); + + const options = { ...baseOptions, model: 'cursor::gpt-4o::' }; + await manager.ensureModelSecret(options); + + expect(secretManager.create).not.toHaveBeenCalled(); + expect(providerRegistry.getInferenceConnectionCredentials).not.toHaveBeenCalled(); + }); + + test('derives secret name from sourcePath when name is omitted (config path)', async () => { + const mockConnection = { id: 'conn-1', sdk: {}, models: [] } as unknown as InferenceProviderConnection; + vi.mocked(providerRegistry.getInferenceConnection).mockReturnValue({ + connection: mockConnection, + extensionId: 'kaiden.mistral', + }); + vi.mocked(configurationRegistry.getConfiguration).mockReturnValue({ + get: vi.fn().mockImplementation((key: string) => { + if (key === 'mistral.connection._type') return 'mistral'; + if (key === 'mistral.connection.token') return 'mistral:conn-1:token'; + return undefined; + }), + } as unknown as ReturnType); + vi.mocked(configurationRegistry.getConfigurationProperties).mockReturnValue({ + 'mistral.connection._type': { + title: 'Mistral', + parentId: 'mistral', + scope: 'InferenceProviderConnection', + hidden: true, + extension: { + id: 'kaiden.mistral', + }, + }, + 'mistral.connection.token': { + title: 'Mistral', + parentId: 'mistral', + scope: 'InferenceProviderConnection', + format: 'password', + hidden: true, + extension: { + id: 'kaiden.mistral', + }, + }, + }); + vi.mocked(extensionStorageMock.get).mockResolvedValue('mistral-key'); + vi.mocked(secretManager.create).mockResolvedValue({ name: 'my-project-token' }); + + const options: AgentWorkspaceCreateOptions = { + sourcePath: '/tmp/my-project', + agent: 'coder', + model: 'mistral::mistral-large::', + }; + await manager.ensureModelSecret(options); + + expect(secretManager.create).toHaveBeenCalledWith(expect.objectContaining({ name: 'my-project-mistral-token' })); + }); }); describe('buildSecretOptions', () => { diff --git a/packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts b/packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts index a39103a634..661ebf8310 100644 --- a/packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts +++ b/packages/main/src/plugin/agent-workspace/agent-workspace-manager.ts @@ -31,6 +31,7 @@ import { FilesystemMonitoring } from '/@/plugin/filesystem-monitoring.js'; import { KdnCli } from '/@/plugin/kdn-cli/kdn-cli.js'; import { OpenshellCli } from '/@/plugin/openshell-cli/openshell-cli.js'; import { ProviderRegistry } from '/@/plugin/provider-registry.js'; +import { SafeStorageRegistry } from '/@/plugin/safe-storage/safe-storage-registry.js'; import { SecretManager } from '/@/plugin/secret-manager/secret-manager.js'; import { TaskManager } from '/@/plugin/tasks/task-manager.js'; import { AgentWorkspaceSettings } from '/@api/agent-workspace/agent-workspace-settings.js'; @@ -81,6 +82,8 @@ export class AgentWorkspaceManager implements Disposable { private readonly secretManager: SecretManager, @inject(OpenshellCli) private readonly openshellCli: OpenshellCli, + @inject(SafeStorageRegistry) + private readonly safeStorageRegistry: SafeStorageRegistry, ) {} async getCliInfo(): Promise { @@ -138,6 +141,12 @@ export class AgentWorkspaceManager implements Disposable { if (options.workspaceConfiguration?.secrets?.length) return; + try { + if (await this.ensureModelSecretFromConfig(options)) return; + } finally { + /* empty */ + } + const connectionInfo = this.providerRegistry.getInferenceConnectionCredentials(options.model); if (!connectionInfo) return; @@ -167,6 +176,58 @@ export class AgentWorkspaceManager implements Disposable { } } + private async ensureModelSecretFromConfig(options: AgentWorkspaceCreateOptions): Promise { + const info = this.providerRegistry.getInferenceConnection(options.model!); + if (!info) return false; + + const config = this.configurationRegistry.getConfiguration(undefined, info.connection); + const allProperties = this.configurationRegistry.getConfigurationProperties(); + + const connectionProperties = Object.entries(allProperties) + .filter(([, schema]) => { + const scope = schema.scope; + return Array.isArray(scope) + ? scope.includes('InferenceProviderConnection') + : scope === 'InferenceProviderConnection'; + }) + .filter(([_, schema]) => schema.extension?.id === info.extensionId); + + const typeEntry = connectionProperties.find(([fullKey]) => fullKey.endsWith('_type')); + if (!typeEntry) return false; + + const typeShortKey = typeEntry[0]; + const secretType = config.get(typeShortKey); + if (!secretType) return false; + + const workspaceName = options.name ?? basename(options.sourcePath); + + const passwordKeys = [ + ...new Set(connectionProperties.filter(([, schema]) => schema.format === 'password').map(([fullKey]) => fullKey)), + ]; + + const extensionStorage = this.safeStorageRegistry.getExtensionStorage(info.extensionId); + + for (const propertyName of passwordKeys) { + const secretRefName = config.get(propertyName); + if (!secretRefName) continue; + + const actualValue = await extensionStorage.get(secretRefName); + if (!actualValue) continue; + + const shortPropertyName = propertyName.split('.').pop()!; + const secretName = `${workspaceName}-${secretType}-${shortPropertyName}`; + await this.secretManager.create({ + name: secretName, + type: secretType, + value: actualValue, + }); + + options.secrets = [...new Set([...(options.secrets ?? []), secretName])]; + } + + return true; + } + private applyVertexAiConfiguration(options: AgentWorkspaceCreateOptions, credentials: Record): void { const { projectId, region, credentialsFile } = credentials; if (!projectId || !region || !credentialsFile) return; diff --git a/packages/main/src/plugin/provider-registry.ts b/packages/main/src/plugin/provider-registry.ts index ab00c6aa56..6321fdcbe9 100644 --- a/packages/main/src/plugin/provider-registry.ts +++ b/packages/main/src/plugin/provider-registry.ts @@ -2260,6 +2260,27 @@ export class ProviderRegistry { return undefined; } + getInferenceConnection( + modelId: string, + ): { connection: InferenceProviderConnection; extensionId: string } | undefined { + const [metadataName = '', modelLabel = '', endpoint = ''] = modelId.split('::'); + + for (const provider of this.providers.values()) { + for (const connection of provider.inferenceConnections) { + const connMetadataName = connection.llmMetadata?.name ?? ''; + const connEndpoint = connection.endpoint ?? ''; + if ( + connMetadataName === metadataName && + connEndpoint === endpoint && + connection.models.some(m => m.label === modelLabel) + ) { + return { connection, extensionId: provider.extensionId }; + } + } + } + return undefined; + } + getFlowProviderConnection(internalProviderId: string): Array { const provider = this.providers.get(internalProviderId); if (!provider) throw new Error('Provider not found');