Skip to content

Surface extensions blocked by the marketplace - #333859

Open
Michael Cummings (MSFT) (mcumming) wants to merge 1 commit into
microsoft:mainfrom
mcumming:dev/mcumming/blocked-extension-reason
Open

Surface extensions blocked by the marketplace#333859
Michael Cummings (MSFT) (mcumming) wants to merge 1 commit into
microsoft:mainfrom
mcumming:dev/mcumming/blocked-extension-reason

Conversation

@mcumming

Copy link
Copy Markdown

The Private Marketplace evaluates an administrative allow/deny policy on every extension query, and until now a denial reached the client only as an absence: the extension was simply not in the results, or the install failed, and the developer had nothing to read. That becomes an internal support ticket, fielded by the same administrators who made the decision.

The registry now returns the record carrying a blocked flag instead of omitting it. This reads that flag and explains the block.

Implements the client half of the Extension Blocking wire contract (spec in review at VSMarketplace.Specs PR 951559). No behaviour changes unless a gallery sets the flag, which no deployed marketplace does yet.

What it does

Blocked extensions are still returned by the gallery, so enforcement lives at the decision points rather than by filtering them out of results:

  • canInstall, on both the platform and workbench services
  • auto-update, and the auto-update toggle
  • the extensionDisallowInstall context key
  • ExtensionStatusAction, where the block takes precedence over deprecation
  • the chat searchExtensionsTool, alongside the existing deprecated and malicious filters

The message distinguishes an organization policy decision from a marketplace statement. Conflating those either alarms users about a routine policy decision or understates a marketplace finding.

Two granularities

The flag applies either to a whole extension or to a single version while the others remain available. Both are read, but they are not distinguished in the result.

A blocked version is skipped when selecting a version to acquire, in display paths as well as install paths. A version pinned out by policy is usually the newest one, so selecting it would strand an extension whose earlier versions are perfectly usable. Because of that, a block only reaches a user once no version is permitted at all - and at that point the extension is unavailable to them whichever granularity caused it, so a version-specific message would only send them looking for an alternative that does not exist.

Install failures now report the actual cause

canInstall computes a precise reason, but checkAndGetCompatibleVersion discarded it and threw a fixed platform-incompatibility message. That was already wrong for an allow list denial, and adding blocked to the same path would have made a blocked extension claim it was unavailable for the machine's platform.

It now throws the message canInstall returned. The error code is deliberately unchanged: both consumers of IncompatibleTargetPlatform - settings sync's skip-and-do-not-retry, and the install error dialog - already do the right thing for a blocked extension.

This only affects paths that bypass the disabled install button: settings sync, --install-extension, the vscode:extension/id URI handler, and workspace recommendations. Those are the ones with no other signal.

Scope

Only the flag and the origin are read. The contract also defines a reason, none of whose values the registry populates today - allow list denials are the only blocks issued, and they carry origin: policy. Origin is kept rather than collapsed to a boolean so that a future marketplace-originated block is not attributed to the user's own organization.

Absent, unrecognized, and wrong-typed values degrade to the more severe reading rather than failing the parse, which would have taken down the whole query rather than the one extension.

Not included, and worth a separate discussion: an extension already installed when a block arrives is gated from updating but is not disabled or reported. The sibling states handle that through EnablementState.DisabledByAllowlist and the extensions notification, which would need the blocked state persisted onto ILocalExtension.

Testing

Unit tests cover the contract's consumer conformance rules: flag without object, object without flag, whole-token flag matching against unknown tokens, degradation of unrecognized, missing, null, and wrong-typed origins, extension blocks outranking version blocks, and the message wording per origin.

Not yet exercised end to end - no deployed marketplace emits the contract, so that needs a stubbed gallery response.

Copilot AI balanced review requested due to automatic review settings September 1, 2026 20:17
The marketplace previously omitted extensions it would not serve, so a
user searching for one saw an empty result and no reason for it. It now
returns the record carrying a `blocked` flag instead, and this reads
that flag and explains the block rather than letting the extension
disappear.

The flag comes at two granularities -- a whole extension, or a single
version while the others remain available -- and both are read. A
blocked version is skipped when selecting one to acquire, so selection
falls through to a permitted version, in display paths as well as
install paths: a version pinned out by policy is usually the newest,
and showing it would strand an extension whose earlier versions are
perfectly usable. The two granularities are not distinguished in the
result, because a block only reaches a user once no version is
permitted, and at that point the extension is unavailable to them
whichever granularity produced it.

