diff --git a/patches/remote-secret-storage.diff b/patches/remote-secret-storage.diff new file mode 100644 index 000000000000..904d1fc6df4c --- /dev/null +++ b/patches/remote-secret-storage.diff @@ -0,0 +1,86 @@ +AI: store secrets on remote for VS Code Web +Index: code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/platform/secrets/common/secrets.ts ++++ code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts +@@ -139,8 +139,13 @@ export class BaseSecretStorageService ex + return await readEncryptedSecret( + key, + (fullKey) => this.getValueFromStorage(key, fullKey, storageService), +- // If the storage service is in-memory, we don't need to decrypt +- this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.decrypt(v), ++ // Don't decrypt if storage is in-memory or if encryption service is not available ++ async (v) => { ++ if (this._type === 'in-memory' || !await this._encryptionService.isEncryptionAvailable()) { ++ return v; ++ } ++ return this._encryptionService.decrypt(v); ++ }, + this._logService, + ); + } catch (e) { +@@ -160,8 +165,13 @@ export class BaseSecretStorageService ex + key, + value, + (fullKey, encrypted) => this.setValueInStorage(key, fullKey, encrypted, storageService), +- // If the storage service is in-memory, we don't need to encrypt +- this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.encrypt(v), ++ // Don't encrypt if storage is in-memory or if encryption service is not available ++ async (v) => { ++ if (this._type === 'in-memory' || !await this._encryptionService.isEncryptionAvailable()) { ++ return v; ++ } ++ return this._encryptionService.encrypt(v); ++ }, + this._logService, + ); + } catch (e) { +@@ -194,8 +204,9 @@ export class BaseSecretStorageService ex + + private async initialize(): Promise { + let storageService; +- if (!this._useInMemoryStorage && await this._encryptionService.isEncryptionAvailable()) { +- this._logService.trace(`[SecretStorageService] Encryption is available, using persisted storage`); ++ if (!this._useInMemoryStorage) { ++ // Use persisted storage when not forced to use in-memory ++ this._logService.trace(`[SecretStorageService] Using persisted storage`); + this._type = 'persisted'; + storageService = this._storageService; + } else { +@@ -203,7 +214,7 @@ export class BaseSecretStorageService ex + if (this._type === 'in-memory') { + return this._storageService; + } +- this._logService.trace('[SecretStorageService] Encryption is not available, falling back to in-memory storage'); ++ this._logService.trace('[SecretStorageService] Falling back to in-memory storage'); + this._type = 'in-memory'; + storageService = this._register(new InMemoryStorageService()); + } +Index: code-server/lib/vscode/src/vs/workbench/services/secrets/browser/secretStorageService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/services/secrets/browser/secretStorageService.ts ++++ code-server/lib/vscode/src/vs/workbench/services/secrets/browser/secretStorageService.ts +@@ -22,9 +22,9 @@ export class BrowserSecretStorageService + @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, + @ILogService logService: ILogService + ) { +- // We don't have encryption in the browser so instead we use the +- // in-memory base class implementation instead. +- super(true, storageService, encryptionService, logService); ++ // Use remote storage if enabled, otherwise use in-memory storage ++ const useRemoteStorage = environmentService.options?.remoteStorageEnabled ?? false; ++ super(!useRemoteStorage, storageService, encryptionService, logService); + + if (environmentService.options?.secretStorageProvider) { + this._secretStorageProvider = environmentService.options.secretStorageProvider; +Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.ts ++++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts +@@ -625,2 +625,2 @@ +- secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath +- ? undefined /* with a remote without embedder-preferred storage, store on the remote */ ++ secretStorageProvider: config.remoteStorageEnabled || (config.remoteAuthority && !secretStorageKeyPath) ++ ? undefined /* with remote storage enabled, or without embedder-prefered storage, store on the remote */ + }); + })(); diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff new file mode 100644 index 000000000000..fad692190cb1 --- /dev/null +++ b/patches/remote-storage.diff @@ -0,0 +1,117 @@ +Reuse VS Code's Electron storage service for remote persistence + +--- code-server.orig/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts ++++ code-server/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts +@@ -14 +14 @@ +-import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; ++import type { ILifecycleMainService, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; +@@ -101 +101 @@ +- @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, ++ private readonly lifecycleMainService: ILifecycleMainService, +@@ -117 +117 @@ +- await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); ++ await this.lifecycleMainService.when(3 /* LifecycleMainPhase.AfterWindowOpen */); +@@ -256 +256 @@ +- if (this.shutdownReason === ShutdownReason.KILL) { ++ if (this.shutdownReason === 2 /* ShutdownReason.KILL */) { +@@ -295 +295 @@ +- if (this.shutdownReason === ShutdownReason.KILL) { ++ if (this.shutdownReason === 2 /* ShutdownReason.KILL */) { + +--- code-server.orig/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts ++++ code-server/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts +@@ -6 +6 @@ +-import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; ++import { IRemoteService } from '../../../../platform/ipc/common/services.js'; +@@ -22 +22 @@ +- mainProcessService: IMainProcessService, ++ remoteService: Pick, +@@ -25 +25 @@ +- super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, mainProcessService, workbenchEnvironmentService); ++ super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, remoteService as IRemoteService, workbenchEnvironmentService); +--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts ++++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +@@ -68,0 +69 @@ ++ 'enable-remote-storage': { type: 'boolean', cat: 'o', description: nls.localize('enable-remote-storage', 'Persist browser storage (settings, state, etc.) on the server instead of browser IndexedDB. Enables state portability across browsers/devices.') }, +@@ -214,0 +216 @@ ++ 'enable-remote-storage'?: boolean; +--- code-server.orig/lib/vscode/src/vs/server/node/serverServices.ts ++++ code-server/lib/vscode/src/vs/server/node/serverServices.ts +@@ -107,0 +108,4 @@ ++import { StorageDatabaseChannel } from '../../platform/storage/electron-main/storageIpc.js'; ++import { StorageMainService } from '../../platform/storage/electron-main/storageMainService.js'; ++import type { ILifecycleMainService } from '../../platform/lifecycle/electron-main/lifecycleMainService.js'; ++import type { IUserDataProfilesMainService } from '../../platform/userDataProfile/electron-main/userDataProfile.js'; +@@ -240,0 +245,15 @@ ++ // Reuse the Electron storage service on the remote. The server has no ++ // windows, so its lifecycle is always ready and never loads a window. ++ const storageMainService = disposables.add(new StorageMainService( ++ logService, ++ environmentService, ++ userDataProfilesService as unknown as IUserDataProfilesMainService, ++ { ++ when: () => Promise.resolve(), ++ onWillLoadWindow: Event.None, ++ onWillShutdown: Event.None, ++ } as unknown as ILifecycleMainService, ++ fileService, ++ uriIdentityService, ++ )); ++ +@@ -419,6 +439,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken + + const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); + socketServer.registerChannel('languagePacks', languagePackChannel); ++ socketServer.registerChannel('storage', disposables.add(new StorageDatabaseChannel(logService, storageMainService))); + + // clean up extensions folder + remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); +--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts ++++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts +@@ -411 +411,2 @@ +- callbackRoute: callbackRoute ++ callbackRoute: callbackRoute, ++ remoteStorageEnabled: this._environmentService.args['enable-remote-storage'] ? true : undefined +--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts ++++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts +@@ -413,0 +414,5 @@ ++ /** ++ * Persist workbench storage on the remote server instead of browser IndexedDB. ++ */ ++ readonly remoteStorageEnabled?: boolean; ++ +--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts ++++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts +@@ -35,0 +36 @@ ++import { NativeWorkbenchStorageService } from '../services/storage/electron-browser/storageService.js'; +@@ -479 +480 @@ +- this.createStorageService(workspace, logService, userDataProfileService).then(service => { ++ this.createStorageService(workspace, environmentService, logService, userDataProfileService, userDataProfilesService, remoteAgentService).then(service => { +@@ -601 +602,15 @@ +- protected async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService): Promise { ++ protected async createStorageService(workspace: IAnyWorkspaceIdentifier, environmentService: IBrowserWorkbenchEnvironmentService, logService: ILogService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, remoteAgentService: IRemoteAgentService): Promise { ++ const connection = remoteAgentService.getConnection(); ++ if (this.configuration.remoteStorageEnabled && connection) { ++ const storageService = new NativeWorkbenchStorageService(workspace, userDataProfileService, userDataProfilesService, connection, environmentService); ++ ++ try { ++ await storageService.initialize(); ++ this.onWillShutdownDisposables.add(toDisposable(() => storageService.close())); ++ return storageService; ++ } catch (error) { ++ onUnexpectedError(error); ++ logService.error('Remote storage service initialization failed, falling back to browser storage', error); ++ } ++ } ++ +--- code-server.orig/lib/vscode/src/vs/sessions/browser/web.main.ts ++++ code-server/lib/vscode/src/vs/sessions/browser/web.main.ts +@@ -42 +42 @@ +- _userDataProfilesService: BrowserUserDataProfilesService, ++ userDataProfilesService: BrowserUserDataProfilesService, +@@ -44 +44 @@ +- _remoteAgentService: IRemoteAgentService, ++ remoteAgentService: IRemoteAgentService, +@@ -85 +85 @@ +- const storageService = await this.createStorageService(workspaceIdentifier, logService, userDataProfileService); ++ const storageService = await this.createStorageService(workspaceIdentifier, environmentService, logService, userDataProfileService, userDataProfilesService, remoteAgentService); diff --git a/patches/series b/patches/series index 5b548e3900dd..ee6698e53e26 100644 --- a/patches/series +++ b/patches/series @@ -25,3 +25,5 @@ signature-verification.diff copilot.diff app-name.diff csp-hashes.diff +remote-storage.diff +remote-secret-storage.diff diff --git a/src/node/cli.ts b/src/node/cli.ts index 926f6ff1fdef..b631a4319e5f 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -46,6 +46,7 @@ export interface UserProvidedCodeArgs { "show-versions"?: boolean category?: string "github-auth"?: string + "enable-remote-storage"?: boolean "disable-update-check"?: boolean "disable-file-downloads"?: boolean "disable-file-uploads"?: boolean @@ -274,6 +275,10 @@ export const options: Options> = { type: "string", description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).", }, + "enable-remote-storage": { + type: "boolean", + description: "Persist VS Code workbench storage on the remote server instead of browser storage.", + }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, "skip-auth-preflight": { type: "boolean", diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 53eb066c1e70..3e50a222ae8f 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -1036,6 +1036,13 @@ describe("toCodeArgs", () => { }) }) + it("should pass through --enable-remote-storage", async () => { + expect(await toCodeArgs(await setDefaults(parse(["--enable-remote-storage"])))).toStrictEqual({ + ...vscodeDefaults, + "enable-remote-storage": true, + }) + }) + it("should collect a repeated --vscode-option into an array", async () => { const args = parse([ "--vscode-option",