Fixes #2890 — Extensions fail to load due to SEA cache directory path mismatch.
When app.js forks an extension subprocess, it builds the bootstrap path from
cliDistDir (the pkg cache directory, e.g., ~/.copilot/pkg/universal/<ver>/).
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/<ver>/ |
npm/.../copilot-win32-x64/ |
Bootstrap from (app.js) |
universal/<ver>/preloads/ |
universal/<ver>/preloads/ |
preloads/ exists in SEA dir? |
No (only in cache) | No (not shipped in platform pkg) |
The security check in index.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" errorIn launchExtension(), resolve the bootstrap path relative to the SEA binary
directory (via process.execPath) instead of cliDistDir:
// 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.
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:
// 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.
Option A was tested locally on Windows 11 (npm install, CLI 1.0.35) by:
- Copying
preloads/to the SEA binary directory - Patching the minified
app.jsto preferdirname(process.execPath)
Both user-level and project-level extensions loaded successfully after the fix.
- CLI 1.0.34 (confirmed macOS + Windows)
- CLI 1.0.35 (confirmed Windows)