From 3b23c54863125b708da2e323f8a12abe5103a2dc Mon Sep 17 00:00:00 2001 From: vritant24 Date: Fri, 22 May 2026 10:39:21 -0700 Subject: [PATCH 01/14] Restrict "Configure..." action to non-default vendors with JSON configuration --- .../chat/browser/chatManagement/chatModelsWidget.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 42906bafc5d096..fea8be7e5cad4a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -906,7 +906,12 @@ class ActionsColumnRenderer extends ModelsTableColumnRenderer 0 || entry.model.metadata.configurationSchema) { + // Only offer the JSON-based "Configure..." entry for non-default vendors that are + // configured via the language models JSON file. The default vendor (Copilot) and + // vendors with a `managementCommand` are configured elsewhere, so this entry would + // do nothing useful for their models. + const vendor = entry.model.provider.vendor; + if (!vendor.isDefault && !vendor.managementCommand && (configActions.length > 0 || entry.model.metadata.configurationSchema)) { secondaryActions.push(toAction({ id: 'configureModel', label: localize('models.configureModel', 'Configure...'), From 9de77c295b1bd405e4c21e3e089a8af538a0c83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Ruales?= <1588988+jruales@users.noreply.github.com> Date: Fri, 22 May 2026 11:26:32 -0700 Subject: [PATCH 02/14] Add browser-integration assignment to classifier.json (#317925) --- .github/classifier.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/classifier.json b/.github/classifier.json index 21a1f237181d4f..f38027d88bbde6 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -15,6 +15,7 @@ "bracket-pair-colorization": {"assign": ["hediet"]}, "bracket-pair-guides": {"assign": ["hediet"]}, "breadcrumbs": {"assign": []}, + "browser-integration": {"assign": ["kycutler", "jruales"]}, "callhierarchy": {"assign": []}, "chat-terminal": {"assign": ["meganrogge"]}, "chat-terminal-output-monitor": {"assign": ["meganrogge"]}, From bcc376cb7811f79ea949526b08d7579e95063706 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 22 May 2026 14:52:03 -0400 Subject: [PATCH 03/14] Use abbreviated numbers for when there are really big credit amounts (#318032) --- .../chat/browser/chatStatus/chatStatusDashboard.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts index 55f6f89c49b8b6..44950d4ecb03f1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.ts @@ -108,6 +108,7 @@ export class ChatStatusDashboard extends DomWidget { private readonly timeFormatter = safeIntl.DateTimeFormat(language, { hour: 'numeric', minute: 'numeric' }); private readonly quotaPercentageFormatter = safeIntl.NumberFormat(undefined, { maximumFractionDigits: 0, minimumFractionDigits: 0 }); private readonly quotaCreditsFormatter = safeIntl.NumberFormat(language, { maximumFractionDigits: 2, minimumFractionDigits: 0 }); + private readonly quotaCreditsCompactFormatter = safeIntl.NumberFormat(language, { notation: 'compact', maximumFractionDigits: 1, minimumFractionDigits: 0 }); constructor( private readonly options: IChatStatusDashboardOptions | undefined, @@ -753,8 +754,13 @@ export class ChatStatusDashboard extends DomWidget { const used = currentQuota.quotaRemaining !== undefined ? total - currentQuota.quotaRemaining : total * (100 - currentQuota.percentRemaining) / 100; - const usedFormatted = this.quotaCreditsFormatter.value.format(used); - const totalFormatted = this.quotaCreditsFormatter.value.format(total); + const compactThreshold = 100_000; + const usedFormatted = used >= compactThreshold + ? this.quotaCreditsCompactFormatter.value.format(used) + : this.quotaCreditsFormatter.value.format(used); + const totalFormatted = total >= compactThreshold + ? this.quotaCreditsCompactFormatter.value.format(total) + : this.quotaCreditsFormatter.value.format(total); quotaValueText.textContent = localize('quotaCreditsDisplay', "{0} / {1}", usedFormatted, totalFormatted); quotaValueSuffix.textContent = isCompact ? localize('quotaLabelUsed', "{0} used", label) From 1fdf66f83d55f9d28d3dbb98b3bb53f82ea0a250 Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Fri, 22 May 2026 12:41:44 -0700 Subject: [PATCH 04/14] Fix Windows agent sandbox setting ID (#318035) --- src/vs/platform/sandbox/common/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/sandbox/common/settings.ts b/src/vs/platform/sandbox/common/settings.ts index b16b21460df15b..30c0144c96ae70 100644 --- a/src/vs/platform/sandbox/common/settings.ts +++ b/src/vs/platform/sandbox/common/settings.ts @@ -8,7 +8,7 @@ */ export const enum AgentSandboxSettingId { AgentSandboxEnabled = 'chat.agent.sandbox.enabled', - AgentSandboxWindowsEnabled = 'chat.agent.sandbox.enabled.windows', + AgentSandboxWindowsEnabled = 'chat.agent.sandbox.enabledWindows', AgentSandboxAllowUnsandboxedCommands = 'chat.agent.sandbox.allowUnsandboxedCommands', AgentSandboxAutoApproveUnsandboxedCommands = 'chat.agent.sandbox.autoApproveUnsandboxedCommands', AgentSandboxAllowAutoApprove = 'chat.agent.sandbox.allowAutoApprove', From a3bcb34a12936608e8aea83857094fdcea21c3e1 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 22 May 2026 13:34:04 -0700 Subject: [PATCH 05/14] Browser: store screen size as part of emulated device (#318024) --- .../browserView/common/browserView.ts | 18 +- .../browserView/electron-main/browserView.ts | 2 +- .../electron-main/browserViewEmulator.ts | 38 +++- .../contrib/browserView/common/browserView.ts | 21 +- .../electron-browser/browserEditor.ts | 2 - .../browserEditorEmulationFeatures.ts | 199 +++++++----------- 6 files changed, 125 insertions(+), 155 deletions(-) diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index ce51cf96222599..9efee64578f347 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -86,8 +86,6 @@ export interface IBrowserViewBounds { zoomFactor: number; cornerRadius: number; emulation?: { - viewportWidth: number; - viewportHeight: number; scale: number; }; } @@ -262,26 +260,16 @@ export function browserZoomAccessibilityLabel(zoomFactor: number): string { } /** - * The "device" half of browser emulation: characteristics the page sees as - * intrinsic to the device (touch / mobile media features, DPR, UA string). + * The active device emulation profile. `undefined` fields mean "use the host default" for that property. */ export interface IBrowserDeviceProfile { + readonly width?: number; + readonly height?: number; readonly mobile?: boolean; readonly userAgent?: string; readonly deviceScaleFactor?: number; } -/** - * The "screen" half of browser emulation: the desired viewport size and zoom. - * - * `undefined` values mean the view should be sized to fit the container. - */ -export interface IBrowserScreenProfile { - readonly width?: number; - readonly height?: number; - readonly scale?: number; -} - /** * This should match the isolated world ID defined in `preload-browserView.ts`. */ diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 6ae74ed36edaca..c5a61dca6b3b88 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -503,7 +503,7 @@ export class BrowserView extends Disposable { this._view.setBorderRadius(Math.round(bounds.cornerRadius * bounds.zoomFactor)); if (bounds.emulation) { - this.emulator.applyScreenEmulation(bounds.emulation.viewportWidth, bounds.emulation.viewportHeight, bounds.emulation.scale, bounds.zoomFactor); + this.emulator.applyScreenEmulation(bounds.width, bounds.height, bounds.emulation.scale, bounds.zoomFactor); } this._view.setBounds({ diff --git a/src/vs/platform/browserView/electron-main/browserViewEmulator.ts b/src/vs/platform/browserView/electron-main/browserViewEmulator.ts index 1dd449920665c2..7075302b91ce9c 100644 --- a/src/vs/platform/browserView/electron-main/browserViewEmulator.ts +++ b/src/vs/platform/browserView/electron-main/browserViewEmulator.ts @@ -11,14 +11,17 @@ import type { BrowserView } from './browserView.js'; /** * Manages device emulation for a browser view. The renderer is authoritative - * for layout (it computes the on-screen size and emulation scale); this class - * just forwards values to `webContents.enableDeviceEmulation` and manages the - * touch / media / user-agent overrides that have no native Electron equivalent. + * for the on-screen container size and scale; this class derives the emulated + * viewport from the current device profile (falling back to container size / + * scale when width/height are unset) and forwards values to + * `webContents.enableDeviceEmulation`. It also manages the touch / media / + * user-agent overrides that have no native Electron equivalent. */ export class BrowserViewEmulator extends Disposable { private _device: IBrowserDeviceProfile | undefined; private readonly _defaultUserAgent: string; + private _lastLayout = { containerWidth: 1024, containerHeight: 768, scale: 1, hostZoom: 1 }; private _lastApplied: { viewportWidth: number; viewportHeight: number; scale: number; hostZoom: number } | undefined; private readonly _onDidChange = this._register(new Emitter()); @@ -64,25 +67,38 @@ export class BrowserViewEmulator extends Disposable { this._lastApplied = undefined; if (!device && this.isSafeToApplyEmulation()) { this.browser.webContents.disableDeviceEmulation(); + } else { + // New device may carry new width / height / deviceScaleFactor — reapply + // using the last container size + scale pushed via layout(). + this._reapply(); } this._onDidChange.fire(device); } /** - * Apply viewport + scale via Chromium's emulation API. `hostZoom` is the host - * window's CSS-to-screen zoom factor: bounds in main are multiplied by it, - * so the emulation scale must be too or the emulated viewport won't fill - * the WebContentsView when the workbench is zoomed. + * Update the cached layout (container size + scale + host zoom) and reapply + * emulation. The emulated viewport is derived from the current device's + * width / height; when those are undefined the viewport auto-fits to the + * container at the given scale. `hostZoom` is the host window's + * CSS-to-screen zoom factor — bounds in main are multiplied by it, so the + * emulation scale must be too or the emulated viewport won't fill the + * WebContentsView when the workbench is zoomed. */ - applyScreenEmulation(viewportWidth: number, viewportHeight: number, scale: number, hostZoom: number): void { + applyScreenEmulation(containerWidth: number, containerHeight: number, scale: number, hostZoom: number): void { + this._lastLayout = { containerWidth, containerHeight, scale, hostZoom }; + this._reapply(); + } + + private _reapply(): void { if (!this._device || !this.isSafeToApplyEmulation()) { return; } - const w = Math.max(1, Math.round(viewportWidth)); - const h = Math.max(1, Math.round(viewportHeight)); - const z = Math.max(0.01, hostZoom); + const { containerWidth, containerHeight, scale, hostZoom } = this._lastLayout; const s = Math.max(0.01, scale); + const z = Math.max(0.01, hostZoom); + const w = Math.max(1, Math.round(this._device.width || containerWidth / s)); + const h = Math.max(1, Math.round(this._device.height || containerHeight / s)); const last = this._lastApplied; if (last && last.viewportWidth === w && last.viewportHeight === h && Math.abs(last.scale - s) < 0.0001 && Math.abs(last.hostZoom - z) < 0.0001) { diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index 8b48b0c1297e9c..84f2b135a7c7f6 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -5,6 +5,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { structuralEquals } from '../../../../base/common/equals.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; @@ -278,6 +279,9 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { private _isElementSelectionActive: boolean = false; private _device: IBrowserDeviceProfile | undefined; + private readonly _onDidChangeDevice = this._register(new Emitter()); + readonly onDidChangeDevice: Event = this._onDidChangeDevice.event; + private readonly _onDidChangeSharingState = this._register(new Emitter()); readonly onDidChangeSharingState: Event = this._onDidChangeSharingState.event; @@ -393,8 +397,11 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { this._visible = visible; })); - this._register(this.onDidChangeDevice(device => { - this._device = device; + this._register(this.browserViewService.onDynamicDidChangeDeviceEmulation(this.id)(device => { + if (!structuralEquals(this._device, device)) { + this._device = device; + this._onDidChangeDevice.fire(device); + } })); this._register(this.onDidChangeElementSelectionActive(active => { @@ -474,10 +481,6 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { return this.browserViewService.onDynamicDidChangeVisibility(this.id); } - get onDidChangeDevice(): Event { - return this.browserViewService.onDynamicDidChangeDeviceEmulation(this.id); - } - get onDidClose(): Event { return this.browserViewService.onDynamicDidClose(this.id); } @@ -612,6 +615,12 @@ export class BrowserViewModel extends Disposable implements IBrowserViewModel { } async setDevice(device: IBrowserDeviceProfile | undefined): Promise { + // Update model state optimistically so dependent UI reacts immediately; + // the echo from the main process is filtered by deep comparison. + if (!structuralEquals(this._device, device)) { + this._device = device; + this._onDidChangeDevice.fire(device); + } return this.browserViewService.setDeviceEmulation(this.id, device); } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index a4cdd5a196142e..5e692f9c47d9ad 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -140,8 +140,6 @@ export interface IContainerLayout { readonly width: number; readonly height: number; readonly emulation?: { - readonly viewportWidth: number; - readonly viewportHeight: number; readonly scale: number; }; } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorEmulationFeatures.ts b/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorEmulationFeatures.ts index 7080c413165c81..52762e3b1785a2 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorEmulationFeatures.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/features/browserEditorEmulationFeatures.ts @@ -20,7 +20,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize, localize2 } from '../../../../../nls.js'; import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IBrowserDeviceProfile, IBrowserScreenProfile } from '../../../../../platform/browserView/common/browserView.js'; +import { IBrowserDeviceProfile } from '../../../../../platform/browserView/common/browserView.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; import { IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; import { IHoverService, WorkbenchHoverDelegate } from '../../../../../platform/hover/browser/hover.js'; @@ -52,23 +52,22 @@ const CONTEXT_BROWSER_EMULATION_HAS_USER_AGENT = new RawContextKey( ); /** - * A named device preset. Applying a preset stamps its `device` onto the - * active device profile and its `screen` (size only) onto the active screen - * profile, preserving the user's current zoom. + * A named device preset. Applying a preset stamps its `device` (including + * any embedded viewport width/height) onto the active device profile, while + * preserving the user's current scale. */ export interface IBrowserDevicePreset { readonly name: string; readonly device?: IBrowserDeviceProfile; - readonly screen?: IBrowserScreenProfile; } /** - * Keep track of the last used device and screen settings so we can restore them when the toolbar is opened. - * Note this isn't (currently) persisted in storage. + * Keep track of the last used device + scale so we can restore them when the + * toolbar is reopened. Note this isn't (currently) persisted in storage. */ const lastSettings = { device: undefined as IBrowserDeviceProfile | undefined, - screen: undefined as IBrowserScreenProfile | undefined, + scale: undefined as number | undefined, }; /** @@ -178,14 +177,13 @@ class BrowserEmulationToolbar extends Disposable { if (this._suppressChange || !model?.device) { return; } - const screen = this._feature.screen ?? {}; const scale = e.index === BrowserEmulationToolbar.AUTO_INDEX ? undefined : BrowserEmulationToolbar.ZOOM_PRESETS[e.index - 1]; - if (scale === screen.scale) { + if (scale === this._feature.scale) { return; } - this._feature.setScreen({ ...screen, scale }); + this._feature.setScale(scale); })); } @@ -221,29 +219,22 @@ class BrowserEmulationToolbar extends Disposable { } refresh(): void { - this._writeInputs(this._feature.model?.device, this._feature.screen); + this._writeInputs(this._feature.model?.device); this._updateZoom(); } - /** - * Update the inputs without touching the model. Used during resize-handle - * drag so the toolbar reflects the in-flight viewport size. - */ - setPreviewScreen(screen: IBrowserScreenProfile | undefined): void { - this._writeInputs(this._feature.model?.device, screen); - } - - private _writeInputs(device: IBrowserDeviceProfile | undefined, screen: IBrowserScreenProfile | undefined): void { + private _writeInputs(device: IBrowserDeviceProfile | undefined): void { + const width = device?.width; + const height = device?.height; this._suppressChange = true; try { - this._widthInput.value = screen?.width ? String(screen.width) : ''; - this._heightInput.value = screen?.height ? String(screen.height) : ''; + this._widthInput.value = width ? String(width) : ''; + this._heightInput.value = height ? String(height) : ''; this._dprInput.value = device?.deviceScaleFactor ? String(device.deviceScaleFactor) : ''; } finally { this._suppressChange = false; } - const canSwapDimensions = !!screen?.width || !!screen?.height; - this._swapDimensionsAction.enabled = canSwapDimensions; + this._swapDimensionsAction.enabled = !!width || !!height; } private _appendGroup(name: string): HTMLElement { @@ -260,7 +251,7 @@ class BrowserEmulationToolbar extends Disposable { } private _currentZoomIndex(): number { - const scale = this._feature.screen?.scale; + const scale = this._feature.scale; if (scale === undefined) { return BrowserEmulationToolbar.AUTO_INDEX; } @@ -297,11 +288,11 @@ class BrowserEmulationToolbar extends Disposable { }; const width = parse(this._widthInput.value); const height = parse(this._heightInput.value); - const screen = this._feature.screen ?? {}; - if (screen.width === width && screen.height === height) { + const device = model.device; + if (device.width === width && device.height === height) { return; } - this._feature.setScreen({ ...screen, width, height }); + void model.setDevice({ ...device, width, height }); } private _onDprInput(): void { @@ -338,11 +329,11 @@ class BrowserEmulationToolbar extends Disposable { } /** - * Editor contribution that owns the device toolbar, the device-emulation - * screen profile (viewport size + scale), and the resize sashes that drive - * it interactively. Also implements {@link computeContainerLayout} so the - * editor delegates container sizing to this contribution whenever device - * emulation is engaged. + * Editor contribution that owns the device toolbar, the renderer-side scale + * for the emulated viewport, and the resize sashes that drive viewport size + * interactively (committed onto {@link IBrowserViewModel.device}). Also + * implements {@link computeContainerLayout} so the editor delegates container + * sizing to this contribution whenever device emulation is engaged. */ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { @@ -351,15 +342,12 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { private readonly _isMobile: IContextKey; private readonly _hasUserAgent: IContextKey; - /** Committed screen profile (viewport size + scale). */ - private _screen: IBrowserScreenProfile | undefined; - /** In-flight screen during a resize-sash drag; takes priority over {@link _screen} when set. */ - private _screenInflight: IBrowserScreenProfile | undefined; + /** Committed renderer-side scale; undefined = auto-fit. Not persisted in the device model (rides on the layout call). */ + private _scale: number | undefined; /** Scale Auto-fit would produce for the current device + pane. Drives the toolbar's "Auto (X%)" label. */ private _autoFitScale = 1; - private readonly _onDidChangeScreen = this._register(new Emitter()); - private readonly _onDidPreviewScreen = this._register(new Emitter()); + private readonly _onDidChangeScale = this._register(new Emitter()); private readonly _onDidChangeAutoFitScale = this._register(new Emitter()); private _eastSash: Sash | undefined; @@ -397,15 +385,11 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { this._toolbar = this._register(instantiationService.createInstance(BrowserEmulationToolbar, this, actionsContainer, hoverDelegate)); - // React to our own screen state: refresh the toolbar, sync context keys, and relayout. - this._register(this._onDidChangeScreen.event(screen => { + // React to our own scale state: refresh the toolbar, sync context keys, and relayout. + this._register(this._onDidChangeScale.event(() => { this._toolbar.refresh(); - this._syncContextKeys(this.editor.model?.device, screen); this.editor.layoutBrowserContainer(); })); - this._register(this._onDidPreviewScreen.event(screen => { - this._toolbar.setPreviewScreen(screen); - })); this._register(this._onDidChangeAutoFitScale.event(scale => this._toolbar.setAutoFitScale(scale))); } @@ -438,74 +422,73 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { } private _computeLayout(paneWidth: number, paneHeight: number): IContainerLayout { - const screen = this._screenInflight ?? this._screen; + const device = this.editor.model?.device; + const width = device?.width; + const height = device?.height; const z = getZoomFactor(this.editor.window); const snap = (v: number) => Math.floor(v * z) / z; const fitScale = paneWidth > 0 && paneHeight > 0 - ? Math.min(screen?.width ? paneWidth / screen.width : 1, screen?.height ? paneHeight / screen.height : 1, 1) + ? Math.min(width ? paneWidth / width : 1, height ? paneHeight / height : 1, 1) : 1; if (this._autoFitScale !== fitScale) { this._autoFitScale = fitScale; this._onDidChangeAutoFitScale.fire(fitScale); } - const scale = screen?.scale ?? fitScale; - const viewportWidth = screen?.width ?? Math.max(1, Math.round(paneWidth / scale)); - const viewportHeight = screen?.height ?? Math.max(1, Math.round(paneHeight / scale)); + const scale = this._scale ?? fitScale; return { - width: snap(Math.min(viewportWidth * scale, paneWidth)), - height: snap(Math.min(viewportHeight * scale, paneHeight)), - emulation: { viewportWidth, viewportHeight, scale }, + width: snap(width ? Math.min(width * scale, paneWidth) : paneWidth), + height: snap(height ? Math.min(height * scale, paneHeight) : paneHeight), + emulation: { scale }, }; } protected override subscribeToModel(model: IBrowserViewModel, store: DisposableStore): void { this._toolbar.refresh(); - this._syncContextKeys(model.device, this._screen); + this._syncContextKeys(model.device); this._updateSashState(); this._setToolbarVisible(!!model.device); store.add(model.onDidChangeDevice(device => { this._updateSashState(); - // Turning emulation off discards any in-progress screen overrides so + // Turning emulation off discards any in-progress scale override so // reopening the toolbar starts clean. - if (!device && this._screen !== undefined) { - this.setScreen(undefined); + if (!device && this._scale !== undefined) { + this.setScale(undefined); } if (device) { lastSettings.device = device; } this._toolbar.refresh(); - this._syncContextKeys(device, this._screen); + this._syncContextKeys(device); this._setToolbarVisible(!!device); this.editor.layoutBrowserContainer(); })); } override clear(): void { - // Editor input is being cleared — drop screen state so a freshly + // Editor input is being cleared — drop renderer-side state so a freshly // reopened input starts without stale viewport overrides. - this._screen = undefined; - this._screenInflight = undefined; + this._scale = undefined; this._toolbar.refresh(); - this._syncContextKeys(undefined, undefined); + this._syncContextKeys(undefined); this._setToolbarVisible(false); } // -- Public API consumed by toolbar + actions -------------------------- - /** Current committed screen profile, or undefined for full-pane fit. */ - get screen(): IBrowserScreenProfile | undefined { return this._screen; } + /** Current renderer-side scale; undefined = auto-fit. */ + get scale(): number | undefined { return this._scale; } /** Convenience accessor for the toolbar — proxies the editor's model. */ get model(): IBrowserViewModel | undefined { return this.editor.model; } - setScreen(screen: IBrowserScreenProfile | undefined): void { - if (this._screen === screen) { + setScale(scale: number | undefined): void { + if (this._scale === scale) { return; } - if (screen) { - lastSettings.screen = screen; + if (scale !== undefined) { + lastSettings.scale = scale; } - this._screen = screen; - this._onDidChangeScreen.fire(screen); + this._scale = scale; + this._onDidChangeScale.fire(scale); } get isVisible(): boolean { @@ -524,7 +507,7 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { if (visible) { if (model && !model.device) { void model.setDevice({ ...lastSettings.device }); - this.setScreen({ ...lastSettings.screen }); + this.setScale(lastSettings.scale); } this._setToolbarVisible(true); } else { @@ -540,22 +523,16 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { return; } void model.setDevice(preset.device ?? {}); - const currentScale = this._screen?.scale; - this.setScreen({ - width: preset.screen?.width, - height: preset.screen?.height, - scale: currentScale, - }); } - /** Reset all device + screen overrides to defaults while keeping emulation engaged. */ + /** Reset all device + scale overrides to defaults while keeping emulation engaged. */ resetAll(): void { const model = this.editor.model; if (!model) { return; } void model.setDevice({}); - this.setScreen({}); + this.setScale(undefined); } /** Set the user agent on the current device. Empty / undefined = default. Engages emulation if not already active. */ @@ -580,11 +557,11 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { /** Swap the current viewport's width and height. No-op without any fixed dim. */ swapDimensions(): void { const model = this.editor.model; - const screen = this._screen; - if (!model || !screen || (!screen.width && !screen.height)) { + const device = model?.device; + if (!model || !device || (!device.width && !device.height)) { return; } - this.setScreen({ ...screen, width: screen.height, height: screen.width }); + void model.setDevice({ ...device, width: device.height, height: device.width }); } /** Flip the mobile flag on the current device (drives touch + pointer media). Engages emulation if not already active. */ @@ -599,7 +576,7 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { // -- Internal helpers --------------------------------------------------- - private _syncContextKeys(device: IBrowserDeviceProfile | undefined, _screen: IBrowserScreenProfile | undefined): void { + private _syncContextKeys(device: IBrowserDeviceProfile | undefined): void { this._isMobile.set(!!device?.mobile); this._hasUserAgent.set(!!device?.userAgent); } @@ -656,8 +633,6 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { readonly scale: number; readonly paneW: number; readonly paneH: number; - screen: IBrowserScreenProfile; - changed: boolean; }; let drag: DragState | undefined; @@ -666,23 +641,21 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { if (!model || !model.device) { return; } - const screen = this._screen ?? {}; + const device = model.device; container.classList.add('browser-container--dragging'); const pane = this.editor.paneSize; const containerRect = container.getBoundingClientRect(); // Mirror computeContainerLayout's fit-scale math to derive starting scale. const fitScale = pane.width > 0 && pane.height > 0 - ? Math.min(screen.width ? pane.width / screen.width : 1, screen.height ? pane.height / screen.height : 1, 1) + ? Math.min(device.width ? pane.width / device.width : 1, device.height ? pane.height / device.height : 1, 1) : 1; - const startScale = screen.scale ?? fitScale; + const startScale = this._scale ?? fitScale; drag = { startContainerW: containerRect.width, startContainerH: containerRect.height, scale: Math.max(0.01, startScale), paneW: pane.width, paneH: pane.height, - screen, - changed: false, }; }; @@ -690,17 +663,14 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { if (!drag) { return; } + const device = this.editor.model?.device ?? {}; if (axis === 'x') { const w = Math.max(50, Math.min(drag.paneW, drag.startContainerW + (evt.currentX - evt.startX) * 2)); - drag.screen = { ...drag.screen, width: Math.max(50, Math.round(w / drag.scale)) }; + void this.editor.model?.setDevice({ ...device, width: Math.max(50, Math.round(w / drag.scale)) }); } else { const h = Math.max(50, Math.min(drag.paneH, drag.startContainerH + (evt.currentY - evt.startY) * 2)); - drag.screen = { ...drag.screen, height: Math.max(50, Math.round(h / drag.scale)) }; + void this.editor.model?.setDevice({ ...device, height: Math.max(50, Math.round(h / drag.scale)) }); } - drag.changed = true; - this._screenInflight = drag.screen; - this.editor.layoutBrowserContainer(); - this._onDidPreviewScreen.fire(drag.screen); }; const onEnd = () => { @@ -708,14 +678,7 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { return; } container.classList.remove('browser-container--dragging'); - const { screen, changed } = drag; drag = undefined; - this._screenInflight = undefined; - if (changed) { - this.setScreen(screen); - } else { - this.editor.layoutBrowserContainer(); - } }; this._register(eastSash.onDidStart(onStart)); @@ -729,14 +692,14 @@ export class BrowserEditorEmulationSupport extends BrowserEditorContribution { } private _resetAxis(axis: 'x' | 'y'): void { - if (!this.editor.model?.device) { + const model = this.editor.model; + if (!model?.device) { return; } - const screen = this._screen ?? {}; - const next: IBrowserScreenProfile = axis === 'x' - ? { ...screen, width: undefined } - : { ...screen, height: undefined }; - this.setScreen(next); + const device = model.device; + void model.setDevice(axis === 'x' + ? { ...device, width: undefined } + : { ...device, height: undefined }); } } @@ -846,23 +809,19 @@ MenuRegistry.appendMenuItem(MenuId.BrowserEmulationToolbar, { const DEFAULT_BROWSER_DEVICE_PRESETS: readonly IBrowserDevicePreset[] = [ { name: 'iPhone 15 Pro', - device: { mobile: true, deviceScaleFactor: 3, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' }, - screen: { width: 393, height: 852 }, + device: { width: 393, height: 852, mobile: true, deviceScaleFactor: 3, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' }, }, { name: 'iPhone SE', - device: { mobile: true, deviceScaleFactor: 2, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' }, - screen: { width: 375, height: 667 }, + device: { width: 375, height: 667, mobile: true, deviceScaleFactor: 2, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' }, }, { name: 'Pixel 8', - device: { mobile: true, deviceScaleFactor: 2.625, userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36' }, - screen: { width: 412, height: 915 }, + device: { width: 412, height: 915, mobile: true, deviceScaleFactor: 2.625, userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36' }, }, { name: 'iPad Mini', - device: { mobile: true, deviceScaleFactor: 2, userAgent: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' }, - screen: { width: 768, height: 1024 }, + device: { width: 768, height: 1024, mobile: true, deviceScaleFactor: 2, userAgent: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' }, }, ]; @@ -893,8 +852,8 @@ class PickBrowserDevicePresetAction extends Action2 { type PresetItem = IQuickPickItem & { preset: IBrowserDevicePreset }; const items: PresetItem[] = DEFAULT_BROWSER_DEVICE_PRESETS.map(p => ({ label: p.name, - description: p.screen?.width && p.screen?.height - ? `${p.screen.width}\u00D7${p.screen.height}${p.device?.mobile ? ` \u2022 ${localize('browser.devicePresets.mobileTag', "mobile")}` : ''}` + description: p.device?.width && p.device?.height + ? `${p.device.width}\u00D7${p.device.height}${p.device?.mobile ? ` \u2022 ${localize('browser.devicePresets.mobileTag', "mobile")}` : ''}` : undefined, preset: p, })); From 9630af635e82a2f25d1ccfd91b8fd90f0be5b377 Mon Sep 17 00:00:00 2001 From: vritant24 Date: Fri, 22 May 2026 13:55:32 -0700 Subject: [PATCH 06/14] Remove condition for displaying custom endpoint in package.json --- extensions/copilot/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 9e78b070f5fb1b..7603e9a22923df 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -1973,7 +1973,6 @@ }, { "vendor": "customendpoint", - "when": "productQualityType != 'stable'", "displayName": "Custom Endpoint", "configuration": { "type": "object", From dd150098075b7aeac4832fb23b04a5426e5d82bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 20:57:46 +0000 Subject: [PATCH 07/14] build(deps): bump uuid, @azure/cosmos, @azure/identity and @azure/msal-node in /build (#317843) * build(deps): bump uuid, @azure/cosmos, @azure/identity and @azure/msal-node Removes [uuid](https://github.com/uuidjs/uuid). It's no longer used after updating ancestor dependencies [uuid](https://github.com/uuidjs/uuid), [@azure/cosmos](https://github.com/Azure/azure-sdk-for-js), [@azure/identity](https://github.com/Azure/azure-sdk-for-js) and [@azure/msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js). These dependencies need to be updated together. Removes `uuid` Updates `@azure/cosmos` from 3.17.3 to 4.9.3 - [Release notes](https://github.com/Azure/azure-sdk-for-js/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md) - [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/cosmos_3.17.3...@azure/cosmos_4.9.3) Updates `@azure/identity` from 4.2.1 to 4.13.1 - [Release notes](https://github.com/Azure/azure-sdk-for-js/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md) - [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/identity_4.2.1...@azure/identity_4.13.1) Updates `@azure/msal-node` from 2.16.1 to 5.2.2 - [Release notes](https://github.com/AzureAD/microsoft-authentication-library-for-js/releases) - [Commits](https://github.com/AzureAD/microsoft-authentication-library-for-js/compare/msal-node-v2.16.1...msal-node-v5.2.2) --- updated-dependencies: - dependency-name: uuid dependency-version: dependency-type: indirect - dependency-name: "@azure/cosmos" dependency-version: 4.9.3 dependency-type: direct:development - dependency-name: "@azure/identity" dependency-version: 4.13.1 dependency-type: direct:development - dependency-name: "@azure/msal-node" dependency-version: 5.2.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] * fix: cast AZDO JSON response to generic type in publish helper Agent-Logs-Url: https://github.com/microsoft/vscode/sessions/281c0c8f-fd7e-49b9-99f3-eb9522cd6cee Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzhao271 <7199958+rzhao271@users.noreply.github.com> --- build/azure-pipelines/common/publish.ts | 2 +- build/package-lock.json | 642 +++++++++++++----------- build/package.json | 6 +- 3 files changed, 365 insertions(+), 285 deletions(-) diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index 711a39e9ad9cb0..4c83e0711e49d8 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -615,7 +615,7 @@ export async function requestAZDOAPI(path: string): Promise { throw new Error(`Unexpected status code: ${res.status}`); } - return await res.json(); + return await res.json() as T; } finally { clearTimeout(timeout); } diff --git a/build/package-lock.json b/build/package-lock.json index 7a33eaa9deb405..d6d5a90b659559 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "devDependencies": { "@azure/core-auth": "^1.9.0", - "@azure/cosmos": "^3", - "@azure/identity": "^4.2.1", - "@azure/msal-node": "^2.16.1", + "@azure/cosmos": "^4", + "@azure/identity": "^4.13.1", + "@azure/msal-node": "^5.2.2", "@azure/storage-blob": "^12.25.0", "@electron/get": "^2.0.0", "@electron/osx-sign": "^2.0.0", @@ -91,44 +91,50 @@ "@azu/format-text": "^1.0.1" } }, - "node_modules/@azure/abort-controller": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz", - "integrity": "sha512-XUyTo+bcyxHEf+jlN2MXA7YU9nxVehaubngHV1MIZZaqYmZqykkoeAz/JMMEeR7t3TcyDwbFa3Zw8BZywmIx4g==", + "node_modules/@azure-rest/core-client": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.6.0.tgz", + "integrity": "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-auth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", - "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dev": true, "license": "MIT", "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/core-client": { @@ -150,45 +156,21 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/core-http-compat": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", - "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", + "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-client": "^1.3.0", - "@azure/core-rest-pipeline": "^1.3.0" + "@azure/abort-controller": "^2.1.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" + "node": ">=20.0.0" }, - "engines": { - "node": ">=18.0.0" + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" } }, "node_modules/@azure/core-lro": { @@ -207,19 +189,6 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@azure/core-paging": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", @@ -234,196 +203,205 @@ } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz", - "integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.8.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-tracing": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", - "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, "license": "MIT", "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-util": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", - "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "node_modules/@azure/core-xml": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz", + "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "tslib": "^2.6.2" + "fast-xml-parser": "^5.0.7", + "tslib": "^2.8.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@azure/cosmos": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.9.3.tgz", + "integrity": "sha512-AWRj+yhw1lybutNcsHJ8syxWXnTLvc3CPwwdCwG1I0I71f25ZcBkxneTeoaB3X57+xl1nO+zJKUqfm0RhpGUFA==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-keys": "^4.9.0", + "@azure/logger": "^1.1.4", + "fast-json-stable-stringify": "^2.1.0", + "priorityqueuejs": "^2.0.0", + "semaphore": "^1.1.0", + "tslib": "^2.8.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/core-xml": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz", - "integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==", + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "dev": true, "license": "MIT", "dependencies": { - "fast-xml-parser": "^5.0.7", - "tslib": "^2.8.1" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@azure/cosmos": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.17.3.tgz", - "integrity": "sha512-wBglkQ6Irjv5Vo2iw8fd6eYj60WYRSSg4/0DBkeOP6BwQ4RA91znsOHd6s3qG6UAbNgYuzC9Nnq07vlFFZkHEw==", + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", - "@azure/core-rest-pipeline": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", - "debug": "^4.1.1", - "fast-json-stable-stringify": "^2.1.0", - "jsbi": "^3.1.3", - "node-abort-controller": "^3.0.0", - "priorityqueuejs": "^1.0.0", - "semaphore": "^1.0.5", - "tslib": "^2.2.0", - "universal-user-agent": "^6.0.0", - "uuid": "^8.3.0" + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" } }, - "node_modules/@azure/identity": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", - "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.5.0", - "@azure/core-client": "^1.4.0", - "@azure/core-rest-pipeline": "^1.1.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.3.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.11.1", - "@azure/msal-node": "^2.9.2", - "events": "^3.0.0", - "jws": "^4.0.0", - "open": "^8.0.0", - "stoppable": "^1.1.0", - "tslib": "^2.2.0" + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@azure/logger": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.1.tgz", - "integrity": "sha512-QYQeaJ+A5x6aMNu8BG5qdsVBnYBop9UMwgUvGihSjf1PdZZXB+c/oMdM2ajKwzobLBh9e9QuMQkN9iL+IxLBLA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/msal-browser": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.17.0.tgz", - "integrity": "sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.11.0.tgz", + "integrity": "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.12.0" + "@azure/msal-common": "16.6.2" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "14.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", - "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.1.tgz", - "integrity": "sha512-1NEFpTmMMT2A7RnZuvRl/hUmJU+GLPjh+ShyIqPktG2PvSd2yvPnzGd/BxIBAAvJG5nr9lH4oYcQXepDbaE7fg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", + "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "14.16.0", - "jsonwebtoken": "^9.0.0", - "uuid": "^8.3.0" + "@azure/msal-common": "16.6.2", + "jsonwebtoken": "^9.0.0" }, "engines": { - "node": ">=16" - } - }, - "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "14.16.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", - "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" + "node": ">=20" } }, "node_modules/@azure/storage-blob": { @@ -451,19 +429,6 @@ "node": ">=18.0.0" } }, - "node_modules/@azure/storage-blob/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1932,6 +1897,21 @@ "@types/node": "*" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@vscode/iconv-lite-umd": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz", @@ -2660,6 +2640,22 @@ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -3033,6 +3029,36 @@ "node": ">=4.0.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -3043,12 +3069,16 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-properties": { @@ -4060,13 +4090,13 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -4154,15 +4184,16 @@ } }, "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4211,6 +4242,25 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4239,15 +4289,19 @@ "optional": true }, "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, + "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isarray": { @@ -4326,12 +4380,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbi": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.4.tgz", - "integrity": "sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg==", - "dev": true - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4381,52 +4429,34 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, + "license": "MIT", "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^1.4.2", - "safe-buffer": "^5.0.1" - } - }, "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4504,12 +4534,61 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -4770,12 +4849,6 @@ "node": ">=10" } }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "dev": true - }, "node_modules/node-addon-api": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", @@ -4970,17 +5043,19 @@ } }, "node_modules/open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, + "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5302,10 +5377,11 @@ } }, "node_modules/priorityqueuejs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", - "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg= sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz", + "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==", + "dev": true, + "license": "MIT" }, "node_modules/process-nextick-args": { "version": "2.0.1", @@ -5596,6 +5672,19 @@ "node": ">=8.0" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5971,16 +6060,6 @@ "dev": true, "optional": true }, - "node_modules/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true, - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, "node_modules/stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -6604,12 +6683,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", - "dev": true - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -6631,15 +6704,6 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "devOptional": true }, - "node_modules/uuid": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", - "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -6965,6 +7029,22 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", diff --git a/build/package.json b/build/package.json index 3cace36754b8ea..d315d8efb272e5 100644 --- a/build/package.json +++ b/build/package.json @@ -4,9 +4,9 @@ "license": "MIT", "devDependencies": { "@azure/core-auth": "^1.9.0", - "@azure/cosmos": "^3", - "@azure/identity": "^4.2.1", - "@azure/msal-node": "^2.16.1", + "@azure/cosmos": "^4", + "@azure/identity": "^4.13.1", + "@azure/msal-node": "^5.2.2", "@azure/storage-blob": "^12.25.0", "@electron/get": "^2.0.0", "@electron/osx-sign": "^2.0.0", From daa3e616eb8b8cbfd919cae5b9f31865d397f901 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 14:41:26 -0700 Subject: [PATCH 08/14] build(deps): bump qs from 6.14.2 to 6.15.2 in /test/mcp (#318052) Bumps [qs](https://github.com/ljharb/qs) from 6.14.2 to 6.15.2. - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.14.2...v6.15.2) --- updated-dependencies: - dependency-name: qs dependency-version: 6.15.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/mcp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index b1f4c7a541de72..f855fbdd26e05f 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -1121,9 +1121,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" From b433c7d5e00834c77f35d4e883d752a4f1127d28 Mon Sep 17 00:00:00 2001 From: Anthony Kim <62267334+anthonykim1@users.noreply.github.com> Date: Fri, 22 May 2026 14:54:51 -0700 Subject: [PATCH 09/14] Mark Publish Node Modules outputs as production (#318028) Mark node_module pipeline artifacts as production --- build/azure-pipelines/darwin/product-build-darwin-ci.yml | 2 +- build/azure-pipelines/darwin/product-build-darwin.yml | 2 +- build/azure-pipelines/linux/product-build-linux-ci.yml | 2 +- build/azure-pipelines/linux/product-build-linux.yml | 2 +- build/azure-pipelines/win32/product-build-win32-ci.yml | 2 +- build/azure-pipelines/win32/product-build-win32.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/azure-pipelines/darwin/product-build-darwin-ci.yml b/build/azure-pipelines/darwin/product-build-darwin-ci.yml index d05dec03ea3ff0..030bf3f9e2821d 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-ci.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-ci.yml @@ -25,7 +25,7 @@ jobs: artifactName: node-modules-macos-$(VSCODE_ARCH)-${{ lower(parameters.VSCODE_TEST_SUITE) }}-$(System.JobAttempt) displayName: Publish Node Modules sbomEnabled: false - isProduction: false + isProduction: true condition: failed() - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/.build/logs diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 339fe6209ae076..a628bb659d97f9 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -36,7 +36,7 @@ jobs: artifactName: node-modules-macos-$(VSCODE_ARCH)-$(System.JobAttempt) displayName: "Publish Node Modules" sbomEnabled: false - isProduction: false + isProduction: true condition: failed() - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/.build/logs diff --git a/build/azure-pipelines/linux/product-build-linux-ci.yml b/build/azure-pipelines/linux/product-build-linux-ci.yml index ef70e180adce74..26e74de0bbaeb9 100644 --- a/build/azure-pipelines/linux/product-build-linux-ci.yml +++ b/build/azure-pipelines/linux/product-build-linux-ci.yml @@ -32,7 +32,7 @@ jobs: artifactName: node-modules-linux-$(VSCODE_ARCH)-${{ lower(parameters.VSCODE_TEST_SUITE) }}-$(System.JobAttempt) displayName: Publish Node Modules sbomEnabled: false - isProduction: false + isProduction: true condition: failed() - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/.build/logs diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 00ffd0aaab07ee..c184f94442b73c 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -52,7 +52,7 @@ jobs: artifactName: node-modules-linux-$(VSCODE_ARCH)-$(System.JobAttempt) displayName: Publish Node Modules sbomEnabled: false - isProduction: false + isProduction: true condition: failed() - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/.build/logs diff --git a/build/azure-pipelines/win32/product-build-win32-ci.yml b/build/azure-pipelines/win32/product-build-win32-ci.yml index f10e44490fab14..2c267fdb5606c9 100644 --- a/build/azure-pipelines/win32/product-build-win32-ci.yml +++ b/build/azure-pipelines/win32/product-build-win32-ci.yml @@ -27,7 +27,7 @@ jobs: artifactName: node-modules-windows-$(VSCODE_ARCH)-${{ lower(parameters.VSCODE_TEST_SUITE) }}-$(System.JobAttempt) displayName: Publish Node Modules sbomEnabled: false - isProduction: false + isProduction: true condition: failed() - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/.build/logs diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 9b4c4e27070abc..188595dcac344c 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -38,7 +38,7 @@ jobs: artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) displayName: Publish Node Modules sbomEnabled: false - isProduction: false + isProduction: true condition: failed() - output: pipelineArtifact targetPath: $(Build.SourcesDirectory)/.build/logs From 6371fe882e3ab49ccc706b62b1cc7428d388d2eb Mon Sep 17 00:00:00 2001 From: dileepyavan <52841896+dileepyavan@users.noreply.github.com> Date: Fri, 22 May 2026 15:24:18 -0700 Subject: [PATCH 10/14] Exclude MXC SDK catalog manifests from Authenticode checks (#318056) --- test/sanity/src/context.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts index 485478438e5525..b568ae3930433c 100644 --- a/test/sanity/src/context.ts +++ b/test/sanity/src/context.ts @@ -32,6 +32,8 @@ interface ITargetMetadata { */ export class TestContext { private static readonly authenticodeInclude = /^.+\.(exe|dll|sys|cab|cat|msi|jar|ocx|ps1|psm1|psd1|ps1xml|pssc1)$/i; + // MXC SDK ships per-arch SPDX catalog manifests that Get-AuthenticodeSignature reports as UnknownError. + private static readonly authenticodeExclude = /[\\/]node_modules[\\/]@microsoft[\\/]mxc-sdk[\\/]bin[\\/][^\\/]+[\\/]_manifest[\\/][^\\/]+[\\/]manifest\.cat$/i; private static readonly versionInfoInclude = /^.+\.(exe|dll|node|msi)$/i; private static readonly versionInfoExclude = /^(dxil\.dll|ffmpeg\.dll|msalruntime\.dll)$/i; private static readonly dpkgLockError = /dpkg frontend lock was locked by another process|unable to acquire the dpkg frontend lock|could not get lock \/var\/lib\/dpkg\/lock-frontend/i; @@ -370,7 +372,11 @@ export class TestContext { if (entry.isDirectory()) { this.collectAuthenticodeFiles(filePath, files); } else if (TestContext.authenticodeInclude.test(entry.name)) { - files.push(filePath); + if (TestContext.authenticodeExclude.test(filePath)) { + this.log(`Skipping excluded file from Authenticode validation: ${filePath}`); + } else { + files.push(filePath); + } } } } From cf77d15bc14d88e806049da251f62c52725412a7 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sat, 23 May 2026 00:52:50 +0200 Subject: [PATCH 11/14] sessions: add max-width to session title in titlebar (#318060) --- .../contrib/sessions/browser/media/sessionsTitleBarWidget.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index b4f7378878e952..1a1b8f1290b2e5 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -92,6 +92,7 @@ .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-label { flex: 0 999 auto; min-width: 0; + max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; From 50b3814f02cadc19b4302b79c34b2fb321aec4bc Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Fri, 22 May 2026 16:22:40 -0700 Subject: [PATCH 12/14] Chat sessions: add legacyResource for one-way URI state migration (#318033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chat sessions: add legacyResource for one-way URI state migration Adds a backwards-compat field on ChatSessionItem so providers can declare that an item was previously known by a different URI. The host adopts archived/pinned/read state stored under the legacy URI forward on first read, then removes the legacy entry — letting providers change their URI shape without losing user state. - vscode.proposed.chatSessionsProvider.d.ts: new `legacyResource?: Uri` field. - IChatSessionItem: matching field. - extHostTypeConverters.ts: propagate through ChatSessionItem.from. - MainThreadChatSessionItem: revive and include in isEqual. - AgentSessionsModel: new private resolveStateEntry helper that consults the legacy URI as a fallback and adopts the entry forward. isArchived and isPinned use it. Cross-scheme and self-referential mappings are rejected. Cache serialization carries legacyResource through restarts. - agentSessionViewModel.test.ts: 7 focused tests covering migration semantics. * Address review: route all state accessors through resolveStateEntry isMarkedUnread and setRead were reading/writing sessionStates directly under the current resource, so an explicit unread marker on the legacy URI could be missed and setArchived's pre-call to setRead could establish an own entry under the new URI before isArchived triggered the migration — orphaning the legacy entry. - isMarkedUnread: route through resolveStateEntry. - isRead: read storedReadDate via resolveStateEntry. - setRead: adopt legacy state forward before composing the new entry. - setArchived/setPinned: read prior state via resolveStateEntry for consistency. - New test: migrates unread marker forward (covers the full per-resource state contract claimed in the docs). --- .../api/browser/mainThreadChatSessions.ts | 5 +- .../api/common/extHostTypeConverters.ts | 1 + .../agentSessions/agentSessionsModel.ts | 52 +++- .../chat/common/chatSessionsService.ts | 7 + .../agentSessionViewModel.test.ts | 237 ++++++++++++++++++ .../vscode.proposed.chatSessionsProvider.d.ts | 20 ++ 6 files changed, 313 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index faceeae5a54bb6..025cd437eff017 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -605,6 +605,7 @@ class MainThreadChatSessionItem implements IChatSessionItem { readonly changes?: IChatSessionItem['changes']; readonly archived?: boolean; readonly metadata?: { readonly [key: string]: unknown }; + readonly legacyResource?: URI; constructor(dto: Dto, model: IChatModel | undefined, detailOverrides: IChatDetail | undefined) { this.resource = URI.revive(dto.resource); @@ -615,6 +616,7 @@ class MainThreadChatSessionItem implements IChatSessionItem { this.tooltip = reviveMarkdownString(dto.tooltip); this.archived = dto.archived; this.metadata = dto.metadata; + this.legacyResource = dto.legacyResource ? URI.revive(dto.legacyResource) : undefined; this.description = (model && getInProgressSessionDescription(model)) ?? reviveMarkdownString(dto.description); this.status = (model && getSessionStatusForModel(model)) ?? dto.status; @@ -647,7 +649,8 @@ class MainThreadChatSessionItem implements IChatSessionItem { && stringOrMarkdownEqual(this.badge, other.badge) && stringOrMarkdownEqual(this.tooltip, other.tooltip) && this.archived === other.archived - && equals(this.metadata, other.metadata); + && equals(this.metadata, other.metadata) + && isEqual(this.legacyResource, other.legacyResource); } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 50934fa8f3da8e..f62a5a7e66c629 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -4241,6 +4241,7 @@ export namespace ChatSessionItem { }, changes: sessionContent.changes instanceof Array ? sessionContent.changes : undefined, metadata: sessionContent.metadata, + legacyResource: sessionContent.legacyResource, }; } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index b8686c5014611c..df0d34d17394cc 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -672,6 +672,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode timing: session.timing, changes: normalizedChanges, metadata: session.metadata, + legacyResource: session.legacyResource, })); } } @@ -716,8 +717,37 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode private readonly sessionStates: ResourceMap; + /** + * Resolve the state entry for a session, honoring a one-way migration from + * {@link IAgentSessionData.legacyResource} when no entry yet exists for the + * session's current resource. Adopts the legacy entry forward (copies it onto + * the current resource key and removes the legacy entry). Returns undefined if + * neither a current nor a legacy entry exists. + */ + private resolveStateEntry(session: IInternalAgentSessionData): IAgentSessionState | undefined { + const own = this.sessionStates.get(session.resource); + if (own !== undefined) { + return own; + } + const legacy = session.legacyResource; + if (!legacy) { + return undefined; + } + // Cross-scheme and self-referential mappings are rejected defensively. + if (legacy.scheme !== session.resource.scheme || legacy.toString() === session.resource.toString()) { + return undefined; + } + const prev = this.sessionStates.get(legacy); + if (prev === undefined) { + return undefined; + } + this.sessionStates.set(session.resource, { ...prev }); + this.sessionStates.delete(legacy); + return this.sessionStates.get(session.resource); + } + private isArchived(session: IInternalAgentSessionData): boolean { - return this.sessionStates.get(session.resource)?.archived ?? Boolean(session.archived); + return this.resolveStateEntry(session)?.archived ?? Boolean(session.archived); } private setArchived(session: IInternalAgentSessionData, archived: boolean): void { @@ -729,7 +759,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return; // no change } - const state = this.sessionStates.get(session.resource) ?? {}; + const state = this.resolveStateEntry(session) ?? {}; this.sessionStates.set(session.resource, { ...state, archived }); const agentSession = this._sessions.get(session.resource); @@ -741,7 +771,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } private isPinned(session: IInternalAgentSessionData): boolean { - return this.sessionStates.get(session.resource)?.pinned ?? false; + return this.resolveStateEntry(session)?.pinned ?? false; } private setPinned(session: IInternalAgentSessionData, pinned: boolean): void { @@ -749,14 +779,14 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return; // no change } - const state = this.sessionStates.get(session.resource) ?? {}; + const state = this.resolveStateEntry(session) ?? {}; this.sessionStates.set(session.resource, { ...state, pinned }); this._onDidChangeSessions.fire(); } private isMarkedUnread(session: IInternalAgentSessionData): boolean { - return this.sessionStates.get(session.resource)?.read === AgentSessionsModel.UNREAD_MARKER; + return this.resolveStateEntry(session)?.read === AgentSessionsModel.UNREAD_MARKER; } private isRead(session: IInternalAgentSessionData): boolean { @@ -764,7 +794,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode return true; // archived sessions are always read } - const storedReadDate = this.sessionStates.get(session.resource)?.read; + const storedReadDate = this.resolveStateEntry(session)?.read; if (storedReadDate === AgentSessionsModel.UNREAD_MARKER) { return false; } @@ -788,7 +818,9 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } private setRead(session: IInternalAgentSessionData, read: boolean, skipEvent?: boolean): void { - const state = this.sessionStates.get(session.resource) ?? {}; + // Adopt any legacy state forward first so we don't establish an own entry + // under the current resource and orphan the legacy one. + const state = this.resolveStateEntry(session) ?? {}; let newRead: number; if (read) { @@ -857,6 +889,8 @@ interface ISerializedAgentSession { readonly metadata: { [key: string]: unknown } | undefined; + readonly legacyResource?: string; + readonly timing: { readonly created: number; readonly lastRequestStarted?: number; @@ -904,7 +938,8 @@ class AgentSessionsCache { timing: session.timing, changes: session.changes, - metadata: session.metadata + metadata: session.metadata, + legacyResource: session.legacyResource?.toString() } satisfies ISerializedAgentSession)); this.storageService.store(AgentSessionsCache.SESSIONS_STORAGE_KEY, safeStringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -946,6 +981,7 @@ class AgentSessionsCache { deletions: change.deletions, })) : session.changes, metadata: session.metadata, + legacyResource: session.legacyResource ? URI.parse(session.legacyResource) : undefined, })); } catch { return []; // invalid data in storage, fallback to empty sessions list diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index c669d0acd784e0..5d01108ec916b7 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -134,6 +134,13 @@ export interface IChatSessionItem { } | readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]; readonly archived?: boolean; readonly metadata?: IChatSessionItemMetadata; + /** + * Resource identifier the item was previously known by. When set, host-stored + * per-resource state (archive, pin, read) recorded under that URI is adopted + * forward onto {@link resource} on first state read, and the legacy entry is + * removed. Scheme must match {@link resource}'s scheme; otherwise ignored. + */ + readonly legacyResource?: URI; } export interface IChatSessionItemMetadata { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts index 9336c6969bc91c..929f0df9f4b3c7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -1410,6 +1410,243 @@ suite('AgentSessions', () => { }); }); + suite('AgentSessionsViewModel - legacyResource migration', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + let viewModel: AgentSessionsModel; + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function uris() { + return { + oldUri: URI.parse(`${chatSessionTestType}://legacy-1`), + newUri: URI.parse(`${chatSessionTestType}://current-1`), + }; + } + + function makeItem(resource: URI, overrides?: Partial): IChatSessionItem { + return { + resource, + label: `Session ${resource.path}`, + timing: makeNewSessionTiming(), + ...overrides, + }; + } + + test('migrates archived state forward from legacyResource to current resource', async () => { + return runWithFakedTimers({}, async () => { + const { oldUri, newUri } = uris(); + // 1. Provider initially emits item under the legacy URI; user archives it. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(oldUri)]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + viewModel.sessions[0].setArchived(true); + + // 2. Provider URI shape changes; new emission carries legacyResource pointing + // at the old URI. Host should adopt the archived state forward. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: oldUri })]), + ); + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.deepStrictEqual( + { resource: session.resource.toString(), archived: session.isArchived() }, + { resource: newUri.toString(), archived: true }, + ); + }); + }); + + test('migrates pinned state forward (not just archived)', async () => { + return runWithFakedTimers({}, async () => { + const { oldUri, newUri } = uris(); + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(oldUri)]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + viewModel.sessions[0].setPinned(true); + + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: oldUri })]), + ); + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.deepStrictEqual( + { pinned: session.isPinned(), archived: session.isArchived() }, + { pinned: true, archived: false }, + ); + }); + }); + + test('migrates unread marker forward (read state, not just archived/pinned)', async () => { + return runWithFakedTimers({}, async () => { + const { oldUri, newUri } = uris(); + // Stage 1: mark the old URI explicitly as unread. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(oldUri)]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + viewModel.sessions[0].setRead(false); + assert.strictEqual(viewModel.sessions[0].isMarkedUnread(), true, 'pre-condition: legacy URI marked unread'); + + // Stage 2: provider URI shape changes; expect the unread marker to migrate + // forward. This proves resolveStateEntry routing covers ALL per-resource + // state (archive, pin, read), not just archived/pinned. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: oldUri })]), + ); + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions[0].isMarkedUnread(), true); + }); + }); + + test('does nothing when no host state exists under legacyResource', async () => { + return runWithFakedTimers({}, async () => { + const { oldUri, newUri } = uris(); + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: oldUri, archived: true })]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + + // Falls back to provider-supplied archived bit; no migration needed. + assert.strictEqual(viewModel.sessions[0].isArchived(), true); + }); + }); + + test('own state wins when both legacy and current URI have host state', async () => { + return runWithFakedTimers({}, async () => { + const { oldUri, newUri } = uris(); + // Stage 1: archive under old URI. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(oldUri)]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + viewModel.sessions[0].setArchived(true); + + // Stage 2: emit new URI (no legacyResource yet) and explicitly toggle archive + // so that host state is established under the new URI (setArchived no-ops on + // values matching the current effective state, so we toggle through true). + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri)]), + ); + await viewModel.resolve(undefined); + viewModel.sessions[0].setArchived(true); + viewModel.sessions[0].setArchived(false); + + // Stage 3: re-emit with legacyResource pointing at the (still-archived) old URI. + // Own (new) entry must win. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: oldUri })]), + ); + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions[0].isArchived(), false); + }); + }); + + test('ignores legacyResource equal to the current resource', async () => { + return runWithFakedTimers({}, async () => { + const { newUri } = uris(); + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: newUri, archived: false })]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + + // Sanity: no infinite loop, falls back to provider value. + assert.strictEqual(viewModel.sessions[0].isArchived(), false); + }); + }); + + test('ignores legacyResource with a different scheme', async () => { + return runWithFakedTimers({}, async () => { + const { newUri } = uris(); + // Pre-archive an item under a different scheme to seed host state there. + const otherScheme = URI.parse('other-scheme://legacy-1'); + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(otherScheme)]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + viewModel.sessions[0].setArchived(true); + + // New emission references the other-scheme legacy URI; migration must be refused. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: otherScheme })]), + ); + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions[0].isArchived(), false); + }); + }); + + test('post-migration setArchived writes under current resource and frees the legacy slot', async () => { + return runWithFakedTimers({}, async () => { + const { oldUri, newUri } = uris(); + // Stage 1: archive under old URI. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(oldUri)]), + ); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + await viewModel.resolve(undefined); + viewModel.sessions[0].setArchived(true); + + // Stage 2: migrate to new URI. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(newUri, { legacyResource: oldUri })]), + ); + await viewModel.resolve(undefined); + viewModel.sessions[0].setArchived(false); + + // Stage 3: provider re-emits the old URI (e.g. backend rollback). Its host + // state should be empty — the legacy entry was consumed by the migration, + // and setArchived(false) wrote to the new URI, not the legacy one. + mockChatSessionsService.registerChatSessionItemController( + chatSessionTestType, + new StaticChatSessionItemController([makeItem(oldUri)]), + ); + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions[0].isArchived(), false); + }); + }); + }); + suite('AgentSessionsViewModel - Session Read State', () => { const disposables = new DisposableStore(); let mockChatSessionsService: MockChatSessionsService; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index b46ec3015d55ef..9f12fbe5db15cb 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -336,6 +336,26 @@ declare module 'vscode' { */ archived?: boolean; + /** + * Resource identifier this item was previously known by. When set, host-stored + * per-resource state (archive, pin, read) recorded under that URI is treated as + * also applying to this item. + * + * On first access of state for {@link resource}, the host adopts the entry + * stored under `legacyResource` forward — copying it onto {@link resource} and + * removing the legacy entry. The migration is transparent: no events fire and + * the effective user-visible state is unchanged. + * + * Intended for providers that need to change the URI shape they emit (e.g. during + * a backend or schema migration) without requiring users to re-archive or re-pin + * items. + * + * The legacy URI's scheme must match {@link resource}'s scheme; otherwise the + * field is ignored. Multi-hop migrations are not supported — providers should + * collapse intermediate hops on their side and emit the original URI. + */ + readonly legacyResource?: Uri; + /** * Timing information for the chat session */ From bf74570b4defe423fc1e7a4090789e9b475d60e9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Fri, 22 May 2026 16:30:30 -0700 Subject: [PATCH 13/14] feat: store MCP OAuth client secrets in OS secret storage (#317950) Some MCP OAuth flows require a client secret in addition to the public client_id. Storing the secret in plain-text mcp.json is unacceptable, so this change moves the secret out of config and into the workbench secret storage service. Changes: - A new "Set/Replace Client Secret" codelens is shown above the oauth.clientId property in mcp.json. The label flips between Set and Replace based on whether a secret is already stored. The codelens is only shown when the server has a url (OAuth requires HTTP). - The codelens opens a QuickInput. When a secret is already stored the input is pre-seeded with the existing value (select-all so it can be replaced or cleared with Backspace), a trash titlebar button deletes the stored secret, and an eye/eye-closed toggle reveals/hides the password masking. - Secrets are keyed by the MCP server URL plus the clientId, so two servers with the same name in different configs (workspace vs user mcp.json) don't collide. - The stored secret is threaded through createDynamicAuthenticationProvider -> IAuthenticationProviderHostDelegate.create -> $registerDynamicAuthProvider so that DynamicAuthProvider is constructed with the resolved secret and uses it in the token exchange. The lookup happens before provider creation in mainThreadMcp.$getTokenFromServerMetadata. - If an MCP auth provider was already registered with a different client secret than the one currently stored (e.g. the user just replaced or deleted it via the QuickInput), the provider is unregistered and re-created so the next token exchange uses the freshly resolved secret. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../api/browser/mainThreadAuthentication.ts | 6 +- src/vs/workbench/api/browser/mainThreadMcp.ts | 38 +++++++- .../contrib/mcp/browser/mcp.contribution.ts | 3 +- .../contrib/mcp/browser/mcpCommands.ts | 94 ++++++++++++++++++- .../mcp/browser/mcpLanguageFeatures.ts | 60 +++++++++++- .../contrib/mcp/common/mcpCommandIds.ts | 1 + .../contrib/mcp/common/mcpConfiguration.ts | 2 +- .../workbench/contrib/mcp/common/mcpTypes.ts | 13 +++ .../browser/authenticationService.ts | 4 +- .../authentication/common/authentication.ts | 4 +- 10 files changed, 211 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 2028933f6b0d1f..a675dafe9347f8 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -154,12 +154,14 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this._register(authenticationService.registerAuthenticationProviderHostDelegate({ // Prefer Node.js extension hosts when they're available. No CORS issues etc. priority: extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1, - create: async (authorizationServer, serverMetadata, resource, overrideClientId) => { + create: async (authorizationServer, serverMetadata, resource, overrideClientId, overrideClientSecret) => { // Auth Provider Id is a combination of the authorization server and the resource, if provided. const authProviderId = resource ? `${authorizationServer.toString(true)} ${resource.resource}` : authorizationServer.toString(true); const clientDetails = await this.dynamicAuthProviderStorageService.getClientRegistration(authProviderId); let clientId = overrideClientId ?? clientDetails?.clientId; - const clientSecret = overrideClientId ? undefined : clientDetails?.clientSecret; + const clientSecret = overrideClientId + ? overrideClientSecret + : (overrideClientSecret ?? clientDetails?.clientSecret); let initialTokens: (IAuthorizationTokenResponse & { created_at: number })[] | undefined = undefined; if (clientId) { initialTokens = await this.dynamicAuthProviderStorageService.getSessionsForDynamicAuthProvider(authProviderId, clientId); diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index a2155428018343..2dca9548f7a652 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -18,9 +18,10 @@ import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/ import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; +import { ISecretStorageService } from '../../../platform/secrets/common/secrets.js'; import { IWorkbenchMcpGatewayService } from '../../contrib/mcp/common/mcpGatewayService.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; -import { extensionPrefixedIdentifier, McpCollectionDefinition, McpCollectionSortOrder, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, McpCollectionDefinition, McpCollectionSortOrder, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, mcpOAuthClientSecretStorageKey, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { IAuthenticationMcpAccessService } from '../../services/authentication/browser/authenticationMcpAccessService.js'; import { IAuthenticationMcpService } from '../../services/authentication/browser/authenticationMcpService.js'; @@ -61,6 +62,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWorkbenchMcpGatewayService private readonly _mcpGatewayService: IWorkbenchMcpGatewayService, + @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { super(); this._register(_authenticationService.onDidChangeSessions(e => this._onDidChangeAuthSessions(e.providerId, e.label))); @@ -236,6 +238,32 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { const resourceServer = authDetails.resourceMetadata?.resource ? URI.parse(authDetails.resourceMetadata.resource) : undefined; const resolvedScopes = authDetails.scopes ?? authDetails.resourceMetadata?.scopes_supported ?? authDetails.authorizationServerMetadata.scopes_supported ?? []; let providerId = await this._authenticationService.getOrActivateProviderIdForServer(authorizationServer, resourceServer); + + const resolvedClientId = clientId ?? authDetails.clientId; + const mcpServerUrl = server.launch.type === McpServerTransportType.HTTP ? server.launch.uri.toString(true) : undefined; + let clientSecret: string | undefined; + let didLookupClientSecret = false; + if (resolvedClientId && mcpServerUrl) { + try { + clientSecret = await this._secretStorageService.get(mcpOAuthClientSecretStorageKey(mcpServerUrl, resolvedClientId)); + didLookupClientSecret = true; + } catch { + // Best-effort lookup; proceed without a client secret. + } + } + + // If the user explicitly configured an OAuth client_id in mcp.json and the stored + // client secret differs from what the existing provider was registered with, force a + // re-registration so the new secret takes effect on subsequent token exchanges. + // Without this, the user can never replace a cached client secret in the extension + // host's DynamicAuthProvider after the provider has been registered. + if (didLookupClientSecret && providerId && !forceNewRegistration && this._authenticationService.isDynamicAuthenticationProvider(providerId)) { + const registered = await this._dynamicAuthenticationProviderStorageService.getClientRegistration(providerId); + if (registered && registered.clientSecret !== clientSecret) { + forceNewRegistration = true; + } + } + if (forceNewRegistration && providerId) { if (!this._authenticationService.isDynamicAuthenticationProvider(providerId)) { throw new Error('Cannot force new registration for a non-dynamic authentication provider.'); @@ -247,14 +275,14 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { } if (!providerId) { - const provider = await this._authenticationService.createDynamicAuthenticationProvider(authorizationServer, authDetails.authorizationServerMetadata, authDetails.resourceMetadata, authDetails.clientId); + const provider = await this._authenticationService.createDynamicAuthenticationProvider(authorizationServer, authDetails.authorizationServerMetadata, authDetails.resourceMetadata, resolvedClientId, clientSecret); if (!provider) { return undefined; } providerId = provider.id; } - return this._getSessionForProvider(id, server, providerId, resolvedScopes, authorizationServer, errorOnUserInteraction, clientId ?? authDetails.clientId, authDetails.resourceMetadata?.resource); + return this._getSessionForProvider(id, server, providerId, resolvedScopes, authorizationServer, errorOnUserInteraction, resolvedClientId, authDetails.resourceMetadata?.resource, clientSecret); } private async _getSessionForProvider( @@ -266,8 +294,9 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { errorOnUserInteraction: boolean = false, clientId?: string, resource?: string, + clientSecret?: string, ): Promise { - const sessions = await this._authenticationService.getSessions(providerId, scopes, { authorizationServer, clientId, resource }, true); + const sessions = await this._authenticationService.getSessions(providerId, scopes, { authorizationServer, clientId, clientSecret, resource }, true); const accountNamePreference = this.authenticationMcpServersService.getAccountPreference(server.id, providerId); let matchingAccountPreferenceSession: AuthenticationSession | undefined; if (accountNamePreference) { @@ -321,6 +350,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { account: accountToCreate, authorizationServer, clientId, + clientSecret, resource }); } while ( diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 06e56c89f9b121..3ebe0342ec1b16 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -40,7 +40,7 @@ import { IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; import { BrowserMcpGatewayService } from './mcpGatewayService.js'; import { McpGatewayToolBrokerContribution } from './mcpGatewayToolBrokerContribution.js'; import { McpAddContextContribution } from './mcpAddContextContribution.js'; -import { AddConfigurationAction, EditStoredInput, InstallFromManifestAction, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, McpConfirmationServerOptionsCommand, MCPServerActionRendering, McpServerOptionsCommand, McpSkipCurrentAutostartCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; +import { AddConfigurationAction, EditStoredInput, InstallFromManifestAction, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, McpConfirmationServerOptionsCommand, MCPServerActionRendering, McpServerOptionsCommand, McpSkipCurrentAutostartCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, SetOAuthClientSecret, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; import { McpDiscovery } from './mcpDiscovery.js'; import { McpElicitationService } from './mcpElicitationService.js'; import { McpLanguageFeatures } from './mcpLanguageFeatures.js'; @@ -82,6 +82,7 @@ registerAction2(AddConfigurationAction); registerAction2(InstallFromManifestAction); registerAction2(RemoveStoredInput); registerAction2(EditStoredInput); +registerAction2(SetOAuthClientSecret); registerAction2(StartServer); registerAction2(StopServer); registerAction2(ShowOutput); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index cd430359345fcb..adf73102fe4df6 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -34,6 +34,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; +import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js'; @@ -61,7 +62,7 @@ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; import { McpCommandIds } from '../common/mcpCommandIds.js'; import { McpContextKeys } from '../common/mcpContextKeys.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; -import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js'; +import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpOAuthClientSecretStorageKey, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js'; import { startServerAndWaitForLiveTools } from '../common/mcpTypesUtils.js'; import { McpAddConfigurationCommand, McpInstallFromManifestCommand } from './mcpCommandsAddConfiguration.js'; import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js'; @@ -783,6 +784,97 @@ export class EditStoredInput extends Action2 { } } +export class SetOAuthClientSecret extends Action2 { + constructor() { + super({ + id: McpCommandIds.SetOAuthClientSecret, + title: localize2('mcp.setOAuthClientSecret', "Set OAuth Client Secret"), + category, + f1: false, + }); + } + + async run(accessor: ServicesAccessor, clientId: string, mcpServerUrl: string, serverName: string): Promise { + const quickInputService = accessor.get(IQuickInputService); + const secretStorageService = accessor.get(ISecretStorageService); + + const key = mcpOAuthClientSecretStorageKey(mcpServerUrl, clientId); + const existing = await secretStorageService.get(key); + + const deleteButton = { + iconClass: ThemeIcon.asClassName(Codicon.trash), + tooltip: localize('mcp.setOAuthClientSecret.delete', "Delete stored client secret"), + }; + const revealButton = { + iconClass: ThemeIcon.asClassName(Codicon.eye), + tooltip: localize('mcp.setOAuthClientSecret.reveal', "Show client secret"), + }; + const hideButton = { + iconClass: ThemeIcon.asClassName(Codicon.eyeClosed), + tooltip: localize('mcp.setOAuthClientSecret.hide', "Hide client secret"), + }; + + const result = await new Promise<{ kind: 'accept'; value: string } | { kind: 'delete' } | undefined>(resolve => { + const input = quickInputService.createInputBox(); + input.title = existing + ? localize('mcp.setOAuthClientSecret.title.replace', "Replace Client Secret for {0}", serverName) + : localize('mcp.setOAuthClientSecret.title.set', "Set Client Secret for {0}", serverName); + input.prompt = localize('mcp.setOAuthClientSecret.prompt', "Enter the client secret for OAuth client '{0}'.", clientId); + input.placeholder = existing + ? localize('mcp.setOAuthClientSecret.placeholder.replace', "Enter a new client secret to replace the stored value") + : localize('mcp.setOAuthClientSecret.placeholder.set', "Enter client secret"); + input.password = true; + input.ignoreFocusOut = true; + if (existing) { + input.value = existing; + input.valueSelection = [0, existing.length]; + } + const updateButtons = () => { + const toggleButton = input.password ? revealButton : hideButton; + input.buttons = existing ? [toggleButton, deleteButton] : [toggleButton]; + }; + updateButtons(); + const disposables = new DisposableStore(); + disposables.add(input.onDidAccept(() => { + const value = input.value; + if (value.length === 0) { + // Empty value: treat as a delete (same as the trash button) + resolve({ kind: 'delete' }); + input.hide(); + return; + } + resolve({ kind: 'accept', value }); + input.hide(); + })); + disposables.add(input.onDidTriggerButton(btn => { + if (btn === deleteButton) { + resolve({ kind: 'delete' }); + input.hide(); + } else if (btn === revealButton || btn === hideButton) { + input.password = !input.password; + updateButtons(); + } + })); + disposables.add(input.onDidHide(() => { + resolve(undefined); + disposables.dispose(); + input.dispose(); + })); + input.show(); + }); + + if (!result) { + return; // cancelled + } + + if (result.kind === 'delete') { + await secretStorageService.delete(key); + } else { + await secretStorageService.set(key, result.value); + } + } +} + export class ShowConfiguration extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts index 9848b1720ceee9..4ee5b4ec1adeb7 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts @@ -10,6 +10,7 @@ import { findNodeAtLocation, Node, parseTree } from '../../../../base/common/jso import { Disposable, DisposableStore, dispose, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; import { Range } from '../../../../editor/common/core/range.js'; import { CodeLens, CodeLensList, CodeLensProvider, InlayHint, InlayHintList } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; @@ -17,6 +18,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan import { localize } from '../../../../nls.js'; import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; @@ -25,7 +27,7 @@ import { McpCommandIds } from '../common/mcpCommandIds.js'; import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { isContributionDisabled } from '../../chat/common/enablement.js'; -import { IMcpConfigPath, IMcpServerStartOpts, IMcpService, IMcpWorkbenchService, McpConnectionState } from '../common/mcpTypes.js'; +import { IMcpConfigPath, IMcpServerStartOpts, IMcpService, IMcpWorkbenchService, McpConnectionState, mcpOAuthClientSecretStorageKey } from '../common/mcpTypes.js'; const diagnosticOwner = 'vscode.mcp'; @@ -43,6 +45,7 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib @IMcpService private readonly _mcpService: IMcpService, @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, + @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { super(); @@ -58,6 +61,11 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib provideCodeLenses: (model, range) => this._provideCodeLenses(model, () => onDidChangeCodeLens.fire(codeLensProvider)), }; this._register(languageFeaturesService.codeLensProvider.register(patterns, codeLensProvider)); + this._register(this._secretStorageService.onDidChangeSecret(key => { + if (key.startsWith('mcp.oauth.clientSecret:')) { + onDidChangeCodeLens.fire(codeLensProvider); + } + })); this._register(languageFeaturesService.inlayHintsProvider.register(patterns, { onDidChangeInlayHints: _mcpRegistry.onDidChangeInputs, @@ -340,6 +348,56 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib } } + // Add "Set/Replace Client Secret" lenses for servers that have oauth.clientId configured. + // Collect candidates first, then batch-resolve secrets with Promise.all to avoid + // sequential awaits for each server (which would slow CodeLens on larger mcp.json files). + type SecretCandidate = { clientId: string; mcpServerUrl: string; serverName: string; clientIdOffset: number }; + const candidates: SecretCandidate[] = []; + for (const node of serversNode.children || []) { + if (node.type !== 'property' || node.children?.[0]?.type !== 'string' || !node.children[1]) { + continue; + } + const serverName = node.children[0].value as string; + const serverValue = node.children[1]; + const clientIdNode = findNodeAtLocation(serverValue, ['oauth', 'clientId']); + if (clientIdNode && clientIdNode.type === 'string') { + const clientId = clientIdNode.value as string; + if (clientId) { + const urlNode = findNodeAtLocation(serverValue, ['url']); + const rawUrl = urlNode && urlNode.type === 'string' ? urlNode.value as string : undefined; + if (!rawUrl) { + continue; // OAuth only meaningful for HTTP servers, which require url + } + // Canonicalize to match the runtime key (URI.parse normalizes authority casing, etc.) + let mcpServerUrl: string; + try { + mcpServerUrl = URI.parse(rawUrl).toString(true); + } catch { + continue; // malformed URL, skip + } + candidates.push({ clientId, mcpServerUrl, serverName, clientIdOffset: clientIdNode.offset }); + } + } + } + const existingSecrets = await Promise.all( + candidates.map(c => this._secretStorageService.get(mcpOAuthClientSecretStorageKey(c.mcpServerUrl, c.clientId))) + ); + for (let i = 0; i < candidates.length; i++) { + const { clientId, mcpServerUrl, serverName, clientIdOffset } = candidates[i]; + const existing = existingSecrets[i]; + const title = existing + ? localize('mcp.replaceClientSecret', "Replace Client Secret") + : localize('mcp.setClientSecret', "Set Client Secret"); + lenses.push({ + range: Range.fromPositions(model.getPositionAt(clientIdOffset)), + command: { + id: McpCommandIds.SetOAuthClientSecret, + title, + arguments: [clientId, mcpServerUrl, serverName], + }, + }); + } + return lensList; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts index 44bbf99a82d1da..9257806938debf 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -26,6 +26,7 @@ export const enum McpCommandIds { RestartServer = 'workbench.mcp.restartServer', ServerOptions = 'workbench.mcp.serverOptions', ServerOptionsInConfirmation = 'workbench.mcp.serverOptionsInConfirmation', + SetOAuthClientSecret = 'workbench.mcp.setOAuthClientSecret', ShowConfiguration = 'workbench.mcp.showConfiguration', ShowInstalled = 'workbench.mcp.showInstalledServers', ShowOutput = 'workbench.mcp.showOutput', diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts index 2261258525ebc7..627c0e43d765fe 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -280,7 +280,7 @@ export const mcpServerSchema: IJSONSchema = { clientId: { type: 'string', minLength: 1, - description: localize('app.mcp.json.oauth.clientId', "The OAuth client ID to use when authenticating with the server.") + markdownDescription: localize('app.mcp.json.oauth.clientId', "The OAuth client ID to use when authenticating with the server. To set the matching client secret securely, use the *Set Client Secret* code lens above this field — secrets are stored in the OS secret store, not in this file.") } } }, diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index f63b7b674cc2db..5943ebd1250050 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -547,6 +547,19 @@ export interface McpServerTransportHTTPOAuth { readonly clientId?: string; } +/** + * Returns the secret-storage key under which an MCP server OAuth client secret is stored. + * Scoped by the MCP server URL AND the OAuth client_id so that two servers sharing the same + * client_id string (e.g. against different authorization servers) cannot clobber each other's + * secret, and so the key is stable across mcp.json configurations that happen to share a label + * (e.g. user mcp.json vs. workspace mcp.json). Set by the "Set Client Secret" code lens in + * mcp.json and read at authentication time so that client secrets are never stored in + * plain-text config files. + */ +export function mcpOAuthClientSecretStorageKey(mcpServerUrl: string, clientId: string): string { + return `mcp.oauth.clientSecret:${mcpServerUrl}:${clientId}`; +} + /** * MCP server launched on the command line which communicated over SSE or Streamable HTTP. * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 73c4ad24af81d7..e57e08edea1063 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -364,13 +364,13 @@ export class AuthenticationService extends Disposable implements IAuthentication return undefined; } - async createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined, clientId?: string): Promise { + async createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined, clientId?: string, clientSecret?: string): Promise { const delegate = this._delegates[0]; if (!delegate) { this._logService.error('No authentication provider host delegate found'); return undefined; } - const providerId = await delegate.create(authorizationServer, serverMetadata, resource, clientId); + const providerId = await delegate.create(authorizationServer, serverMetadata, resource, clientId, clientSecret); const provider = this._authenticationProviders.get(providerId); if (provider) { this._logService.debug(`Created dynamic authentication provider: ${providerId}`); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index c70d7ffce94ebc..2c2c87ccacc489 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -152,7 +152,7 @@ export interface AllowedExtension { export interface IAuthenticationProviderHostDelegate { /** Priority for this delegate, delegates are tested in descending priority order */ readonly priority: number; - create(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined, clientId?: string): Promise; + create(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resource: IAuthorizationProtectedResourceMetadata | undefined, clientId?: string, clientSecret?: string): Promise; } export const IAuthenticationService = createDecorator('IAuthenticationService'); @@ -281,7 +281,7 @@ export interface IAuthenticationService { * Creates a dynamic authentication provider for the given server metadata * @param serverMetadata The metadata for the server that is being authenticated against */ - createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined, clientId?: string): Promise; + createDynamicAuthenticationProvider(authorizationServer: URI, serverMetadata: IAuthorizationServerMetadata, resourceMetadata: IAuthorizationProtectedResourceMetadata | undefined, clientId?: string, clientSecret?: string): Promise; } export function isAuthenticationSession(thing: unknown): thing is AuthenticationSession { From 3027c82e6d4c72f9f649b27d51f7809d50763c07 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 22 May 2026 16:34:58 -0700 Subject: [PATCH 14/14] Reject path traversal in Create Workspace file tree (#318057) - fileTreeParser: reject node names that are empty, '.', '..', or contain '/' or '\\'; throw on unsafe project root names. Filters unsafe child node names from the parsed tree. - newWorkspaceFollowup: replace the platform-aware path.relative destination computation (which resolved a relative projectRoot against process.cwd() on Windows) with a posix prefix-strip helper, resolveProjectFileUri. Add a runtime isUriContained guard before writeFile so any traversal that slips past the parser cannot escape the generated workspace folder. - Tests: cover unsafe node names, the PoC tree, isUriContained edge cases (prefix collision, scheme/authority, trailing slash), and resolveProjectFileUri for both copilot and GitHub repo-template path shapes. --- .../vscode-node/newWorkspaceFollowup.ts | 60 +++++++++- .../test/newWorkspaceFollowup.spec.ts | 103 ++++++++++++++++++ .../extension/prompt/common/fileTreeParser.ts | 19 +++- .../prompt/test/common/fileTreeParser.spec.ts | 36 +++++- .../newWorkspaceFileTreeTraversal.poc.spec.ts | 59 ++++++++++ 5 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 extensions/copilot/src/extension/conversation/vscode-node/test/newWorkspaceFollowup.spec.ts create mode 100644 extensions/copilot/src/extension/prompt/test/common/newWorkspaceFileTreeTraversal.poc.spec.ts diff --git a/extensions/copilot/src/extension/conversation/vscode-node/newWorkspaceFollowup.ts b/extensions/copilot/src/extension/conversation/vscode-node/newWorkspaceFollowup.ts index aea5e6eced9593..1e1ffceea59124 100644 --- a/extensions/copilot/src/extension/conversation/vscode-node/newWorkspaceFollowup.ts +++ b/extensions/copilot/src/extension/conversation/vscode-node/newWorkspaceFollowup.ts @@ -95,8 +95,14 @@ async function createWorkspace(logService: ILogService, workspaceRoot: Uri | und try { await window.withProgress({ location: ProgressLocation.Notification, cancellable: true }, async (progress, token) => { for (const file of files) { - const relativeFilePath = path.relative(projectRoot, file); - const fileUri = Uri.joinPath(workspaceUri, relativeFilePath); + const fileUri = resolveProjectFileUri(workspaceUri, projectRoot, file); + // Guard against path traversal: a malicious or prompt-injected file tree + // can contain `..` segments that escape the generated workspace folder. + // Skip any file whose resolved destination is not contained within it. + if (!isUriContained(workspaceUri, fileUri)) { + logService.warn(`[newIntent] Skipping file outside of workspace: ${file}`); + continue; + } progress.report({ message: l10n.t(`Creating file {0}...`, fileUri.fsPath) }); const content = await workspace.fs.readFile(Uri.joinPath(fileTreePart.baseUri, file)); await workspace.fs.createDirectory(Uri.joinPath(fileUri, '..')); @@ -134,6 +140,56 @@ async function createWorkspace(logService: ILogService, workspaceRoot: Uri | und } } +/** + * Resolves the destination URI for a file emitted by `listFilesInResponseFileTree` + * inside a generated workspace. + * + * The emitted shape depends on the source flow: + * - Copilot new-workspace flow: `/` (no leading slash). + * - GitHub repo-template flow: `/` (leading slash; the repo-name + * level has been collapsed out of the tree). + * - GitHub subdir/non-repo-root: `/`. + * + * In every case the part *inside the project* is what we want to join with + * `workspaceUri`. We compute it by stripping any leading `/` and any leading + * `/` prefix. Any remaining `..` segments are preserved so the + * downstream `isUriContained` check can reject traversal attempts. + * + * Using a string-strip (rather than the platform-aware `path.relative` against + * `cwd`) avoids the Windows-only failure mode where the relative `projectRoot` + * gets resolved against `process.cwd()` and produces a nonsense `..`-laden + * traversal that escapes the workspace. + * + * Exported for tests only. + */ +export function resolveProjectFileUri(workspaceUri: Uri, projectRoot: string, file: string): Uri { + let rel = file.startsWith('/') ? file.slice(1) : file; + const prefix = projectRoot + '/'; + if (rel === projectRoot) { + rel = ''; + } else if (rel.startsWith(prefix)) { + rel = rel.slice(prefix.length); + } + return Uri.joinPath(workspaceUri, rel); +} + +/** + * Returns `true` if `child` resolves to a location at or within `parent`. + * Used to prevent path traversal (`..`) from escaping the generated workspace folder. + * Note: `Uri.joinPath` normalizes `..` segments, so `child.path` is already resolved here. + * + * Exported for tests only. + */ +export function isUriContained(parent: Uri, child: Uri): boolean { + if (parent.scheme !== child.scheme || parent.authority !== child.authority) { + return false; + } + if (child.path === parent.path) { + return true; + } + const parentPath = parent.path.endsWith('/') ? parent.path : parent.path + '/'; + return child.path.startsWith(parentPath); +} async function getUniqueProjectName(projectFolder: Uri, projectName: string): Promise { let i = 0; diff --git a/extensions/copilot/src/extension/conversation/vscode-node/test/newWorkspaceFollowup.spec.ts b/extensions/copilot/src/extension/conversation/vscode-node/test/newWorkspaceFollowup.spec.ts new file mode 100644 index 00000000000000..09c329719f1654 --- /dev/null +++ b/extensions/copilot/src/extension/conversation/vscode-node/test/newWorkspaceFollowup.spec.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri } from 'vscode'; +import { expect, suite, test } from 'vitest'; +import { isUriContained, resolveProjectFileUri } from '../newWorkspaceFollowup'; + +/** + * Unit tests for the path-traversal containment helper guarding the + * "Create Workspace" file-write loop. This is the runtime defense-in-depth + * for the MSRC-reported path traversal in the new-workspace flow. + */ +suite('isUriContained', () => { + const parent = Uri.file('/home/u/proj'); + + test('accepts paths at or within the parent folder', () => { + expect(isUriContained(parent, parent)).toBe(true); + expect(isUriContained(parent, Uri.file('/home/u/proj/a.txt'))).toBe(true); + expect(isUriContained(parent, Uri.file('/home/u/proj/sub/dir/a.txt'))).toBe(true); + }); + + test('rejects sibling paths with a shared name prefix', () => { + // Classic prefix-collision: /home/u/proj vs /home/u/proj-evil + expect(isUriContained(parent, Uri.file('/home/u/proj-evil/x'))).toBe(false); + expect(isUriContained(parent, Uri.file('/home/u/projx'))).toBe(false); + }); + + test('rejects paths that escape the parent via traversal', () => { + // `Uri.joinPath` normalizes `..` before the comparison runs, so we + // supply the resolved URI that the production code would actually see. + expect(isUriContained(parent, Uri.file('/home/u/package.json'))).toBe(false); + expect(isUriContained(parent, Uri.file('/etc/passwd'))).toBe(false); + }); + + test('rejects URIs with mismatched scheme or authority', () => { + const remoteParent = Uri.parse('vscode-remote://host/proj'); + const localChild = Uri.file('/proj/a.txt'); + expect(isUriContained(remoteParent, localChild)).toBe(false); + + const otherHost = Uri.parse('vscode-remote://other/proj/a.txt'); + expect(isUriContained(remoteParent, otherHost)).toBe(false); + + const sameHostChild = Uri.parse('vscode-remote://host/proj/a.txt'); + expect(isUriContained(remoteParent, sameHostChild)).toBe(true); + }); + + test('handles a parent path that already ends with a slash', () => { + const withSlash = parent.with({ path: parent.path + '/' }); + expect(isUriContained(withSlash, Uri.file('/home/u/proj/a.txt'))).toBe(true); + expect(isUriContained(withSlash, Uri.file('/home/u/proj-evil/x'))).toBe(false); + }); +}); + +/** + * Regression test for the GitHub repo-template "Create Workspace" flow. + * + * `listFilesInResponseFileTree` emits paths that may or may not include a + * leading `/` depending on the source (the GitHub repo-template flow emits + * `//...`, the copilot flow emits `/...`). The platform- + * aware `path.relative` previously resolved a relative `projectRoot` against + * `cwd` for the absolute `file`, producing a nonsense `..`-laden traversal that + * `Uri.joinPath` normalized outside `workspaceUri` — so the runtime containment + * guard added by the MSRC fix would silently skip every file. The fix forces + * posix semantics with absolutized inputs. + */ +suite('resolveProjectFileUri', () => { + const workspaceUri = Uri.file('/home/u/parent/myrepo'); + + test('handles a `/`-prefixed file path (GitHub repo-template flow)', () => { + const fileUri = resolveProjectFileUri(workspaceUri, 'myrepo', '/src/index.ts'); + expect(fileUri.path).toBe('/home/u/parent/myrepo/src/index.ts'); + }); + + test('handles a non-prefixed file path (copilot new-workspace flow)', () => { + const fileUri = resolveProjectFileUri(workspaceUri, 'myrepo', 'myrepo/src/index.ts'); + expect(fileUri.path).toBe('/home/u/parent/myrepo/src/index.ts'); + }); + + test('preserves traversal segments so the containment check can reject them', () => { + // A bypass of the parser must still be caught downstream by `isUriContained`. + // `Uri.joinPath` normalizes `..`, so the resolved path escapes the workspace + // and `isUriContained(workspaceUri, fileUri)` will return false. + const fileUri = resolveProjectFileUri(workspaceUri, 'myrepo', 'myrepo/../package.json'); + expect(isUriContained(workspaceUri, fileUri)).toBe(false); + }); + + test('source preview URI built from the unmodified `file` resolves to the expected preview path', () => { + // `createWorkspace` reads preview content via `Uri.joinPath(baseUri, file)` + // using the *unmodified* `file` emitted by `listFilesInResponseFileTree`. + // `Uri.joinPath` is backed by `paths.posix.join`, which treats arguments + // as path segments (it does NOT drop `baseUri.path` when the next segment + // starts with `/`). Asserting this here so the source/destination URI + // shapes do not drift in the future. + const baseUri = Uri.parse('vscode-copilot-github-workspace://req/myrepo'); + // GitHub repo-template flow: emitted file = `/src/index.ts`. + expect(Uri.joinPath(baseUri, '/src/index.ts').path).toBe('/myrepo/src/index.ts'); + // Copilot new-workspace flow: emitted file = `myproject/src/index.ts`. + const copilotBase = Uri.parse('vscode-copilot-workspace://req/myproject'); + expect(Uri.joinPath(copilotBase, 'myproject/src/index.ts').path).toBe('/myproject/myproject/src/index.ts'); + }); +}); diff --git a/extensions/copilot/src/extension/prompt/common/fileTreeParser.ts b/extensions/copilot/src/extension/prompt/common/fileTreeParser.ts index 30e844ed5690da..bd87f7614a14fa 100644 --- a/extensions/copilot/src/extension/prompt/common/fileTreeParser.ts +++ b/extensions/copilot/src/extension/prompt/common/fileTreeParser.ts @@ -31,6 +31,9 @@ export function convertFileTreeToChatResponseFileTree( const fileNode: vscode.ChatResponseFileTree = { name }; if (depth === 0) { + if (isUnsafeNodeName(name)) { + throw new Error(`Invalid project root name in file tree: ${name}`); + } baseUri = generatePreviewURI(name); root.name = name; continue; @@ -100,7 +103,7 @@ function filterChatResponseFileTree(fileTree: vscode.ChatResponseFileTree[]): vs for (const node of fileTree) { - if (!isNodeInFilterList(node)) { + if (!isNodeInFilterList(node) && !isUnsafeNodeName(node.name)) { if (node.children) { node.children = filterChatResponseFileTree(node.children); } @@ -118,3 +121,17 @@ function isNodeInFilterList(node: vscode.ChatResponseFileTree): boolean { return false; } + +/** + * Guards against path traversal: a file tree node name must be a single, plain + * path segment. Names that are empty, the current/parent directory (`.`/`..`), + * or that contain path separators (`/` or `\`) could escape the generated + * workspace folder when joined to a destination path and must be rejected. + */ +export function isUnsafeNodeName(name: string): boolean { + if (!name || name === '.' || name === '..') { + return true; + } + + return name.includes('/') || name.includes('\\'); +} diff --git a/extensions/copilot/src/extension/prompt/test/common/fileTreeParser.spec.ts b/extensions/copilot/src/extension/prompt/test/common/fileTreeParser.spec.ts index 84d75a510b8140..4e28820e9f1fb3 100644 --- a/extensions/copilot/src/extension/prompt/test/common/fileTreeParser.spec.ts +++ b/extensions/copilot/src/extension/prompt/test/common/fileTreeParser.spec.ts @@ -7,7 +7,7 @@ import { expect, suite, test } from 'vitest'; import { URI } from '../../../../util/vs/base/common/uri'; import { ChatResponseFileTreePart } from '../../../../vscodeTypes'; -import { convertFileTreeToChatResponseFileTree } from '../../common/fileTreeParser'; +import { convertFileTreeToChatResponseFileTree, isUnsafeNodeName } from '../../common/fileTreeParser'; suite('convertFileTreeToChatResponseFileTree', () => { const generatePreviewURI = (filename: string) => URI.file(`/preview/${filename}`); @@ -83,4 +83,38 @@ project } ], URI.file('/preview/project'))); }); + + test('should filter out path traversal segments', () => { + const fileStructure = ` +project +├── .. +├── file1.txt +└── file2.txt + `; + + const { chatResponseTree, projectName } = convertFileTreeToChatResponseFileTree(fileStructure, generatePreviewURI); + + expect(projectName).toBe('project'); + expect(chatResponseTree).to.deep.equal(new ChatResponseFileTreePart([ + { + name: 'project', + children: [ + { name: 'file1.txt' }, + { name: 'file2.txt' } + ] + } + ], URI.file('/preview/project'))); + }); +}); + +suite('isUnsafeNodeName', () => { + test('rejects path traversal and separator segments', () => { + expect(['', '.', '..', 'a/b', 'a\\b', '../x', 'foo/'].map(isUnsafeNodeName)) + .to.deep.equal([true, true, true, true, true, true, true]); + }); + + test('accepts plain path segments', () => { + expect(['file.txt', 'src', 'my-project', 'index.ts', '.gitignore'].map(isUnsafeNodeName)) + .to.deep.equal([false, false, false, false, false]); + }); }); diff --git a/extensions/copilot/src/extension/prompt/test/common/newWorkspaceFileTreeTraversal.poc.spec.ts b/extensions/copilot/src/extension/prompt/test/common/newWorkspaceFileTreeTraversal.poc.spec.ts new file mode 100644 index 00000000000000..38c748a733a02e --- /dev/null +++ b/extensions/copilot/src/extension/prompt/test/common/newWorkspaceFileTreeTraversal.poc.spec.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect, suite, test } from 'vitest'; +import { URI } from '../../../../util/vs/base/common/uri'; +import { convertFileTreeToChatResponseFileTree, listFilesInResponseFileTree } from '../../common/fileTreeParser'; + +/** + * Regression test for the "Create Workspace" path-traversal vulnerability. + * + * A malicious or prompt-injected model response could embed `..` segments in the + * generated markdown file tree. When the tree was copied into the user-selected + * parent folder, `path.relative` + `Uri.joinPath` resolved those segments and let + * the write escape the generated workspace folder (e.g. overwriting a sibling's + * `package.json`). + * + * The parser now drops any node whose name is a traversal/separator segment, so + * the escaping path is never produced. (A second, independent containment check + * in `createWorkspace` guards the actual file write as defense in depth.) + */ +suite('newWorkspace file tree traversal (PoC)', () => { + const generatePreviewURI = (filename: string) => URI.file(`/preview/${filename}`); + + test('parser does not emit traversal paths from a malicious tree', () => { + // The PoC tree embeds a `..` directory that points at the parent folder. + const fileStructure = ` +project +├── package.json +├── .. +│ └── package.json +└── safe.txt + `; + + const { chatResponseTree } = convertFileTreeToChatResponseFileTree(fileStructure, generatePreviewURI); + const files = listFilesInResponseFileTree(chatResponseTree.value); + + // No produced path may contain a `..` (or other) traversal segment. + for (const file of files) { + expect(file.split('/')).not.toContain('..'); + expect(file.split('/')).not.toContain('.'); + } + + // The legitimate, in-tree files are still produced. + expect(files).toContain('project/package.json'); + expect(files).toContain('project/safe.txt'); + }); + + test('parser rejects a malicious project root name', () => { + const fileStructure = ` +.. +├── package.json +└── safe.txt + `; + + expect(() => convertFileTreeToChatResponseFileTree(fileStructure, generatePreviewURI)).toThrow(); + }); +});