From 4853f6bfcfd0060e2ba5f330f6dded33c9ef2373 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 21 Jul 2026 21:06:09 +0300 Subject: [PATCH 1/4] deps: upgrade Microsoft.Agents.AI.GitHub.Copilot to 1.14.0-rc1 and GitHub.Copilot.SDK to 1.0.7 Upgrades both packages together to resolve the version-range mismatch left by Dependabot PR #1508, which only bumped GitHub.Copilot.SDK to 1.0.7 without also bumping Microsoft.Agents.AI.GitHub.Copilot. The 1.11.0-rc1 adapter's nuspec did not declare a compatible range for SDK 1.0.7, so an SDK-only bump was incomplete. Microsoft.Agents.AI.GitHub.Copilot 1.14.0-rc1 is the first adapter release after upstream microsoft/agent-framework#6949 and requires GitHub.Copilot.SDK >= 1.0.5, so SDK 1.0.7 now satisfies its own nuspec. No source changes were required. All Squad.Agents.AI usage (CopilotClient, CopilotClientOptions, SessionConfig, PermissionHandler.ApproveAll, the GitHub.Copilot namespace, ConfigDirectory, and the SessionEvent subtypes consumed by SquadSubagentTraceMapper) is unaffected by the 29 breaking API changes between SDK 1.0.3 and 1.0.7 -- all of them are in RPC/server/canvas/ MCP/model-billing types Squad does not reference. Verified with dnx dotnet-inspect: Microsoft.Agents.AI.GitHub.Copilot 1.14.0-rc1's assembly reference to GitHub.Copilot.SDK carries public key token cc7b13ffcd2ddd51, matching GitHub.Copilot.SDK 1.0.7's own strong-name token, confirming a single compatible strong-named lineage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44f53a4e-d384-4c60-a43d-cfc158160f1f --- src/Squad.Agents.AI/Squad.Agents.AI.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Squad.Agents.AI/Squad.Agents.AI.csproj b/src/Squad.Agents.AI/Squad.Agents.AI.csproj index 0bc711898..49d3acf67 100644 --- a/src/Squad.Agents.AI/Squad.Agents.AI.csproj +++ b/src/Squad.Agents.AI/Squad.Agents.AI.csproj @@ -24,7 +24,7 @@ - + - + From 50f12122346fc19e4fb1c503ef540b6a2c12a738 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 21 Jul 2026 21:55:10 +0300 Subject: [PATCH 2/4] fix(agents-ai): propagate Copilot CLI 1.0.71 to all consumers, resolve NU5104 Reviewer findings on PR #1519 (SDK 1.14.0-rc1 / GitHub.Copilot.SDK 1.0.7 upgrade) both confirmed against real CI logs and fixed: 1. Native payload propagation. Microsoft.Agents.AI.GitHub.Copilot ships GitHub.Copilot.SDK's CLI-download build/ targets to transitive consumers via a buildTransitive/ bridge that defaults to the SDK version the adapter itself was packed against (1.0.5, its nuspec dependency floor), not the version Squad.Agents.AI actually references (1.0.7). Verified in PR #1519 run 29856026557: "Build package" downloaded Copilot CLI 1.0.71 (src/Squad.Agents.AI has the direct GitHub.Copilot.SDK PackageReference), while "Build tests" and "Build sample" downloaded the stale CLI 1.0.67 on both ubuntu-latest and windows-latest, because neither project has that direct reference. Fixed via the documented override contract (_MicrosoftAgentsAICopilotSdkVersion) that Microsoft.Agents.AI.GitHub.Copilot.targets itself exposes for exactly this purpose: - Directory.Build.props sets it repo-wide from a new single source of truth, SquadCopilotSdkVersion, fixing test/Squad.Agents.AI.Tests and samples/Squad.Agents.AI.Sample. - Squad.Agents.AI.csproj generates and packs its own buildTransitive/Squad.Agents.AI.props at pack time (mirroring how the adapter package itself does it) so REAL external NuGet consumers of Squad.Agents.AI get the same fix, not just this repo's internal ProjectReference-based projects. - GitHub.Copilot.SDK PackageReference now uses $(SquadCopilotSdkVersion) instead of a literal, so the two can never drift apart again. Added scripts/verify-squad-agents-ai-consumer.ps1, a hermetic consumer-level packaging check: packs Squad.Agents.AI, restores a scratch consumer against only the local nupkg + nuget.org, builds it for the runner's RID, and asserts the native Copilot CLI executable and runtime.node-derived library land under runtimes/*/native/ at the expected CLI version, with no stale 1.0.67 payload anywhere. Wired into squad-agents-ai-ci.yml (runs on both ubuntu-latest and windows-latest), alongside a lighter in-repo regression guard that checks the same thing for src/test/sample directly. Neither check executes the downloaded CLI binary or touches package source/TLS policy. 2. NU5104. Squad.Agents.AI was versioned as stable (0.5.5) while depending on prerelease Microsoft.Agents.AI.GitHub.Copilot 1.14.0-rc1, so `dotnet pack` emitted NU5104. Bumped to 0.5.6-rc1 (smallest correct next prerelease, matching the adapter's own "-rc1" convention) instead of suppressing the warning, since a genuinely mixed stable/prerelease dependency graph is exactly what NU5104 exists to flag. Verified `dotnet pack` no longer emits NU5104. dev->main promotions already require bumping by hand before a stable release (see squad-agents-ai-release.yml), so this does not block a future GA publish once Microsoft.Agents.AI.GitHub.Copilot itself leaves prerelease. Validated locally: single resolved GitHub.Copilot.SDK 1.0.7 across net8.0/net9.0/net10.0 for all three projects, no NU1605/NU1608/NU5104, no vulnerable packages, 82/82 tests passing on all three TFMs, and a real scratch PackageReference-only consumer (no ProjectReference, no Directory.Build.props visible) correctly attempting Copilot CLI 1.0.71 (not 1.0.67). Actual binary download could not be exercised end-to-end in this sandboxed dev environment (outbound TLS to registry.npmjs.org is blocked here, independent of CLI version); this is expected to complete normally on GitHub-hosted runners exactly as the existing CI already does today. Refs #1519, #1508 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44f53a4e-d384-4c60-a43d-cfc158160f1f --- .github/workflows/squad-agents-ai-ci.yml | 84 +++++++++ Directory.Build.props | 64 +++++++ scripts/verify-squad-agents-ai-consumer.ps1 | 189 ++++++++++++++++++++ src/Squad.Agents.AI/README.md | 2 +- src/Squad.Agents.AI/Squad.Agents.AI.csproj | 74 +++++++- 5 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 Directory.Build.props create mode 100644 scripts/verify-squad-agents-ai-consumer.ps1 diff --git a/.github/workflows/squad-agents-ai-ci.yml b/.github/workflows/squad-agents-ai-ci.yml index f9d257130..18dc5f0b7 100644 --- a/.github/workflows/squad-agents-ai-ci.yml +++ b/.github/workflows/squad-agents-ai-ci.yml @@ -60,6 +60,37 @@ jobs: - name: Restore sample run: dotnet restore src/Squad.Agents.AI/samples/Squad.Agents.AI.Sample/Squad.Agents.AI.Sample.csproj -p:NuGetAudit=true + # Resolves the Copilot CLI version that GitHub.Copilot.SDK (the version this repo + # actually references, $(SquadCopilotSdkVersion)) pins, straight from that package's + # own build/GitHub.Copilot.SDK.props in the local NuGet cache -- so the expected/ + # forbidden CLI versions used by the checks below never need to be hardcoded here + # and can't drift out of sync with a future SDK version bump. + - name: Resolve expected Copilot CLI version + id: copilot-cli-version + shell: pwsh + run: | + $sdkVersion = (dotnet msbuild src/Squad.Agents.AI/Squad.Agents.AI.csproj -getProperty:SquadCopilotSdkVersion -nologo).Trim() + if ([string]::IsNullOrWhiteSpace($sdkVersion)) { + throw "Could not resolve SquadCopilotSdkVersion from Squad.Agents.AI.csproj." + } + + $globalPackagesOutput = dotnet nuget locals global-packages --list + $nugetPackagesRoot = ($globalPackagesOutput -split ':', 2)[1].Trim() + $sdkPropsPath = Join-Path $nugetPackagesRoot "github.copilot.sdk/$sdkVersion/build/GitHub.Copilot.SDK.props" + if (-not (Test-Path $sdkPropsPath)) { + throw "GitHub.Copilot.SDK $sdkVersion props file not found at '$sdkPropsPath'. Was the package restored yet?" + } + + [xml]$sdkProps = Get-Content $sdkPropsPath + $cliVersion = $sdkProps.Project.PropertyGroup.CopilotCliVersion + if ([string]::IsNullOrWhiteSpace($cliVersion)) { + throw "Could not parse CopilotCliVersion out of '$sdkPropsPath'." + } + + Write-Host "GitHub.Copilot.SDK $sdkVersion pins Copilot CLI $cliVersion" + echo "sdk-version=$sdkVersion" >> $env:GITHUB_OUTPUT + echo "cli-version=$cliVersion" >> $env:GITHUB_OUTPUT + - name: Build package run: dotnet build src/Squad.Agents.AI/Squad.Agents.AI.csproj --no-restore -c Release @@ -69,12 +100,65 @@ jobs: - name: Build sample run: dotnet build src/Squad.Agents.AI/samples/Squad.Agents.AI.Sample/Squad.Agents.AI.Sample.csproj --no-restore -c Release + # Regression guard for Reviewer finding #1 (Squad.Agents.AI PR #1519): before the + # Directory.Build.props fix, test/Squad.Agents.AI.Tests and + # src/Squad.Agents.AI/samples/Squad.Agents.AI.Sample downloaded an OLDER Copilot CLI + # (pinned by whatever GitHub.Copilot.SDK version Microsoft.Agents.AI.GitHub.Copilot's + # buildTransitive bridge defaults to) instead of the CLI this repo actually depends + # on, because neither project has a direct PackageReference to GitHub.Copilot.SDK. + - name: Verify no stale Copilot CLI payload in repo builds + shell: pwsh + run: | + $expected = "${{ steps.copilot-cli-version.outputs.cli-version }}" + $forbidden = @('1.0.67') + $projectObjDirs = @( + 'src/Squad.Agents.AI/obj', + 'test/Squad.Agents.AI.Tests/obj', + 'src/Squad.Agents.AI/samples/Squad.Agents.AI.Sample/obj' + ) + + $failed = $false + foreach ($objDir in $projectObjDirs) { + $cliDirs = Get-ChildItem -Path $objDir -Recurse -Directory -Filter 'copilot-cli' -ErrorAction SilentlyContinue + $versions = $cliDirs | ForEach-Object { Get-ChildItem -Path $_.FullName -Directory } | Select-Object -ExpandProperty Name -Unique + + Write-Host "$objDir -> Copilot CLI version(s): $($versions -join ', ')" + + if ($versions -notcontains $expected) { + Write-Host "::error::$objDir did not download the expected Copilot CLI $expected (found: $($versions -join ', '))" + $failed = $true + } + foreach ($bad in $forbidden) { + if ($versions -contains $bad) { + Write-Host "::error::$objDir downloaded forbidden stale Copilot CLI $bad" + $failed = $true + } + } + } + + if ($failed) { + throw "Stale or missing Copilot CLI payload detected -- see ::error annotations above." + } + Write-Host "All Squad.Agents.AI-related builds resolved Copilot CLI $expected." + - name: Test run: dotnet test test/Squad.Agents.AI.Tests/Squad.Agents.AI.Tests.csproj --no-build -c Release --logger trx --results-directory TestResults - name: Pack run: dotnet pack src/Squad.Agents.AI/Squad.Agents.AI.csproj --no-build -c Release -o nupkgs + # Hermetic consumer-level packaging check (Reviewer finding #1): proves the fix also + # works for a REAL external NuGet consumer of Squad.Agents.AI, not just this repo's + # own ProjectReference-based test/sample projects above (which Directory.Build.props + # alone would cover even without the buildTransitive/Squad.Agents.AI.props fix in + # Squad.Agents.AI.csproj). See scripts/verify-squad-agents-ai-consumer.ps1. + - name: Verify Squad.Agents.AI consumer packaging (native Copilot CLI payload) + shell: pwsh + run: | + ./scripts/verify-squad-agents-ai-consumer.ps1 ` + -NupkgsDir nupkgs ` + -ExpectedCopilotCliVersion "${{ steps.copilot-cli-version.outputs.cli-version }}" + - name: Upload test results if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..83c0f1e50 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,64 @@ + + + + + 1.0.7 + + + <_MicrosoftAgentsAICopilotSdkVersion Condition="'$(MSBuildProjectName)' != 'Squad.Agents.AI'">$(SquadCopilotSdkVersion) + + diff --git a/scripts/verify-squad-agents-ai-consumer.ps1 b/scripts/verify-squad-agents-ai-consumer.ps1 new file mode 100644 index 000000000..80f985bee --- /dev/null +++ b/scripts/verify-squad-agents-ai-consumer.ps1 @@ -0,0 +1,189 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Hermetic consumer-level packaging check for Squad.Agents.AI. + +.DESCRIPTION + Restores a scratch consumer project against ONLY the locally-produced Squad.Agents.AI + nupkg (plus nuget.org for its transitive dependencies), builds it for the current + runner's default portable RID, and asserts that the native Copilot CLI payload (the + copilot/copilot.exe executable and its runtime.node-derived native library) actually + propagated to a REAL PackageReference consumer -- not just to this repo's own + ProjectReference-based test/sample projects. + + This exists because Microsoft.Agents.AI.GitHub.Copilot ships GitHub.Copilot.SDK's CLI + download targets to transitive consumers via a buildTransitive/ bridge that -- unless + overridden -- resolves an OLDER GitHub.Copilot.SDK version (whatever version the adapter + package itself was built/floor-pinned against) instead of the version Squad.Agents.AI + actually references. See Directory.Build.props and the GenerateCopilotSdkVersionBridge + target in src/Squad.Agents.AI/Squad.Agents.AI.csproj for the fix; this script is the + regression test for it. A green run here is the ONLY thing that proves the fix works for + real external NuGet consumers, since this repo's own test/sample projects are covered by + Directory.Build.props alone (a repo-internal mechanism a real external consumer never + sees). + + Does not execute the downloaded Copilot CLI binary -- only inspects file paths that + MSBuild's own download/copy targets produced, so it never runs untrusted downloaded + code. Does not touch package source or TLS/signature verification policy: only two + well-known, standard sources are used (nuget.org over HTTPS, and the local nupkgs + folder produced by `dotnet pack` in this same repo checkout). + +.PARAMETER NupkgsDir + Directory containing exactly one packed Squad.Agents.AI .nupkg (e.g. the CI job's + existing `dotnet pack ... -o nupkgs` output). The package version is parsed from the + .nupkg filename itself, so it can never drift out of sync with whatever version was + actually packed. + +.PARAMETER ExpectedCopilotCliVersion + The Copilot CLI version the consumer is expected to download (the CLI version pinned by + the GitHub.Copilot.SDK version Squad.Agents.AI references), e.g. "1.0.71". + +.PARAMETER ForbiddenCopilotCliVersions + Copilot CLI versions that must NOT appear anywhere in the consumer's build output -- + i.e. the stale versions this script exists to catch a regression to. + +.PARAMETER TargetFramework + Target framework moniker for the scratch consumer project. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$NupkgsDir, + + [Parameter(Mandatory = $true)] + [string]$ExpectedCopilotCliVersion, + + [string[]]$ForbiddenCopilotCliVersions = @('1.0.67'), + + [string]$TargetFramework = 'net10.0' +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +function Write-Section([string]$Title) { + Write-Host "" + Write-Host "=== $Title ===" -ForegroundColor Cyan +} + +$nupkgsFull = (Resolve-Path $NupkgsDir).Path +$candidateNupkgs = @(Get-ChildItem -Path $nupkgsFull -Filter "squad.agents.ai.*.nupkg") +if ($candidateNupkgs.Count -eq 0) { + throw "No 'squad.agents.ai.*.nupkg' found in '$nupkgsFull'. Did the Pack step run before this check?" +} +if ($candidateNupkgs.Count -gt 1) { + throw "Expected exactly one packed Squad.Agents.AI nupkg in '$nupkgsFull', found $($candidateNupkgs.Count): $($candidateNupkgs.Name -join ', ')" +} + +$nupkgMatch = [System.Text.RegularExpressions.Regex]::Match($candidateNupkgs[0].Name, '^squad\.agents\.ai\.(?.+)\.nupkg$', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) +if (-not $nupkgMatch.Success) { + throw "Could not parse a package version out of nupkg filename '$($candidateNupkgs[0].Name)'." +} +$PackageVersion = $nupkgMatch.Groups['version'].Value +Write-Host "Detected packed Squad.Agents.AI version: $PackageVersion (from $($candidateNupkgs[0].Name))" + +$scratchDir = Join-Path ([System.IO.Path]::GetTempPath()) "squad-agents-ai-consumer-$([guid]::NewGuid())" +New-Item -ItemType Directory -Path $scratchDir | Out-Null +Write-Host "Scratch consumer project: $scratchDir" + +try { + $csprojPath = Join-Path $scratchDir "ConsumerCheck.csproj" + $csprojContent = @" + + + $TargetFramework + Exe + enable + enable + false + + + + + +"@ + Set-Content -Path $csprojPath -Value $csprojContent -Encoding utf8 + + $programContent = 'System.Console.WriteLine("Squad.Agents.AI consumer packaging check placeholder.");' + Set-Content -Path (Join-Path $scratchDir "Program.cs") -Value $programContent -Encoding utf8 + + Write-Section "Restoring scratch consumer (local nupkg + nuget.org only)" + & dotnet restore $csprojPath --source $nupkgsFull --source "https://api.nuget.org/v3/index.json" + if ($LASTEXITCODE -ne 0) { + throw "Restore of scratch consumer project failed (exit code $LASTEXITCODE)." + } + + Write-Section "Building scratch consumer for the current runner's default RID" + & dotnet build $csprojPath -c Release --no-restore + if ($LASTEXITCODE -ne 0) { + throw "Build of scratch consumer project failed (exit code $LASTEXITCODE)." + } + + $outDir = Join-Path $scratchDir "bin/Release/$TargetFramework" + if (-not (Test-Path $outDir)) { + throw "Consumer build output directory '$outDir' does not exist." + } + + Write-Section "Inspecting consumer build output for the native Copilot payload" + $nativeRoot = Get-ChildItem -Path $outDir -Directory -Filter "runtimes" -ErrorAction SilentlyContinue + if (-not $nativeRoot) { + throw "No 'runtimes' directory found in consumer build output '$outDir'. The Copilot CLI native payload did not propagate to a real PackageReference consumer of Squad.Agents.AI." + } + + $nativeFiles = Get-ChildItem -Path $outDir -Recurse -File | + Where-Object { $_.FullName -match '[\\/]runtimes[\\/][^\\/]+[\\/]native[\\/]' } + + if (-not $nativeFiles -or $nativeFiles.Count -eq 0) { + throw "No files found under runtimes/*/native/ in consumer build output '$outDir'." + } + + Write-Host "Found native payload files:" + foreach ($f in $nativeFiles) { Write-Host " $($f.FullName)" } + + $cliBinary = $nativeFiles | Where-Object { $_.Name -in @('copilot', 'copilot.exe') } + if (-not $cliBinary) { + throw "Copilot CLI executable ('copilot' or 'copilot.exe') not found under runtimes/*/native/ in consumer output. Expected the payload from GitHub.Copilot.SDK / Copilot CLI $ExpectedCopilotCliVersion." + } + + $runtimeLibNames = @('copilot_runtime.dll', 'libcopilot_runtime.so', 'libcopilot_runtime.dylib', 'runtime.node') + $runtimeLib = $nativeFiles | Where-Object { $_.Name -in $runtimeLibNames } + if (-not $runtimeLib) { + throw "No runtime.node-derived native library ($($runtimeLibNames -join ', ')) found under runtimes/*/native/ in consumer output." + } + Write-Host "Native Copilot CLI executable: $($cliBinary.FullName)" + Write-Host "Native runtime.node-derived library: $($runtimeLib.FullName)" + + Write-Section "Proving the downloaded Copilot CLI version" + # The SDK's build/ targets cache the extracted CLI per-version at + # obj/{config}/{tfm}/copilot-cli/{CopilotCliVersion}/{platform}/..., so the directory + # names under copilot-cli/ are a direct, tamper-evident record of which CLI version(s) + # were actually downloaded for this consumer -- independent of file content inspection, + # and without ever executing the binary itself. + $objDir = Join-Path $scratchDir "obj" + $copilotCliDirs = Get-ChildItem -Path $objDir -Recurse -Directory -Filter "copilot-cli" -ErrorAction SilentlyContinue + if (-not $copilotCliDirs) { + throw "No 'copilot-cli' cache directory found under '$objDir'. Cannot verify which Copilot CLI version was downloaded." + } + + $downloadedVersions = $copilotCliDirs | + ForEach-Object { Get-ChildItem -Path $_.FullName -Directory } | + Select-Object -ExpandProperty Name -Unique + + Write-Host "Copilot CLI version(s) downloaded for the consumer: $($downloadedVersions -join ', ')" + + if ($downloadedVersions -notcontains $ExpectedCopilotCliVersion) { + throw "Expected Copilot CLI version '$ExpectedCopilotCliVersion' was not downloaded for the consumer (found: $($downloadedVersions -join ', ')). The native-payload propagation fix did not take effect." + } + + foreach ($forbidden in $ForbiddenCopilotCliVersions) { + if ($downloadedVersions -contains $forbidden) { + throw "Forbidden stale Copilot CLI version '$forbidden' was downloaded for the consumer. This is the exact regression (Squad.Agents.AI PR #1519 / Reviewer finding #1) this check exists to catch: the Microsoft.Agents.AI.GitHub.Copilot buildTransitive bridge resolved an older GitHub.Copilot.SDK version instead of the one Squad.Agents.AI references." + } + } + + Write-Host "" + Write-Host "PASS: consumer resolved Copilot CLI $ExpectedCopilotCliVersion (not $($ForbiddenCopilotCliVersions -join ', ')); native executable and runtime library both present under runtimes/*/native/." -ForegroundColor Green +} +finally { + Remove-Item -Path $scratchDir -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/src/Squad.Agents.AI/README.md b/src/Squad.Agents.AI/README.md index 1b88a3552..76ef2847e 100644 --- a/src/Squad.Agents.AI/README.md +++ b/src/Squad.Agents.AI/README.md @@ -1,6 +1,6 @@ # Squad.Agents.AI -> **Preview package.** `Squad.Agents.AI` is a `0.5.1` preview NuGet package for early adopters. It multi-targets `net8.0`, `net9.0`, and `net10.0`, and depends on preview Microsoft Agent Framework / GitHub Copilot SDK packages, so APIs may change before a stable release. +> **Preview package.** `Squad.Agents.AI` is a preview NuGet package for early adopters. It multi-targets `net8.0`, `net9.0`, and `net10.0`, and depends on preview Microsoft Agent Framework / GitHub Copilot SDK packages, so APIs may change before a stable release. ## What it does diff --git a/src/Squad.Agents.AI/Squad.Agents.AI.csproj b/src/Squad.Agents.AI/Squad.Agents.AI.csproj index 49d3acf67..2812b20a1 100644 --- a/src/Squad.Agents.AI/Squad.Agents.AI.csproj +++ b/src/Squad.Agents.AI/Squad.Agents.AI.csproj @@ -17,7 +17,18 @@ MIT README.md true - 0.5.5 + + 0.5.6-rc1 @@ -28,12 +39,19 @@ - + @@ -44,4 +62,54 @@ + + + + + <_CopilotSdkVersionBridgePropsPath>$(IntermediateOutputPath)Squad.Agents.AI.props + + + <_CopilotSdkVersionBridgeLines Include="<Project>" /> + <_CopilotSdkVersionBridgeLines Include=" <!--" /> + <_CopilotSdkVersionBridgeLines Include=" Generated at Squad.Agents.AI pack time. Do not edit, see" /> + <_CopilotSdkVersionBridgeLines Include=" GenerateCopilotSdkVersionBridge in Squad.Agents.AI.csproj." /> + <_CopilotSdkVersionBridgeLines Include=" " /> + <_CopilotSdkVersionBridgeLines Include=" Microsoft.Agents.AI.GitHub.Copilot's own buildTransitive bridge" /> + <_CopilotSdkVersionBridgeLines Include=" (Microsoft.Agents.AI.GitHub.Copilot.targets) imports GitHub.Copilot.SDK's" /> + <_CopilotSdkVersionBridgeLines Include=" build/ CLI-download targets from a path keyed on" /> + <_CopilotSdkVersionBridgeLines Include=" the _MicrosoftAgentsAICopilotSdkVersion property, which (unless overridden)" /> + <_CopilotSdkVersionBridgeLines Include=" defaults" /> + <_CopilotSdkVersionBridgeLines Include=" to the SDK version THAT PACKAGE was built against (its nuspec dependency" /> + <_CopilotSdkVersionBridgeLines Include=" floor), not the version Squad.Agents.AI actually references. Setting it here" /> + <_CopilotSdkVersionBridgeLines Include=" (unconditionally, so it wins regardless of NuGet's props import order" /> + <_CopilotSdkVersionBridgeLines Include=" relative to that package) makes every consumer of Squad.Agents.AI resolve" /> + <_CopilotSdkVersionBridgeLines Include=" the SAME GitHub.Copilot.SDK build/ targets, and therefore the same native" /> + <_CopilotSdkVersionBridgeLines Include=" Copilot CLI / runtime.node payload, that Squad.Agents.AI itself does." /> + <_CopilotSdkVersionBridgeLines Include=" -->" /> + <_CopilotSdkVersionBridgeLines Include=" <PropertyGroup>" /> + <_CopilotSdkVersionBridgeLines Include=" <_MicrosoftAgentsAICopilotSdkVersion>$(SquadCopilotSdkVersion)</_MicrosoftAgentsAICopilotSdkVersion>" /> + <_CopilotSdkVersionBridgeLines Include=" </PropertyGroup>" /> + <_CopilotSdkVersionBridgeLines Include="</Project>" /> + + + + + + + From 02bd4d606ff5df8f3c2f5ffb9bce58632c18fb27 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 21 Jul 2026 22:03:01 +0300 Subject: [PATCH 3/4] fix(agents-ai): use scratch nuget.config instead of --source args in consumer check The .NET windows-latest job's consumer packaging check failed with NU1301 ("local source ...\https:\api.nuget.org\v3\index.json doesn't exist"): passing two --source values to `dotnet restore` from this PowerShell script produced a bogus combined path on Windows (ubuntu-latest resolved the same two --source arguments correctly). Replaced the --source flags with a scratch nuget.config colocated with the scratch consumer project, listing the same two sources (nuget.org over HTTPS, local nupkgs folder). This is picked up automatically by dotnet restore/build on both platforms and avoids the platform-specific argument-passing quirk entirely. Verified locally on Windows: restore now succeeds and the build correctly attempts Copilot CLI 1.0.71 (failing only on this sandbox's expected outbound TLS block to registry.npmjs.org, unrelated to this fix). Refs #1519 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44f53a4e-d384-4c60-a43d-cfc158160f1f --- scripts/verify-squad-agents-ai-consumer.ps1 | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/verify-squad-agents-ai-consumer.ps1 b/scripts/verify-squad-agents-ai-consumer.ps1 index 80f985bee..fc8fb8c63 100644 --- a/scripts/verify-squad-agents-ai-consumer.ps1 +++ b/scripts/verify-squad-agents-ai-consumer.ps1 @@ -107,8 +107,27 @@ try { $programContent = 'System.Console.WriteLine("Squad.Agents.AI consumer packaging check placeholder.");' Set-Content -Path (Join-Path $scratchDir "Program.cs") -Value $programContent -Encoding utf8 - Write-Section "Restoring scratch consumer (local nupkg + nuget.org only)" - & dotnet restore $csprojPath --source $nupkgsFull --source "https://api.nuget.org/v3/index.json" + # Use a scratch nuget.config instead of repeated `dotnet restore --source` arguments: passing + # multiple --source values has proven unreliable across platforms when invoked from a PowerShell + # script (observed producing a bogus combined path on Windows), while an explicit, + # colocated nuget.config is picked up automatically and unambiguously by both platforms. Only + # two well-known, standard sources are listed: nuget.org over HTTPS, and the local nupkgs folder + # this same CI job just produced with `dotnet pack`. + $nugetConfigPath = Join-Path $scratchDir "nuget.config" + $nugetConfigContent = @" + + + + + + + + +"@ + Set-Content -Path $nugetConfigPath -Value $nugetConfigContent -Encoding utf8 + + Write-Section "Restoring scratch consumer (local nupkg + nuget.org only, via scratch nuget.config)" + & dotnet restore $csprojPath if ($LASTEXITCODE -ne 0) { throw "Restore of scratch consumer project failed (exit code $LASTEXITCODE)." } From a6e8bfe916c1b02da8058257699e6135671513a0 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 21 Jul 2026 22:33:28 +0300 Subject: [PATCH 4/4] fix(agents-ai): harden CI trigger paths, XML generation, and consumer source proof Focused hardening pass on the PR #1519 packaging fix, addressing four reviewer-noted gaps directly coupled to it: 1. squad-agents-ai-ci.yml now includes scripts/verify-squad-agents-ai-consumer.ps1 in both pull_request.paths and push.paths, so a script-only change to the consumer check itself triggers the AI CI workflow instead of silently going unvalidated. 2. Squad.Agents.AI.csproj's GenerateCopilotSdkVersionBridge target no longer hand-builds the packed buildTransitive/Squad.Agents.AI.props file from entity-escaped string literals. It now uses a small inline RoslynCodeTaskFactory task built on System.Xml.Linq (XDocument/XElement), so the interpolated $(SquadCopilotSdkVersion) value is escaped exactly as the .NET XML writer itself defines, not by ad hoc replacement. scripts/verify-squad-agents-ai-consumer.ps1 was rewritten the same way: the scratch .csproj and nuget.config it generates are built with System.Xml.Linq (XElement/XAttribute) instead of string interpolation, so PackageVersion, TargetFramework and the local nupkgs path are all escaped correctly regardless of content. Verified locally that the generated buildTransitive/Squad.Agents.AI.props still parses as valid XML with the exact expected value after this change. 3. CopilotCliVersion is untrusted content sourced from a third-party NuGet package (GitHub.Copilot.SDK), so it is now validated in squad-agents-ai-ci.yml against a narrow grammar (^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.]*)?$) immediately after being parsed and before being written to $GITHUB_OUTPUT, rejecting empty, multiline, or otherwise malformed values loudly. The two downstream steps that previously interpolated ${{ steps.copilot-cli-version.outputs.cli-version }} directly into their run: script bodies now instead receive it via an env: mapping (EXPECTED_COPILOT_CLI_VERSION), which GitHub Actions passes as a plain environment variable rather than substituting into the script text, removing the script-injection surface entirely. scripts/verify-squad-agents-ai-consumer.ps1 independently re-validates ExpectedCopilotCliVersion and every ForbiddenCopilotCliVersions entry against the same grammar as its very first action, regardless of how it is invoked. 4. The scratch consumer's nuget.config now uses NuGet Package Source Mapping: the Squad.Agents.AI package id is mapped to ONLY the local nupkgs folder this same job just produced, while a "*" catch-all keeps every other (transitive) dependency resolving from nuget.org as before, so future dependencies never need to be enumerated by hand. On top of that, deterministic mapping, the script now also computes the SHA512 of the freshly packed local nupkg and compares it, after restore, against the .nupkg.sha512 NuGet itself writes into the global packages cache for whatever it actually extracted, failing loudly on any mismatch. Verified locally that this hash check passes for a genuinely fresh restore and that clearing this repo's own stale global-cache entry from earlier local testing was required to reproduce a clean pass, confirming the check is sensitive to exactly the failure mode it targets. Validation: dotnet restore/build for all three projects clean (0 errors, the same 12 pre-existing XML-doc warnings, no NU1605/NU1608/NU5104); dotnet test 82/82 passing on net8.0/net9.0/net10.0; dotnet pack succeeds with no NU5104; the repo-internal Copilot CLI version guard and the consumer packaging script both still correctly resolve Copilot CLI 1.0.71 (never 1.0.67) end to end locally, including the new Package Source Mapping and SHA512 checks. squad-agents-ai-ci.yml's existing "permissions: contents: read" is unchanged; no new permissions were added. Refs #1519 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 44f53a4e-d384-4c60-a43d-cfc158160f1f --- .github/workflows/squad-agents-ai-ci.yml | 44 +++-- scripts/verify-squad-agents-ai-consumer.ps1 | 189 +++++++++++++++----- src/Squad.Agents.AI/Squad.Agents.AI.csproj | 69 ++++--- 3 files changed, 224 insertions(+), 78 deletions(-) diff --git a/.github/workflows/squad-agents-ai-ci.yml b/.github/workflows/squad-agents-ai-ci.yml index 18dc5f0b7..e09dbe6c8 100644 --- a/.github/workflows/squad-agents-ai-ci.yml +++ b/.github/workflows/squad-agents-ai-ci.yml @@ -6,6 +6,7 @@ on: - 'src/Squad.Agents.AI/**' - 'test/Squad.Agents.AI.Tests/**' - 'src/Squad.Agents.AI/samples/Squad.Agents.AI.Sample/**' + - 'scripts/verify-squad-agents-ai-consumer.ps1' - '.github/workflows/squad-agents-ai-ci.yml' - 'Directory.*.props' - 'Directory.*.json' @@ -17,6 +18,7 @@ on: - 'src/Squad.Agents.AI/**' - 'test/Squad.Agents.AI.Tests/**' - 'src/Squad.Agents.AI/samples/Squad.Agents.AI.Sample/**' + - 'scripts/verify-squad-agents-ai-consumer.ps1' - '.github/workflows/squad-agents-ai-ci.yml' - 'Directory.*.props' - 'Directory.*.json' @@ -62,16 +64,29 @@ jobs: # Resolves the Copilot CLI version that GitHub.Copilot.SDK (the version this repo # actually references, $(SquadCopilotSdkVersion)) pins, straight from that package's - # own build/GitHub.Copilot.SDK.props in the local NuGet cache -- so the expected/ + # own build/GitHub.Copilot.SDK.props in the local NuGet cache, so the expected/ # forbidden CLI versions used by the checks below never need to be hardcoded here # and can't drift out of sync with a future SDK version bump. + # + # CopilotCliVersion is untrusted third-party content (it comes from the + # GitHub.Copilot.SDK NuGet package, not this repo), so it is validated against a + # narrow version grammar before being written to $GITHUB_OUTPUT. Downstream steps + # then read it via env: rather than interpolating the ${{ }} expression directly + # into their run: script bodies, so a malformed or hostile value can never be + # concatenated into, and executed as, PowerShell source. - name: Resolve expected Copilot CLI version id: copilot-cli-version shell: pwsh run: | + # Same grammar as the version-validation guard in + # scripts/verify-squad-agents-ai-consumer.ps1 (kept in sync by hand: both + # accept dotted release/prerelease CLI versions such as 1.0.71 or 1.0.64-1, + # single line, no surrounding whitespace). + $versionGrammar = '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.]*)?$' + $sdkVersion = (dotnet msbuild src/Squad.Agents.AI/Squad.Agents.AI.csproj -getProperty:SquadCopilotSdkVersion -nologo).Trim() - if ([string]::IsNullOrWhiteSpace($sdkVersion)) { - throw "Could not resolve SquadCopilotSdkVersion from Squad.Agents.AI.csproj." + if ([string]::IsNullOrWhiteSpace($sdkVersion) -or $sdkVersion -notmatch $versionGrammar) { + throw "SquadCopilotSdkVersion resolved to an empty or invalid value from Squad.Agents.AI.csproj: '$sdkVersion'." } $globalPackagesOutput = dotnet nuget locals global-packages --list @@ -82,9 +97,9 @@ jobs: } [xml]$sdkProps = Get-Content $sdkPropsPath - $cliVersion = $sdkProps.Project.PropertyGroup.CopilotCliVersion - if ([string]::IsNullOrWhiteSpace($cliVersion)) { - throw "Could not parse CopilotCliVersion out of '$sdkPropsPath'." + $cliVersion = "$($sdkProps.Project.PropertyGroup.CopilotCliVersion)".Trim() + if ([string]::IsNullOrWhiteSpace($cliVersion) -or $cliVersion -notmatch $versionGrammar -or $cliVersion.Contains("`n")) { + throw "CopilotCliVersion parsed from '$sdkPropsPath' is empty, multiline, or does not match the expected grammar ($versionGrammar): '$cliVersion'." } Write-Host "GitHub.Copilot.SDK $sdkVersion pins Copilot CLI $cliVersion" @@ -108,8 +123,15 @@ jobs: # on, because neither project has a direct PackageReference to GitHub.Copilot.SDK. - name: Verify no stale Copilot CLI payload in repo builds shell: pwsh + env: + EXPECTED_COPILOT_CLI_VERSION: ${{ steps.copilot-cli-version.outputs.cli-version }} run: | - $expected = "${{ steps.copilot-cli-version.outputs.cli-version }}" + $versionGrammar = '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.]*)?$' + $expected = $env:EXPECTED_COPILOT_CLI_VERSION + if ([string]::IsNullOrWhiteSpace($expected) -or $expected -notmatch $versionGrammar) { + throw "EXPECTED_COPILOT_CLI_VERSION is empty or does not match the expected grammar ($versionGrammar): '$expected'." + } + $forbidden = @('1.0.67') $projectObjDirs = @( 'src/Squad.Agents.AI/obj', @@ -137,7 +159,7 @@ jobs: } if ($failed) { - throw "Stale or missing Copilot CLI payload detected -- see ::error annotations above." + throw "Stale or missing Copilot CLI payload detected, see ::error annotations above." } Write-Host "All Squad.Agents.AI-related builds resolved Copilot CLI $expected." @@ -154,10 +176,10 @@ jobs: # Squad.Agents.AI.csproj). See scripts/verify-squad-agents-ai-consumer.ps1. - name: Verify Squad.Agents.AI consumer packaging (native Copilot CLI payload) shell: pwsh + env: + EXPECTED_COPILOT_CLI_VERSION: ${{ steps.copilot-cli-version.outputs.cli-version }} run: | - ./scripts/verify-squad-agents-ai-consumer.ps1 ` - -NupkgsDir nupkgs ` - -ExpectedCopilotCliVersion "${{ steps.copilot-cli-version.outputs.cli-version }}" + ./scripts/verify-squad-agents-ai-consumer.ps1 -NupkgsDir nupkgs -ExpectedCopilotCliVersion $env:EXPECTED_COPILOT_CLI_VERSION - name: Upload test results if: always() diff --git a/scripts/verify-squad-agents-ai-consumer.ps1 b/scripts/verify-squad-agents-ai-consumer.ps1 index fc8fb8c63..4735d5748 100644 --- a/scripts/verify-squad-agents-ai-consumer.ps1 +++ b/scripts/verify-squad-agents-ai-consumer.ps1 @@ -8,12 +8,12 @@ nupkg (plus nuget.org for its transitive dependencies), builds it for the current runner's default portable RID, and asserts that the native Copilot CLI payload (the copilot/copilot.exe executable and its runtime.node-derived native library) actually - propagated to a REAL PackageReference consumer -- not just to this repo's own + propagated to a REAL PackageReference consumer, not just to this repo's own ProjectReference-based test/sample projects. This exists because Microsoft.Agents.AI.GitHub.Copilot ships GitHub.Copilot.SDK's CLI - download targets to transitive consumers via a buildTransitive/ bridge that -- unless - overridden -- resolves an OLDER GitHub.Copilot.SDK version (whatever version the adapter + download targets to transitive consumers via a buildTransitive/ bridge that, unless + overridden, resolves an OLDER GitHub.Copilot.SDK version (whatever version the adapter package itself was built/floor-pinned against) instead of the version Squad.Agents.AI actually references. See Directory.Build.props and the GenerateCopilotSdkVersionBridge target in src/Squad.Agents.AI/Squad.Agents.AI.csproj for the fix; this script is the @@ -22,11 +22,20 @@ Directory.Build.props alone (a repo-internal mechanism a real external consumer never sees). - Does not execute the downloaded Copilot CLI binary -- only inspects file paths that + Does not execute the downloaded Copilot CLI binary, only inspects file paths that MSBuild's own download/copy targets produced, so it never runs untrusted downloaded code. Does not touch package source or TLS/signature verification policy: only two well-known, standard sources are used (nuget.org over HTTPS, and the local nupkgs - folder produced by `dotnet pack` in this same repo checkout). + folder produced by `dotnet pack` in this same repo checkout). The Squad.Agents.AI + package itself is pinned to the local source only, via NuGet Package Source Mapping, + so it can never be silently satisfied by an identically named/versioned package on + nuget.org; a post-restore SHA512 hash comparison against the exact bytes this script + just packed is an additional, independent proof of the same thing. + + All XML this script generates (the scratch .csproj and nuget.config) is built with + System.Xml.Linq (XElement/XAttribute/XDocument), which escapes every interpolated value + according to the XML specification. Nothing is embedded via ad hoc string + concatenation or hand-written entity substitution. .PARAMETER NupkgsDir Directory containing exactly one packed Squad.Agents.AI .nupkg (e.g. the CI job's @@ -36,10 +45,13 @@ .PARAMETER ExpectedCopilotCliVersion The Copilot CLI version the consumer is expected to download (the CLI version pinned by - the GitHub.Copilot.SDK version Squad.Agents.AI references), e.g. "1.0.71". + the GitHub.Copilot.SDK version Squad.Agents.AI references), e.g. "1.0.71". Must match + the same narrow version grammar validated in squad-agents-ai-ci.yml; this script + re-validates it independently since it is untrusted content that ultimately comes from + a third-party NuGet package. .PARAMETER ForbiddenCopilotCliVersions - Copilot CLI versions that must NOT appear anywhere in the consumer's build output -- + Copilot CLI versions that must NOT appear anywhere in the consumer's build output, i.e. the stale versions this script exists to catch a regression to. .PARAMETER TargetFramework @@ -61,11 +73,47 @@ param( $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest +Add-Type -AssemblyName System.Xml.Linq + +# Same grammar as the version-validation guard in squad-agents-ai-ci.yml (kept in sync by +# hand: both accept dotted release/prerelease CLI versions such as 1.0.71 or 1.0.64-1, +# single line, no surrounding whitespace). CopilotCliVersion ultimately comes from a +# third-party NuGet package, so it is treated as untrusted input here too, independent of +# whatever validation the caller already performed. +$VersionGrammar = '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.]*)?$' + +function Assert-ValidVersion([string]$Value, [string]$Description) { + if ([string]::IsNullOrWhiteSpace($Value) -or $Value.Contains("`n") -or $Value.Contains("`r") -or ($Value -notmatch $VersionGrammar)) { + throw "$Description is empty, multiline, or does not match the expected grammar ($VersionGrammar): '$Value'." + } +} + +function Get-Sha512Base64([string]$FilePath) { + $hasher = [System.Security.Cryptography.SHA512]::Create() + try { + $stream = [System.IO.File]::OpenRead($FilePath) + try { + return [Convert]::ToBase64String($hasher.ComputeHash($stream)) + } + finally { + $stream.Dispose() + } + } + finally { + $hasher.Dispose() + } +} + function Write-Section([string]$Title) { Write-Host "" Write-Host "=== $Title ===" -ForegroundColor Cyan } +Assert-ValidVersion -Value $ExpectedCopilotCliVersion -Description 'ExpectedCopilotCliVersion' +foreach ($forbidden in $ForbiddenCopilotCliVersions) { + Assert-ValidVersion -Value $forbidden -Description 'ForbiddenCopilotCliVersions entry' +} + $nupkgsFull = (Resolve-Path $NupkgsDir).Path $candidateNupkgs = @(Get-ChildItem -Path $nupkgsFull -Filter "squad.agents.ai.*.nupkg") if ($candidateNupkgs.Count -eq 0) { @@ -75,6 +123,7 @@ if ($candidateNupkgs.Count -gt 1) { throw "Expected exactly one packed Squad.Agents.AI nupkg in '$nupkgsFull', found $($candidateNupkgs.Count): $($candidateNupkgs.Name -join ', ')" } +$packedNupkgPath = $candidateNupkgs[0].FullName $nupkgMatch = [System.Text.RegularExpressions.Regex]::Match($candidateNupkgs[0].Name, '^squad\.agents\.ai\.(?.+)\.nupkg$', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) if (-not $nupkgMatch.Success) { throw "Could not parse a package version out of nupkg filename '$($candidateNupkgs[0].Name)'." @@ -82,56 +131,110 @@ if (-not $nupkgMatch.Success) { $PackageVersion = $nupkgMatch.Groups['version'].Value Write-Host "Detected packed Squad.Agents.AI version: $PackageVersion (from $($candidateNupkgs[0].Name))" +$packedNupkgHash = Get-Sha512Base64 -FilePath $packedNupkgPath +Write-Host "Packed nupkg SHA512 (base64): $packedNupkgHash" + $scratchDir = Join-Path ([System.IO.Path]::GetTempPath()) "squad-agents-ai-consumer-$([guid]::NewGuid())" New-Item -ItemType Directory -Path $scratchDir | Out-Null Write-Host "Scratch consumer project: $scratchDir" try { + # --- Scratch .csproj, built with System.Xml.Linq so PackageVersion (and any future + # dynamic value) is always escaped correctly regardless of its content. --- $csprojPath = Join-Path $scratchDir "ConsumerCheck.csproj" - $csprojContent = @" - - - $TargetFramework - Exe - enable - enable - false - - - - - -"@ - Set-Content -Path $csprojPath -Value $csprojContent -Encoding utf8 + $xns = [System.Xml.Linq.XName] + $csprojRoot = [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('Project'), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('PropertyGroup'), + [System.Xml.Linq.XElement]::new($xns::op_Implicit('TargetFramework'), $TargetFramework), + [System.Xml.Linq.XElement]::new($xns::op_Implicit('OutputType'), 'Exe'), + [System.Xml.Linq.XElement]::new($xns::op_Implicit('ImplicitUsings'), 'enable'), + [System.Xml.Linq.XElement]::new($xns::op_Implicit('Nullable'), 'enable'), + [System.Xml.Linq.XElement]::new($xns::op_Implicit('IsPackable'), 'false') + ), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('ItemGroup'), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('PackageReference'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('Include'), 'Squad.Agents.AI'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('Version'), $PackageVersion) + ) + ) + ) + $csprojRoot.SetAttributeValue($xns::op_Implicit('Sdk'), 'Microsoft.NET.Sdk') + [System.Xml.Linq.XDocument]::new($csprojRoot).Save($csprojPath) $programContent = 'System.Console.WriteLine("Squad.Agents.AI consumer packaging check placeholder.");' Set-Content -Path (Join-Path $scratchDir "Program.cs") -Value $programContent -Encoding utf8 - # Use a scratch nuget.config instead of repeated `dotnet restore --source` arguments: passing - # multiple --source values has proven unreliable across platforms when invoked from a PowerShell - # script (observed producing a bogus combined path on Windows), while an explicit, - # colocated nuget.config is picked up automatically and unambiguously by both platforms. Only - # two well-known, standard sources are listed: nuget.org over HTTPS, and the local nupkgs folder - # this same CI job just produced with `dotnet pack`. + # --- Scratch nuget.config, also built with System.Xml.Linq. Two sources only: + # nuget.org and the local nupkgs folder this same job just produced. Package Source + # Mapping pins Squad.Agents.AI to ONLY the local source (an identically named/versioned + # package could otherwise exist, now or in future, on nuget.org and be silently + # substituted); everything else (Squad.Agents.AI's transitive dependencies, whatever + # they are, now or later) falls through the "*" catch-all to nuget.org, so this does + # not need to enumerate them and cannot silently miss a future one. --- $nugetConfigPath = Join-Path $scratchDir "nuget.config" - $nugetConfigContent = @" - - - - - - - - -"@ - Set-Content -Path $nugetConfigPath -Value $nugetConfigContent -Encoding utf8 - - Write-Section "Restoring scratch consumer (local nupkg + nuget.org only, via scratch nuget.config)" + $packageSources = [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('packageSources'), + [System.Xml.Linq.XElement]::new($xns::op_Implicit('clear')), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('add'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('key'), 'local-squad-agents-ai'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('value'), $nupkgsFull) + ), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('add'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('key'), 'nuget.org'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('value'), 'https://api.nuget.org/v3/index.json') + ) + ) + $packageSourceMapping = [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('packageSourceMapping'), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('packageSource'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('key'), 'local-squad-agents-ai'), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('package'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('pattern'), 'Squad.Agents.AI') + ) + ), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('packageSource'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('key'), 'nuget.org'), + [System.Xml.Linq.XElement]::new( + $xns::op_Implicit('package'), + [System.Xml.Linq.XAttribute]::new($xns::op_Implicit('pattern'), '*') + ) + ) + ) + $configRoot = [System.Xml.Linq.XElement]::new($xns::op_Implicit('configuration'), $packageSources, $packageSourceMapping) + [System.Xml.Linq.XDocument]::new($configRoot).Save($nugetConfigPath) + + Write-Section "Restoring scratch consumer (local nupkg + nuget.org only, via scratch nuget.config with Package Source Mapping)" & dotnet restore $csprojPath if ($LASTEXITCODE -ne 0) { throw "Restore of scratch consumer project failed (exit code $LASTEXITCODE)." } + Write-Section "Proving Squad.Agents.AI was restored from the freshly packed local nupkg" + # Package Source Mapping above already restricts restore of Squad.Agents.AI to the + # local source, but this independently proves it by byte-for-byte hash: the NuGet + # global packages cache records the SHA512 of whatever nupkg it actually extracted, + # in a sibling .nupkg.sha512 file, for every restored package. + $globalPackagesOutput = dotnet nuget locals global-packages --list + $nugetPackagesRoot = ($globalPackagesOutput -split ':', 2)[1].Trim() + $restoredHashPath = Join-Path $nugetPackagesRoot "squad.agents.ai/$($PackageVersion.ToLowerInvariant())/squad.agents.ai.$($PackageVersion.ToLowerInvariant()).nupkg.sha512" + if (-not (Test-Path $restoredHashPath)) { + throw "Restored Squad.Agents.AI package hash file not found at '$restoredHashPath'. Cannot verify restore came from the local nupkg." + } + $restoredHash = (Get-Content -Raw -Path $restoredHashPath).Trim() + if ($restoredHash -ne $packedNupkgHash) { + throw "Restored Squad.Agents.AI package SHA512 ('$restoredHash') does not match the freshly packed local nupkg ('$packedNupkgHash'). This would mean the consumer resolved a DIFFERENT Squad.Agents.AI package than the one this job just built, defeating the purpose of this check." + } + Write-Host "Restored Squad.Agents.AI SHA512 matches the freshly packed local nupkg exactly." + Write-Section "Building scratch consumer for the current runner's default RID" & dotnet build $csprojPath -c Release --no-restore if ($LASTEXITCODE -ne 0) { @@ -176,7 +279,7 @@ try { # The SDK's build/ targets cache the extracted CLI per-version at # obj/{config}/{tfm}/copilot-cli/{CopilotCliVersion}/{platform}/..., so the directory # names under copilot-cli/ are a direct, tamper-evident record of which CLI version(s) - # were actually downloaded for this consumer -- independent of file content inspection, + # were actually downloaded for this consumer, independent of file content inspection, # and without ever executing the binary itself. $objDir = Join-Path $scratchDir "obj" $copilotCliDirs = Get-ChildItem -Path $objDir -Recurse -Directory -Filter "copilot-cli" -ErrorAction SilentlyContinue @@ -201,7 +304,7 @@ try { } Write-Host "" - Write-Host "PASS: consumer resolved Copilot CLI $ExpectedCopilotCliVersion (not $($ForbiddenCopilotCliVersions -join ', ')); native executable and runtime library both present under runtimes/*/native/." -ForegroundColor Green + Write-Host "PASS: consumer resolved Copilot CLI $ExpectedCopilotCliVersion (not $($ForbiddenCopilotCliVersions -join ', ')); native executable and runtime library both present under runtimes/*/native/; Squad.Agents.AI restored from the freshly packed local nupkg (SHA512 verified)." -ForegroundColor Green } finally { Remove-Item -Path $scratchDir -Recurse -Force -ErrorAction SilentlyContinue diff --git a/src/Squad.Agents.AI/Squad.Agents.AI.csproj b/src/Squad.Agents.AI/Squad.Agents.AI.csproj index 2812b20a1..ff82243c0 100644 --- a/src/Squad.Agents.AI/Squad.Agents.AI.csproj +++ b/src/Squad.Agents.AI/Squad.Agents.AI.csproj @@ -78,35 +78,56 @@ out of sync with $(SquadCopilotSdkVersion) / the GitHub.Copilot.SDK PackageReference above; mirroring exactly how Microsoft.Agents.AI.GitHub.Copilot.props itself is "generated at [that package's] pack time" per its own doc comments. + + Generated with System.Xml.Linq (XDocument/XElement) via an inline RoslynCodeTaskFactory + task, not hand-built entity-escaped strings, so the interpolated + $(SquadCopilotSdkVersion) value is guaranteed to be escaped correctly for XML by the + same writer .NET itself uses, regardless of what characters that property ever + contains. --> + + + + + + + + + + + + <_CopilotSdkVersionBridgePropsPath>$(IntermediateOutputPath)Squad.Agents.AI.props - - <_CopilotSdkVersionBridgeLines Include="<Project>" /> - <_CopilotSdkVersionBridgeLines Include=" <!--" /> - <_CopilotSdkVersionBridgeLines Include=" Generated at Squad.Agents.AI pack time. Do not edit, see" /> - <_CopilotSdkVersionBridgeLines Include=" GenerateCopilotSdkVersionBridge in Squad.Agents.AI.csproj." /> - <_CopilotSdkVersionBridgeLines Include=" " /> - <_CopilotSdkVersionBridgeLines Include=" Microsoft.Agents.AI.GitHub.Copilot's own buildTransitive bridge" /> - <_CopilotSdkVersionBridgeLines Include=" (Microsoft.Agents.AI.GitHub.Copilot.targets) imports GitHub.Copilot.SDK's" /> - <_CopilotSdkVersionBridgeLines Include=" build/ CLI-download targets from a path keyed on" /> - <_CopilotSdkVersionBridgeLines Include=" the _MicrosoftAgentsAICopilotSdkVersion property, which (unless overridden)" /> - <_CopilotSdkVersionBridgeLines Include=" defaults" /> - <_CopilotSdkVersionBridgeLines Include=" to the SDK version THAT PACKAGE was built against (its nuspec dependency" /> - <_CopilotSdkVersionBridgeLines Include=" floor), not the version Squad.Agents.AI actually references. Setting it here" /> - <_CopilotSdkVersionBridgeLines Include=" (unconditionally, so it wins regardless of NuGet's props import order" /> - <_CopilotSdkVersionBridgeLines Include=" relative to that package) makes every consumer of Squad.Agents.AI resolve" /> - <_CopilotSdkVersionBridgeLines Include=" the SAME GitHub.Copilot.SDK build/ targets, and therefore the same native" /> - <_CopilotSdkVersionBridgeLines Include=" Copilot CLI / runtime.node payload, that Squad.Agents.AI itself does." /> - <_CopilotSdkVersionBridgeLines Include=" -->" /> - <_CopilotSdkVersionBridgeLines Include=" <PropertyGroup>" /> - <_CopilotSdkVersionBridgeLines Include=" <_MicrosoftAgentsAICopilotSdkVersion>$(SquadCopilotSdkVersion)</_MicrosoftAgentsAICopilotSdkVersion>" /> - <_CopilotSdkVersionBridgeLines Include=" </PropertyGroup>" /> - <_CopilotSdkVersionBridgeLines Include="</Project>" /> - - +