From e66c4ff1b7e13f15a26dd44c358bde3b559c513d Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Tue, 4 Aug 2026 12:37:05 -0700 Subject: [PATCH] package-firewall: add VS Code as a target ecosystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes VS Code and VS Code Insiders extension traffic through the Endor Package Firewall on macOS, Linux and Windows, by patching the extensionsGallery object in product.json. Why product.json and not the ExtensionGalleryServiceUrl enterprise policy — that policy looks like the obvious fit, but (a) it expects the URL to return an IExtensionGalleryManifest document rather than a marketplace API root, (b) VS Code gates it behind a signed-in GitHub account holding a Copilot Business/Enterprise seat, and a failed gate disables the Extensions view entirely, and (c) it is never consulted by `code --install-extension`. Details and what would unblock it are in package-firewall/docs/vscode-enterprise-policy.md. The patch sets one key and removes one: serviceUrl -> the firewall's /firewall/vscode/_ak/ endpoint; VS Code derives both /extensionquery and /vscode/{publisher}/{name}/latest from it extensionUrlTemplate -> removed. It is the fallback VS Code uses when the resource API returns 5xx; left in place, a firewall outage would silently resolve versions straight from www.vscode-unpkg.net. Removing it redirects that failure into the firewall's own extensionquery. controlUrl (Microsoft's malicious-extension revocation list), resourceUrlTemplate, itemUrl, publisherUrl, nlsBaseUrl, mcpUrl and accessSKUs are deliberately left alone, and the writer merges key-by-key so keys added by future VS Code versions survive. Because product.json is JSON it can carry neither a sentinel comment nor an env-var reference, so the managed marker is a top-level key that also stores the original extensionsGallery verbatim — removal restores it byte-for-byte. Re-running is a no-op when current; a rotated credential is detected as stale and triggers restore-then-patch, so the captured original is never lost. VS Code replaces product.json on every update (monthly for stable, nightly for Insiders), so an update watcher re-applies the patch: launchd WatchPaths on macOS, a systemd .path/.timer pair (or hourly cron) on Linux, a Scheduled Task on Windows. The remaining race is made countable via repatch_count rather than left invisible. New prerequisites, called out in the READMEs: - macOS Ventura+ needs the App Management (SystemPolicyAppBundles) TCC grant for the MDM agent; root is not exempt. The script detects EPERM and says so. - The gallery token lands in world-readable product.json and cannot not — VS Code offers no indirection. Use a dedicated, separately revocable API key. - Extension downloads still come from Microsoft's CDN by design, so *.vsassets.io and *.vscode-unpkg.net must stay reachable. - codesign --verify will report the bundle as modified. Expected; do not re-sign. endor-vscode.* is deliberately not folded into endor-all.*: it is the only script that writes inside an application bundle and the only one installing a persistent daemon. Also fixes stale documentation predating the user-attribution change: the placeholder tables conflated generation-time and install-time tokens, and both READMEs still described pip/uv/go credentials as env-var references. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 + package-firewall/README.md | 25 +- package-firewall/bash/README.md | 153 ++- package-firewall/bash/generate.sh | 154 ++- package-firewall/bash/lib/common.sh | 899 ++++++++++++++++++ package-firewall/bash/templates/remove.sh | 35 + package-firewall/bash/templates/vscode.sh | 161 ++++ .../docs/vscode-enterprise-policy.md | 126 +++ package-firewall/powershell/README.md | 146 ++- package-firewall/powershell/generate.ps1 | 122 ++- package-firewall/powershell/lib/common.ps1 | 797 ++++++++++++++++ .../powershell/templates/remove.ps1 | 25 + .../powershell/templates/script-header.ps1 | 9 +- .../powershell/templates/vscode.ps1 | 157 +++ .../shared/blocks/vscodegallery.txt | 50 + 15 files changed, 2827 insertions(+), 34 deletions(-) create mode 100644 package-firewall/bash/templates/vscode.sh create mode 100644 package-firewall/docs/vscode-enterprise-policy.md create mode 100644 package-firewall/powershell/templates/vscode.ps1 create mode 100644 package-firewall/shared/blocks/vscodegallery.txt diff --git a/README.md b/README.md index 4fb070c..82af9b6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Scripts and generators for deploying Endor Labs configuration via MDM. Generates self-contained MDM scripts that configure developer machines to route package installations through the [Endor Package Firewall](https://docs.endorlabs.com/integrations/package-firewall). +Covers JavaScript, Python, Go, Maven, and VS Code extensions. + ### [`agent-governance/`](agent-governance/README.md) Generates MDM-deployable Endor Labs audit hooks for every AI coding agent on your fleet. \ No newline at end of file diff --git a/package-firewall/README.md b/package-firewall/README.md index 5fa25df..bf0b3d2 100644 --- a/package-firewall/README.md +++ b/package-firewall/README.md @@ -25,6 +25,18 @@ See each directory's README for generation and deployment instructions. | Python | pip, uv, poetry | | Go | go modules (via GOPROXY) | | Java | Maven (via `~/.m2/settings.xml` mirror); Gradle when it reads `~/.m2` | +| VS Code extensions | VS Code and VS Code Insiders (via `product.json` extension gallery), including `code --install-extension` | + +VS Code has prerequisites the other ecosystems do not — a macOS App Management (TCC) grant, +and a credential that necessarily lands in a world-readable file. Read the +[VS Code prerequisites](bash/README.md#vs-code-prerequisites) before deploying it. + +VS Code is configured by patching `product.json` rather than through VS Code's +`ExtensionGalleryServiceUrl` enterprise policy. That policy looks like the natural fit but is +gated behind a GitHub Copilot Business/Enterprise entitlement, expects a different document +shape, and does not reach `code --install-extension` — +[docs/vscode-enterprise-policy.md](docs/vscode-enterprise-policy.md) records the details and +what would change that. --- @@ -41,9 +53,14 @@ shared/blocks/ ├── pipconf.txt ← pip.conf / pip.ini content ├── uvtoml.txt ← uv.toml content ├── goenv.txt ← go env file content (GOPROXY) -└── mavensettings.txt ← ~/.m2/settings.xml fragment (Maven mirror + server) +├── mavensettings.txt ← ~/.m2/settings.xml fragment (Maven mirror + server) +└── vscodegallery.txt ← product.json extensionsGallery overrides (key-level merge) ``` +`vscodegallery.txt` is the one block that is not written verbatim into a config file: it is +applied as a key-level merge into VS Code's `product.json`, so keys it does not mention are +left exactly as VS Code shipped them. Its syntax is documented in the file itself. + Edit these files to customise what gets written to developer machines. The orchestration scripts (`templates/*.sh` / `templates/*.ps1`) control which files get written and in what order. --- @@ -59,8 +76,14 @@ Running either generator produces these scripts in `out//`: | `endor-go.*` | Configure Go modules only | | `endor-maven.*` | Configure Maven only | | `endor-all.*` | Configure all package managers (single-script deploy) | +| `endor-vscode.*` | Configure VS Code + Insiders extension gallery. **Not** included in `endor-all.*` — deploy alongside it | +| `endor-vscode-repatch.*` | Installed by `endor-vscode.*` and run by the OS after VS Code updates. Written to `out/` only so you can read it; do not upload it | | `endor-remove.*` | Strip all Endor configuration from a machine | +`endor-vscode.*` is deliberately kept out of `endor-all.*`: it is the only script that writes +inside an application bundle and the only one that installs a persistent daemon, so folding it +in would silently widen the blast radius of every existing `endor-all` deployment. + Each generated script is fully self-contained — no external files or dependencies at runtime. > **Security**: add `out/` to `.gitignore`. Generated scripts contain API credentials in plaintext. diff --git a/package-firewall/bash/README.md b/package-firewall/bash/README.md index 6398c3a..c48a3c7 100644 --- a/package-firewall/bash/README.md +++ b/package-firewall/bash/README.md @@ -17,6 +17,7 @@ bash/ │ ├── python.sh ← orchestration: pip / uv config file writes │ ├── go.sh ← orchestration: go env file write │ ├── maven.sh ← orchestration: ~/.m2/settings.xml write (XML-aware) +│ ├── vscode.sh ← orchestration: product.json gallery patch (JSON-aware) │ └── remove.sh ← orchestration: sentinel block removal └── out/ ← generated scripts (gitignore this) └── / @@ -25,6 +26,8 @@ bash/ ├── endor-go.sh ├── endor-maven.sh ├── endor-all.sh + ├── endor-vscode.sh + ├── endor-vscode-repatch.sh └── endor-remove.sh ../shared/blocks/ ← edit these to customise what gets written to config files @@ -35,7 +38,8 @@ bash/ ├── pipconf.txt ← pip.conf content ├── uvtoml.txt ← ~/.config/uv/uv.toml content ├── goenv.txt ← go env file content (path resolved via `go env GOENV`) -└── mavensettings.txt ← ~/.m2/settings.xml fragment (Maven mirror + server) +├── mavensettings.txt ← ~/.m2/settings.xml fragment (Maven mirror + server) +└── vscodegallery.txt ← product.json extensionsGallery overrides (key-level merge) ``` --- @@ -92,7 +96,10 @@ Each script in `out/-/` is **fully self-contained** — no exter | `endor-go.sh` | Team uses Go only | | `endor-maven.sh` | Team uses Java / Maven only | | `endor-all.sh` | Team uses multiple ecosystems — single-script deploy | +| `endor-vscode.sh` | Team uses VS Code and/or VS Code Insiders extensions. **Not** part of `endor-all.sh` — deploy it alongside. See [VS Code prerequisites](#vs-code-prerequisites) first. | +`endor-vscode-repatch.sh` is *not* uploaded. `endor-vscode.sh` embeds and installs it; it +is written out only so you can read what gets installed. Upload the script file. Ensure it runs as **root** — the script detects the logged-in console user internally and writes config files to the correct home directory. @@ -107,15 +114,23 @@ All scripts share a common credential architecture: export ENDOR_API_KEY_ID="..." export ENDOR_API_SECRET="..." export ENDOR_AUTH_B64="..." # base64(key:secret) — used by npm/pnpm/yarn/bun +export ENDOR_ATTR_USER="..." # attributed username — @ +export ENDOR_API_SECRET_B64="..." # base64(secret) export ENDOR_NPM_REGISTRY_URL="..." # used by npm, yarn 2+ -export ENDOR_PYPI_URL="..." # used by uv export POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME="..." export POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD="..." ``` +pip, uv, go and VS Code are **not** in that list: none of them can expand env vars in +their config, so those four get literal credentials baked in at install time. + Shell profiles (`.zshrc`, `.bash_profile`, `.bashrc`) each get a one-line sentinel block that sources this file. Config files reference env vars rather than baking credentials — except pip, which cannot expand env vars. -**Credential rotation**: update `env.sh` on target machines (redeploy MDM script). No config file changes needed. +**Credential rotation**: update `env.sh` on target machines (redeploy MDM script). No config +file changes needed — except for the four consumers that cannot use env vars (pip, uv, go, +VS Code), which the redeployed script rewrites in place. `endor-vscode.sh` detects a rotated +credential as `stale`, restores the original `product.json` gallery, then re-patches it, so +the captured original is never lost across rotations. --- @@ -162,11 +177,11 @@ Writes `~/.config/endor/env.sh` and an Endor-managed block to: | `~/.pip/pip.conf` | pip (legacy path) | Literal — pip cannot expand env vars | | `~/.config/pip/pip.conf` | pip (XDG / Linux standard) | Literal | | `~/Library/Application Support/pip/pip.conf` | pip (macOS primary) | Literal | -| `~/.config/uv/uv.toml` | uv (does **not** read pip.conf) | `${ENDOR_PYPI_URL}` env var ref | +| `~/.config/uv/uv.toml` | uv (does **not** read pip.conf) | literal credential (uv cannot expand env vars) | Key behaviour: - **pip**: uses a named `[endor-firewall]` section — preserves any existing `[global]` settings; credentials are literal (pip limitation) -- **uv**: uv ignores pip.conf entirely; `~/.config/uv/uv.toml` is the user-level global config; references `${ENDOR_PYPI_URL}` +- **uv**: uv ignores pip.conf entirely; `~/.config/uv/uv.toml` is the user-level global config; carries a literal credential baked in at install time - **poetry**: credentials are in `env.sh` as `POETRY_HTTP_BASIC_ENDOR_FIREWALL_*` — no separate write step > For poetry, developers still need to add the source to `pyproject.toml` (URL only, no credentials): @@ -197,6 +212,111 @@ Key behaviour: --- +### `endor-vscode.sh` + +Patches the `extensionsGallery` object in VS Code's `product.json` so extension search, +install, update **and `code --install-extension`** all resolve through the firewall. + +| Install path | Editions | +|---|---| +| `/Applications/Visual Studio Code{, - Insiders}.app/Contents/Resources/app/product.json` | macOS, system | +| `~/Applications/Visual Studio Code{, - Insiders}.app/...` | macOS, per-user | +| `/usr/share/code{,-insiders}/resources/app/product.json` | Linux (.deb / .rpm) | +| `/opt/visual-studio-code{,-insiders}/resources/app/product.json` | Linux (tarball / AUR) | + +Exactly one key is set and one removed; every other key is left as VS Code shipped it, +including keys added by future VS Code versions: + +| Key | Action | Why | +|---|---|---| +| `serviceUrl` | set | VS Code derives both `${serviceUrl}/extensionquery` (search) and `${serviceUrl}/vscode/{publisher}/{name}/latest` (install/update) from it | +| `extensionUrlTemplate` | **removed** | It is the fallback VS Code uses when the resource API returns 5xx. Left in place, a firewall outage would silently resolve versions from `www.vscode-unpkg.net` — bypassing the firewall at precisely the wrong moment. With the key absent that failure retries the firewall's own `extensionquery` instead, and fails the install if that fails too. Fail-closed. | +| `controlUrl` | untouched | Microsoft's malicious-extension revocation list — free defense in depth alongside Endor | +| `resourceUrlTemplate`, `itemUrl`, `publisherUrl`, `nlsBaseUrl`, `mcpUrl`, `accessSKUs` | untouched | README/changelog rendering, marketplace web views, language packs, Copilot entitlement | + +**How enforcement works.** Blocked versions are filtered out of the gallery response, so +they are never offered and cannot be selected. Extension *downloads* still come from +Microsoft's CDN by design — the asset URLs come from the gallery response, and the firewall +filters rather than rewrites them. Consequences worth knowing: + +- Keep `*.vsassets.io` and `*.vscode-unpkg.net` reachable through any egress proxy, or + installs break. +- Enforcement is **discovery-time**. Extensions already installed, sideloaded `.vsix` + files, and anything installed before the patch landed are not retroactively caught. + +**Managed marker instead of a sentinel block.** `product.json` is JSON, so it can carry +neither a `#` comment nor an `${ENDOR_*}` reference. The script adds one top-level key: + +```json +"_endorPackageFirewall": {"schema":1,"namespace":"…","appVersion":"…","via":"awk","originalExtensionsGalleryB64":"…"} +``` + +`originalExtensionsGalleryB64` is the original `extensionsGallery` block verbatim, so +`endor-remove.sh` restores it byte-for-byte. It travels with the file and cannot desync +from it. Re-running is a no-op when already current; a changed credential or namespace is +detected as `stale`, which restores the original first and then re-patches — the script +never patches on top of a patch. + +**The update watcher.** VS Code replaces `product.json` on every update — roughly monthly +for stable, **nightly for Insiders** — on a schedule unrelated to MDM check-in. So +`endor-vscode.sh` also installs a re-apply hook: + +| Platform | Hook | +|---|---| +| macOS | `/Library/LaunchDaemons/com.endorlabs.pkgfirewall.vscode.plist` — `WatchPaths` on each `product.json` *and* its parent directory (the updater swaps the whole directory), plus an hourly `StartInterval` backstop | +| Linux | `endor-vscode-firewall.{service,path,timer}` under systemd; `/etc/cron.hourly/endor-vscode-firewall` when systemd is absent | + +The race is not fully closable: if a developer relaunches VS Code before the watcher fires, +that one session talks to the public marketplace. It is therefore made *countable* rather +than invisible — each re-apply bumps `repatch_count` in the sidecar state, and subsequent +runs print it (`watcher has re-applied the patch 4× (last: …)`). Pass +`--no-vscode-watcher` to skip the hook; the script then says so loudly but still exits 0, +because failing every MDM check-in over a deliberate setting is just alert fatigue. + +Sidecar state (not inside the app bundle, `0600`, root-owned): +`/Library/Application Support/Endor/package-firewall/vscode/` on macOS, +`/var/lib/endor/package-firewall/vscode/` on Linux. + + +#### VS Code prerequisites — read before deploying + +1. **macOS Ventura+ needs the App Management TCC grant.** Writing inside an `.app` bundle + signed by another developer is gated by `SystemPolicyAppBundles`, and **root is not + exempt**. Grant your MDM agent App Management (or Full Disk Access) via a PPPC profile. + Without it the script fails loudly with an explanation rather than silently no-op'ing. +2. **The gallery token is world-readable, and that is unavoidable.** `product.json` is + `root:wheel 0644` and must stay readable by every user who runs VS Code. Because the + credential is a URL path segment, any local user can read a working firewall token, and + it also appears in VS Code's own logs. VS Code offers no env-var indirection in + `product.json` — this is a property of the only delivery channel it gives us. Mitigate + blast radius, not exposure: **use a dedicated, separately revocable API key for VS + Code**, never the same one as npm/PyPI/Maven. +3. **`codesign --verify` will report the bundle as modified.** Expected — `product.json` is + inside the `CodeResources` seal. VS Code still runs (this is how every Open VSX + deployment works), though it may re-prompt for Keychain access once. **Do not re-sign to + "fix" it**: ad-hoc re-signing strips the hardened-runtime entitlements and changes the + designated requirement, which *would* durably break stored GitHub auth. +4. **snap and flatpak installs cannot be patched.** Their payload is mounted read-only. The + script detects them by path, explains, and warns rather than failing silently. Use the + `.deb`/`.rpm`/tarball build instead. +5. **Restart VS Code** after install or removal — `product.json` is read once at startup. + +#### Troubleshooting + +A blocked extension produces an **error line in the Output → Window channel** even though +enforcement is working correctly: + +``` +Error while getting the latest version for the extension . +``` + +That is the expected path: the firewall answers `/latest` with `400`, VS Code classifies it +as a client error and retries `extensionquery`, where the blocked version is filtered out. +Set `Developer: Set Log Level… → Trace` and filter on `[Marketplace]` to see it. There is no +"Marketplace" output channel — those strings are view names. + +--- + ## Customising To change what gets written to a config file on target machines, edit the relevant file in `../shared/blocks/` directly: @@ -211,6 +331,7 @@ To change what gets written to a config file on target machines, edit the releva | `../shared/blocks/uvtoml.txt` | `~/.config/uv/uv.toml` | | `../shared/blocks/goenv.txt` | `~/.config/go/env` | | `../shared/blocks/mavensettings.txt` | `~/.m2/settings.xml` | +| `../shared/blocks/vscodegallery.txt` | VS Code `product.json` → `extensionsGallery` (merged key-by-key, not written verbatim) | To change orchestration logic (which files get written, in what order, with what warnings), edit the relevant `templates/*.sh` file directly. @@ -221,7 +342,14 @@ Both support `{{PLACEHOLDER}}` substitution at generation time and `${ENDOR_VAR} | `{{PLACEHOLDER}}` | Generation time by `generate.sh` | Values baked into the config file (e.g. registry host in a key position) | | `${ENDOR_VAR}` | Runtime by the tool reading the config file | Credential values — kept out of config files, resolved from `env.sh` | -Available placeholders: `{{API_KEY_ID}}`, `{{API_SECRET}}`, `{{NPM_REGISTRY_URL}}`, `{{NPM_REGISTRY_HOST}}`, `{{NPM_AUTH_B64}}`, `{{PYPI_URL}}`, `{{PIP_INDEX_URL}}`, `{{TRUSTED_HOST}}`, `{{GO_PROXY_URL}}`, `{{MAVEN_REGISTRY_URL}}`, `{{NAMESPACE}}`, `{{FQDN}}` +There are in fact **two** rounds of `{{...}}` substitution, because per-developer +attribution (`@`) does not exist until the script runs on the +developer's machine: + +| Placeholder | Resolved | +|---|---| +| `{{NAMESPACE}}`, `{{FQDN}}`, `{{FQDN_HOST}}`, `{{API_KEY_ID}}`, `{{API_SECRET}}`, `{{API_SECRET_B64}}`, `{{NPM_REGISTRY_URL}}`, `{{NPM_REGISTRY_HOST}}`, `{{PYPI_URL}}`, `{{TRUSTED_HOST}}`, `{{MAVEN_REGISTRY_URL}}`, `{{VSCODE_GALLERY_BASE}}` | Generation time, by `substitute()` in `generate.sh` | +| `{{ATTR_USER}}`, `{{NPM_AUTH_B64}}`, `{{PIP_INDEX_URL}}`, `{{ENDOR_PYPI_URL}}`, `{{GO_PROXY_URL}}`, `{{VSCODE_GALLERY_URL}}` | **Install time**, by the orchestration template on the developer's machine | --- @@ -254,7 +382,15 @@ always-auth=true ## Removing the configuration -To remove the Endor firewall configuration from a machine, delete the sentinel block from each file — everything between and including the `BEGIN` and `END` marker lines. +Deploy `endor-remove.sh`. It strips the sentinel block from every managed config file and, +for VS Code, removes the update watcher, restores the original `extensionsGallery` from the +marker, drops the marker, and deletes the sidecar state. Restart VS Code afterwards. + +For the sentinel-block files you can also do it by hand — delete everything between and +including the `BEGIN` and `END` marker lines. `product.json` is the exception: it has no +sentinel block, so restoring it by hand means base64-decoding +`_endorPackageFirewall.originalExtensionsGalleryB64` back over the `extensionsGallery` +object, or simply reinstalling VS Code. You can deploy a removal script that does this automatically: @@ -289,4 +425,7 @@ remove_block() { | `~/.m2/settings.xml` | Contains `${env.*}` references only — no credentials baked in. File is `chmod 600`. | | Shell profiles | Contain a single `source ~/.config/endor/env.sh` line. No credentials. | | API secret in MDM | The generated scripts contain the API key and secret in plaintext (used to write `env.sh`). Restrict access to the MDM policy and the generated `out/` directory. | +| VS Code `product.json` | **Contains the gallery token in a world-readable file (`0644`)** and must, because VS Code reads it as the user and offers no indirection. Any local user can read a working firewall credential, and it also lands in VS Code's own logs. Use a **dedicated, separately revocable API key** for VS Code so rotation and revocation do not disturb the other ecosystems. | +| VS Code sidecar state | `/Library/Application Support/Endor/package-firewall/vscode/` (macOS) or `/var/lib/endor/package-firewall/vscode/` (Linux). Holds the rendered gallery URL for the watcher. `0600`, root-owned — strictly better protected than `product.json` itself. | +| `--dry-run` output | Redacts the VS Code `_ak/` path segment. The other ecosystems echo full credentialed URLs; VS Code deviates deliberately, because this token is a bearer credential in a URL *path* and MDM consoles retain script output for far more people than can read the target file. | | `out/` directory | Add to `.gitignore`. Do not commit generated scripts to source control. | diff --git a/package-firewall/bash/generate.sh b/package-firewall/bash/generate.sh index ec5cd66..a5a792d 100755 --- a/package-firewall/bash/generate.sh +++ b/package-firewall/bash/generate.sh @@ -66,6 +66,10 @@ NPM_REGISTRY_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/npm/" NPM_REGISTRY_HOST="${FQDN_HOST}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/npm/" PYPI_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/pypi/simple/" MAVEN_REGISTRY_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/maven/" +# VS Code carries its credential as a URL path segment (_ak/) rather than +# in userinfo, so only the base is known here; the token is appended at install +# time once the attribution label exists. +VSCODE_GALLERY_BASE="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/vscode" API_SECRET_B64=$(printf '%s' "${ENDOR_API_SECRET}" | base64 | tr -d '\n') # ─── Output directory ───────────────────────────────────────────────────────── @@ -85,7 +89,8 @@ substitute() { -e "s|{{NPM_REGISTRY_HOST}}|${NPM_REGISTRY_HOST}|g" \ -e "s|{{PYPI_URL}}|${PYPI_URL}|g" \ -e "s|{{TRUSTED_HOST}}|${TRUSTED_HOST}|g" \ - -e "s|{{MAVEN_REGISTRY_URL}}|${MAVEN_REGISTRY_URL}|g" + -e "s|{{MAVEN_REGISTRY_URL}}|${MAVEN_REGISTRY_URL}|g" \ + -e "s|{{VSCODE_GALLERY_BASE}}|${VSCODE_GALLERY_BASE}|g" } # inline_common @@ -121,6 +126,7 @@ emit_all_blocks() { emit_block_assignment "UV_BLOCK" "$SHARED_BLOCKS_DIR/uvtoml.txt" emit_block_assignment "GO_BLOCK" "$SHARED_BLOCKS_DIR/goenv.txt" emit_block_assignment "MAVEN_BLOCK" "$SHARED_BLOCKS_DIR/mavensettings.txt" + emit_block_assignment "VSCODE_GALLERY_BLOCK" "$SHARED_BLOCKS_DIR/vscodegallery.txt" echo "# ─────────────────────────────────────────────────────────────────────────────" echo "" } @@ -129,11 +135,13 @@ arg_parsing_block() { cat << 'ARGBLOCK' # ── Argument parsing ────────────────────────────────────────────────────────── DRY_RUN=0 +VSCODE_WATCHER=1 _ENDOR_WARNED=0 for _arg in "$@"; do case "$_arg" in --dry-run) DRY_RUN=1 ;; - *) echo "[endor] Unknown argument: $_arg (supported: --dry-run)" >&2; exit 1 ;; + --no-vscode-watcher) VSCODE_WATCHER=0 ;; + *) echo "[endor] Unknown argument: $_arg (supported: --dry-run, --no-vscode-watcher)" >&2; exit 1 ;; esac done unset _arg @@ -179,6 +187,13 @@ ENDOR_AUTH_B64="$(printf '%s:%s' "$ENDOR_ATTR_USER" "$ENDOR_API_SECRET" | endor_ ENDOR_PYPI_URL="https://$(endor_urlenc_b64 "$ENDOR_ATTR_USER"):$(endor_urlenc_b64 "$ENDOR_API_SECRET")@{{FQDN_HOST}}/v1/namespaces/{{NAMESPACE}}/firewall/pypi/simple/" ENDOR_GO_PROXY_URL="https://$(endor_urlenc_b64 "$ENDOR_ATTR_USER"):$(endor_urlenc_b64 "$ENDOR_API_SECRET")@{{FQDN_HOST}}/v1/namespaces/{{NAMESPACE}}/firewall/go/,direct" +# VS Code cannot send Basic auth for the gallery and cannot expand env vars in +# product.json, so the credential travels as a base64url path segment instead. +# Same attributed username as every other ecosystem — the firewall runs +# applyUserAttribution after resolving the _ak path token. +ENDOR_VSCODE_TOKEN="$(printf '%s:%s' "$ENDOR_ATTR_USER" "$ENDOR_API_SECRET" | endor_b64url)" +ENDOR_VSCODE_GALLERY_URL="{{VSCODE_GALLERY_BASE}}/_ak/${ENDOR_VSCODE_TOKEN}" + # No exports — every consumer is same-process template code inlined below. echo "[endor] user attribution → ${ENDOR_ATTR_LABEL}" @@ -207,22 +222,114 @@ script_header() { echo "" } -# build_script