diff --git a/Directory.Build.targets b/Directory.Build.targets index 5ec9801ce..48f354345 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -90,4 +90,11 @@ + + + diff --git a/build/Reactor.CopilotCli.targets b/build/Reactor.CopilotCli.targets new file mode 100644 index 000000000..40f3457dd --- /dev/null +++ b/build/Reactor.CopilotCli.targets @@ -0,0 +1,158 @@ + + + + + + + <_ReactorCopilotManage>true + true + + <_ReactorCopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform) + <_ReactorCopilotCacheBin>$(_ReactorCopilotCacheDir)\$(_CopilotBinary) + <_ReactorCopilotRegistry>$([System.String]::Copy('$(CopilotNpmRegistryUrl)').TrimEnd('/')) + <_ReactorCopilotRegistry Condition="'$(_ReactorCopilotRegistry)' == ''">https://registry.npmjs.org + <_ReactorCopilotRegistryIsPublic>false + <_ReactorCopilotRegistryIsPublic Condition="'$(_ReactorCopilotRegistry)' == 'https://registry.npmjs.org'">true + + + + + + + <_ReactorCopilotTgzUrl>$(_ReactorCopilotRegistry)/@github/copilot-$(_CopilotPlatform)/-/copilot-$(_CopilotPlatform)-$(CopilotCliVersion).tgz + <_ReactorCopilotArchive>$(_ReactorCopilotCacheDir)\copilot.tgz + <_ReactorCopilotNpmTgz>$(_ReactorCopilotCacheDir)\github-copilot-$(_CopilotPlatform)-$(CopilotCliVersion).tgz + <_ReactorCopilotNpmRegistryArg Condition="'$(_ReactorCopilotRegistryIsPublic)' != 'true'"> --registry $(_ReactorCopilotRegistry) + <_ReactorCopilotTar Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(SystemRoot)\System32\tar.exe + <_ReactorCopilotTar Condition="'$(_ReactorCopilotTar)' == ''">tar + + + + + + + + + + + + + + + + + + + + + + + <_ReactorCopilotRuntimeNode>$(_ReactorCopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node + + + + + + + + + + + <_ReactorCopilotOutDir>$(OutDir)runtimes\$(_CopilotRid)\native + <_ReactorCopilotRuntimeNode>$(_ReactorCopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node + + + + + + + diff --git a/build/pipelines/templates/reactor-build-steps.yml b/build/pipelines/templates/reactor-build-steps.yml index 5333c4b78..a11cc5984 100644 --- a/build/pipelines/templates/reactor-build-steps.yml +++ b/build/pipelines/templates/reactor-build-steps.yml @@ -27,7 +27,13 @@ parameters: - name: npmRegistry displayName: 'Internal npm registry (ADO feed) for transitive npm downloads.' type: string - default: 'https://github-private.pkgs.visualstudio.com/microsoft/_packaging/microsoft-ui-reactor/npm/registry/' + # Modern pkgs.dev.azure.com domain (org/project scoped), matching nuget.internal.config's + # feed. Equivalent to the legacy https://github-private.pkgs.visualstudio.com/microsoft/... + # form; the pkgs.dev.azure.com domain is Microsoft's preferred/required endpoint (agent + # firewall rules, reliability). The `microsoft-ui-reactor` feed proxies registry.npmjs.org + # via its npm upstream source, so transitive npm downloads (e.g. the GitHub.Copilot.SDK CLI) + # resolve internally with feed auth. + default: 'https://pkgs.dev.azure.com/github-private/microsoft/_packaging/microsoft-ui-reactor/npm/registry/' - name: version displayName: 'Explicit version; "auto" or blank => MinVer.' type: string diff --git a/samples/apps/widget-creator/README.md b/samples/apps/widget-creator/README.md index 100aef5dd..66d0e1e6a 100644 --- a/samples/apps/widget-creator/README.md +++ b/samples/apps/widget-creator/README.md @@ -69,6 +69,28 @@ dotnet run --project samples/apps/widget-creator/widget-creator.csproj -p:Platfo feed. Run `mur pack-local` if it's missing. Override the feed path with `WIDGET_CREATOR_NUPKGS`. +### The Copilot CLI at build time (internal vs external) + +The `GitHub.Copilot.SDK` bundles a native `copilot.exe` that it downloads at +**build** time from an npm registry. The repo makes this resilient for everyone +(`build/Reactor.CopilotCli.targets`), so `dotnet build` / `dotnet run` never +hard-fails on a restricted network: + +- **External / OSS** — the committed `nuget.config` + `.npmrc` stay on public + `nuget.org` / `registry.npmjs.org`; the CLI downloads directly. Nothing to set up. +- **Internal (Microsoft)** — point npm at the internal Azure Artifacts feed + (`https://pkgs.dev.azure.com/github-private/microsoft/_packaging/microsoft-ui-reactor/npm/registry/`) + via an authenticated `.npmrc` (CI does this automatically; locally use + `az artifacts` or the feed's *Connect to feed* → npm instructions). The build then + acquires the CLI through that feed with `npm pack`, since Azure Artifacts feeds + require auth that the SDK's raw download can't provide. +- **Offline / no feed** — the build still succeeds *without* a bundled CLI (a + `REACTORCLI001` warning is emitted). To run generation you then need a Copilot + CLI binary available at run time: set `COPILOT_CLI_PATH` to a local `copilot.exe` + (or install the standalone Copilot CLI in its default per-user location). Note + `gh auth` only supplies Copilot credentials — it is not itself a runnable + `copilot.exe`. + Type a prompt, click **Generate & Run**. The generated source streams into the right panel; the build + `wxc-exec` log streams below it. The widget window opens sandboxed — close it to finish the run. If it crashes instead, the creator keeps diff --git a/samples/apps/widget-creator/Services/CopilotSdkClient.cs b/samples/apps/widget-creator/Services/CopilotSdkClient.cs index 03b507837..f2235ecff 100644 --- a/samples/apps/widget-creator/Services/CopilotSdkClient.cs +++ b/samples/apps/widget-creator/Services/CopilotSdkClient.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -42,6 +44,18 @@ async Task GetClientAsync(CancellationToken ct) { if (_client is not null) return _client; var options = new CopilotClientOptions(); + // The SDK's default stdio transport looks ONLY for a CLI bundled next to + // the app (runtimes//native/copilot.exe) and does not consult + // COPILOT_CLI_PATH. On builds where the CLI could not be bundled at build + // time (offline / restricted-network / authenticated-feed environments — + // see build/Reactor.CopilotCli.targets), resolve one at run time and pass + // it explicitly so generation still works. + var cliPath = ResolveCliPath(); + if (cliPath is not null) + { + SessionLog.Write($"[CopilotSdk] no bundled CLI; using resolved CLI at {cliPath}"); + options.Connection = RuntimeConnection.ForStdio(path: cliPath); + } var client = new CopilotClient(options); SessionLog.Write($"[CopilotSdk] starting CLI server (model={_model})"); await client.StartAsync().ConfigureAwait(false); @@ -55,6 +69,44 @@ async Task GetClientAsync(CancellationToken ct) } } + /// + /// Resolves a Copilot CLI to spawn when the SDK's bundled binary is absent. + /// Returns null when the bundled CLI is present (let the SDK use it) or + /// when nothing could be found (let the SDK throw its descriptive error). + /// Order: bundled next to the app → COPILOT_CLI_PATH → a locally + /// installed Copilot CLI in its well-known per-user location. We deliberately + /// do NOT probe PATH for copilot.exe — that would let any + /// unrelated (or malicious) binary earlier on PATH be launched. To use + /// a CLI in a non-standard location, set COPILOT_CLI_PATH explicitly. + /// + static string? ResolveCliPath() + { + var rid = "win-" + (RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "arm64" : "x64"); + var bundled = Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "native", "copilot.exe"); + if (File.Exists(bundled)) + return null; // SDK's default resolution works. + + var env = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); + if (!string.IsNullOrWhiteSpace(env) && File.Exists(env)) + return env; + + foreach (var candidate in EnumerateInstalledCliPaths()) + if (File.Exists(candidate)) + return candidate; + + return null; + } + + static IEnumerable EnumerateInstalledCliPaths() + { + // Standalone GitHub Copilot CLI install (winget / gh), in its well-known + // per-user location. Intentionally a fixed, trusted path — never a PATH + // scan, so an unrelated copilot.exe on PATH can't be picked up and run. + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (!string.IsNullOrEmpty(localAppData)) + yield return Path.Combine(localAppData, "GitHub CLI", "copilot", "copilot.exe"); + } + public async Task StartConversationAsync(string systemPrompt, CancellationToken ct) { var client = await GetClientAsync(ct).ConfigureAwait(false); diff --git a/samples/apps/widget-creator/Services/MxcBinaryManifest.cs b/samples/apps/widget-creator/Services/MxcBinaryManifest.cs index 0eaa1274e..055ea249a 100644 --- a/samples/apps/widget-creator/Services/MxcBinaryManifest.cs +++ b/samples/apps/widget-creator/Services/MxcBinaryManifest.cs @@ -25,12 +25,12 @@ public static class MxcBinaryManifest { ["win-arm64"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["winhttp-proxy-shim.exe"] = "aca7cd110b09a1045c53a5c9bd73089f63e6c66f0b3d2627a5bdfb55bb524fdc", - ["wxc-exec.exe"] = "e430d0e4f44f616e91db684f8d825a6dc93e06a1262b8d00bcaac7522a317aab", - ["wxc-host-prep.exe"] = "3ef702332286a39153fc259310b5021e3de3c191751d7522684f6475f73af5ef", - ["wxc-test-proxy.exe"] = "1d1a5821a65c9b4aceb2f1788ca54b08d06b92b784daa1926ab978f4a49f1f00", - ["wxc-windows-sandbox-daemon.exe"] = "fc8079bddf5db77ee4ecea91d7f22a543fbec3618945a7ab97269dcfef3f66b1", - ["wxc-windows-sandbox-guest.exe"] = "69c972ce4a65d337f15d828e40b92fcb2f89665d32f2cb598606b45c76adfde3", + ["winhttp-proxy-shim.exe"] = "f1fdf37c66af032f4472f5de24fae766544d2fac19da75e45cecb38c5437caa8", + ["wxc-exec.exe"] = "d12957f434871af4cee0fbccfe12a91a615d3e91d8b81c7713f4141eb1f466df", + ["wxc-host-prep.exe"] = "ec91ae4a8d8b537b11c8401f3c1efd7ffc0fb4fba26d0f822f5138aa6cb08aee", + ["wxc-test-proxy.exe"] = "553fbc073ff820b674a87e0a348ddb8814031144812e5df2bc87e4ce7ba34ffa", + ["wxc-windows-sandbox-daemon.exe"] = "68054c012e8fcea6f8b82336edad58f96debb4d02b6f0a145befd9d7eaf301c1", + ["wxc-windows-sandbox-guest.exe"] = "985b88a7871653518f6db0103abf491c2e510be2405bf6a3dff8e64dece7f6f2", }, ["win-x64"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { diff --git a/samples/apps/widget-creator/tools/mxc/win-arm64/winhttp-proxy-shim.exe b/samples/apps/widget-creator/tools/mxc/win-arm64/winhttp-proxy-shim.exe index 3a3536894..a5f26b7e7 100644 Binary files a/samples/apps/widget-creator/tools/mxc/win-arm64/winhttp-proxy-shim.exe and b/samples/apps/widget-creator/tools/mxc/win-arm64/winhttp-proxy-shim.exe differ diff --git a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-exec.exe b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-exec.exe index f15b34c5c..18e42fdb4 100644 Binary files a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-exec.exe and b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-exec.exe differ diff --git a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-host-prep.exe b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-host-prep.exe index eb23f88ba..cb7d194cc 100644 Binary files a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-host-prep.exe and b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-host-prep.exe differ diff --git a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-test-proxy.exe b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-test-proxy.exe index 17fe97a50..ef102584a 100644 Binary files a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-test-proxy.exe and b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-test-proxy.exe differ diff --git a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-daemon.exe b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-daemon.exe index d74b038ec..546076128 100644 Binary files a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-daemon.exe and b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-daemon.exe differ diff --git a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-guest.exe b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-guest.exe index 78aeb591d..f30cc87af 100644 Binary files a/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-guest.exe and b/samples/apps/widget-creator/tools/mxc/win-arm64/wxc-windows-sandbox-guest.exe differ