From 4846c6910fc2c0f212816a28b57f1d541fadf337 Mon Sep 17 00:00:00 2001 From: Hillary Mutisya <150286414+hillary-mutisya@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:08:04 -0700 Subject: [PATCH 1/5] Fix optional-agent install policy and shell certificate construction Preserve the explicit protobufjs build denial in temporary bundle workspaces and pass certificate bytes as a single typed constructor argument. Add packaging regression coverage and validation guidance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pipelines/README.md | 10 +++ ts/tools/scripts/package-signed-shell.ps1 | 2 +- ts/tools/scripts/packageOptionalAgents.mjs | 7 +- .../test/optionalAgentInstallPolicy.spec.mjs | 35 ++++++++ .../test/windowsInstallerTrust.spec.mjs | 83 +++++++++++++++++-- ts/tools/scripts/verify-shell-publication.ps1 | 2 +- ts/tools/scripts/windows-package-trust.ps1 | 2 +- 7 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 ts/tools/scripts/test/optionalAgentInstallPolicy.spec.mjs diff --git a/pipelines/README.md b/pipelines/README.md index 743599f38c..07d2c288cb 100644 --- a/pipelines/README.md +++ b/pipelines/README.md @@ -27,6 +27,16 @@ This folder contains Azure DevOps pipelines for the TypeAgent repo. | `include-install-pnpm.yml` | corepack enable + pnpm store configuration | | `include-update-package-version.yml` | Stamp prerelease version across all workspace packages | +## Packaging regression checks + +From `ts`, run `node --test tools/scripts/test/optionalAgentInstallPolicy.spec.mjs tools/scripts/test/windowsInstallerTrust.spec.mjs`. +The optional-agent check verifies that the temporary pnpm workspace explicitly +disables `protobufjs` install scripts, matching the main workspace policy. +On Windows, the trust checks exercise certificate-byte conversion and cleanup +with a synthetic certificate and a mocked trust store; they do not modify +certificate stores or retrieve signing secrets. PowerShell 7 (`pwsh`) is required +for the synthetic-certificate check, matching the shell packaging pipeline. + ## Pipeline variables (configure in ADO UI) The consolidated pipeline requires these variables to be set on the pipeline diff --git a/ts/tools/scripts/package-signed-shell.ps1 b/ts/tools/scripts/package-signed-shell.ps1 index 2a51b82a14..24e8bdfa83 100644 --- a/ts/tools/scripts/package-signed-shell.ps1 +++ b/ts/tools/scripts/package-signed-shell.ps1 @@ -26,7 +26,7 @@ try { Remove-Item -LiteralPath $passwordFile -Force } $env:CSC_LINK = Join-Path $HOME ".typeagent\TypeAgent-Development-Certificate.pfx" - $certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2( + $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new( $env:CSC_LINK, $env:CSC_KEY_PASSWORD, [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet) if ($certificate.Subject -ne $policy.publisher -or $certificate.Thumbprint -notin $policy.thumbprints) { diff --git a/ts/tools/scripts/packageOptionalAgents.mjs b/ts/tools/scripts/packageOptionalAgents.mjs index 15fd117e25..c64d16a3f0 100644 --- a/ts/tools/scripts/packageOptionalAgents.mjs +++ b/ts/tools/scripts/packageOptionalAgents.mjs @@ -65,7 +65,7 @@ function run(cmd, cmdArgs, cwd, env = process.env) { throw new Error(`Command failed (${res.status}): ${cmd}`); } -function writeRuntimeInstallWorkspace(directory) { +export function writeRuntimeInstallWorkspace(directory) { const content = [ 'packages: ["."]', "allowBuilds:", @@ -75,6 +75,7 @@ function writeRuntimeInstallWorkspace(directory) { " keytar: true", " koffi: true", " onnxruntime-node: true", + " protobufjs: false", " puppeteer: false", " sharp: true", "", @@ -257,4 +258,6 @@ async function main() { ); } -await main(); +if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { + await main(); +} diff --git a/ts/tools/scripts/test/optionalAgentInstallPolicy.spec.mjs b/ts/tools/scripts/test/optionalAgentInstallPolicy.spec.mjs new file mode 100644 index 0000000000..ec5e6cf0d2 --- /dev/null +++ b/ts/tools/scripts/test/optionalAgentInstallPolicy.spec.mjs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { writeRuntimeInstallWorkspace } from "../packageOptionalAgents.mjs"; + +const yaml = createRequire(import.meta.url)("js-yaml"); + +test("optional-agent installs explicitly deny protobufjs builds without weakening native builds", (t) => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), "typeagent-optional-policy-"), + ); + t.after(() => fs.rmSync(directory, { recursive: true, force: true })); + const workspace = writeRuntimeInstallWorkspace(directory); + const config = yaml.load(fs.readFileSync(workspace, "utf8")); + const canonical = yaml.load( + fs.readFileSync( + new URL("../../../pnpm-workspace.yaml", import.meta.url), + "utf8", + ), + ); + assert.deepEqual(config.packages, ["."]); + assert.equal(config.allowBuilds.protobufjs, false); + assert.equal(config.allowBuilds.puppeteer, false); + assert.equal(config.allowBuilds["onnxruntime-node"], true); + assert.equal(config.allowBuilds["better-sqlite3"], true); + for (const [name, allowed] of Object.entries(config.allowBuilds)) { + assert.equal(allowed, canonical.allowBuilds[name], name); + } +}); diff --git a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs index 892502f40e..344e9272fb 100644 --- a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs +++ b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs @@ -36,15 +36,19 @@ function importFunctions(file) { `; } -function runPowerShell(code, expectedStatus = 0) { +function runPowerShell( + code, + expectedStatus = 0, + executable = path.join( + process.env.SystemRoot, + "System32", + "WindowsPowerShell", + "v1.0", + "powershell.exe", + ), +) { const result = spawnSync( - path.join( - process.env.SystemRoot, - "System32", - "WindowsPowerShell", - "v1.0", - "powershell.exe", - ), + executable, [ "-NoProfile", "-NonInteractive", @@ -90,6 +94,69 @@ function fixture(t) { return { dir, metadata }; } +test( + "build trust converts certificate bytes and restores trust on success and failure", + { skip: !windows }, + () => { + runPowerShell( + ` + . ${quote(path.join(scripts, "windows-package-trust.ps1"))} + $rsa = [Security.Cryptography.RSA]::Create(2048) + $request = [Security.Cryptography.X509Certificates.CertificateRequest]::new( + 'CN=TypeAgent-Test', $rsa, [Security.Cryptography.HashAlgorithmName]::SHA256, + [Security.Cryptography.RSASignaturePadding]::Pkcs1) + $certificate = $request.CreateSelfSigned( + [DateTimeOffset]::UtcNow.AddMinutes(-1), [DateTimeOffset]::UtcNow.AddMinutes(5)) + try { + $script:testPolicy = @{ + publisher = $certificate.Subject + thumbprints = @($certificate.Thumbprint) + } | ConvertTo-Json + function Get-Content { $script:testPolicy } + $script:added = 0 + $script:removed = 0 + $script:closed = 0 + $script:actions = 0 + $script:store = [pscustomobject]@{ Certificates = @() } + $script:store | Add-Member ScriptMethod Open { param($flags) } + $script:store | Add-Member ScriptMethod Add { + param($public) + if ($public.HasPrivateKey) { throw 'Private key reached trust store' } + if ($public.Thumbprint -ne $certificate.Thumbprint) { throw 'Certificate changed' } + $script:added++ + } + $script:store | Add-Member ScriptMethod Remove { param($public) $script:removed++ } + $script:store | Add-Member ScriptMethod Close { $script:closed++ } + function New-Object { + param($TypeName, $ArgumentList) + if ($TypeName -eq 'Security.Cryptography.X509Certificates.X509Store') { + return $script:store + } + Microsoft.PowerShell.Utility\\New-Object -TypeName $TypeName -ArgumentList $ArgumentList + } + Use-TypeAgentBuildTrust -Certificate $certificate -Action { $script:actions++ } + $failed = $false + try { + Use-TypeAgentBuildTrust -Certificate $certificate -Action { throw 'Expected action failure' } + } catch { + if ($_.Exception.Message -ne 'Expected action failure') { throw } + $failed = $true + } + if (-not $failed -or $script:actions -ne 1 -or $script:added -ne 2 -or + $script:removed -ne 2 -or $script:closed -ne 2) { + throw 'Temporary trust was not restored' + } + } finally { + $certificate.Dispose() + $rsa.Dispose() + } + `, + 0, + "pwsh", + ); + }, +); + test("publication verifies final hashes, metadata and signatures", (t) => { const { dir, metadata } = fixture(t); let calls = 0; diff --git a/ts/tools/scripts/verify-shell-publication.ps1 b/ts/tools/scripts/verify-shell-publication.ps1 index a5a839beb2..0e871facec 100644 --- a/ts/tools/scripts/verify-shell-publication.ps1 +++ b/ts/tools/scripts/verify-shell-publication.ps1 @@ -8,7 +8,7 @@ $ErrorActionPreference = "Stop" . (Join-Path $PSScriptRoot "windows-package-trust.ps1") $certificate = $null try { - $certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2( + $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new( (Join-Path $Directory "windows-package-trust.cer")) Use-TypeAgentBuildTrust -Certificate $certificate -Action { & node (Join-Path $PSScriptRoot "verify-shell-artifacts.mjs") $Directory diff --git a/ts/tools/scripts/windows-package-trust.ps1 b/ts/tools/scripts/windows-package-trust.ps1 index f40105d991..1638f972e9 100644 --- a/ts/tools/scripts/windows-package-trust.ps1 +++ b/ts/tools/scripts/windows-package-trust.ps1 @@ -36,7 +36,7 @@ function Use-TypeAgentBuildTrust { if ($Certificate.Subject -ne $policy.publisher -or $Certificate.Thumbprint -notin $policy.thumbprints) { throw "Build certificate does not match the independently pinned signing identity." } - $publicCertificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2( + $publicCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new( $Certificate.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)) $store = New-Object Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser") $added = $false From 50b0d1a255b51478cdaac09936cf38881512ad15 Mon Sep 17 00:00:00 2001 From: Hillary Mutisya <150286414+hillary-mutisya@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:06:04 -0700 Subject: [PATCH 2/5] Avoid interactive certificate trust prompts during shell signing --- pipelines/README.md | 22 +++++++++ ts/tools/scripts/package-signed-shell.ps1 | 5 ++ .../test/windowsInstallerTrust.spec.mjs | 47 ++++++++++++++++++- ts/tools/scripts/windows-package-trust.ps1 | 18 +++++-- 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/pipelines/README.md b/pipelines/README.md index 07d2c288cb..c13f2fc60a 100644 --- a/pipelines/README.md +++ b/pipelines/README.md @@ -36,6 +36,28 @@ On Windows, the trust checks exercise certificate-byte conversion and cleanup with a synthetic certificate and a mocked trust store; they do not modify certificate stores or retrieve signing secrets. PowerShell 7 (`pwsh`) is required for the synthetic-certificate check, matching the shell packaging pipeline. +The checks also enforce machine-level build trust, cleanup after success and +failure, preservation of pre-existing trust, and rejection of unpinned certificates. + +### Windows shell signing hangs after certificate retrieval + +Run `20261020.1` (build `139732`) stopped logging after writing the PFX and +was canceled about 41 minutes later. The previous build-trust helper added the +public certificate to `CurrentUser\Root`, which can display a native Windows +trust-confirmation dialog even under PowerShell `-NonInteractive`. +The log alone does not prove where it blocked, since there were no progress +messages between certificate retrieval and packaging. + +Build and publication verification now temporarily trust only the policy-pinned +public certificate in `LocalMachine\Root`, avoiding the per-user dialog. +Both operations require an administrator Windows agent (as provided by +`windows-latest`); store-access failures fail the task rather than falling back +to interactive per-user trust. Existing matching trust is left unchanged, and +certificates added by the helper are removed in `finally`, including when the +package action fails. A forcibly terminated agent may not execute cleanup, so +use disposable agents for these jobs. End-user installer trust is unchanged. +Signing logs identify certificate retrieval, password retrieval, policy checking, +trust-store access, packaging, verification, and trust cleanup separately. ## Pipeline variables (configure in ADO UI) diff --git a/ts/tools/scripts/package-signed-shell.ps1 b/ts/tools/scripts/package-signed-shell.ps1 index 24e8bdfa83..3903fe242c 100644 --- a/ts/tools/scripts/package-signed-shell.ps1 +++ b/ts/tools/scripts/package-signed-shell.ps1 @@ -14,8 +14,10 @@ try { if ($env:TYPEAGENT_SHELL_VALIDATION_ONLY -eq "1") { throw "Release signing must not use unsigned validation-only packaging." } + Write-Host "Retrieving shell signing certificate." & node (Join-Path $PSScriptRoot "getCert.mjs") pull if ($LASTEXITCODE -ne 0) { throw "Could not retrieve the development signing certificate." } + Write-Host "Retrieving shell signing certificate password." $passwordOutput = & node (Join-Path $PSScriptRoot "getCert.mjs") get-password if ($LASTEXITCODE -ne 0) { throw "Could not retrieve the signing certificate password." } $passwordFile = @($passwordOutput | Where-Object { $_ -match '^[A-Za-z]:\\.+\.txt$' }) | Select-Object -Last 1 @@ -26,6 +28,7 @@ try { Remove-Item -LiteralPath $passwordFile -Force } $env:CSC_LINK = Join-Path $HOME ".typeagent\TypeAgent-Development-Certificate.pfx" + Write-Host "Loading shell signing certificate and checking publisher policy." $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new( $env:CSC_LINK, $env:CSC_KEY_PASSWORD, [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet) @@ -36,9 +39,11 @@ try { Push-Location (Join-Path $PSScriptRoot "..\..") try { Use-TypeAgentBuildTrust -Certificate $certificate -Action { + Write-Host "Packaging and signing Windows shell." & $env:ComSpec /d /c "pnpm run shell:package" if ($LASTEXITCODE -ne 0) { throw "Signed shell packaging failed." } $dist = Join-Path $PSScriptRoot "..\..\packages\shell\dist" + Write-Host "Verifying signed Windows shell artifacts." & node (Join-Path $PSScriptRoot "verify-shell-artifacts.mjs") $dist if ($LASTEXITCODE -ne 0) { throw "Signed shell artifact verification failed." } [IO.File]::WriteAllBytes((Join-Path $dist "windows-package-trust.cer"), diff --git a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs index 344e9272fb..f02ebd0678 100644 --- a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs +++ b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs @@ -95,7 +95,7 @@ function fixture(t) { } test( - "build trust converts certificate bytes and restores trust on success and failure", + "build trust uses prompt-free machine trust and restores it on success and failure", { skip: !windows }, () => { runPowerShell( @@ -117,8 +117,14 @@ test( $script:removed = 0 $script:closed = 0 $script:actions = 0 + $script:stores = 0 $script:store = [pscustomobject]@{ Certificates = @() } - $script:store | Add-Member ScriptMethod Open { param($flags) } + $script:store | Add-Member ScriptMethod Open { + param($flags) + if ($flags -ne [Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) { + throw 'Unexpected store flags' + } + } $script:store | Add-Member ScriptMethod Add { param($public) if ($public.HasPrivateKey) { throw 'Private key reached trust store' } @@ -130,6 +136,11 @@ test( function New-Object { param($TypeName, $ArgumentList) if ($TypeName -eq 'Security.Cryptography.X509Certificates.X509Store') { + if ($ArgumentList.Count -ne 2 -or $ArgumentList[0] -ne 'Root' -or + $ArgumentList[1] -ne 'LocalMachine') { + throw 'Build trust must avoid the CurrentUser Root confirmation dialog' + } + $script:stores++ return $script:store } Microsoft.PowerShell.Utility\\New-Object -TypeName $TypeName -ArgumentList $ArgumentList @@ -146,6 +157,38 @@ test( $script:removed -ne 2 -or $script:closed -ne 2) { throw 'Temporary trust was not restored' } + $script:store.Certificates = @($certificate) + Use-TypeAgentBuildTrust -Certificate $certificate -Action { $script:actions++ } + if ($script:actions -ne 2 -or $script:added -ne 2 -or + $script:removed -ne 2 -or $script:closed -ne 3) { + throw 'Pre-existing machine trust was modified' + } + $script:store | Add-Member ScriptMethod Open { throw 'Store access denied' } -Force + $failed = $false + try { + Use-TypeAgentBuildTrust -Certificate $certificate -Action { throw 'Action ran without trust' } + } catch { + if ($_.Exception.Message -notlike '*Store access denied*') { throw } + $failed = $true + } + if (-not $failed -or $script:added -ne 2 -or $script:removed -ne 2 -or + $script:closed -ne 4) { + throw 'Store access failure was not propagated and cleaned up' + } + $script:testPolicy = @{ + publisher = $certificate.Subject + thumbprints = @('0' * 40) + } | ConvertTo-Json + $failed = $false + try { + Use-TypeAgentBuildTrust -Certificate $certificate -Action { throw 'Unpinned action ran' } + } catch { + if ($_.Exception.Message -ne 'Build certificate does not match the independently pinned signing identity.') { throw } + $failed = $true + } + if (-not $failed -or $script:stores -ne 4) { + throw 'Unpinned certificate reached the machine trust store' + } } finally { $certificate.Dispose() $rsa.Dispose() diff --git a/ts/tools/scripts/windows-package-trust.ps1 b/ts/tools/scripts/windows-package-trust.ps1 index 1638f972e9..deb2086b94 100644 --- a/ts/tools/scripts/windows-package-trust.ps1 +++ b/ts/tools/scripts/windows-package-trust.ps1 @@ -38,19 +38,29 @@ function Use-TypeAgentBuildTrust { } $publicCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new( $Certificate.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)) - $store = New-Object Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser") + # CurrentUser\Root can display a native confirmation dialog even in non-interactive PowerShell. + $store = New-Object Security.Cryptography.X509Certificates.X509Store("Root", "LocalMachine") $added = $false try { + Write-Host "Opening temporary build trust in LocalMachine\Root (requires administrator rights)." $store.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) if (-not @($store.Certificates | Where-Object { $_.Thumbprint -eq $publicCertificate.Thumbprint }).Count) { + Write-Host "Adding pinned build certificate $($publicCertificate.Thumbprint) to LocalMachine\Root." $store.Add($publicCertificate) $added = $true } + Write-Host "Build trust ready; running package action." & $Action } finally { - if ($added) { $store.Remove($publicCertificate) } - $store.Close() - $publicCertificate.Dispose() + try { + if ($added) { + Write-Host "Removing temporary build certificate $($publicCertificate.Thumbprint) from LocalMachine\Root." + $store.Remove($publicCertificate) + } + } finally { + $store.Close() + $publicCertificate.Dispose() + } } } From 6b0d75562b35eaac027136188ec2bf8e3b30ca54 Mon Sep 17 00:00:00 2001 From: Hillary Mutisya <150286414+hillary-mutisya@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:50:18 -0700 Subject: [PATCH 3/5] Use compatible RFC 3161 timestamp URL for Windows shell signing --- pipelines/README.md | 14 ++++++++++++++ ts/packages/shell/electron-builder.config.js | 2 +- .../scripts/test/windowsInstallerTrust.spec.mjs | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pipelines/README.md b/pipelines/README.md index c13f2fc60a..83d39ae568 100644 --- a/pipelines/README.md +++ b/pipelines/README.md @@ -59,6 +59,20 @@ use disposable agents for these jobs. End-user installer trust is unchanged. Signing logs identify certificate retrieval, password retrieval, policy checking, trust-store access, packaging, verification, and trust cleanup separately. +### Windows shell signing rejects the timestamp URL + +Run `20261020.2` (build `139753`) reached SignTool but failed with +`Invalid Timestamp URL: https://timestamp.digicert.com`. Electron-builder's +bundled `winCodeSign-2.6.0` SignTool rejects that HTTPS timestamp URL. +The shell uses `http://timestamp.digicert.com`, matching MSI signing, while +retaining RFC 3161 timestamping, SHA-256, and required code signing. +The packaging regression checks pin this compatible endpoint. +Local validation reproduced the HTTPS rejection with the same bundled SignTool, +then successfully signed a disposable executable using the configured HTTP URL +and a synthetic code-signing certificate. The executable's CMS signature and the +returned RFC 3161 timestamp signature and certificate chain verified successfully. +This test did not retrieve development credentials or modify certificate stores. + ## Pipeline variables (configure in ADO UI) The consolidated pipeline requires these variables to be set on the pipeline diff --git a/ts/packages/shell/electron-builder.config.js b/ts/packages/shell/electron-builder.config.js index d44b2222bf..b1bf8ba6e5 100644 --- a/ts/packages/shell/electron-builder.config.js +++ b/ts/packages/shell/electron-builder.config.js @@ -100,7 +100,7 @@ export default { signtoolOptions: { publisherName: windowsTrust.publisher, signingHashAlgorithms: ["sha256"], - rfc3161TimeStampServer: "https://timestamp.digicert.com", + rfc3161TimeStampServer: "http://timestamp.digicert.com", }, verifyUpdateCodeSignature: true, appId: `Microsoft.TypeAgentShell`, diff --git a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs index f02ebd0678..daba59902e 100644 --- a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs +++ b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs @@ -250,6 +250,10 @@ test("electron-builder validates the required Windows signing and publisher conf assert.deepEqual(config.win.signtoolOptions.signingHashAlgorithms, [ "sha256", ]); + assert.equal( + config.win.signtoolOptions.rfc3161TimeStampServer, + "http://timestamp.digicert.com", + ); const trustResource = config.extraResources.find( (resource) => resource.to === "windows-package-trust", ); From 657557d0437aa7749fbb77141f6973e18d6da26c Mon Sep 17 00:00:00 2001 From: Hillary Mutisya <150286414+hillary-mutisya@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:35:25 -0700 Subject: [PATCH 4/5] Exclude builder diagnostics from shell channel metadata verification --- pipelines/README.md | 15 +++++++ .../test/windowsInstallerTrust.spec.mjs | 39 +++++++++++++++++++ ts/tools/scripts/verify-shell-artifacts.mjs | 5 ++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/pipelines/README.md b/pipelines/README.md index 83d39ae568..c8ad9f1641 100644 --- a/pipelines/README.md +++ b/pipelines/README.md @@ -73,6 +73,21 @@ and a synthetic code-signing certificate. The executable's CMS signature and the returned RFC 3161 timestamp signature and certificate chain verified successfully. This test did not retrieve development credentials or modify certificate stores. +### Shell verification rejects builder-debug.yml + +Run `20261020.3` (build `139769`, log `137`) completed signing the shell and +NSIS installer, then failed with `Unsafe installer path in builder-debug.yml`. +The verifier had treated every `.yml` file in the build output as update-channel +metadata, including electron-builder's diagnostic file. + +The shared build/publication verifier now excludes only `builder-debug.yml` +from channel-metadata discovery. It still requires actual channel metadata and +checks installer paths, final hashes, sizes, signatures, and unreferenced +executables. Unexpected `.yml` files are still validated, not silently skipped. +Publication download patterns already omit the builder diagnostic files. +Regression checks cover mixed build output and rejection of diagnostic-only +output, unexpected metadata, and extra executables. + ## Pipeline variables (configure in ADO UI) The consolidated pipeline requires these variables to be set on the pipeline diff --git a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs index daba59902e..3485a2de92 100644 --- a/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs +++ b/ts/tools/scripts/test/windowsInstallerTrust.spec.mjs @@ -225,6 +225,45 @@ test("publication verifies final hashes, metadata and signatures", (t) => { ); }); +test("publication ignores builder diagnostics but still requires channel metadata", (t) => { + const { dir } = fixture(t); + writeFileSync( + path.join(dir, "builder-debug.yml"), + "x64:\n target: nsis\n", + ); + writeFileSync( + path.join(dir, "builder-effective-config.yaml"), + "win:\n executableName: typeagentshell\n", + ); + const verified = []; + assert.equal( + verifyShellArtifacts(dir, (file) => verified.push(file)), + 1, + ); + assert.deepEqual(verified, [path.join(dir, installer)]); + rmSync(path.join(dir, "ci-x64.yml")); + assert.throws( + () => verifyShellArtifacts(dir, () => {}), + /No shell channel metadata found/, + ); +}); + +test("publication rejects unexpected YAML and unreferenced executables beside builder diagnostics", (t) => { + const { dir } = fixture(t); + writeFileSync(path.join(dir, "builder-debug.yml"), "x64: {}\n"); + writeFileSync(path.join(dir, "unexpected.yml"), "path: ../outside.exe\n"); + assert.throws( + () => verifyShellArtifacts(dir, () => {}), + /Unsafe installer path in unexpected\.yml/, + ); + rmSync(path.join(dir, "unexpected.yml")); + writeFileSync(path.join(dir, "unexpected.exe"), "unsigned fixture"); + assert.throws( + () => verifyShellArtifacts(dir, () => {}), + /Unreferenced executable in shell publication: unexpected\.exe/, + ); +}); + test("electron-builder validates the required Windows signing and publisher configuration", async () => { const builderRequire = createRequire( shellRequire.resolve("electron-builder"), diff --git a/ts/tools/scripts/verify-shell-artifacts.mjs b/ts/tools/scripts/verify-shell-artifacts.mjs index a561b49999..2e70ccd3a6 100644 --- a/ts/tools/scripts/verify-shell-artifacts.mjs +++ b/ts/tools/scripts/verify-shell-artifacts.mjs @@ -53,7 +53,10 @@ function verifySignature(packagePath) { export function verifyShellArtifacts(directory, verify = verifySignature) { const names = readdirSync(directory); - const metadataNames = names.filter((name) => name.endsWith(".yml")); + // Electron-builder diagnostics share the output directory with channel metadata. + const metadataNames = names.filter( + (name) => name.endsWith(".yml") && name !== "builder-debug.yml", + ); if (metadataNames.length === 0) { throw new Error("No shell channel metadata found."); } From 9b99bfc22ceadef312b45efe4abfcc354cffb682 Mon Sep 17 00:00:00 2001 From: Hillary Mutisya <150286414+hillary-mutisya@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:40:47 -0700 Subject: [PATCH 5/5] Update README.md --- pipelines/README.md | 61 --------------------------------------------- 1 file changed, 61 deletions(-) diff --git a/pipelines/README.md b/pipelines/README.md index c8ad9f1641..743599f38c 100644 --- a/pipelines/README.md +++ b/pipelines/README.md @@ -27,67 +27,6 @@ This folder contains Azure DevOps pipelines for the TypeAgent repo. | `include-install-pnpm.yml` | corepack enable + pnpm store configuration | | `include-update-package-version.yml` | Stamp prerelease version across all workspace packages | -## Packaging regression checks - -From `ts`, run `node --test tools/scripts/test/optionalAgentInstallPolicy.spec.mjs tools/scripts/test/windowsInstallerTrust.spec.mjs`. -The optional-agent check verifies that the temporary pnpm workspace explicitly -disables `protobufjs` install scripts, matching the main workspace policy. -On Windows, the trust checks exercise certificate-byte conversion and cleanup -with a synthetic certificate and a mocked trust store; they do not modify -certificate stores or retrieve signing secrets. PowerShell 7 (`pwsh`) is required -for the synthetic-certificate check, matching the shell packaging pipeline. -The checks also enforce machine-level build trust, cleanup after success and -failure, preservation of pre-existing trust, and rejection of unpinned certificates. - -### Windows shell signing hangs after certificate retrieval - -Run `20261020.1` (build `139732`) stopped logging after writing the PFX and -was canceled about 41 minutes later. The previous build-trust helper added the -public certificate to `CurrentUser\Root`, which can display a native Windows -trust-confirmation dialog even under PowerShell `-NonInteractive`. -The log alone does not prove where it blocked, since there were no progress -messages between certificate retrieval and packaging. - -Build and publication verification now temporarily trust only the policy-pinned -public certificate in `LocalMachine\Root`, avoiding the per-user dialog. -Both operations require an administrator Windows agent (as provided by -`windows-latest`); store-access failures fail the task rather than falling back -to interactive per-user trust. Existing matching trust is left unchanged, and -certificates added by the helper are removed in `finally`, including when the -package action fails. A forcibly terminated agent may not execute cleanup, so -use disposable agents for these jobs. End-user installer trust is unchanged. -Signing logs identify certificate retrieval, password retrieval, policy checking, -trust-store access, packaging, verification, and trust cleanup separately. - -### Windows shell signing rejects the timestamp URL - -Run `20261020.2` (build `139753`) reached SignTool but failed with -`Invalid Timestamp URL: https://timestamp.digicert.com`. Electron-builder's -bundled `winCodeSign-2.6.0` SignTool rejects that HTTPS timestamp URL. -The shell uses `http://timestamp.digicert.com`, matching MSI signing, while -retaining RFC 3161 timestamping, SHA-256, and required code signing. -The packaging regression checks pin this compatible endpoint. -Local validation reproduced the HTTPS rejection with the same bundled SignTool, -then successfully signed a disposable executable using the configured HTTP URL -and a synthetic code-signing certificate. The executable's CMS signature and the -returned RFC 3161 timestamp signature and certificate chain verified successfully. -This test did not retrieve development credentials or modify certificate stores. - -### Shell verification rejects builder-debug.yml - -Run `20261020.3` (build `139769`, log `137`) completed signing the shell and -NSIS installer, then failed with `Unsafe installer path in builder-debug.yml`. -The verifier had treated every `.yml` file in the build output as update-channel -metadata, including electron-builder's diagnostic file. - -The shared build/publication verifier now excludes only `builder-debug.yml` -from channel-metadata discovery. It still requires actual channel metadata and -checks installer paths, final hashes, sizes, signatures, and unreferenced -executables. Unexpected `.yml` files are still validated, not silently skipped. -Publication download patterns already omit the builder diagnostic files. -Regression checks cover mixed build output and rejection of diagnostic-only -output, unexpected metadata, and extra executables. - ## Pipeline variables (configure in ADO UI) The consolidated pipeline requires these variables to be set on the pipeline