From dc43836448567928965542890ecc5d31002784de Mon Sep 17 00:00:00 2001
From: Hillary Mutisya <150286414+hillary-mutisya@users.noreply.github.com>
Date: Mon, 14 Sep 2026 17:51:11 -0700
Subject: [PATCH] Add managed Copilot runtime provisioning
Install SDK-compatible Copilot runtimes from the authenticated TypeAgent feed, expose setup and status commands, and configure agent startup to use the managed executable. Update standalone and MSI installers to provision runtimes, launch interactive Copilot setup after installation, verify feed packages in publishing, and handle disabled plugin registrations.
---
pipelines/azure-build-publish-all.yml | 11 +
ts/packages/aiclient/src/copilotModels.ts | 8 +-
ts/tools/installers/common/package-feed.json | 4 +
.../installers/common/register-plugin.mjs | 42 +-
.../installers/wix/TypeAgent-AgentServer.wxs | 33 +-
ts/tools/installers/wix/install-prereqs.ps1 | 143 +++-
.../installers/wix/launch-copilot-setup.ps1 | 62 ++
ts/tools/scripts/bundleAgentServer.mjs | 8 +-
ts/tools/scripts/copilotRuntime.mjs | 722 ++++++++++++++++++
ts/tools/scripts/copilotRuntimeManifest.mjs | 122 +++
ts/tools/scripts/deployAgentServer.mjs | 18 +-
ts/tools/scripts/install-typeagent.ps1 | 185 +++--
ts/tools/scripts/pruneSdkBinaries.mjs | 6 +-
ts/tools/scripts/setup-typeagent-prereqs.ps1 | 42 +-
.../test/copilotManagedRuntime.spec.mjs | 289 +++++++
ts/tools/scripts/typeagent-serve.mjs | 58 ++
16 files changed, 1602 insertions(+), 151 deletions(-)
create mode 100644 ts/tools/installers/common/package-feed.json
create mode 100644 ts/tools/installers/wix/launch-copilot-setup.ps1
create mode 100644 ts/tools/scripts/copilotRuntime.mjs
create mode 100644 ts/tools/scripts/copilotRuntimeManifest.mjs
create mode 100644 ts/tools/scripts/test/copilotManagedRuntime.spec.mjs
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();