feat: config manager using secret store

This commit is contained in:
sam hoang
2025-01-05 00:52:00 +07:00
committed by Matt Rubens
parent c30e9c6ed3
commit 352f34d8ce
10 changed files with 1026 additions and 96 deletions

View File

@@ -12,9 +12,9 @@ import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
import { WebviewMessage } from "../../shared/WebviewMessage"
import { fileExistsAtPath } from "../../utils/fs"
@@ -23,8 +23,10 @@ import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { enhancePrompt } from "../../utils/enhance-prompt"
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
import { ConfigManager } from "../config/ConfigManager"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -43,6 +45,7 @@ type SecretKey =
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "apiConfigPassword"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
@@ -85,6 +88,9 @@ type GlobalStateKey =
| "mcpEnabled"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "currentApiConfigName"
| "listApiConfigMeta"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
@@ -103,6 +109,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
private latestAnnouncementId = "dec-10-2024" // update to some unique identifier when we add a new announcement
configManager: ConfigManager
constructor(
readonly context: vscode.ExtensionContext,
@@ -112,6 +119,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
ClineProvider.activeInstances.add(this)
this.workspaceTracker = new WorkspaceTracker(this)
this.mcpHub = new McpHub(this)
this.configManager = new ConfigManager(this.context)
}
/*
@@ -235,7 +243,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled,
fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
@@ -255,7 +263,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled,
fuzzyMatchThreshold
} = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
@@ -321,15 +329,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
*/
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
@@ -410,6 +418,33 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
})
this.configManager.ListConfig().then(async (listApiConfig) => {
if (!listApiConfig) {
return
}
if (listApiConfig.length === 1) {
// check if first time init then sync with exist config
if (!checkExistKey(listApiConfig[0]) && listApiConfig[0].name === "default") {
const {
apiConfiguration,
} = await this.getState()
await this.configManager.SaveConfig("default", apiConfiguration)
listApiConfig[0].apiProvider = apiConfiguration.apiProvider
}
}
await Promise.all(
[
await this.updateGlobalState("listApiConfigMeta", listApiConfig),
await this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
]
)
}).catch(console.error);
break
case "newTask":
// Code that should run in response to the hello message command
@@ -424,70 +459,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "apiConfiguration":
if (message.apiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
glamaModelId,
glamaModelInfo,
glamaApiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
ollamaModelId,
ollamaBaseUrl,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
azureApiVersion,
openAiStreamingEnabled,
openRouterModelId,
openRouterModelInfo,
openRouterUseMiddleOutTransform,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
await this.storeSecret("apiKey", apiKey)
await this.updateGlobalState("glamaModelId", glamaModelId)
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
await this.storeSecret("glamaApiKey", glamaApiKey)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
await this.storeSecret("awsSessionToken", awsSessionToken)
await this.updateGlobalState("awsRegion", awsRegion)
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
await this.updateGlobalState("vertexProjectId", vertexProjectId)
await this.updateGlobalState("vertexRegion", vertexRegion)
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
await this.storeSecret("openAiApiKey", openAiApiKey)
await this.updateGlobalState("openAiModelId", openAiModelId)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", message.apiConfiguration.deepSeekApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
await this.updateApiConfiguration(message.apiConfiguration)
}
await this.postStateToWebview()
break
@@ -566,7 +538,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (message?.values?.baseUrl && message?.values?.apiKey) {
const openAiModels = await this.getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
}
}
break
case "openImage":
openImage(message.text!)
@@ -805,6 +777,106 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "upsertApiConfiguration":
if (message.text && message.apiConfiguration) {
try {
await this.configManager.SaveConfig(message.text, message.apiConfiguration);
let listApiConfig = await this.configManager.ListConfig();
await Promise.all([
this.updateGlobalState("currentApiConfigName", message.text),
this.updateGlobalState("listApiConfigMeta", listApiConfig),
])
this.postStateToWebview()
} catch (error) {
console.error("Error create new api configuration:", error)
vscode.window.showErrorMessage("Failed to create api configuration")
}
}
break
case "renameApiConfiguration":
if (message.values && message.apiConfiguration) {
try {
const {oldName, newName} = message.values
await this.configManager.SaveConfig(newName, message.apiConfiguration);
await this.configManager.DeleteConfig(oldName)
let listApiConfig = await this.configManager.ListConfig();
await Promise.all([
this.updateGlobalState("currentApiConfigName", newName),
this.updateGlobalState("listApiConfigMeta", listApiConfig),
])
this.postStateToWebview()
} catch (error) {
console.error("Error create new api configuration:", error)
vscode.window.showErrorMessage("Failed to create api configuration")
}
}
break
case "loadApiConfiguration":
if (message.text) {
try {
const apiConfig = await this.configManager.LoadConfig(message.text);
await Promise.all([
this.updateGlobalState("currentApiConfigName", message.text),
this.updateApiConfiguration(apiConfig),
])
await this.postStateToWebview()
} catch (error) {
console.error("Error load api configuration:", error)
vscode.window.showErrorMessage("Failed to load api configuration")
}
}
break
case "deleteApiConfiguration":
if (message.text) {
try {
await this.configManager.DeleteConfig(message.text);
let currentApiConfigName = (await this.getGlobalState("currentApiConfigName") as string) ?? "default"
if (message.text === currentApiConfigName) {
await this.updateGlobalState("currentApiConfigName", "default")
}
let listApiConfig = await this.configManager.ListConfig();
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
} catch (error) {
console.error("Error delete api configuration:", error)
vscode.window.showErrorMessage("Failed to delete api configuration")
}
}
break
case "getListApiConfiguration":
try {
let listApiConfig = await this.configManager.ListConfig();
await this.updateGlobalState("listApiConfigMeta", listApiConfig)
this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
} catch (error) {
console.error("Error get list api configuration:", error)
vscode.window.showErrorMessage("Failed to get list api configuration")
}
break
case "setApiConfigPassword":
if (message.text) {
try {
await this.storeSecret("apiConfigPassword", message.text !== "" ? message.text : undefined)
} catch (error) {
console.error("Error set apiKey password:", error)
vscode.window.showErrorMessage("Failed to set apiKey password")
}
}
break
}
},
null,
@@ -812,6 +884,74 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
private async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
glamaModelId,
glamaModelInfo,
glamaApiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
ollamaModelId,
ollamaBaseUrl,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
azureApiVersion,
openAiStreamingEnabled,
openRouterModelId,
openRouterModelInfo,
openRouterUseMiddleOutTransform,
} = apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
await this.storeSecret("apiKey", apiKey)
await this.updateGlobalState("glamaModelId", glamaModelId)
await this.updateGlobalState("glamaModelInfo", glamaModelInfo)
await this.storeSecret("glamaApiKey", glamaApiKey)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
await this.storeSecret("awsSessionToken", awsSessionToken)
await this.updateGlobalState("awsRegion", awsRegion)
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
await this.updateGlobalState("vertexProjectId", vertexProjectId)
await this.updateGlobalState("vertexRegion", vertexRegion)
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
await this.storeSecret("openAiApiKey", openAiApiKey)
await this.updateGlobalState("openAiModelId", openAiModelId)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform)
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
}
}
async updateCustomInstructions(instructions?: string) {
// User may be clearing the field
await this.updateGlobalState("customInstructions", instructions || undefined)
@@ -1256,8 +1396,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
currentApiConfigName,
listApiConfigMeta,
apiKeyPassword
} = await this.getState()
const allowedCommands = vscode.workspace
.getConfiguration('roo-cline')
.get<string[]>('allowedCommands') || []
@@ -1290,6 +1433,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled: mcpEnabled ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 5,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
apiKeyPassword: apiKeyPassword ?? ""
}
}
@@ -1397,6 +1543,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
currentApiConfigName,
listApiConfigMeta,
apiKeyPassword,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1449,6 +1598,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
this.getSecret("apiConfigPassword") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
@@ -1545,6 +1697,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
mcpEnabled: mcpEnabled ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 5,
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
apiKeyPassword: apiKeyPassword ?? ""
}
}
@@ -1622,6 +1777,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"apiConfigPassword"
]
for (const key of secretKeys) {
await this.storeSecret(key, undefined)