Blocked extensions are still returned by the gallery, so enforcement
lives at the decision points -- canInstall, auto-update, and the
extensionDisallowInstall context key -- rather than by filtering them
out of results. The block takes precedence over deprecation, and the
message distinguishes an organization policy decision from a
marketplace statement, since conflating the two either alarms users
about a routine policy decision or understates a marketplace finding.

Report the reason canInstall determined when refusing an install, rather
than discarding it for a fixed platform-incompatibility message. That
message was already wrong for an allow list denial, and adding a second
cause to the same path would have made a blocked extension claim it was
unavailable for the machine's platform, sending the user to look for one
that could run it. The paths reaching this are the ones with no other
signal: settings sync, the command line, and the URI handler.

Only the flag and the origin are read. The contract also defines a
reason, none of whose values the registry populates today: allow-list
denials are the only blocks issued, and they carry origin policy. Origin
is kept rather than collapsed to a boolean because attributing a
marketplace block to the user's own organization would be misleading.
Absent, unrecognized, and wrong-typed values degrade to the marketplace
wording rather than failing the parse, which would have taken down the
whole query rather than the one extension.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2c07f98f-e549-41c2-af5b-7f197c533c8c

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Installed extension models can retain stale blocking metadata and bypass the new UI and chat safeguards.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 1 Medium severity · 3 Low severity

New issues introduced by this change (4)
Severity Finding
Medium severity src/​vs/​workbench/​contrib/​extensions/​browser/​extensionsWorkbenchService.tsblockingInfo can remain stale for installed extensions. queryGallery/getExtensions return an…
Low severity src/​vs/​platform/​extensionManagement/​common/​abstractExtensionManagementService.ts — This multi-line comment restates the branch and error construction. A short note captures the…
Low severity src/​vs/​platform/​extensionManagement/​common/​extensionGalleryService.ts — This long JSDoc duplicates implementation details and caller behavior, so it is likely to drift as…
Low severity src/​vs/​platform/​extensionManagement/​common/​extensionGalleryService.ts — This multi-line inline comment narrates the surrounding selection logic and includes a fallback…
What changed in this PR

Adds client support for marketplace extension-blocking metadata and enforces it across installation, updates, UI, and chat search.

Changes:

  • Parses extension- and version-level blocking metadata.
  • Prevents blocked extension acquisition and surfaces origin-specific messages.
  • Adds contract parsing and message tests.
File Description
searchExtensionsTool.ts Excludes blocked extensions from chat results.
extensions.ts Exposes blocking metadata in the workbench model.
extensionsWorkbenchService.ts Applies blocking to updates and installation.
extensionsActions.ts Disables actions and displays blocked status.
extensionManagement.test.ts Tests origin-specific messages.
extensionGalleryService.test.ts Tests blocking contract parsing.
extensionManagementUtil.ts Builds localized blocking messages.
extensionManagement.ts Defines public blocking types.
extensionGalleryService.ts Parses blocking data and filters versions.
abstractExtensionManagementService.ts Enforces blocks and preserves failure reasons.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +238 to +240
get blockingInfo(): IExtensionBlockingInfo | undefined {
return this.gallery?.blockingInfo;
}
Comment on lines +729 to +731
// Report the reason canInstall determined - a policy block or an allow list
// denial is not a platform incompatibility, and saying so sends the user
// looking for a machine that can run it.
Comment on lines +387 to +397
/**
* Reads the blocked state of an extension at a given version.
*
* At either granularity the flag and the object are two representations of one fact, so either
* alone still means blocked. A producer emitting one without the other is defective, and failing
* safe is the correct response.
*
* The two are not distinguished in the result. A blocked version is skipped when selecting one
* to acquire, so it only reaches a user when no version is permitted - and at that point the
* extension is unavailable to them, whichever granularity produced it.
*/
Comment on lines +1061 to +1065
// A version the registry has blocked is skipped so that selection falls through to a
// permitted one, in display paths as well as install paths: showing a blocked latest
// version would strand an extension whose earlier versions are perfectly usable. When
// no version is permitted the caller's fallback still surfaces the extension, so it
// explains itself rather than vanishing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants