From a76323df6ee5e32d3ec5274460c35d5a623bef9c Mon Sep 17 00:00:00 2001 From: Alexei Polajenko Date: Fri, 24 Apr 2026 10:45:11 -0400 Subject: [PATCH] fix: extension bootstrap path mismatch in launchExtension() Extensions fail to load on macOS and Windows because the forked child process's security check in index.js expects the bootstrap path under dirname(process.execPath), but app.js passes a path from the pkg cache (universal//preloads/). This document describes the root cause analysis and two proposed fixes: - Option A: resolve bootstrap path from SEA binary dir + ship preloads in platform packages - Option B: expand the security check to trust the pkg cache directory Fixes #2890 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fix-extension-bootstrap-path.md | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 fix-extension-bootstrap-path.md diff --git a/fix-extension-bootstrap-path.md b/fix-extension-bootstrap-path.md new file mode 100644 index 00000000..e9bf427c --- /dev/null +++ b/fix-extension-bootstrap-path.md @@ -0,0 +1,84 @@ +# Fix: Extension bootstrap path mismatch in `launchExtension()` + +Fixes #2890 — Extensions fail to load due to SEA cache directory path mismatch. + +## Root Cause + +When `app.js` forks an extension subprocess, it builds the bootstrap path from +`cliDistDir` (the pkg cache directory, e.g., `~/.copilot/pkg/universal//`). + +But the forked child process runs the SEA binary's embedded `index.js`, which +validates the bootstrap path against `dirname(import.meta.url)` — the SEA +binary's own directory. These paths never match: + +| | macOS | Windows (npm) | +|---|---|---| +| SEA dir (`index.js`) | `darwin-arm64//` | `npm/.../copilot-win32-x64/` | +| Bootstrap from (`app.js`) | `universal//preloads/` | `universal//preloads/` | +| `preloads/` exists in SEA dir? | No (only in cache) | No (not shipped in platform pkg) | + +The security check in `index.js`: + +```js +var z = dirname(fileURLToPath(import.meta.url)); // → SEA binary dir +var K = process.argv.find(e => basename(e) === "extension_bootstrap.mjs"); +var Q = K ? resolve(K) : undefined; +var q = Q?.startsWith(resolve(z, "preloads") + sep) ? Q : undefined; +// q is undefined → falls through → "Invalid command format" error +``` + +## Proposed Fix (Option A — smallest change) + +In `launchExtension()`, resolve the bootstrap path relative to the SEA binary +directory (via `process.execPath`) instead of `cliDistDir`: + +```js +// Before (current): +const bootstrapPath = path.join(this.options.cliDistDir, "preloads", "extension_bootstrap.mjs"); + +// After (proposed): +const seaBootstrap = path.join(path.dirname(process.execPath), "preloads", "extension_bootstrap.mjs"); +const cacheBootstrap = path.join(this.options.cliDistDir, "preloads", "extension_bootstrap.mjs"); +const bootstrapPath = fs.existsSync(seaBootstrap) ? seaBootstrap : cacheBootstrap; +``` + +This also requires shipping `preloads/extension_bootstrap.mjs` and +`preloads/extension_sdk_resolver.mjs` in the platform-specific npm packages +(`@github/copilot-win32-x64`, `@github/copilot-darwin-arm64`, etc.). + +**Note:** `COPILOT_SDK_PATH` does not need to change — it correctly points to +the pkg cache's `copilot-sdk/` directory and is resolved via environment +variable in the bootstrap, not via relative path. + +## Alternative Fix (Option B — no platform package changes) + +Instead of requiring `preloads/` in the SEA directory, expand the security check +in `index.js` to accept bootstrap paths from any trusted pkg cache directory: + +```js +// Parent (app.js) passes the trusted directory when forking: +env: { COPILOT_TRUSTED_DIST_DIR: this.options.cliDistDir, ... } + +// Child (index.js) validates against multiple trusted prefixes: +const trustedDirs = [resolve(z, "preloads")]; +if (process.env.COPILOT_TRUSTED_DIST_DIR) { + trustedDirs.push(resolve(process.env.COPILOT_TRUSTED_DIST_DIR, "preloads")); +} +const q = trustedDirs.some(d => Q?.startsWith(d + sep)) ? Q : undefined; +``` + +Option B is more robust (no platform package changes needed) but slightly +loosens the security boundary by trusting an env-var-specified directory. + +## Verified + +Option A was tested locally on Windows 11 (npm install, CLI 1.0.35) by: +1. Copying `preloads/` to the SEA binary directory +2. Patching the minified `app.js` to prefer `dirname(process.execPath)` + +Both user-level and project-level extensions loaded successfully after the fix. + +## Affected Versions + +- CLI 1.0.34 (confirmed macOS + Windows) +- CLI 1.0.35 (confirmed Windows)