Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,42 @@ concurrency:
cancel-in-progress: true

jobs:
nvidia-runtime:
name: NVIDIA runtime clean startup regression
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Restore the known incomplete official release runtime
shell: pwsh
run: |
$archive = Join-Path $env:RUNNER_TEMP 'GoAgent-0.4.21-win-x64-nvidia-portable.7z'
Invoke-WebRequest 'https://github.com/wimi321/GoAgent/releases/download/v0.4.21/GoAgent-0.4.21-win-x64-nvidia-portable.7z' -OutFile $archive
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne '7b52ab7e87e3356c22f28ec028bb7c91f2e825f9e78ddcb894f0d313ab2ac8a8') { throw 'Regression archive SHA-256 mismatch' }
& 7z x $archive '-o.nvidia-regression' 'resources/data/katago/bin/win32-x64/*' -y
if ($LASTEXITCODE -ne 0) { throw 'Regression runtime extraction failed' }
- name: Reject original missing NVRTC even if runner has CUDA installed
shell: pwsh
run: |
node --input-type=module -e "import assert from 'node:assert/strict'; import {checkWindowsRuntimeDependencies} from './scripts/lib/windows_pe_dependencies.mjs'; await assert.rejects(checkWindowsRuntimeDependencies('.nvidia-regression/resources/data/katago/bin/win32-x64', {nvidia:true}), {message:'Missing bundled dependency: katago.exe -> nvrtc64_120_0.dll'}); console.log('Original package rejected: missing bundled nvrtc64_120_0.dll');" | Tee-Object -FilePath .nvidia-regression/before.log
if ($LASTEXITCODE -ne 0) { throw 'Missing NVRTC regression assertion failed' }
- name: Prepare and execute corrected runtime with clean environment
shell: pwsh
run: |
node scripts/prepare_nvidia_nvrtc.mjs --runtime-dir=.nvidia-regression/resources/data/katago/bin/win32-x64
if ($LASTEXITCODE -ne 0) { throw 'NVRTC preparation failed' }
node scripts/check_windows_katago_runtime.mjs --runtime-dir=.nvidia-regression/resources/data/katago/bin/win32-x64 --nvidia --evidence=.nvidia-regression/after.json
if ($LASTEXITCODE -ne 0) { throw 'Clean executable version probe failed' }
- uses: actions/upload-artifact@v4
if: always()
with:
name: nvidia-clean-startup-evidence
path: |
.nvidia-regression/before.log
.nvidia-regression/after.json

website:
name: Website build and download checks
runs-on: ubuntu-latest
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,12 @@ jobs:
--flavor=nvidia \
--source-label="${GOAGENT_NVIDIA_KATAGO_ASSET_REPO}@${GOAGENT_NVIDIA_KATAGO_ASSET_RELEASE_TAG}:${GOAGENT_NVIDIA_KATAGO_ASSET_PATTERN}"

- name: Bundle verified official CUDA NVRTC
run: node scripts/prepare_nvidia_nvrtc.mjs

- name: Check NVIDIA runtime with clean DLL search paths
run: node scripts/check_windows_katago_runtime.mjs --runtime-dir=data/katago/bin/win32-x64 --nvidia

- name: Bundle official KataGo Transformer model (NVIDIA)
shell: bash
run: |
Expand Down Expand Up @@ -664,6 +670,13 @@ jobs:
- name: Verify packaged Codex App Server
run: node scripts/smoke_codex_packaged_runtime.mjs --root=release --expect=win32-x64

- name: Check final NVIDIA portable and NSIS payload with clean DLL search paths
shell: pwsh
run: |
$version = node -p "JSON.parse(require('fs').readFileSync('package.json','utf8')).version"
node scripts/check_windows_katago_runtime.mjs --nvidia "--artifact=release/$version/GoAgent-$version-win-x64-nvidia-portable.7z" "--artifact=release/$version/GoAgent-$version-win-x64-nvidia.exe" "--evidence=release/$version/nvidia-clean-katago.json"
if ($LASTEXITCODE -ne 0) { throw "Final NVIDIA KataGo runtime check failed" }

- name: Smoke NVIDIA packaged Windows app
run: node scripts/smoke_windows_packaged_app.mjs --mode=nvidia --require-katago

