diff --git a/pipelines/azure-build-publish-all.yml b/pipelines/azure-build-publish-all.yml
index 673f23d13a..c42f447ad0 100644
--- a/pipelines/azure-build-publish-all.yml
+++ b/pipelines/azure-build-publish-all.yml
@@ -972,6 +972,17 @@ stages:
artifact: vscode-shell
displayName: "Download vscode-shell"
+ - pwsh: |
+ $agentDir = "$(Pipeline.Workspace)/agent-server-win32-x64"
+ node "$agentDir/tools/copilotRuntime.mjs" verify-feed --non-interactive
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "The exact Copilot runtime packages are unavailable through the TypeAgent npm feed."
+ exit $LASTEXITCODE
+ }
+ displayName: "Verify Copilot runtime packages in TypeAgent feed"
+ env:
+ TYPEAGENT_FEED_TOKEN: $(System.AccessToken)
+
# Set up internal npm registry (required by org policy).
- bash: |
echo "registry=$INSTALL_REGISTRY" > .npmrc
diff --git a/ts/packages/aiclient/src/copilotModels.ts b/ts/packages/aiclient/src/copilotModels.ts
index 09b35931e1..e87bf4fdd2 100644
--- a/ts/packages/aiclient/src/copilotModels.ts
+++ b/ts/packages/aiclient/src/copilotModels.ts
@@ -160,6 +160,12 @@ export interface CopilotClientOptions {
}
function findCopilotPath(): string {
+ const configuredPath =
+ process.env.TYPEAGENT_COPILOT_CLI_PATH ?? process.env.COPILOT_CLI_PATH;
+ if (configuredPath) {
+ debug(`Using configured copilot CLI: ${configuredPath}`);
+ return configuredPath;
+ }
try {
const isWindows = process.platform === "win32";
const command = isWindows ? "where copilot" : "which copilot";
@@ -207,7 +213,7 @@ async function getClient(
`Failed to start GitHub Copilot CLI client (${target}). ` +
(cliUrl
? `Ensure a Copilot CLI server is running and reachable at '${cliUrl}'.\n`
- : `Ensure 'copilot' is installed and authenticated (try 'copilot auth login').\n`) +
+ : `Run 'node typeagent-serve.mjs setup --provider copilot', or install and authenticate a compatible 'copilot' CLI.\n`) +
`Underlying error: ${err instanceof Error ? err.message : String(err)}`,
);
}
diff --git a/ts/tools/installers/common/package-feed.json b/ts/tools/installers/common/package-feed.json
new file mode 100644
index 0000000000..8a55f041cb
--- /dev/null
+++ b/ts/tools/installers/common/package-feed.json
@@ -0,0 +1,4 @@
+{
+ "registry": "https://pkgs.dev.azure.com/msctoproj/AI_Systems/_packaging/typeagent-feed/npm/registry/",
+ "azureDevOpsResource": "499b84ac-1321-427f-aa17-267ca6975798"
+}
diff --git a/ts/tools/installers/common/register-plugin.mjs b/ts/tools/installers/common/register-plugin.mjs
index ef07c0db95..029bdf8f45 100644
--- a/ts/tools/installers/common/register-plugin.mjs
+++ b/ts/tools/installers/common/register-plugin.mjs
@@ -6,6 +6,9 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+const scriptPath = fileURLToPath(import.meta.url);
function parseArgs(argv) {
const opts = {
@@ -113,6 +116,12 @@ function hasListedEntry(output, identifier) {
return findListedEntry(output, identifier) !== undefined;
}
+export function listedEntryState(output, identifier) {
+ const entry = findListedEntry(output, identifier);
+ if (!entry) return "absent";
+ return /\(disabled\)/i.test(entry) ? "disabled" : "enabled";
+}
+
function quoteCmdArgument(value) {
return `"${value.replace(/%/g, "%%").replace(/"/g, '""')}"`;
}
@@ -501,7 +510,11 @@ function installPlugin(opts, logger) {
logger,
);
const pluginIdentifier = `${opts.pluginName}@${opts.marketplaceName}`;
- if (hasListedEntry(pluginListResult.output, pluginIdentifier)) {
+ const pluginState = listedEntryState(
+ pluginListResult.output,
+ pluginIdentifier,
+ );
+ if (pluginState === "enabled") {
const update = runCopilot(
opts.copilotPath,
["plugin", "update", pluginIdentifier],
@@ -521,6 +534,11 @@ function installPlugin(opts, logger) {
retryUpdateFromCleanSnapshot(opts, logger, pluginIdentifier);
}
} else {
+ if (pluginState === "disabled") {
+ logger.write(
+ `Plugin '${pluginIdentifier}' is available but disabled; installing it to enable the plugin.`,
+ );
+ }
runCopilot(
opts.copilotPath,
["plugin", "install", pluginIdentifier],
@@ -533,9 +551,13 @@ function installPlugin(opts, logger) {
["plugin", "list"],
logger,
);
- if (!hasListedEntry(verifyListResult.output, pluginIdentifier)) {
+ const verifiedState = listedEntryState(
+ verifyListResult.output,
+ pluginIdentifier,
+ );
+ if (verifiedState !== "enabled") {
throw new Error(
- `Plugin verification failed: '${pluginIdentifier}' not found in copilot plugin list.`,
+ `Plugin verification failed: '${pluginIdentifier}' is ${verifiedState}.`,
);
}
@@ -580,10 +602,12 @@ function main() {
process.exit(0);
}
-try {
- main();
-} catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- console.error(`[TypeAgent] Registration failed: ${message}`);
- process.exit(1);
+if (path.resolve(process.argv[1] ?? "") === scriptPath) {
+ try {
+ main();
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(`[TypeAgent] Registration failed: ${message}`);
+ process.exit(1);
+ }
}
diff --git a/ts/tools/installers/wix/TypeAgent-AgentServer.wxs b/ts/tools/installers/wix/TypeAgent-AgentServer.wxs
index 90bdec4006..c95e4d41a9 100644
--- a/ts/tools/installers/wix/TypeAgent-AgentServer.wxs
+++ b/ts/tools/installers/wix/TypeAgent-AgentServer.wxs
@@ -186,6 +186,7 @@
+
@@ -264,6 +265,10 @@
+
+
+
@@ -449,17 +454,16 @@
Impersonate="yes" />
+ Value=""[WindowsFolder]System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -NoProfile -NonInteractive -File "[TYPEAGENTROOT]install-prereqs.ps1" -Provider "[PROVIDER]" -AgentServerDir "[INSTALLFOLDER]." -PluginInstallDir "[TYPEAGENTROOT]." -LogPath "[LocalAppDataFolder]TypeAgent\logs\msi-install-prereqs.log"" />
+
+
+
+
NOT REMOVE~="ALL"
NOT REMOVE~="ALL"
NOT REMOVE~="ALL"
@@ -695,6 +709,7 @@
(NOT REMOVE~="ALL") AND (SHELL="1")
(NOT REMOVE~="ALL") AND (SHELL="1")
(NOT REMOVE~="ALL") AND (SHELL="1")
+ (NOT Installed) AND (PROVIDER="COPILOT") AND (UILevel >= 4)
REMOVE~="ALL"
REMOVE~="ALL"
REMOVE~="ALL"
diff --git a/ts/tools/installers/wix/install-prereqs.ps1 b/ts/tools/installers/wix/install-prereqs.ps1
index fd4517eac8..550902bb40 100644
--- a/ts/tools/installers/wix/install-prereqs.ps1
+++ b/ts/tools/installers/wix/install-prereqs.ps1
@@ -4,28 +4,28 @@
<#
.SYNOPSIS
Provisions the runtime prerequisites the external agent-server variant needs:
- the Claude Code and GitHub Copilot CLIs (resolved from PATH at runtime), plus
- a Node.js >= 22 check. Mirrors the external-CLI step in install-typeagent.ps1
- so an MSI install matches the standalone installer.
+ Claude Code from PATH, the TypeAgent-managed GitHub Copilot runtime, and a
+ Node.js >= 22 check. Mirrors install-typeagent.ps1 so MSI and standalone
+ installs use the same runtime contract.
.DESCRIPTION
The MSI ships the 'external' agent-server variant, which prunes the bundled
- Claude/Copilot runtimes and expects `claude` and `copilot` on PATH. This
- script installs them per-user via `npm i -g` when missing. It is intentionally
- lightweight (no winget/admin/UAC): npm global installs land in the user's npm
- prefix, so it runs impersonated as the installing user. Every failure is
- non-fatal (logged + warned) so a prerequisite hiccup never rolls back the MSI;
- the agent-server install itself succeeds and the user is told what to fix.
+ Claude/Copilot runtimes. Claude remains a per-user global prerequisite.
+ Copilot is installed only for the Copilot provider into TypeAgent's versioned
+ per-user runtime cache by the deployed setup command. Every failure is
+ non-fatal (logged + warned) so a prerequisite hiccup never rolls back the
+ MSI; the agent-server install itself succeeds and setup remains retryable.
.PARAMETER LogPath
Optional log file; the script also writes to stdout (captured by the MSI log).
#>
param(
[string]$LogPath,
- # Authoritative Azure Artifacts npm registry the CLIs are pulled through
- # (public npmjs is blocked by policy; this feed proxies it as upstream).
- # Overridable so the MSI can bake an org-specific feed if needed.
- [string]$FeedRegistry = "https://pkgs.dev.azure.com/msctoproj/AI_Systems/_packaging/typeagent-feed/npm/registry/"
+ [ValidateSet("AISYSTEMS", "COPILOT")]
+ [string]$Provider = "AISYSTEMS",
+ [string]$AgentServerDir = "$env:LOCALAPPDATA\TypeAgent\agent-server",
+ [string]$PluginInstallDir = "$env:LOCALAPPDATA\TypeAgent",
+ [string]$FeedRegistry = ""
)
$ErrorActionPreference = "Continue"
@@ -71,8 +71,8 @@ function Resolve-AzCmd {
}
# Mint a short-lived Azure DevOps bearer token for the feed (feedAuth.ts
-# pattern). Non-interactive when an `az` session exists; attempts a one-time
-# `az login` (browser) fallback otherwise. Returns the token or $null.
+# pattern). Uses an existing `az` session only; interactive sign-in is deferred
+# to the post-install setup command so silent MSI installs never block.
function Get-FeedToken([string]$azCmd) {
if (-not $azCmd) { return $null }
function Invoke-AzToken {
@@ -84,14 +84,6 @@ function Get-FeedToken([string]$azCmd) {
} catch { }
return $null
}
- $token = Invoke-AzToken
- if ($token) { return $token }
- Write-Log " No active 'az' session; attempting 'az login' for feed access."
- try {
- & $azCmd login --only-show-errors 2>&1 | ForEach-Object { Write-Log " $_" }
- } catch {
- Write-Log " WARNING: 'az login' failed: $($_.Exception.Message)"
- }
return (Invoke-AzToken)
}
@@ -99,11 +91,11 @@ function Get-FeedToken([string]$azCmd) {
# the feed. Returns the file path; caller removes its directory when done.
function New-TransientNpmrc([string]$registry, [string]$token) {
$authKey = $registry -replace '^https:', ''
- $base = $registry -replace 'registry/?$', ''
+ $baseAuthKey = $authKey -replace 'registry/?$', ''
$dir = Join-Path $env:TEMP ("ta-npmauth-" + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $dir | Out-Null
$file = Join-Path $dir ".npmrc"
- $content = "$($base):_authToken=$token`n$($authKey):_authToken=$token`n$($authKey):always-auth=true`n"
+ $content = "registry=$registry`n$($baseAuthKey):_authToken=$token`n$($authKey):_authToken=$token`n$($authKey):always-auth=true`n"
Set-Content -Path $file -Value $content -NoNewline -Encoding ascii
return $file
}
@@ -116,29 +108,88 @@ function Install-Cli([string]$command, [string]$package, [string]$friendly, [str
Write-Log " $friendly already on PATH: $((Get-Command $command).Source)"
return
}
- Write-Log " Installing $friendly (npm i -g $package)"
- $npmArgs = @("install", "-g", $package)
- if ($registry -and $userconfig) {
- $npmArgs += @("--registry", $registry, "--userconfig", $userconfig)
+ Write-Log " Installing $friendly from $registry"
+ if (-not $registry -or -not $userconfig) {
+ Write-Log " WARNING: authenticated TypeAgent feed access is required to install $friendly. npm was not invoked."
+ return
}
+ $npmArgs = @("install", "-g", $package, "--registry", $registry, "--userconfig", $userconfig)
try {
& npm @npmArgs 2>&1 | ForEach-Object { Write-Log " $_" }
if ($LASTEXITCODE -ne 0) {
- Write-Log " WARNING: '$package' install exited with code $LASTEXITCODE. Install it manually: npm i -g $package"
+ Write-Log " WARNING: '$package' install exited with code $LASTEXITCODE. Re-run TypeAgent setup after authenticating to the package feed."
return
}
} catch {
- Write-Log " WARNING: '$package' install failed: $($_.Exception.Message). Install it manually: npm i -g $package"
+ Write-Log " WARNING: '$package' install failed: $($_.Exception.Message). Re-run TypeAgent setup after authenticating to the package feed."
return
}
if (Test-Command $command) {
Write-Log " ${friendly}: $((Get-Command $command).Source)"
} else {
- Write-Log " WARNING: '$command' not found on PATH after install (open a new session, or install manually: npm i -g $package)."
+ Write-Log " WARNING: '$command' not found on PATH after install. Open a new session and retry TypeAgent setup."
+ }
+}
+
+function Get-RuntimeManifest {
+ $manifestPath = Join-Path $AgentServerDir "copilot-runtime.json"
+ if (-not (Test-Path $manifestPath)) {
+ return $null
+ }
+ try {
+ return Get-Content -Raw -Path $manifestPath | ConvertFrom-Json
+ } catch {
+ Write-Log " WARNING: could not read Copilot runtime manifest: $($_.Exception.Message)"
+ return $null
+ }
+}
+
+function Invoke-CopilotRuntimeSetup([string]$nodeExe) {
+ $serve = Join-Path $AgentServerDir "typeagent-serve.mjs"
+ if (-not (Test-Path $serve)) {
+ Write-Log " WARNING: Copilot setup launcher not found at $serve."
+ return $null
+ }
+ Write-Log " Installing the SDK-compatible Copilot runtime from the TypeAgent package feed."
+ & $nodeExe $serve setup --provider copilot --runtime-only --non-interactive 2>&1 |
+ ForEach-Object { Write-Log " $_" }
+ if ($LASTEXITCODE -ne 0) {
+ Write-Log " WARNING: Copilot runtime setup is incomplete. Run: node `"$serve`" setup --provider copilot"
+ return $null
+ }
+ $runtimeTool = Join-Path $AgentServerDir "tools\copilotRuntime.mjs"
+ $pathOutput = & $nodeExe $runtimeTool path 2>$null | Select-Object -First 1
+ if ($LASTEXITCODE -eq 0 -and $pathOutput -and (Test-Path $pathOutput)) {
+ Write-Log " Copilot runtime: $pathOutput"
+ return [string]$pathOutput
}
+ Write-Log " WARNING: Copilot runtime was installed but its executable could not be resolved."
+ return $null
}
-Write-Log "Provisioning external-CLI prerequisites (claude, copilot)."
+function Invoke-PluginRegistration([string]$copilotPath) {
+ $registerScript = Join-Path $PluginInstallDir "register-plugin.ps1"
+ if (-not (Test-Path $registerScript)) {
+ Write-Log " WARNING: plugin registration script not found at $registerScript."
+ return
+ }
+ $previousPath = $env:COPILOT_CLI_PATH
+ try {
+ if ($copilotPath) {
+ $env:COPILOT_CLI_PATH = $copilotPath
+ }
+ Write-Log " Registering the TypeAgent Copilot plugin after CLI/runtime resolution."
+ & $registerScript -InstallDir $PluginInstallDir -LogPath (Join-Path $env:LOCALAPPDATA "TypeAgent\logs\msi-register-plugin.log") 2>&1 |
+ ForEach-Object { Write-Log " $_" }
+ if ($LASTEXITCODE -ne 0) {
+ Write-Log " WARNING: Copilot plugin registration is deferred until a usable CLI is available."
+ }
+ } finally {
+ $env:COPILOT_CLI_PATH = $previousPath
+ }
+}
+
+Write-Log "Provisioning external runtime prerequisites for provider $Provider."
# Resolve node from an MSI service context (refreshes PATH + probes managers),
# so a bare `node`/`npm` on the interactive PATH is found here too.
@@ -156,39 +207,47 @@ if (-not $nodeExe) {
}
}
-# --- External CLIs (claude, copilot) -----------------------------------------
+# --- External runtimes -------------------------------------------------------
if (-not (Test-Command npm)) {
- Write-Log " WARNING: npm (ships with Node.js) was not found; cannot install claude/copilot. Install Node.js >= 22, then run: npm i -g @anthropic-ai/claude-code @github/copilot"
+ Write-Log " WARNING: npm (ships with Node.js) was not found; cannot install Claude or the managed Copilot runtime. Install Node.js >= 22 and retry TypeAgent setup."
} else {
# Authenticate to the Azure feed non-interactively (public npmjs is blocked
# by policy; the feed proxies it). The interactive broker tokenHelper in the
# user's ~/.npmrc cannot run in the installer service (session 0), so we mint
# a bearer token via the Azure CLI and pass a transient auth config instead.
- $registry = $null
+ $manifest = Get-RuntimeManifest
+ $registry = if ($FeedRegistry) { $FeedRegistry } elseif ($manifest -and $manifest.registry) { [string]$manifest.registry } else { $null }
$userconfig = $null
$azCmd = Resolve-AzCmd
if (-not $azCmd) {
- Write-Log " WARNING: Azure CLI ('az') not found; cannot authenticate to the package feed. CLIs may fail to install. Install az + run 'az login', then: npm i -g @anthropic-ai/claude-code @github/copilot"
+ Write-Log " WARNING: Azure CLI ('az') not found; authenticated TypeAgent feed installation is unavailable. npm will not use another registry."
} else {
$token = Get-FeedToken $azCmd
- if ($token) {
- $registry = $FeedRegistry
+ if ($token -and $registry) {
$userconfig = New-TransientNpmrc $registry $token
Write-Log " Feed auth ready (registry: $registry)"
} else {
- Write-Log " WARNING: could not obtain a feed access token from 'az'. CLIs may fail to install. Run 'az login', then: npm i -g @anthropic-ai/claude-code @github/copilot"
+ Write-Log " WARNING: TypeAgent feed authentication or configuration is unavailable. npm will not use another registry."
}
}
try {
Install-Cli "claude" "@anthropic-ai/claude-code" "Claude Code CLI" $registry $userconfig
- Install-Cli "copilot" "@github/copilot" "GitHub Copilot CLI" $registry $userconfig
} finally {
if ($userconfig) {
try { Remove-Item (Split-Path -Parent $userconfig) -Recurse -Force -ErrorAction SilentlyContinue } catch { }
}
}
- Write-Log " NOTE: both CLIs require a one-time sign-in (run 'claude' and 'copilot' once) before agent actions work."
+}
+
+$copilotPath = $null
+if ($Provider -eq "COPILOT" -and $nodeExe) {
+ $copilotPath = Invoke-CopilotRuntimeSetup $nodeExe
+}
+Invoke-PluginRegistration $copilotPath
+
+if ($Provider -eq "COPILOT") {
+ Write-Log " NOTE: complete GitHub sign-in with: node `"$AgentServerDir\typeagent-serve.mjs`" setup --provider copilot"
}
Write-Log "Prerequisite provisioning complete."
diff --git a/ts/tools/installers/wix/launch-copilot-setup.ps1 b/ts/tools/installers/wix/launch-copilot-setup.ps1
new file mode 100644
index 0000000000..0f3545cf11
--- /dev/null
+++ b/ts/tools/installers/wix/launch-copilot-setup.ps1
@@ -0,0 +1,62 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$ServePath,
+ [string]$LogPath = "$env:LOCALAPPDATA\TypeAgent\logs\copilot-setup-launch.log"
+)
+
+$ErrorActionPreference = "Stop"
+
+. (Join-Path $PSScriptRoot "resolve-node.ps1")
+
+function Write-LaunchLog([string]$Message) {
+ try {
+ $directory = Split-Path -Parent $LogPath
+ if ($directory -and -not (Test-Path $directory)) {
+ New-Item -ItemType Directory -Path $directory -Force | Out-Null
+ }
+ Add-Content -Path $LogPath -Value ("{0} {1}" -f (Get-Date -Format "s"), $Message)
+ } catch { }
+}
+
+try {
+ $nodeExe = Resolve-NodeExe
+ if (-not $nodeExe) {
+ Write-LaunchLog "Node.js >= 22 was not found; Copilot setup was not launched."
+ exit 0
+ }
+ if (-not (Test-Path $ServePath)) {
+ Write-LaunchLog "TypeAgent setup launcher was not found at $ServePath."
+ exit 0
+ }
+
+ $escapedNode = $nodeExe.Replace("'", "''")
+ $escapedServe = $ServePath.Replace("'", "''")
+ $setupCommand = @"
+`$Host.UI.RawUI.WindowTitle = 'TypeAgent GitHub Copilot Setup'
+& '$escapedNode' '$escapedServe' setup --provider copilot --device-code
+if (`$LASTEXITCODE -eq 0) {
+ Write-Host ''
+ Write-Host 'GitHub Copilot setup completed successfully.' -ForegroundColor Green
+} else {
+ Write-Host ''
+ Write-Host 'GitHub Copilot setup is incomplete. Run this command again to retry:' -ForegroundColor Yellow
+ Write-Host "& '$escapedNode' '$escapedServe' setup --provider copilot --device-code"
+}
+Read-Host 'Press Enter to close'
+"@
+ $encodedCommand = [Convert]::ToBase64String(
+ [Text.Encoding]::Unicode.GetBytes($setupCommand)
+ )
+ $powershellExe = Join-Path $PSHOME "powershell.exe"
+ Start-Process -FilePath $powershellExe -ArgumentList @(
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "-EncodedCommand", $encodedCommand
+ ) | Out-Null
+ Write-LaunchLog "Started interactive GitHub Copilot setup."
+} catch {
+ Write-LaunchLog "Could not launch GitHub Copilot setup: $($_.Exception.Message)"
+}
diff --git a/ts/tools/scripts/bundleAgentServer.mjs b/ts/tools/scripts/bundleAgentServer.mjs
index 1e509c47bf..3111979ceb 100644
--- a/ts/tools/scripts/bundleAgentServer.mjs
+++ b/ts/tools/scripts/bundleAgentServer.mjs
@@ -7,6 +7,7 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { bundleProfileAgents } from "./bundleProductAgents.mjs";
+import { writeCopilotRuntimeManifest } from "./copilotRuntimeManifest.mjs";
import {
bundleEntry,
copyFile,
@@ -232,7 +233,12 @@ export async function bundleAgentServer(args) {
path.join(scriptsDir, "typeagent-serve.mjs"),
path.join(out, "typeagent-serve.mjs"),
);
+ writeCopilotRuntimeManifest(path.join(out, "copilot-runtime.json"), {
+ platform: args.platform,
+ arch: args.arch,
+ });
for (const script of [
+ "copilotRuntime.mjs",
"getKeys.mjs",
"generate-selfhost-config.mjs",
"setup-devtunnel.mjs",
@@ -268,7 +274,7 @@ export async function bundleAgentServer(args) {
if (args.externalCli) {
fs.writeFileSync(
path.join(out, ".typeagent-external-cli"),
- "claude,copilot must be on PATH\n",
+ "claude must be on PATH; copilot uses the TypeAgent managed runtime\n",
);
}
diff --git a/ts/tools/scripts/copilotRuntime.mjs b/ts/tools/scripts/copilotRuntime.mjs
new file mode 100644
index 0000000000..9478d20edd
--- /dev/null
+++ b/ts/tools/scripts/copilotRuntime.mjs
@@ -0,0 +1,722 @@
+#!/usr/bin/env node
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { spawnSync } from "node:child_process";
+import { fileURLToPath, pathToFileURL } from "node:url";
+
+const scriptPath = fileURLToPath(import.meta.url);
+const scriptDir = path.dirname(scriptPath);
+const defaultArtifactDir =
+ path.basename(scriptDir).toLowerCase() === "tools"
+ ? path.dirname(scriptDir)
+ : path.resolve(scriptDir, "..", "..");
+
+function parseArgs(argv) {
+ const options = {
+ command: "setup",
+ artifactDir: defaultArtifactDir,
+ interactive: true,
+ login: true,
+ deviceCode: false,
+ host: "https://github.com",
+ };
+ let commandSet = false;
+ for (let i = 2; i < argv.length; i++) {
+ const value = argv[i];
+ if (!value.startsWith("-") && !commandSet) {
+ options.command = value;
+ commandSet = true;
+ } else if (value === "--artifact-dir") {
+ options.artifactDir = path.resolve(argv[++i]);
+ } else if (value === "--manifest") {
+ options.manifestPath = path.resolve(argv[++i]);
+ } else if (value === "--runtime-root") {
+ options.runtimeRoot = path.resolve(argv[++i]);
+ } else if (value === "--non-interactive") {
+ options.interactive = false;
+ } else if (value === "--runtime-only" || value === "--skip-login") {
+ options.login = false;
+ } else if (value === "--device-code") {
+ options.deviceCode = true;
+ } else if (value === "--host") {
+ options.host = argv[++i];
+ } else if (value === "--help") {
+ options.command = "help";
+ } else {
+ throw new Error(`Unknown argument: ${value}`);
+ }
+ }
+ return options;
+}
+
+export function readCopilotRuntimeManifest(manifestPath) {
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
+ const required = [
+ "sdkVersion",
+ "cliPackage",
+ "cliVersion",
+ "platformPackage",
+ "platformVersion",
+ "registry",
+ "azureDevOpsResource",
+ ];
+ for (const key of required) {
+ if (typeof manifest[key] !== "string" || manifest[key].length === 0) {
+ throw new Error(
+ `Invalid Copilot runtime manifest: missing ${key}.`,
+ );
+ }
+ }
+ if (manifest.cliVersion !== manifest.platformVersion) {
+ throw new Error(
+ "Invalid Copilot runtime manifest: CLI and platform versions differ.",
+ );
+ }
+ if (!manifest.registry.startsWith("https://")) {
+ throw new Error(
+ "Invalid Copilot runtime manifest: registry must use HTTPS.",
+ );
+ }
+ return manifest;
+}
+
+export function defaultRuntimeRoot(env = process.env) {
+ if (env.TYPEAGENT_RUNTIME_ROOT) {
+ return path.resolve(env.TYPEAGENT_RUNTIME_ROOT);
+ }
+ if (process.platform === "win32" && env.LOCALAPPDATA) {
+ return path.join(env.LOCALAPPDATA, "TypeAgent", "runtimes");
+ }
+ return path.join(
+ env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"),
+ "TypeAgent",
+ "runtimes",
+ );
+}
+
+export function managedRuntimeDirectory(manifest, runtimeRoot) {
+ return path.join(
+ runtimeRoot ?? defaultRuntimeRoot(),
+ "copilot",
+ manifest.cliVersion,
+ );
+}
+
+function platformPackageDirectory(runtimeDir, manifest) {
+ return path.join(
+ runtimeDir,
+ "node_modules",
+ ...manifest.platformPackage.split("/"),
+ );
+}
+
+export function resolveInstalledCopilotPath(runtimeDir, manifest) {
+ const packageDir = platformPackageDirectory(runtimeDir, manifest);
+ const packageJsonPath = path.join(packageDir, "package.json");
+ if (!fs.existsSync(packageJsonPath)) {
+ return undefined;
+ }
+ try {
+ const metadata = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
+ if (
+ metadata.name !== manifest.platformPackage ||
+ metadata.version !== manifest.platformVersion ||
+ typeof metadata.bin !== "object" ||
+ metadata.bin === null
+ ) {
+ return undefined;
+ }
+ const target = Object.values(metadata.bin).find(
+ (value) => typeof value === "string",
+ );
+ if (typeof target !== "string") {
+ return undefined;
+ }
+ const executable = path.resolve(packageDir, target);
+ return fs.existsSync(executable) ? executable : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+function run(command, args, options = {}) {
+ const isWindowsShim =
+ process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
+ return spawnSync(command, args, {
+ encoding: options.encoding,
+ env: options.env ?? process.env,
+ stdio: options.stdio,
+ windowsHide: options.windowsHide ?? false,
+ shell: isWindowsShim,
+ });
+}
+
+export function npmInvocation(
+ npmCommand,
+ platform = process.platform,
+ nodeExecutable = process.execPath,
+) {
+ if (platform !== "win32" || !/\.(?:cmd|bat)$/i.test(npmCommand)) {
+ return { command: npmCommand, argsPrefix: [] };
+ }
+
+ const npmDirectory = path.dirname(npmCommand);
+ const npmCli = path.join(
+ npmDirectory,
+ "node_modules",
+ "npm",
+ "bin",
+ "npm-cli.js",
+ );
+ if (!fs.existsSync(npmCli)) {
+ throw new Error(`Could not find npm-cli.js next to ${npmCommand}.`);
+ }
+
+ const bundledNode = path.join(npmDirectory, "node.exe");
+ return {
+ command: fs.existsSync(bundledNode) ? bundledNode : nodeExecutable,
+ argsPrefix: [npmCli],
+ };
+}
+
+function resolveCommand(name) {
+ const lookup =
+ process.platform === "win32"
+ ? run("where.exe", [name], { encoding: "utf8" })
+ : run("which", [name], { encoding: "utf8" });
+ if (lookup.status !== 0) {
+ return undefined;
+ }
+ return lookup.stdout
+ .split(/\r?\n/)
+ .map((entry) => entry.trim())
+ .find(Boolean);
+}
+
+function runNpm(args, options = {}) {
+ const npm =
+ resolveCommand(process.platform === "win32" ? "npm.cmd" : "npm") ??
+ (process.platform === "win32" ? "npm.cmd" : "npm");
+ const invocation = npmInvocation(npm);
+ return run(
+ invocation.command,
+ [...invocation.argsPrefix, ...args],
+ options,
+ );
+}
+
+export function globalCopilotPackageVersion(npmRoot) {
+ const packageJson = path.join(
+ npmRoot,
+ "@github",
+ "copilot",
+ "package.json",
+ );
+ if (!fs.existsSync(packageJson)) {
+ return undefined;
+ }
+ try {
+ const metadata = JSON.parse(fs.readFileSync(packageJson, "utf8"));
+ return metadata.name === "@github/copilot" &&
+ typeof metadata.version === "string"
+ ? metadata.version
+ : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+export function npmInstallArgs(manifest, runtimeDir, userconfig) {
+ return [
+ "install",
+ "--prefix",
+ runtimeDir,
+ "--omit=dev",
+ "--no-save",
+ "--package-lock=false",
+ "--registry",
+ manifest.registry,
+ "--userconfig",
+ userconfig,
+ `${manifest.cliPackage}@${manifest.cliVersion}`,
+ ];
+}
+
+export function transientNpmrcContent(registry, token) {
+ const normalized = registry.endsWith("/") ? registry : `${registry}/`;
+ const authKey = normalized.replace(/^https:/, "");
+ const baseAuthKey = authKey.replace(/registry\/?$/, "");
+ return (
+ `registry=${normalized}\n` +
+ `${baseAuthKey}:_authToken=${token}\n` +
+ `${authKey}:_authToken=${token}\n` +
+ `${authKey}:always-auth=true\n`
+ );
+}
+
+function writeTransientNpmrc(registry, token) {
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ta-npmauth-"));
+ const userconfig = path.join(directory, ".npmrc");
+ fs.writeFileSync(userconfig, transientNpmrcContent(registry, token), {
+ encoding: "utf8",
+ mode: 0o600,
+ });
+ return userconfig;
+}
+
+function acquireFeedToken(manifest, interactive) {
+ if (process.env.TYPEAGENT_FEED_TOKEN) {
+ return process.env.TYPEAGENT_FEED_TOKEN;
+ }
+ const getToken = () =>
+ run(
+ process.platform === "win32" ? "az.cmd" : "az",
+ [
+ "account",
+ "get-access-token",
+ "--resource",
+ manifest.azureDevOpsResource,
+ "--output",
+ "json",
+ "--only-show-errors",
+ ],
+ { encoding: "utf8", windowsHide: true },
+ );
+
+ let result = getToken();
+ if (result.status !== 0 && interactive) {
+ console.log(
+ "TypeAgent package feed sign-in is required before downloading GitHub Copilot.",
+ );
+ const login = run(
+ process.platform === "win32" ? "az.cmd" : "az",
+ ["login", "--only-show-errors"],
+ { stdio: "inherit" },
+ );
+ if (login.status === 0) {
+ result = getToken();
+ }
+ }
+ if (result.status !== 0) {
+ throw new Error(
+ "Could not authenticate to the TypeAgent package feed. Install Azure CLI, run 'az login', and retry.",
+ );
+ }
+ try {
+ const token = JSON.parse(result.stdout).accessToken;
+ if (typeof token === "string" && token.length > 0) {
+ return token;
+ }
+ } catch {}
+ throw new Error(
+ "Azure CLI did not return a usable TypeAgent package feed token.",
+ );
+}
+
+export function npmViewArgs(manifest, packageName, version, userconfig) {
+ return [
+ "view",
+ `${packageName}@${version}`,
+ "version",
+ "--registry",
+ manifest.registry,
+ "--userconfig",
+ userconfig,
+ ];
+}
+
+function verifyFeedPackage(manifest, packageName, version, userconfig) {
+ const result = runNpm(
+ npmViewArgs(manifest, packageName, version, userconfig),
+ { encoding: "utf8", windowsHide: true },
+ );
+ if (result.status !== 0 || result.stdout.trim() !== version) {
+ const detail = `${result.stderr ?? result.stdout ?? ""}`.trim();
+ throw new Error(
+ `The TypeAgent package feed could not resolve ${packageName}@${version}${detail ? `: ${detail}` : "."}`,
+ );
+ }
+}
+
+function verifyCopilotRuntimeFeed(manifest, interactive) {
+ const token = acquireFeedToken(manifest, interactive);
+ const userconfig = writeTransientNpmrc(manifest.registry, token);
+ try {
+ verifyFeedPackage(
+ manifest,
+ manifest.cliPackage,
+ manifest.cliVersion,
+ userconfig,
+ );
+ verifyFeedPackage(
+ manifest,
+ manifest.platformPackage,
+ manifest.platformVersion,
+ userconfig,
+ );
+ console.log(
+ `Verified ${manifest.cliPackage}@${manifest.cliVersion} and ${manifest.platformPackage}@${manifest.platformVersion} through the TypeAgent package feed.`,
+ );
+ } finally {
+ fs.rmSync(path.dirname(userconfig), { recursive: true, force: true });
+ }
+}
+
+export function setupStatePath(runtimeRoot = defaultRuntimeRoot()) {
+ return path.join(path.dirname(runtimeRoot), "setup", "copilot.json");
+}
+
+function writeSetupState(runtimeRoot, state) {
+ const statePath = setupStatePath(runtimeRoot);
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
+ fs.writeFileSync(
+ statePath,
+ `${JSON.stringify({ updatedAt: new Date().toISOString(), ...state }, null, 2)}\n`,
+ );
+}
+
+export function installManagedCopilotRuntime(
+ manifest,
+ { runtimeRoot = defaultRuntimeRoot(), interactive = true } = {},
+) {
+ const finalDirectory = managedRuntimeDirectory(manifest, runtimeRoot);
+ const existing = resolveInstalledCopilotPath(finalDirectory, manifest);
+ if (existing) {
+ return existing;
+ }
+
+ const token = acquireFeedToken(manifest, interactive);
+ const parent = path.dirname(finalDirectory);
+ fs.mkdirSync(parent, { recursive: true });
+ const temporaryDirectory = fs.mkdtempSync(
+ path.join(parent, `.tmp-${manifest.cliVersion}-`),
+ );
+ const userconfig = writeTransientNpmrc(manifest.registry, token);
+ try {
+ console.log(
+ `Installing ${manifest.cliPackage}@${manifest.cliVersion} from the TypeAgent package feed...`,
+ );
+ const result = runNpm(
+ npmInstallArgs(manifest, temporaryDirectory, userconfig),
+ { stdio: "inherit" },
+ );
+ if (result.status !== 0) {
+ throw new Error(
+ `npm exited with code ${result.status ?? "unknown"} while installing the Copilot runtime.`,
+ );
+ }
+ const executable = resolveInstalledCopilotPath(
+ temporaryDirectory,
+ manifest,
+ );
+ if (!executable) {
+ throw new Error(
+ `The TypeAgent feed install did not produce ${manifest.platformPackage}@${manifest.platformVersion}.`,
+ );
+ }
+ const backupDirectory = `${finalDirectory}.backup-${process.pid}-${Date.now()}`;
+ let previousMoved = false;
+ let adopted = false;
+ try {
+ if (fs.existsSync(finalDirectory)) {
+ fs.renameSync(finalDirectory, backupDirectory);
+ previousMoved = true;
+ }
+ fs.renameSync(temporaryDirectory, finalDirectory);
+ adopted = true;
+ const installed = resolveInstalledCopilotPath(
+ finalDirectory,
+ manifest,
+ );
+ if (!installed) {
+ throw new Error(
+ "Installed Copilot runtime could not be verified.",
+ );
+ }
+ if (previousMoved) {
+ fs.rmSync(backupDirectory, { recursive: true, force: true });
+ }
+ writeSetupState(runtimeRoot, {
+ status: "installed",
+ cliVersion: manifest.cliVersion,
+ cliPath: installed,
+ });
+ return installed;
+ } catch (error) {
+ if (adopted) {
+ fs.rmSync(finalDirectory, { recursive: true, force: true });
+ }
+ if (previousMoved && fs.existsSync(backupDirectory)) {
+ fs.renameSync(backupDirectory, finalDirectory);
+ }
+ throw error;
+ }
+ } catch (error) {
+ writeSetupState(runtimeRoot, {
+ status:
+ error instanceof Error && error.message.includes("authenticate")
+ ? "feed-auth-required"
+ : "feed-unavailable",
+ cliVersion: manifest.cliVersion,
+ message: error instanceof Error ? error.message : String(error),
+ });
+ throw error;
+ } finally {
+ fs.rmSync(path.dirname(userconfig), { recursive: true, force: true });
+ fs.rmSync(temporaryDirectory, { recursive: true, force: true });
+ }
+}
+
+function resolveExactSystemCopilot(manifest) {
+ const explicit =
+ process.env.TYPEAGENT_COPILOT_CLI_PATH ?? process.env.COPILOT_CLI_PATH;
+ if (explicit && fs.existsSync(explicit)) {
+ return explicit;
+ }
+ const system = resolveCommand("copilot");
+ if (!system) {
+ return undefined;
+ }
+ const npmRoot = runNpm(["root", "-g"], {
+ encoding: "utf8",
+ windowsHide: true,
+ });
+ if (npmRoot.status !== 0) {
+ return undefined;
+ }
+ return globalCopilotPackageVersion(npmRoot.stdout.trim()) ===
+ manifest.cliVersion
+ ? system
+ : undefined;
+}
+
+export function resolveCopilotRuntime(
+ manifest,
+ { runtimeRoot = defaultRuntimeRoot() } = {},
+) {
+ return (
+ resolveInstalledCopilotPath(
+ managedRuntimeDirectory(manifest, runtimeRoot),
+ manifest,
+ ) ?? resolveExactSystemCopilot(manifest)
+ );
+}
+
+async function inspectCopilot(executable) {
+ let sdk;
+ try {
+ sdk = await import("@github/copilot-sdk");
+ } catch {
+ const sourceSdk = path.resolve(
+ scriptDir,
+ "..",
+ "..",
+ "packages",
+ "agentServer",
+ "bundledRuntime",
+ "node_modules",
+ "@github",
+ "copilot-sdk",
+ "dist",
+ "index.js",
+ );
+ sdk = await import(pathToFileURL(sourceSdk).href);
+ }
+ const { CopilotClient, RuntimeConnection } = sdk;
+ const client = new CopilotClient({
+ connection: RuntimeConnection.forStdio({ path: executable }),
+ });
+ try {
+ await client.start();
+ const runtime = await client.getStatus();
+ const auth = await client.getAuthStatus();
+ const models = auth.isAuthenticated ? await client.listModels() : [];
+ return {
+ runtime,
+ auth,
+ models: models.filter(
+ (model) => model.policy?.state !== "disabled",
+ ),
+ };
+ } finally {
+ await client.stop().catch(() => {});
+ }
+}
+
+function runLogin(executable, options) {
+ const args = ["login"];
+ if (options.deviceCode) {
+ args.push("--device-code");
+ }
+ if (options.host && options.host !== "https://github.com") {
+ args.push("--host", options.host);
+ }
+ return run(executable, args, { stdio: "inherit" }).status === 0;
+}
+
+async function setupCopilot(manifest, options) {
+ const runtimeRoot = options.runtimeRoot ?? defaultRuntimeRoot();
+ let executable = resolveCopilotRuntime(manifest, { runtimeRoot });
+ if (!executable) {
+ executable = installManagedCopilotRuntime(manifest, {
+ runtimeRoot,
+ interactive: options.interactive,
+ });
+ }
+ console.log(`GitHub Copilot runtime: ${executable}`);
+
+ if (!options.login) {
+ return 0;
+ }
+
+ let status = await inspectCopilot(executable);
+ if (!status.auth.isAuthenticated) {
+ if (!options.interactive) {
+ writeSetupState(runtimeRoot, {
+ status: "auth-required",
+ cliVersion: manifest.cliVersion,
+ cliPath: executable,
+ });
+ console.error(
+ "GitHub Copilot sign-in is required. Run 'node typeagent-serve.mjs setup --provider copilot'.",
+ );
+ return 2;
+ }
+ console.log("Starting GitHub Copilot sign-in...");
+ if (!runLogin(executable, options)) {
+ writeSetupState(runtimeRoot, {
+ status: "auth-cancelled",
+ cliVersion: manifest.cliVersion,
+ cliPath: executable,
+ });
+ return 2;
+ }
+ status = await inspectCopilot(executable);
+ }
+
+ if (!status.auth.isAuthenticated) {
+ writeSetupState(runtimeRoot, {
+ status: "auth-required",
+ cliVersion: manifest.cliVersion,
+ cliPath: executable,
+ });
+ console.error("GitHub Copilot still reports as not authenticated.");
+ return 2;
+ }
+ if (status.models.length === 0) {
+ writeSetupState(runtimeRoot, {
+ status: "model-unavailable",
+ cliVersion: manifest.cliVersion,
+ cliPath: executable,
+ login: status.auth.login,
+ });
+ console.error(
+ "GitHub Copilot authentication succeeded, but no enabled models are available.",
+ );
+ return 2;
+ }
+
+ writeSetupState(runtimeRoot, {
+ status: "ready",
+ cliVersion: manifest.cliVersion,
+ cliPath: executable,
+ login: status.auth.login,
+ authType: status.auth.authType,
+ models: status.models.map((model) => model.id),
+ });
+ console.log(
+ `GitHub Copilot is ready${status.auth.login ? ` for ${status.auth.login}` : ""} (${status.models.length} model${status.models.length === 1 ? "" : "s"} available).`,
+ );
+ return 0;
+}
+
+function printHelp() {
+ console.log(
+ [
+ "Usage: node copilotRuntime.mjs [setup|install|path|status|verify-feed] [options]",
+ "",
+ "Options:",
+ " --artifact-dir ",
+ " --manifest ",
+ " --runtime-root ",
+ " --runtime-only",
+ " --non-interactive",
+ " --device-code",
+ " --host ",
+ ].join("\n"),
+ );
+}
+
+async function main() {
+ const options = parseArgs(process.argv);
+ if (options.command === "help") {
+ printHelp();
+ return 0;
+ }
+ const manifestPath =
+ options.manifestPath ??
+ path.join(options.artifactDir, "copilot-runtime.json");
+ const manifest = readCopilotRuntimeManifest(manifestPath);
+ const runtimeRoot = options.runtimeRoot ?? defaultRuntimeRoot();
+
+ switch (options.command) {
+ case "setup":
+ return setupCopilot(manifest, { ...options, runtimeRoot });
+ case "install": {
+ const executable = installManagedCopilotRuntime(manifest, {
+ runtimeRoot,
+ interactive: options.interactive,
+ });
+ console.log(executable);
+ return 0;
+ }
+ case "path": {
+ const executable = resolveCopilotRuntime(manifest, { runtimeRoot });
+ if (!executable) {
+ return 1;
+ }
+ console.log(executable);
+ return 0;
+ }
+ case "status": {
+ const executable = resolveCopilotRuntime(manifest, { runtimeRoot });
+ if (!executable) {
+ console.log("GitHub Copilot runtime is not installed.");
+ return 1;
+ }
+ const status = await inspectCopilot(executable);
+ console.log(
+ JSON.stringify(
+ {
+ cliPath: executable,
+ cliVersion: manifest.cliVersion,
+ runtime: status.runtime,
+ auth: status.auth,
+ models: status.models.map((model) => model.id),
+ },
+ null,
+ 2,
+ ),
+ );
+ return status.auth.isAuthenticated ? 0 : 2;
+ }
+ case "verify-feed":
+ verifyCopilotRuntimeFeed(manifest, options.interactive);
+ return 0;
+ default:
+ throw new Error(`Unknown command '${options.command}'.`);
+ }
+}
+
+if (path.resolve(process.argv[1] ?? "") === scriptPath) {
+ main()
+ .then((code) => process.exit(code))
+ .catch((error) => {
+ console.error(error instanceof Error ? error.message : error);
+ process.exit(1);
+ });
+}
diff --git a/ts/tools/scripts/copilotRuntimeManifest.mjs b/ts/tools/scripts/copilotRuntimeManifest.mjs
new file mode 100644
index 0000000000..3f4925bd7c
--- /dev/null
+++ b/ts/tools/scripts/copilotRuntimeManifest.mjs
@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
+const tsRoot = path.resolve(scriptsDir, "..", "..");
+
+function readPackage(packageJsonPath, expectedName) {
+ if (!fs.existsSync(packageJsonPath)) {
+ throw new Error(`Could not locate package.json for ${expectedName}.`);
+ }
+ const metadata = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
+ if (metadata.name !== expectedName) {
+ throw new Error(
+ `Expected ${expectedName} metadata at ${packageJsonPath}.`,
+ );
+ }
+ return { path: packageJsonPath, metadata };
+}
+
+export function copilotPlatformPackage(platform, arch) {
+ const normalizedArch = arch === "ia32" ? "x64" : arch;
+ const suffix =
+ platform === "linux"
+ ? `linux-${normalizedArch}`
+ : `${platform}-${normalizedArch}`;
+ return `@github/copilot-${suffix}`;
+}
+
+export function createCopilotRuntimeManifest({
+ platform = process.platform,
+ arch = process.arch,
+ registry,
+} = {}) {
+ const sdkLink = path.join(
+ tsRoot,
+ "packages",
+ "agentServer",
+ "bundledRuntime",
+ "node_modules",
+ "@github",
+ "copilot-sdk",
+ );
+ const sdkDirectory = fs.realpathSync(sdkLink);
+ const sdk = readPackage(
+ path.join(sdkDirectory, "package.json"),
+ "@github/copilot-sdk",
+ ).metadata;
+ const cliRequirement = sdk.dependencies?.["@github/copilot"];
+ if (typeof sdk.version !== "string" || typeof cliRequirement !== "string") {
+ throw new Error(
+ "@github/copilot-sdk must declare an @github/copilot dependency.",
+ );
+ }
+
+ const cli = readPackage(
+ path.join(path.dirname(sdkDirectory), "copilot", "package.json"),
+ "@github/copilot",
+ ).metadata;
+ const cliVersion = cli.version;
+ if (
+ typeof cliVersion !== "string" ||
+ !/^\d+\.\d+\.\d+(?:[-+].+)?$/.test(cliVersion)
+ ) {
+ throw new Error(
+ "The resolved @github/copilot version is not concrete.",
+ );
+ }
+
+ const platformPackage = copilotPlatformPackage(platform, arch);
+ const platformVersion = cli.optionalDependencies?.[platformPackage];
+ if (platformVersion !== cliVersion) {
+ throw new Error(
+ `${platformPackage} must resolve to ${cliVersion}; found ${platformVersion ?? "nothing"}.`,
+ );
+ }
+
+ const feedConfig = JSON.parse(
+ fs.readFileSync(
+ path.join(
+ tsRoot,
+ "tools",
+ "installers",
+ "common",
+ "package-feed.json",
+ ),
+ "utf8",
+ ),
+ );
+ const resolvedRegistry =
+ registry ?? process.env.TYPEAGENT_FEED_REGISTRY ?? feedConfig.registry;
+ if (
+ typeof resolvedRegistry !== "string" ||
+ !resolvedRegistry.startsWith("https://")
+ ) {
+ throw new Error("A valid HTTPS TypeAgent npm feed is required.");
+ }
+
+ return {
+ schemaVersion: 1,
+ sdkPackage: "@github/copilot-sdk",
+ sdkVersion: sdk.version,
+ sdkCliRequirement: cliRequirement,
+ cliPackage: "@github/copilot",
+ cliVersion,
+ platformPackage,
+ platformVersion,
+ registry: resolvedRegistry,
+ azureDevOpsResource: feedConfig.azureDevOpsResource,
+ };
+}
+
+export function writeCopilotRuntimeManifest(outputPath, options) {
+ const manifest = createCopilotRuntimeManifest(options);
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+ fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
+ return manifest;
+}
diff --git a/ts/tools/scripts/deployAgentServer.mjs b/ts/tools/scripts/deployAgentServer.mjs
index 20f2017bb9..f753d96ce5 100644
--- a/ts/tools/scripts/deployAgentServer.mjs
+++ b/ts/tools/scripts/deployAgentServer.mjs
@@ -28,6 +28,7 @@ import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
+import { writeCopilotRuntimeManifest } from "./copilotRuntimeManifest.mjs";
const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
const tsRoot = path.resolve(scriptsDir, "..", ".."); // ts/
@@ -145,10 +146,9 @@ function main() {
);
}
- // 2c. External-CLI variant: drop the bundled Claude/Copilot runtimes. Only
- // valid where `claude`/`copilot` are on PATH (managed machines / the
- // standalone installer); the runtime query() callers are wired to resolve
- // the PATH binary (claudeExecutableOption), so they don't need the bundle.
+ // 2c. External-runtime variant: drop the bundled Claude/Copilot runtimes.
+ // Claude resolves from PATH. Copilot resolves from TypeAgent's managed
+ // runtime cache or an explicit compatible path.
if (args.externalCli) {
run(
"node",
@@ -157,7 +157,7 @@ function main() {
);
fs.writeFileSync(
path.join(args.out, ".typeagent-external-cli"),
- "claude,copilot must be on PATH\n",
+ "claude must be on PATH; copilot uses the TypeAgent managed runtime\n",
"utf8",
);
console.log(" recorded external-cli mode (.typeagent-external-cli)");
@@ -177,6 +177,14 @@ function main() {
// closure (chalk, @azure/keyvault-secrets, @azure/identity, js-yaml,
// @typeagent/config), so it runs as `node tools/getKeys.mjs`.
const toolsOut = path.join(args.out, "tools");
+ writeCopilotRuntimeManifest(path.join(args.out, "copilot-runtime.json"), {
+ platform: args.platform,
+ arch: args.arch,
+ });
+ copyInto(
+ path.join(scriptsDir, "copilotRuntime.mjs"),
+ path.join(toolsOut, "copilotRuntime.mjs"),
+ );
copyInto(
path.join(scriptsDir, "getKeys.mjs"),
path.join(toolsOut, "getKeys.mjs"),
diff --git a/ts/tools/scripts/install-typeagent.ps1 b/ts/tools/scripts/install-typeagent.ps1
index 7474edf8ef..71241adf8e 100644
--- a/ts/tools/scripts/install-typeagent.ps1
+++ b/ts/tools/scripts/install-typeagent.ps1
@@ -11,10 +11,10 @@
and prerequisites exist). This script provisions what a bare machine lacks:
1. Verifies Node >= 22.
- 2. For the 'external' artifact variant, provisions the Claude Code + GitHub
- Copilot CLIs on PATH (the external-cli agent-server resolves them at
- runtime; see @typeagent/agent-sdk/node claudeExecutableOption). The 'full'
- variant bundles those runtimes, so this step is skipped.
+ 2. For the 'external' artifact variant, provisions Claude Code on PATH and,
+ when Copilot is selected, installs the exact SDK-compatible Copilot
+ runtime into TypeAgent's per-user cache. The 'full' variant bundles those
+ runtimes, so this step is skipped.
3. Downloads the agent-server Universal package for this RID from the feed.
4. Installs and registers the Copilot CLI plugin (from feed by default, or -PluginSource).
5. When VS Code 1.133 or newer is present, installs TypeAgent Chat and
@@ -55,6 +55,7 @@ param(
[string]$Org = "https://dev.azure.com/msctoproj",
[string]$Project = "AI_Systems",
[string]$Feed = "typeagent",
+ [string]$NpmFeedRegistry = "",
[string]$PluginSource = "",
[string]$PluginVersion = "latest",
[string]$PluginPackageName = "typeagent-copilot-plugin",
@@ -130,6 +131,53 @@ function Test-Command($name) {
return [bool](Get-Command $name -ErrorAction SilentlyContinue)
}
+$AzureDevOpsResource = "499b84ac-1321-427f-aa17-267ca6975798"
+
+function Get-NpmFeedToken {
+ $output = & az account get-access-token `
+ --resource $AzureDevOpsResource `
+ --output json `
+ --only-show-errors 2>$null | Out-String
+ if ($LASTEXITCODE -ne 0 -or -not $output) {
+ return $null
+ }
+ try {
+ return ($output | ConvertFrom-Json).accessToken
+ } catch {
+ return $null
+ }
+}
+
+function New-TransientNpmrc {
+ param(
+ [Parameter(Mandatory = $true)][string]$Registry,
+ [Parameter(Mandatory = $true)][string]$Token
+ )
+
+ $normalized = if ($Registry.EndsWith("/")) { $Registry } else { "$Registry/" }
+ $authKey = $normalized -replace '^https:', ''
+ $baseAuthKey = $authKey -replace 'registry/?$', ''
+ $directory = Join-Path $env:TEMP ("ta-npmauth-" + [guid]::NewGuid().ToString("N"))
+ New-Item -ItemType Directory -Force -Path $directory | Out-Null
+ $userconfig = Join-Path $directory ".npmrc"
+ $content = "registry=$normalized`n$($baseAuthKey):_authToken=$Token`n$($authKey):_authToken=$Token`n$($authKey):always-auth=true`n"
+ Set-Content -Path $userconfig -Value $content -NoNewline -Encoding ascii
+ return $userconfig
+}
+
+function Install-GlobalPackageFromTypeAgentFeed {
+ param(
+ [Parameter(Mandatory = $true)][string]$Package,
+ [Parameter(Mandatory = $true)][string]$Registry,
+ [Parameter(Mandatory = $true)][string]$UserConfig
+ )
+
+ & npm install -g $Package --registry $Registry --userconfig $UserConfig
+ if ($LASTEXITCODE -ne 0) {
+ Fail "Failed to install '$Package' from the TypeAgent package feed."
+ }
+}
+
function Test-AzureDevOpsAuthError {
param(
[Parameter(Mandatory = $true)][string]$Text
@@ -426,23 +474,11 @@ Write-Host " Node $(& node --version)"
# --- 2. External-CLI prerequisites (Claude Code + Copilot CLI) ---------------
if ($Variant -eq "external") {
- Write-Step "Provisioning external CLIs (claude, copilot)"
+ Write-Step "Checking external runtime prerequisites"
if (-not (Test-Command npm)) {
- Fail "npm is required to install the CLIs (ships with Node)."
- }
- if (-not (Test-Command claude)) {
- Write-Host " Installing Claude Code CLI (npm i -g @anthropic-ai/claude-code)"
- & npm install -g "@anthropic-ai/claude-code"
- } else {
- Write-Host " claude already on PATH: $((Get-Command claude).Source)"
+ Fail "npm is required to install external runtimes from the TypeAgent package feed."
}
- if (-not (Test-Command copilot)) {
- Write-Host " Installing GitHub Copilot CLI (npm i -g @github/copilot)"
- & npm install -g "@github/copilot"
- } else {
- Write-Host " copilot already on PATH: $((Get-Command copilot).Source)"
- }
- Write-Host " NOTE: both CLIs require a one-time auth (e.g. 'claude' / 'copilot' login) before agent actions work."
+ Write-Host " Runtime packages will be provisioned after the agent-server artifact is available."
}
# --- 3. Download the agent-server artifact from the feed ---------------------
@@ -491,6 +527,60 @@ if ($assetExists) {
if (-not (Test-Path $serve)) { Fail "Agent-server assets missing typeagent-serve.mjs (unexpected layout)." }
+$managedCopilotPath = $null
+if ($Variant -eq "external") {
+ $runtimeManifestPath = Join-Path $InstallDir "copilot-runtime.json"
+ if (-not (Test-Path $runtimeManifestPath)) {
+ Fail "Agent-server assets are missing copilot-runtime.json."
+ }
+ $runtimeManifest = Get-Content -Raw -Path $runtimeManifestPath | ConvertFrom-Json
+ $resolvedNpmRegistry = if ($NpmFeedRegistry) {
+ $NpmFeedRegistry
+ } else {
+ [string]$runtimeManifest.registry
+ }
+ if (-not $resolvedNpmRegistry) {
+ Fail "The TypeAgent npm feed registry is not configured."
+ }
+
+ if (-not (Test-Command claude)) {
+ $feedToken = Get-NpmFeedToken
+ if (-not $feedToken) {
+ Invoke-AzLoginForAccess -Reason "TypeAgent package feed sign-in is required to install Claude Code."
+ $feedToken = Get-NpmFeedToken
+ }
+ if (-not $feedToken) {
+ Fail "Could not authenticate to the TypeAgent package feed. npm was not invoked."
+ }
+ $userconfig = New-TransientNpmrc -Registry $resolvedNpmRegistry -Token $feedToken
+ try {
+ Write-Host " Installing Claude Code CLI from the TypeAgent package feed"
+ Install-GlobalPackageFromTypeAgentFeed `
+ -Package "@anthropic-ai/claude-code" `
+ -Registry $resolvedNpmRegistry `
+ -UserConfig $userconfig
+ } finally {
+ Remove-Item (Split-Path -Parent $userconfig) -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ } else {
+ Write-Host " claude already on PATH: $((Get-Command claude).Source)"
+ }
+
+ if ($Provider -eq "copilot") {
+ Write-Step "Installing the SDK-compatible Copilot runtime"
+ & node $serve setup --provider copilot --runtime-only
+ if ($LASTEXITCODE -ne 0) {
+ Fail "Copilot runtime installation from the TypeAgent package feed failed."
+ }
+ $runtimeTool = Join-Path $InstallDir "tools\copilotRuntime.mjs"
+ $managedCopilotPath = (& node $runtimeTool path | Select-Object -First 1)
+ if ($LASTEXITCODE -ne 0 -or -not $managedCopilotPath -or -not (Test-Path $managedCopilotPath)) {
+ Fail "The managed Copilot runtime was installed but its executable could not be resolved."
+ }
+ Write-Host " Copilot runtime: $managedCopilotPath"
+ }
+}
+
# --- 4. Install and register the Copilot CLI plugin ---------------------------
Write-Step "Installing Copilot CLI plugin"
$pluginSourceDir = $PluginInstallDir
@@ -500,16 +590,10 @@ $pluginName = "typeagent"
$pluginDescription = "TypeAgent Copilot CLI plugin"
$pluginResolvedVersion = $PluginVersion
-if (-not (Test-Command copilot)) {
- if (-not (Test-Command npm)) {
- Fail "GitHub Copilot CLI is required to register the plugin, and npm is not available to install it."
- }
-
- Write-Host " Installing GitHub Copilot CLI (npm i -g @github/copilot)"
- & npm install -g "@github/copilot"
- if (-not (Test-Command copilot)) {
- Fail "GitHub Copilot CLI was not found after installation."
- }
+$pluginCopilotPath = if (Test-Command copilot) {
+ (Get-Command copilot).Source
+} else {
+ $managedCopilotPath
}
if ($Upgrade -and (Test-Path $pluginSourceDir)) {
@@ -576,23 +660,28 @@ if (-not (Test-Path $registerPluginScript)) {
}
$pluginRegisterLogPath = Join-Path (Join-Path $env:USERPROFILE ".typeagent") "logs\register-plugin.log"
-Write-Host " Registering plugin with shared script"
-$registerArgs = @(
- $registerPluginScript,
- "--install-dir", $InstallDir,
- "--plugin-source-dir", $pluginSourceDir,
- "--marketplace-name", $PluginMarketplaceName,
- "--marketplace-root", $PluginMarketplaceDir,
- "--plugin-name", $pluginName,
- "--plugin-description", $pluginDescription,
- "--plugin-version", $pluginResolvedVersion,
- "--log-path", $pluginRegisterLogPath
-)
-& node @registerArgs
-if ($LASTEXITCODE -ne 0) {
- Fail "Copilot plugin registration failed. See log: $pluginRegisterLogPath"
+if ($pluginCopilotPath) {
+ Write-Host " Registering plugin with shared script via $pluginCopilotPath"
+ $registerArgs = @(
+ $registerPluginScript,
+ "--install-dir", $InstallDir,
+ "--plugin-source-dir", $pluginSourceDir,
+ "--marketplace-name", $PluginMarketplaceName,
+ "--marketplace-root", $PluginMarketplaceDir,
+ "--plugin-name", $pluginName,
+ "--plugin-description", $pluginDescription,
+ "--plugin-version", $pluginResolvedVersion,
+ "--copilot-path", $pluginCopilotPath,
+ "--log-path", $pluginRegisterLogPath
+ )
+ & node @registerArgs
+ if ($LASTEXITCODE -ne 0) {
+ Fail "Copilot plugin registration failed. See log: $pluginRegisterLogPath"
+ }
+ Write-Host " Copilot plugin '$pluginName' registered successfully"
+} else {
+ Write-Host " Copilot CLI is not available. Plugin registration is deferred; rerun setup after installing a CLI." -ForegroundColor Yellow
}
-Write-Host " Copilot plugin '$pluginName' registered successfully"
# --- 5. Install the native VS Code Chat extension ----------------------------
$vscodeChatInstalled = $false
@@ -754,7 +843,11 @@ if ($Provider -eq "aisystems") {
}
}
if ($Provider -eq "copilot") {
- Write-Host " Reminder: the 'copilot' CLI must be installed and authenticated (github login)." -ForegroundColor Yellow
+ Write-Step "Signing in to and verifying GitHub Copilot"
+ & node $serve setup --provider copilot
+ if ($LASTEXITCODE -ne 0) {
+ Fail "GitHub Copilot setup did not complete successfully."
+ }
}
}
diff --git a/ts/tools/scripts/pruneSdkBinaries.mjs b/ts/tools/scripts/pruneSdkBinaries.mjs
index 24964ef1bc..c6ccda7782 100644
--- a/ts/tools/scripts/pruneSdkBinaries.mjs
+++ b/ts/tools/scripts/pruneSdkBinaries.mjs
@@ -10,8 +10,8 @@
* Safe ONLY when:
* (1) every runtime `query()` caller passes pathToClaudeCodeExecutable (we wire
* them via claudeExecutableOption()), AND
- * (2) `claude` and `copilot` are guaranteed on PATH in the target environment
- * (managed machines; the standalone installer provisions them).
+ * (2) `claude` is available on PATH and Copilot is resolved from TypeAgent's
+ * managed runtime cache or an explicit compatible path.
* Otherwise the SDKs lose their bundled binary with no fallback. This is why it
* is opt-in (deployAgentServer --external-cli), never the default artifact.
*
@@ -111,7 +111,7 @@ function main() {
}
console.log(
`${args.dryRun ? "Would free" : "Freed"} ${fmt(freed)} across ${removed.length} bundled-runtime package(s). ` +
- `(claude/copilot must be on PATH in the target environment.)`,
+ `(external Claude and managed Copilot runtimes are required.)`,
);
}
diff --git a/ts/tools/scripts/setup-typeagent-prereqs.ps1 b/ts/tools/scripts/setup-typeagent-prereqs.ps1
index c9552c1bc0..8e44805a51 100644
--- a/ts/tools/scripts/setup-typeagent-prereqs.ps1
+++ b/ts/tools/scripts/setup-typeagent-prereqs.ps1
@@ -13,10 +13,11 @@
3. Azure CLI (Microsoft.AzureCLI via winget when missing)
4. Azure DevOps az extension
5. Optional: az login
- 6. For Variant=external: claude + copilot CLIs via npm -g
- 7. Optional: devtunnel CLI when -DevTunnel is specified
+ 6. Optional: devtunnel CLI when -DevTunnel is specified
This script is standalone and does not require a local TypeAgent repository.
+ External runtime packages are installed later by install-typeagent.ps1 after
+ the artifact supplies its exact compatibility manifest and package feed.
.EXAMPLE
pwsh ./setup-typeagent-prereqs.ps1
@@ -298,38 +299,6 @@ function Ensure-AzureCli {
}
}
-function Ensure-NpmGlobalCli {
- param(
- [Parameter(Mandatory = $true)][string]$Command,
- [Parameter(Mandatory = $true)][string]$PackageName,
- [Parameter(Mandatory = $true)][string]$FriendlyName
- )
-
- $shouldInstall = $ForceReinstallCli -or -not (Test-Command $Command)
-
- if ($shouldInstall) {
- Write-Info "Installing $FriendlyName (npm i -g $PackageName)"
- & npm install -g $PackageName
- if ($LASTEXITCODE -ne 0) {
- Fail "npm global install failed for $PackageName"
- }
- Refresh-Path
- }
-
- if (-not (Test-Command $Command)) {
- Fail "$FriendlyName command '$Command' was not found on PATH after install."
- }
-
- Write-Ok "${FriendlyName}: $((Get-Command $Command).Source)"
-}
-
-function Ensure-ExternalClis {
- Write-Step "Ensuring external CLIs (claude, copilot)"
- Ensure-NpmGlobalCli -Command "claude" -PackageName "@anthropic-ai/claude-code" -FriendlyName "Claude Code CLI"
- Ensure-NpmGlobalCli -Command "copilot" -PackageName "@github/copilot" -FriendlyName "GitHub Copilot CLI"
- Write-WarnMsg "Remember to sign in once: run 'claude' and 'copilot' interactively."
-}
-
function Ensure-DevTunnel {
Write-Step "Ensuring devtunnel CLI"
@@ -372,7 +341,10 @@ Ensure-Node
Ensure-AzureCli
if ($Variant -eq "external") {
- Ensure-ExternalClis
+ Write-Info "External runtimes will be installed from the TypeAgent package feed after the agent-server artifact is downloaded."
+}
+if ($ForceReinstallCli) {
+ Write-WarnMsg "-ForceReinstallCli is deprecated; install-typeagent.ps1 now owns exact runtime installation."
}
if ($DevTunnel) {
diff --git a/ts/tools/scripts/test/copilotManagedRuntime.spec.mjs b/ts/tools/scripts/test/copilotManagedRuntime.spec.mjs
new file mode 100644
index 0000000000..29b3a00c7f
--- /dev/null
+++ b/ts/tools/scripts/test/copilotManagedRuntime.spec.mjs
@@ -0,0 +1,289 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+import {
+ copilotPlatformPackage,
+ createCopilotRuntimeManifest,
+} from "../copilotRuntimeManifest.mjs";
+import {
+ globalCopilotPackageVersion,
+ managedRuntimeDirectory,
+ npmInstallArgs,
+ npmInvocation,
+ npmViewArgs,
+ readCopilotRuntimeManifest,
+ resolveInstalledCopilotPath,
+ transientNpmrcContent,
+} from "../copilotRuntime.mjs";
+import { listedEntryState } from "../../installers/common/register-plugin.mjs";
+
+const testDir = path.dirname(fileURLToPath(import.meta.url));
+const tsRoot = path.resolve(testDir, "..", "..", "..");
+
+test("manifest records the resolved SDK-compatible Windows runtime", () => {
+ const manifest = createCopilotRuntimeManifest({
+ platform: "win32",
+ arch: "x64",
+ });
+ assert.equal(manifest.sdkPackage, "@github/copilot-sdk");
+ assert.equal(manifest.sdkVersion, "1.0.9");
+ assert.equal(manifest.sdkCliRequirement, "^1.0.78");
+ assert.equal(manifest.cliPackage, "@github/copilot");
+ assert.equal(manifest.cliVersion, "1.0.79");
+ assert.equal(manifest.platformPackage, "@github/copilot-win32-x64");
+ assert.equal(manifest.platformVersion, manifest.cliVersion);
+ assert.match(manifest.registry, /^https:\/\/pkgs\.dev\.azure\.com\//);
+});
+
+test("platform package naming follows Copilot package conventions", () => {
+ assert.equal(
+ copilotPlatformPackage("win32", "x64"),
+ "@github/copilot-win32-x64",
+ );
+ assert.equal(
+ copilotPlatformPackage("darwin", "arm64"),
+ "@github/copilot-darwin-arm64",
+ );
+});
+
+test("npm install is exact and locked to the authenticated feed", () => {
+ const manifest = createCopilotRuntimeManifest({
+ platform: "win32",
+ arch: "x64",
+ });
+ const args = npmInstallArgs(manifest, "C:\\runtime", "C:\\auth\\.npmrc");
+ assert.deepEqual(args.slice(0, 3), ["install", "--prefix", "C:\\runtime"]);
+ assert.ok(args.includes("--registry"));
+ assert.ok(args.includes(manifest.registry));
+ assert.ok(args.includes("--userconfig"));
+ assert.ok(args.includes("C:\\auth\\.npmrc"));
+ assert.ok(args.includes(`@github/copilot@${manifest.cliVersion}`));
+ assert.ok(!args.includes("-g"));
+});
+
+test("Windows npm shims run through node without shell parsing", () => {
+ const root = fs.mkdtempSync(
+ path.join(os.tmpdir(), "TypeAgent Program Files "),
+ );
+ try {
+ const npmCommand = path.join(root, "nodejs", "npm.cmd");
+ const nodeExecutable = path.join(root, "nodejs", "node.exe");
+ const npmCli = path.join(
+ root,
+ "nodejs",
+ "node_modules",
+ "npm",
+ "bin",
+ "npm-cli.js",
+ );
+ fs.mkdirSync(path.dirname(npmCli), { recursive: true });
+ fs.writeFileSync(npmCommand, "");
+ fs.writeFileSync(nodeExecutable, "");
+ fs.writeFileSync(npmCli, "");
+
+ assert.deepEqual(npmInvocation(npmCommand, "win32", "fallback.exe"), {
+ command: nodeExecutable,
+ argsPrefix: [npmCli],
+ });
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("transient npm config pins the registry and scopes the token", () => {
+ const registry =
+ "https://pkgs.dev.azure.com/org/project/_packaging/feed/npm/registry/";
+ const contents = transientNpmrcContent(registry, "token-value");
+ assert.match(contents, /^registry=https:\/\/pkgs\.dev\.azure\.com\//);
+ assert.match(
+ contents,
+ /\n\/\/pkgs\.dev\.azure\.com\/org\/project\/_packaging\/feed\/npm\/:_authToken=token-value/,
+ );
+ assert.match(
+ contents,
+ /\n\/\/pkgs\.dev\.azure\.com\/org\/project\/_packaging\/feed\/npm\/registry\/:_authToken=token-value/,
+ );
+ assert.doesNotMatch(contents, /\nhttps:.*_authToken/);
+});
+
+test("feed verification resolves both exact packages through the configured registry", () => {
+ const manifest = createCopilotRuntimeManifest({
+ platform: "win32",
+ arch: "x64",
+ });
+ const args = npmViewArgs(
+ manifest,
+ manifest.platformPackage,
+ manifest.platformVersion,
+ "C:\\auth\\.npmrc",
+ );
+ assert.deepEqual(args.slice(0, 3), [
+ "view",
+ `${manifest.platformPackage}@${manifest.platformVersion}`,
+ "version",
+ ]);
+ assert.ok(args.includes(manifest.registry));
+ assert.ok(args.includes("C:\\auth\\.npmrc"));
+});
+
+test("managed runtime resolves the platform executable only at the exact version", () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ta-copilot-test-"));
+ try {
+ const manifest = createCopilotRuntimeManifest({
+ platform: "win32",
+ arch: "x64",
+ });
+
+ test("global Copilot compatibility uses npm package metadata", () => {
+ const root = fs.mkdtempSync(
+ path.join(os.tmpdir(), "ta-copilot-global-"),
+ );
+ try {
+ const packageDir = path.join(root, "@github", "copilot");
+ fs.mkdirSync(packageDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(packageDir, "package.json"),
+ JSON.stringify({
+ name: "@github/copilot",
+ version: "1.0.79",
+ }),
+ );
+ assert.equal(globalCopilotPackageVersion(root), "1.0.79");
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+ });
+ const runtimeDir = managedRuntimeDirectory(manifest, root);
+ const packageDir = path.join(
+ runtimeDir,
+ "node_modules",
+ "@github",
+ "copilot-win32-x64",
+ );
+ fs.mkdirSync(packageDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(packageDir, "package.json"),
+ JSON.stringify({
+ name: manifest.platformPackage,
+ version: manifest.platformVersion,
+ bin: { "copilot-win32-x64": "copilot.exe" },
+ }),
+ );
+ const executable = path.join(packageDir, "copilot.exe");
+ fs.writeFileSync(executable, "");
+ assert.equal(
+ resolveInstalledCopilotPath(runtimeDir, manifest),
+ executable,
+ );
+
+ const manifestPath = path.join(root, "copilot-runtime.json");
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest));
+ assert.deepEqual(readCopilotRuntimeManifest(manifestPath), manifest);
+
+ const wrong = { ...manifest, platformVersion: "9.9.9" };
+ assert.equal(resolveInstalledCopilotPath(runtimeDir, wrong), undefined);
+ } finally {
+ fs.rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("installer integration uses managed setup without adding UI properties", () => {
+ const prereqs = fs.readFileSync(
+ path.join(tsRoot, "tools", "installers", "wix", "install-prereqs.ps1"),
+ "utf8",
+ );
+ const wix = fs.readFileSync(
+ path.join(
+ tsRoot,
+ "tools",
+ "installers",
+ "wix",
+ "TypeAgent-AgentServer.wxs",
+ ),
+ "utf8",
+ );
+ const standalone = fs.readFileSync(
+ path.join(tsRoot, "tools", "scripts", "install-typeagent.ps1"),
+ "utf8",
+ );
+ const bootstrap = fs.readFileSync(
+ path.join(tsRoot, "tools", "scripts", "setup-typeagent-prereqs.ps1"),
+ "utf8",
+ );
+ const artifactBuilders = ["bundleAgentServer.mjs", "deployAgentServer.mjs"]
+ .map((file) =>
+ fs.readFileSync(
+ path.join(tsRoot, "tools", "scripts", file),
+ "utf8",
+ ),
+ )
+ .join("\n");
+ assert.match(
+ prereqs,
+ /setup --provider copilot --runtime-only --non-interactive/,
+ );
+ assert.doesNotMatch(
+ prereqs,
+ /Install-Cli\s+"copilot"\s+"@github\/copilot"/,
+ );
+ assert.match(wix, /-Provider "\[PROVIDER\]"/);
+ assert.doesNotMatch(wix, /Property Id="COPILOTRUNTIME"/);
+ assert.match(
+ wix,
+ /Custom Action="LaunchCopilotSetup"\s+After="InstallFinalize"/,
+ );
+ assert.match(
+ wix,
+ /\(NOT Installed\) AND \(PROVIDER="COPILOT"\) AND \(UILevel >= 4\)/,
+ );
+ assert.doesNotMatch(standalone, /npm install -g ["']?@github\/copilot/);
+ assert.doesNotMatch(bootstrap, /npm install -g/);
+ assert.doesNotMatch(artifactBuilders, /copilot must be on PATH/);
+ assert.match(artifactBuilders, /copilot-runtime\.json/);
+});
+
+test("deployed launcher dispatches the Copilot setup command", () => {
+ const launcher = path.join(
+ tsRoot,
+ "tools",
+ "scripts",
+ "typeagent-serve.mjs",
+ );
+ const result = spawnSync(
+ process.execPath,
+ [launcher, "setup", "--provider", "invalid"],
+ { encoding: "utf8" },
+ );
+ assert.equal(result.status, 1);
+ assert.match(
+ result.stderr,
+ /Setup currently supports only '--provider copilot'/,
+ );
+ assert.doesNotMatch(result.stderr, /cmdSetup is not defined/);
+});
+
+test("plugin registration distinguishes disabled marketplace entries", () => {
+ const identifier = "typeagent@typeagent-local";
+ assert.equal(listedEntryState("", identifier), "absent");
+ assert.equal(
+ listedEntryState(
+ ` • ${identifier} (v0.0.1) (disabled)\n from C:\\marketplace`,
+ identifier,
+ ),
+ "disabled",
+ );
+ assert.equal(
+ listedEntryState(
+ ` • ${identifier} (v0.0.1) (enabled)\n from C:\\marketplace`,
+ identifier,
+ ),
+ "enabled",
+ );
+});
diff --git a/ts/tools/scripts/typeagent-serve.mjs b/ts/tools/scripts/typeagent-serve.mjs
index 8b281b8fba..8f88df5252 100644
--- a/ts/tools/scripts/typeagent-serve.mjs
+++ b/ts/tools/scripts/typeagent-serve.mjs
@@ -19,6 +19,8 @@
* Usage (from the artifact root):
* node typeagent-serve.mjs [start] # ensure the daemon is up (default)
* node typeagent-serve.mjs provision # run getKeys to write config.local.yaml
+ * node typeagent-serve.mjs setup --provider copilot
+ * # install and authenticate Copilot
* node typeagent-serve.mjs status # report whether the daemon is listening
* node typeagent-serve.mjs stop # stop the daemon (best effort)
* node typeagent-serve.mjs autostart [enable|disable|status] # register a
@@ -49,6 +51,11 @@ const generateConfigEntry = path.join(
"tools",
"generate-selfhost-config.mjs",
);
+const copilotRuntimeEntry = path.join(
+ artifactDir,
+ "tools",
+ "copilotRuntime.mjs",
+);
// Profile recorded by deployAgentServer when the artifact was profile-pruned.
function readProfileMarker() {
@@ -125,6 +132,30 @@ function runInline(entry, extraArgs) {
});
}
+function configureManagedCopilotRuntime() {
+ if (
+ process.env.TYPEAGENT_COPILOT_CLI_PATH ||
+ process.env.COPILOT_CLI_PATH ||
+ !fs.existsSync(copilotRuntimeEntry)
+ ) {
+ return;
+ }
+ const result = spawnSync(process.execPath, [copilotRuntimeEntry, "path"], {
+ encoding: "utf8",
+ env: process.env,
+ windowsHide: true,
+ });
+ if (result.status === 0) {
+ const executable = result.stdout
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .find(Boolean);
+ if (executable) {
+ process.env.TYPEAGENT_COPILOT_CLI_PATH = executable;
+ }
+ }
+}
+
function daemonLogPath() {
return path.join(userDataDir(), "agent-server.log");
}
@@ -252,6 +283,30 @@ async function cmdStart() {
return 1;
}
+async function cmdSetup() {
+ const provider = (arg("--provider") ?? "").toLowerCase();
+ if (provider !== "copilot") {
+ console.error("Setup currently supports only '--provider copilot'.");
+ return 1;
+ }
+ if (!fs.existsSync(copilotRuntimeEntry)) {
+ console.error(
+ `Copilot runtime setup tool not found at ${copilotRuntimeEntry}.`,
+ );
+ return 1;
+ }
+ const passthrough = [];
+ const rest = process.argv.slice(3);
+ for (let i = 0; i < rest.length; i++) {
+ if (rest[i] === "--provider") {
+ i++;
+ continue;
+ }
+ passthrough.push(rest[i]);
+ }
+ return runInline(copilotRuntimeEntry, ["setup", ...passthrough]);
+}
+
async function cmdProvision() {
// Chat endpoint provider. Default 'aisystems' preserves today's behavior
// (Key Vault download via getKeys). 'ollama'/'copilot' synthesize a
@@ -875,7 +930,10 @@ async function main() {
: "start";
switch (cmd) {
case "start":
+ configureManagedCopilotRuntime();
return cmdStart();
+ case "setup":
+ return cmdSetup();
case "provision":
case "getkeys":
return cmdProvision();