From 001633c2bdc79d49bfbe95add9ba078a6ef2f161 Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Tue, 4 Aug 2026 15:26:58 -0700 Subject: [PATCH] package-firewall: record why VS Code needs a product.json patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design note, no code. Lands ahead of the VS Code ecosystem implementation so the mechanism can be agreed before the implementation is reviewed. The obvious way to point VS Code's extension gallery at the firewall is the ExtensionGalleryServiceUrl enterprise policy: MDM-delivered, survives updates, not user-editable. It cannot carry this, for three reasons verified against the shipping 1.131.0 bundle and upstream release/1.99 + main: 1. It expects an IExtensionGalleryManifest, not a marketplace API root. 2. VS Code gates it behind a GitHub entitlement — no Copilot Business/Enterprise seat means a null manifest, which disables the Extensions view entirely. A failed gate is worse than not deploying. 3. `code --install-extension` never consults it; cliProcessMain.js reads productService.extensionsGallery.serviceUrl directly. Patching product.json has no entitlement gate, needs no manifest endpoint, and covers the CLI for free. Also records what would unblock the policy path (a factory-side manifest endpoint, which would make it an additional delivery option rather than a replacement), why AllowedExtensions is left under admin control rather than generated, and how to verify policy state on a machine. Co-Authored-By: Claude Opus 5 (1M context) --- .../docs/vscode-enterprise-policy.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 package-firewall/docs/vscode-enterprise-policy.md diff --git a/package-firewall/docs/vscode-enterprise-policy.md b/package-firewall/docs/vscode-enterprise-policy.md new file mode 100644 index 0000000..74d38ad --- /dev/null +++ b/package-firewall/docs/vscode-enterprise-policy.md @@ -0,0 +1,126 @@ +# Why VS Code is configured via `product.json` and not an enterprise policy + +VS Code ships an enterprise policy named `ExtensionGalleryServiceUrl`, and it looks like +exactly the right tool for pointing the extension gallery at the Endor Package Firewall: it +is delivered through the MDM channels this repo already uses, it survives VS Code updates, +and it cannot be edited by the developer. + +We do not use it. This note records why, so the question does not have to be re-investigated +from scratch, and what would have to change for it to become viable. + +Findings verified against the shipping **VS Code 1.131.0** bundle +(`Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js`) and upstream source at +tags `release/1.99` and `main`. + +## What the policy is, and where it reaches + +`ExtensionGalleryServiceUrl` backs the hidden, application-scoped setting +`extensions.gallery.serviceUrl` and carries `minimumVersion: "1.99"`. Platform delivery is +genuinely broad: + +| Platform | Mechanism | Available since | +|---|---|---| +| Windows | `HKLM\Software\Policies\Microsoft\VSCode` (ADMX / Intune) | policies from 1.69 | +| macOS | `.mobileconfig`, `PayloadType` `com.microsoft.VSCode`, read from `/Library/Managed Preferences/` by the bundled `@vscode/policy-watcher` | sample profile ships from 1.99 | +| Linux | `/etc/vscode/policy.json` | **1.106** | + +So platform coverage is not the problem. Three other things are. + +## 1. The policy URL must return a gallery *manifest*, not an API root + +`getExtensionGalleryManifestFromServiceUrl()` issues a plain `GET` against the policy value +and parses the body as an `IExtensionGalleryManifest`: + +``` +{ version, resources: [ { id, type } ], capabilities: { … } } +``` + +The firewall endpoint is a VS Marketplace **API root** — the `_apis/public/gallery` +analogue. Pointing the policy at it does not work, because VS Code is asking for a service +description document, not a marketplace. + +## 2. VS Code gates the policy behind a GitHub entitlement + +This is the decisive one. `doGetExtensionGalleryManifest()` → `handleDefaultAccountAccess()`: + +- no signed-in default (GitHub/Copilot) account → status `requiresSignIn` +- otherwise `checkAccess()` requires `account.entitlementsData.access_type_sku` to appear in + `product.json`'s `extensionsGallery.accessSKUs` — the `copilot_enterprise_*` and + `copilot_for_business_*` seat SKUs — **or** `account.enterprise === true` +- failing either yields a `null` manifest, which disables the Extensions view entirely + +That last point matters: a failed gate is *worse* than not deploying the policy at all. A +developer without a Copilot Business/Enterprise seat loses the ability to search or install +any extension, and the failure mode is a sign-in prompt rather than anything that points at +the policy. The gate has been present since `release/1.99`, i.e. for the policy's entire +life — it is the GitHub Enterprise "private marketplace" hook, not a neutral redirect. + +Field reports consistent with the gate failing closed and VS Code silently continuing to use +`product.json`: + +- [microsoft/vscode#246420](https://github.com/microsoft/vscode/issues/246420) — Windows GPO ignored +- [Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5740686/macbook-vscode-use-mobleconfig-file-to-update-exte) — macOS Jamf `.mobileconfig` ignored + +## 3. The policy does not reach the CLI + +`ExtensionGalleryServiceUrl` appears only in `workbench.desktop.main.js`. +`out/vs/code/node/cliProcessMain.js` — which serves `code --install-extension` and +`code --list-extensions` — builds its gallery manifest purely from +`productService.extensionsGallery.serviceUrl`. So even where the policy works, the CLI is an +unmitigated bypass. + +Patching `product.json` covers the CLI for free, because that is the value the CLI reads. + +## Also ruled out + +| Mechanism | Why not | +|---|---| +| `product.overrides.json` | Merged only when `VSCODE_DEV` is set — dev builds only | +| An env var or CLI flag | None exists; there is no `EXTENSIONS_GALLERY` or `--extensions-gallery-service-url` | +| `extensions.gallery.serviceUrl` in user `settings.json` | Same gated code path, and user-writable. The setting is `included: false`, so the policy is otherwise the only way to set it | + +## What would unblock the policy path + +A factory endpoint that serves an `IExtensionGalleryManifest`. That would make the policy a +viable **additional** delivery option — attractive because it is tamper-resistant and needs +no app-bundle write, hence no App Management TCC grant and no `codesign` complaints. + +It would not be a replacement. The entitlement gate would still restrict it to fleets where +every developer holds a Copilot Business/Enterprise seat or a GitHub Enterprise account, and +it would still miss `code --install-extension`. The `product.json` patch would remain the +mechanism with full coverage. + +## What admins can use today, alongside this + +`AllowedExtensions` (setting `extensions.allowed`, `minimumVersion: 1.96`) is a genuinely +useful policy and a good complement: + +- no entitlement gate +- works on Windows, macOS and Linux +- **is** enforced by the CLI as well as the workbench (`cliProcessMain.js` builds a real + `NativePolicyService` / `FilePolicyService` and registers `IAllowedExtensionsService`) + +It takes an object keyed by `*`, `publisher`, or `publisher.name`, with values +`true | false | "stable" | ["1.2.3", …]`. + +This repo does **not** generate one. Endor's filtering criteria change over time, so a +snapshot baked into every client would drift immediately; the gallery URL is where +per-request evaluation belongs. `AllowedExtensions` is static admin intent, so it stays under +admin control — see +[Microsoft's documentation](https://code.visualstudio.com/docs/enterprise/policies) for how +to deploy it. The two compose cleanly: neither replaces the other. + +## Verifying policy state on a machine + +Useful when working out whether a policy is live at all, independently of Endor: + +- Command Palette → **`Developer: Policy Diagnostics`** — shows each policy's definition, + resolved value and source +- **`Developer: Set Log Level… → Trace`**, then Output → **Window**, filtered on + `[Marketplace]`. Expected lines include + `[Marketplace] Enterprise marketplace configured but user not signed in`, + `[Marketplace] User signed in but lacks access to enterprise marketplace`, and + `[Marketplace] Checking Account SKU access for configured gallery ` +- macOS: `sudo /usr/libexec/PlistBuddy -c Print "/Library/Managed Preferences/com.microsoft.VSCode.plist"` +- Windows: `reg query "HKLM\Software\Policies\Microsoft\VSCode"` +- Linux: `cat /etc/vscode/policy.json`