Expand Down
9 changes: 8 additions & 1 deletion build/afterPack.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const { createHash } = require('node:crypto')
const { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync } = require('node:fs')
const { join } = require('node:path')
const { Arch } = require('builder-util')
const { pathToFileURL } = require('node:url')
const { Arch } = require('electron-builder')

module.exports = async function afterPack(context) {
const projectDir = context.packager.projectDir
Expand Down Expand Up @@ -29,4 +30,10 @@ module.exports = async function afterPack(context) {
copyFileSync(manifestPath, join(destinationDir, 'manifest.json'))
copyFileSync(join(projectDir, 'data', 'codex', 'LICENSE'), join(destinationDir, 'LICENSE'))
if (context.electronPlatformName !== 'win32') chmodSync(destination, 0o755)

const editionPath = join(resources, 'data', 'katago', 'edition.json')
if (context.electronPlatformName === 'win32' && existsSync(editionPath) && JSON.parse(readFileSync(editionPath, 'utf8')).flavor === 'nvidia') {
const { checkRuntime } = await import(pathToFileURL(join(projectDir, 'scripts', 'check_windows_katago_runtime.mjs')).href)
await checkRuntime(join(resources, 'data', 'katago', 'bin', 'win32-x64'), true)
}
}
79 changes: 79 additions & 0 deletions docs/WINDOWS_NVIDIA_RUNTIME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Windows NVIDIA runtime packaging and clean startup gate

The v0.4.21 NVIDIA portable release includes KataGo 1.17.1 but omits its direct
`nvrtc64_120_0.dll` dependency. An isolated Windows test returned loader status
`0xC0000135`; adding the official NVRTC pair made `katago.exe version` succeed,
and withdrawing the pair reproduced the failure. The source runtime manifest
lists CUDA Runtime, cuBLAS, nvJitLink and cuDNN, but not NVRTC.

## Preparation

After restoring the existing NVIDIA source runtime, run:

```sh
node scripts/prepare_nvidia_nvrtc.mjs
node scripts/check_windows_katago_runtime.mjs --runtime-dir=data/katago/bin/win32-x64 --nvidia
```

Preparation supplements the source bundle with official CUDA NVRTC 12.1.105,
matching its CUDA 12.1 runtime and KataGo's compiled CUDA 12.1.66. The archive,
both DLLs and license have pinned SHA-256 checksums in
`scripts/lib/nvidia_nvrtc.mjs`. An offline archive can be provided with
`--archive=<local-zip>`; it receives the same checks. Generated binaries remain
ignored by Git, and a generated `goagent-nvrtc.json` records provenance.

Official metadata:
https://developer.download.nvidia.com/compute/cuda/redist/redistrib_12.1.1.json

Official archive:
https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/windows-x86_64/cuda_nvrtc-windows-x86_64-12.1.105-archive.zip

Archive SHA-256:
`7fa294726483b10815305fe1c07c9ceb23e048fc43bb459775eb68dee82d1a8f`.

## License handling

The archive's `LICENSE` is byte-identical (SHA-256
`b9ee12782ca117b887588d99ae1b8c24deb59105e05417fe28ffbd7a55896dd1`) to the
CUDA Runtime license in the source bundle. CUDA Supplement Attachment A lists
the Windows NVIDIA Runtime Compilation Library (`nvrtc.dll` and
`nvrtc-builtins.dll`) as distributable components, subject to the agreement's
distribution conditions. The unmodified license is carried alongside the
existing NVIDIA notices as `licenses/nvidia-runtime/cuda_nvrtc-LICENSE`.
There is no license-acceptance flag, installer or system Toolkit dependency.
Changes to this pinned version or license require maintainer review; this
patch does not accept a new license or publish a distribution.

## Final artifact verification

Requires Windows, Node 22+, PowerShell 7 (`pwsh.exe`) and full 7-Zip (`7z`,
including its NSIS handler; `7za` / `7zr` alone cannot read NSIS installers).

```sh
node scripts/check_windows_katago_runtime.mjs --nvidia --artifact=release/0.4.21/GoAgent-0.4.21-win-x64-nvidia-portable.7z --artifact=release/0.4.21/GoAgent-0.4.21-win-x64-nvidia.exe --evidence=release/0.4.21/nvidia-clean-katago.json
```

