Repository navigation
[Fix] upsertProviderProfile: enforce org allow-list and roll back on failure #2117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
604d28c
22b95b2
403bf78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1885,21 +1885,111 @@ | |
| providerSettings: ProviderSettings, | ||
| activate: boolean = true, | ||
| ): Promise<string | undefined> { | ||
| let partialState = false | ||
|
|
||
| try { | ||
| return await this.enqueueProviderProfileMutation( | ||
| (signal) => this.persistProviderProfile(name, providerSettings, activate, signal), | ||
| async (signal) => { | ||
| const { organizationAllowList, mode } = await this.getState() | ||
|
|
||
| // Reject before touching disk or in-memory state. | ||
| if (!ProfileValidator.isProfileAllowed(providerSettings, organizationAllowList)) { | ||
| throw new OrganizationAllowListViolationError( | ||
| t("common:errors.violated_organization_allowlist"), | ||
| ) | ||
| } | ||
|
|
||
| const snapshot = await this.snapshotProviderProfileState(name, mode) | ||
|
|
||
| try { | ||
| return await this.persistProviderProfile(name, providerSettings, activate, signal) | ||
| } catch (persistError) { | ||
| // Save, activate and propagate are separate writes; put everything back so | ||
| // storage, context settings, the mode binding and the task agree again. | ||
| try { | ||
| await this.restoreProviderProfileState(name, mode, snapshot, activate) | ||
| } catch (restoreError) { | ||
| partialState = true | ||
| this.log( | ||
| `Error restoring profile state after failed upsert: ${JSON.stringify(restoreError, Object.getOwnPropertyNames(restoreError), 2)}`, | ||
|
Check warning on line 1914 in src/core/webview/ClineProvider.ts
|
||
| ) | ||
| } | ||
|
|
||
| throw persistError | ||
| } | ||
| }, | ||
| { runToCompletion: true }, | ||
| ) | ||
| } catch (error) { | ||
| this.log( | ||
| `Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, | ||
| ) | ||
|
|
||
| vscode.window.showErrorMessage(t("common:errors.create_api_config")) | ||
| vscode.window.showErrorMessage( | ||
| partialState | ||
| ? t("common:errors.update_profile_model_partial") | ||
| : error instanceof OrganizationAllowListViolationError | ||
| ? error.message | ||
| : t("common:errors.create_api_config"), | ||
| ) | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| /** Captures every value `persistProviderProfile` can change, so a failed upsert can be undone. */ | ||
| private async snapshotProviderProfileState(name: string, mode: Mode) { | ||
| let previousProfile: Omit<Awaited<ReturnType<ProviderSettingsManager["getProfile"]>>, "name"> | undefined | ||
|
|
||
| if (await this.providerSettingsManager.hasConfig(name)) { | ||
| const { name: _name, ...stored } = await this.providerSettingsManager.getProfile({ name }) | ||
|
Check warning on line 1944 in src/core/webview/ClineProvider.ts
|
||
| previousProfile = stored | ||
| } | ||
|
|
||
| // A delegated child can run with a handoff configuration that differs from the global | ||
| // settings, so rollback rebuilds this task from its own captured configuration. | ||
| const task = this.getCurrentTask() | ||
|
|
||
| return { | ||
| previousProfile, | ||
| listApiConfigMeta: this.getGlobalState("listApiConfigMeta"), | ||
|
Check warning on line 1954 in src/core/webview/ClineProvider.ts
|
||
| currentApiConfigName: this.getGlobalState("currentApiConfigName"), | ||
| modeConfigId: await this.providerSettingsManager.getModeConfigId(mode), | ||
| providerSettings: this.contextProxy.getProviderSettings(), | ||
| task, | ||
| taskApiConfiguration: task?.apiConfiguration ? structuredClone(task.apiConfiguration) : undefined, | ||
|
Comment on lines
+1958
to
+1959
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| private async restoreProviderProfileState( | ||
| name: string, | ||
| mode: Mode, | ||
| snapshot: Awaited<ReturnType<ClineProvider["snapshotProviderProfileState"]>>, | ||
| activate: boolean, | ||
| ): Promise<void> { | ||
| // Run every step even if one fails, then report the first failure. | ||
| const results = await Promise.allSettled([ | ||
| snapshot.previousProfile | ||
| ? this.providerSettingsManager.saveConfig(name, snapshot.previousProfile) | ||
| : this.providerSettingsManager.hasConfig(name).then(async (exists) => { | ||
| if (exists) await this.providerSettingsManager.deleteConfig(name) | ||
| }), | ||
| this.updateGlobalState("listApiConfigMeta", snapshot.listApiConfigMeta), | ||
|
Check warning on line 1976 in src/core/webview/ClineProvider.ts
|
||
| this.updateGlobalState("currentApiConfigName", snapshot.currentApiConfigName), | ||
| // A mode with no previous binding must not stay bound to a profile that is being removed. | ||
| snapshot.modeConfigId | ||
| ? this.providerSettingsManager.setModeConfig(mode, snapshot.modeConfigId) | ||
| : this.providerSettingsManager.clearModeConfig(mode), | ||
|
Comment on lines
+1978
to
+1981
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| this.contextProxy.setProviderSettings(snapshot.providerSettings), | ||
| ]) | ||
|
|
||
| if (activate && snapshot.task && snapshot.taskApiConfiguration && this.getCurrentTask() === snapshot.task) { | ||
|
Check warning on line 1985 in src/core/webview/ClineProvider.ts
|
||
| this.updateTaskApiHandlerIfNeeded(snapshot.taskApiConfiguration, { forceRebuild: true }) | ||
| } | ||
|
|
||
| const failed = results.find((result): result is PromiseRejectedResult => result.status === "rejected") | ||
| if (failed) throw failed.reason | ||
| } | ||
|
|
||
| /** | ||
| * Applies a model change to the latest stored profile inside the mutation queue, | ||
| * so a stale webview snapshot cannot overwrite newer profile settings. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.