This extracts each final artifact to a private temporary directory. For NSIS,
it extracts `app-64.7z` without executing the installer. It validates the x64
PE normal and delay import closure, requiring non-system dependencies to exist
beside the engine, then checks the explicit NVRTC pair and license hashes.
CUDA DLLs cannot be satisfied by the developer's PATH or System32.

The actual `version` subprocess runs in an empty temporary working directory
with a cleared environment, Windows-only PATH, hidden window and process-local
loader dialog suppression. It must exit zero and report executable KataGo
1.17.1 with the CUDA backend within ten seconds. Embedded binary metadata and
GPU-less smoke exemptions cannot satisfy this gate. The existing NVIDIA app
smoke also invokes this preflight before launching Electron.

CI restores the hash-pinned official v0.4.21 portable release as a regression
fixture, requires the original missing dependency to be rejected, supplements
NVRTC, and requires clean executable startup. The release workflow checks the
prepared runtime and both final NVIDIA artifacts before upload. The afterPack
hook also checks packaged NVIDIA runtimes, including local builds. The packaging
hook imports `Arch` from its declared `electron-builder` dependency, avoiding
an undeclared transitive import under pnpm's isolated layout.

This is a loader / executable version check, not GPU model initialization,
analysis throughput, complete LoadLibrary discovery or interactive installation
validation. A real NVIDIA GPU analysis smoke is still needed for release QA.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
"check:katago-assets:release": "node scripts/check_katago_assets.mjs --mode=release",
"check:no-lite-release-assets": "node scripts/check_no_lite_release_assets.mjs",
"check:nvidia-release-assets": "node scripts/check_nvidia_release_assets.mjs",
"prepare:nvidia-nvrtc": "node scripts/prepare_nvidia_nvrtc.mjs",
"check:windows-katago-runtime": "node scripts/check_windows_katago_runtime.mjs",
"check:release-notes-i18n": "node scripts/check_release_notes_i18n.mjs",
"check:release-quality": "pnpm check:teacher-quality && pnpm check:no-lite-release-assets && pnpm check:nvidia-release-assets && pnpm check:release-notes-i18n && pnpm check:tts-assets && pnpm smoke:tts && pnpm smoke:release-artifacts",
"check:p0-beta": "node scripts/p0_beta_acceptance.mjs",
Expand Down
3 changes: 3 additions & 0 deletions scripts/check_nvidia_release_assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const requiredWorkflowFragments = [
'nvidia_katago_asset_release_tag',
'nvidia_katago_asset_pattern',
'package-nvidia-windows',
'node scripts/prepare_nvidia_nvrtc.mjs',
'Check final NVIDIA portable and NSIS payload with clean DLL search paths',
'node scripts/check_windows_katago_runtime.mjs --nvidia',
'wimi321/lizzieyzy-next',
'*windows64.nvidia.portable.zip',
'--copy-runtime-dir',
Expand Down
64 changes: 64 additions & 0 deletions scripts/check_windows_katago_runtime.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env node
import { execFile } from 'node:child_process'
import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { checkWindowsRuntimeDependencies } from './lib/windows_pe_dependencies.mjs'
import { parseKataGoVersion } from './lib/katago_asset_metadata.mjs'

const execFileAsync = promisify(execFile)
const arg = (name, fallback = '') => process.argv.find((item) => item.startsWith(`--${name}=`))?.slice(name.length + 3) ?? fallback
const helper = join(dirname(fileURLToPath(import.meta.url)), 'probe_windows_katago.ps1')
export async function checkRuntime(runtime, nvidia) {
const dependencies = await checkWindowsRuntimeDependencies(runtime, { nvidia })
const { stdout } = await execFileAsync('pwsh.exe', ['-NoProfile', '-NonInteractive', '-File', helper, '-Binary', join(runtime, 'katago.exe')], { windowsHide: true, timeout: 30_000, maxBuffer: 1024 * 1024 })
const probe = JSON.parse(stdout.trim())
if (probe.exitCode !== 0) throw new Error(`KataGo clean version probe failed: 0x${probe.exitHex}; ${probe.stderr || probe.stdout || 'no output'}`)
if (parseKataGoVersion(probe.stdout) !== '1.17.1') throw new Error(`Expected executable KataGo v1.17.1: ${probe.stdout}`)
if (nvidia && !/Using CUDA backend/i.test(probe.stdout)) throw new Error(`Expected CUDA backend: ${probe.stdout}`)
return { runtime, ...probe, dependencies }
}

async function walk(root) {
const files = []
for (const entry of await readdir(root, { withFileTypes: true })) {
const full = join(root, entry.name)
if (entry.isDirectory()) files.push(...await walk(full))
else files.push(full)
}
return files
}
async function checkArtifact(artifact, nvidia, sevenZip) {
const work = await mkdtemp(join(tmpdir(), 'goagent-katago-artifact-'))
try {
const extract = async (archive, destination) => execFileAsync(sevenZip, ['x', resolve(archive), `-o${destination}`, '-y'], { windowsHide: true, timeout: 300_000, maxBuffer: 4 * 1024 * 1024 })
await extract(artifact, join(work, 'outer'))
let files = await walk(join(work, 'outer'))
if (/\.exe$/i.test(artifact)) {
// Inspect NSIS payload without executing the installer or touching an install.
const payloads = files.filter((p) => /(?:^|[\\/])app-64\.7z$/i.test(p))
if (payloads.length !== 1) throw new Error(`Expected one NSIS app-64.7z payload: ${artifact}`)
await extract(payloads[0], join(work, 'payload'))
files = await walk(join(work, 'payload'))
}
const engines = files.filter((p) => /[\\/]resources[\\/]data[\\/]katago[\\/]bin[\\/]win32-x64[\\/]katago\.exe$/i.test(p))
if (engines.length !== 1) throw new Error(`Expected one packaged KataGo runtime: ${artifact}`)
return { artifact: resolve(artifact), ...await checkRuntime(dirname(engines[0]), nvidia) }
} finally {
await rm(work, { recursive: true, force: true })
}
}
async function main() {
if (process.platform !== 'win32') throw new Error('Clean KataGo runtime check requires Windows')
const nvidia = process.argv.includes('--nvidia')
const results = []
if (arg('runtime-dir')) results.push(await checkRuntime(resolve(arg('runtime-dir')), nvidia))
for (const item of process.argv.filter((item) => item.startsWith('--artifact='))) results.push(await checkArtifact(item.slice(11), nvidia, arg('7z', '7z')))
if (!results.length) throw new Error('Provide --runtime-dir=... or --artifact=... (repeatable)')
const report = JSON.stringify(results, null, 2) + '\n'
if (arg('evidence')) await writeFile(resolve(arg('evidence')), report)
console.log(report)
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main().catch((error) => { console.error(error); process.exitCode = 1 })
14 changes: 14 additions & 0 deletions scripts/lib/nvidia_nvrtc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// CUDA 12.1 matches the existing cuda12.1-cudnn9 runtime and KataGo build.
// Hashes are from NVIDIA's redistrib_12.1.1.json and the verified archive.
export const NVRTC = Object.freeze({
version: '12.1.105',
url: 'https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvrtc/windows-x86_64/cuda_nvrtc-windows-x86_64-12.1.105-archive.zip',
archiveSha256: '7fa294726483b10815305fe1c07c9ceb23e048fc43bb459775eb68dee82d1a8f',
archiveRoot: 'cuda_nvrtc-windows-x86_64-12.1.105-archive',
// Identical to the CUDA Runtime license already shipped by the source bundle.
licenseSha256: 'b9ee12782ca117b887588d99ae1b8c24deb59105e05417fe28ffbd7a55896dd1',
files: {
'nvrtc64_120_0.dll': '62b0975b7fe2940bfb367e257d734ef1a7cd512fcf3b53a5660082c742b244bb',
'nvrtc-builtins64_121.dll': '0baa6563c0edfa4a36744f0d680398619b596d3167eeb0f29fa7d3f2ec5b6f18'
}
})
80 changes: 80 additions & 0 deletions scripts/lib/windows_pe_dependencies.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { readdir, readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import { NVRTC } from './nvidia_nvrtc.mjs'

// Parse normal and delay imports; this deliberately does not claim to discover
// DLL names assembled at runtime via LoadLibrary (NVRTC builtins is explicit).
export function peImports(bytes) {
if (bytes.toString('ascii', 0, 2) !== 'MZ') throw new Error('Not a PE binary')
const pe = bytes.readUInt32LE(0x3c)
if (bytes.toString('ascii', pe, pe + 4) !== 'PE\0\0') throw new Error('Invalid PE signature')
const machine = bytes.readUInt16LE(pe + 4)
if (machine !== 0x8664) throw new Error(`Expected x64 PE, machine=0x${machine.toString(16)}`)
const optional = pe + 24
if (bytes.readUInt16LE(optional) !== 0x20b) throw new Error('Expected PE32+')
const directory = optional + 112
const sections = []
for (let i = 0; i < bytes.readUInt16LE(pe + 6); i++) {
const s = optional + bytes.readUInt16LE(pe + 20) + 40 * i
sections.push({ va: bytes.readUInt32LE(s + 12), size: Math.max(bytes.readUInt32LE(s + 8), bytes.readUInt32LE(s + 16)), raw: bytes.readUInt32LE(s + 20) })
}
const offset = (rva) => {
const s = sections.find((s) => rva >= s.va && rva < s.va + s.size)
if (!s) throw new Error(`Invalid PE RVA: ${rva}`)
return s.raw + rva - s.va
}
const name = (rva) => {
const start = offset(rva), end = bytes.indexOf(0, start)
if (end < 0) throw new Error('Unterminated PE import name')
const value = bytes.toString('ascii', start, end)
if (!/^[\w.-]+\.dll$/i.test(value)) throw new Error(`Invalid import DLL: ${value}`)
return value
}
const imports = []
for (const [index, stride, nameOffset] of [[1, 20, 12], [13, 32, 4]]) {
const rva = bytes.readUInt32LE(directory + index * 8)
if (!rva) continue
for (let p = offset(rva); bytes.readUInt32LE(p + nameOffset); p += stride) {
// PE32+ delay descriptors must use RVAs, not truncated 32-bit VAs.
if (index === 13 && !(bytes.readUInt32LE(p) & 1)) throw new Error('Unsupported delay-import VA descriptor')
imports.push(name(bytes.readUInt32LE(p + nameOffset)))
}
}
return imports
}

// OS/driver dependencies only. Never allow a CUDA Toolkit DLL to be supplied
// by System32 or the developer's PATH; every other import must be app-local.
export function isWindowsSystemDependency(name) {
return /^(?:api-ms-|ext-ms-)/i.test(name) || /^(?:kernel32|user32|gdi32|advapi32|shell32|shlwapi|ole32|oleaut32|ws2_32|crypt32|bcrypt|ncrypt|secur32|normaliz|version|winmm|winhttp|wininet|iphlpapi|psapi|setupapi|cfgmgr32|ntdll|rpcrt4|comdlg32|comctl32|dbghelp|msvcrt|ucrtbase|d3d12|dxgi|nvcuda)\.dll$/i.test(name)
}

export async function checkWindowsRuntimeDependencies(runtime, { nvidia = false } = {}) {
const names = new Map((await readdir(runtime)).map((name) => [name.toLowerCase(), name]))
const queue = ['katago.exe'], seen = new Set(), imports = []
while (queue.length) {
const next = queue.shift().toLowerCase()
if (seen.has(next)) continue
seen.add(next)
const actual = names.get(next)
if (!actual) throw new Error(`Missing bundled dependency: ${next}`)
const deps = peImports(await readFile(join(runtime, actual)))
for (const dep of deps) {
imports.push({ from: actual, dll: dep })
if (names.has(dep.toLowerCase())) queue.push(dep)
else if (!isWindowsSystemDependency(dep)) throw new Error(`Missing bundled dependency: ${actual} -> ${dep}`)
}
}
if (nvidia) {
for (const [name, expected] of Object.entries(NVRTC.files)) {
const actual = names.get(name.toLowerCase())
if (!actual) throw new Error(`Missing bundled NVRTC component: ${name}`)
const hash = createHash('sha256').update(await readFile(join(runtime, actual))).digest('hex')
if (hash !== expected) throw new Error(`Bundled NVRTC hash mismatch: ${name}`)
}
const license = await readFile(join(runtime, 'licenses/nvidia-runtime/cuda_nvrtc-LICENSE'))
if (createHash('sha256').update(license).digest('hex') !== NVRTC.licenseSha256) throw new Error('NVRTC license hash mismatch')
}
return { checkedBinaries: [...seen], imports }
}
Loading
Loading