Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .agents/skills/build-openshell-mxc-windows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ compatibility under emulation is not part of these tasks. The aggregate
commands above on an ARM64 host.

The repository-wide `mise run pre-commit` task is also supported on Windows.
Run `rust:lockfiles:check`, `sdk:ts:ci`, `go:ci`, and `test:e2e-parity` through
the Windows-aware tasks when validating those surfaces. Do not count the Go
Windows ARM64 race-detector exclusion or POSIX permission-bit skips as security
coverage. SDK test dependencies must remain at their lockfile versions.
Its Rust check, Clippy, and test dependencies enter the same MSVC environment
for the native host target and use an inherited compiler wrapper when it is
available. Linux glibc
Expand Down
18 changes: 18 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ mise run test:rust # cargo test --workspace

Rust validation checks tracked Cargo lockfiles; run `mise run rust:lockfiles:check` to check them directly. If one is stale, refresh it with Cargo using its adjacent manifest, review the diff, and commit the update.

### Native Windows validation

Use `mise run --skip-tools pre-commit` with the existing Rust/MSVC toolchain.
Windows now checks tracked Cargo lockfiles through PowerShell rather than
skipping them. The deterministic gateway parity task uses Git for Windows Bash,
with temporary Python launchers confined to a unique checkout-owned directory.

`mise run --skip-tools sdk:ts:ci` selects the x64 Biome executable on Windows
(including ARM64 hosts running it under emulation), resolves the protobuf
plugin through its Windows `.cmd` launcher, and installs the matching locked
ARM64 Rolldown binding when Node itself is ARM64. The helper preserves lockfile
resolution; it must not upgrade unrelated test dependencies.

`mise run --skip-tools go:ci` retains race detection except on Windows ARM64,
where Go does not support it. Windows token-file tests explicitly skip POSIX
mode-bit assertions; those skips do not establish Windows ACL protection.
Use a checkout with LF text files when running Unix-shell fixture checks.

## Python Unit Tests

Python unit tests use the `*_test.py` suffix convention (not `test_*` prefix)
Expand Down
6 changes: 6 additions & 0 deletions architecture/windows-msvc-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ async functions caused by cfg-gated Windows stubs. Repository-wide pre-commit
skips only Linux-specific installer, build-environment shell-helper, and
packaging-asset tests; its
cross-platform Python, Markdown, license, and documentation checks still run.
Tracked Cargo lockfiles are checked natively through PowerShell. Deterministic
gateway parity uses Git for Windows Bash with temporary, checkout-scoped Python
launchers. The TypeScript SDK uses Windows protobuf plugin paths and x64 Biome
under emulation on ARM64, while its test binding follows Node's architecture
and the locked Rolldown version. Go tests retain race coverage wherever the
toolchain supports it; POSIX permission-bit checks are not Windows ACL tests.
Test tasks require the Rust target architecture to match the Windows host, so
an ARM64 test result is native coverage rather than x64 emulation coverage.
By default it enables the `z3-sys` prebuilt-release feature and pins Z3 4.16.0.
Expand Down
2 changes: 1 addition & 1 deletion sdk/go/openshell/v1/gateway/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestUserConfigDir_XDGUnset(t *testing.T) {

func TestSystemGatewayDir(t *testing.T) {
dir := systemGatewayDir()
assert.Equal(t, "/etc/openshell/gateways", dir)
assert.Equal(t, filepath.FromSlash("/etc/openshell/gateways"), dir)
}

func TestResolveGatewayDir_UserDir(t *testing.T) {
Expand Down
5 changes: 2 additions & 3 deletions sdk/go/openshell/v1/oidc/browser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ func TestBrowserCommand_Platform(t *testing.T) {
assert.Equal(t, "xdg-open", name)
assert.Equal(t, []string{"https://example.com/auth"}, args)
case "windows":
assert.Equal(t, "cmd", name)
assert.Contains(t, args, "/c")
assert.Contains(t, args, "start")
assert.Equal(t, "rundll32", name)
assert.Equal(t, []string{"url.dll,FileProtocolHandler", "https://example.com/auth"}, args)
default:
// Unknown platform should still return something (even if it fails).
assert.NotEmpty(t, name)
Expand Down
17 changes: 12 additions & 5 deletions sdk/go/openshell/v1/oidc/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"time"

Expand Down Expand Up @@ -41,9 +42,7 @@ func TestWriteToken_ReplacesInsecureExistingFileWithOwnerOnlyFile(t *testing.T)

require.NoError(t, writeToken(dir, &oauth2.Token{AccessToken: "secret"}))

info, err := os.Stat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
assertOwnerOnlyFilePermissions(t, path)
}

func TestWriteToken_ExpiresInCalculated(t *testing.T) {
Expand Down Expand Up @@ -187,8 +186,16 @@ func TestWriteToken_FilePermissions(t *testing.T) {
err := writeToken(dir, tok)
require.NoError(t, err)

info, err := os.Stat(filepath.Join(dir, "oidc_token.json"))
assertOwnerOnlyFilePermissions(t, filepath.Join(dir, "oidc_token.json"))
}

func assertOwnerOnlyFilePermissions(t *testing.T, path string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("POSIX file permission bits are not supported on Windows")
}

info, err := os.Stat(path)
require.NoError(t, err)
// File should be owner-only readable (0600).
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}
48 changes: 44 additions & 4 deletions tasks/go.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@
["go:test"]
description = "Run Go SDK unit tests with coverage"
dir = "sdk/go"
run = "go test -coverprofile=coverage.out -coverpkg=./openshell/... -race ./..."
run = """
#!/usr/bin/env bash
set -euo pipefail

if [ "$(go env GOOS)" = "windows" ] && [ "$(go env GOARCH)" = "arm64" ]; then
echo "Go race detection is unsupported on windows/arm64; running without -race."
go test -coverprofile=coverage.out -coverpkg=./openshell/... ./...
else
go test -coverprofile=coverage.out -coverpkg=./openshell/... -race ./...
fi
"""
hide = true

["go:test:integration"]
Expand Down Expand Up @@ -120,11 +130,26 @@ if find proto -maxdepth 1 -name '*.proto' -print -quit | grep -q .; then
exit 1
fi

BUF_TEMPLATE="$SDK_ROOT/buf.gen.yaml"
if command -v cygpath &>/dev/null; then
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
GO_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go)")
GRPC_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go-grpc)")
GO_PLUGIN=${GO_PLUGIN//&/\\&}
GRPC_PLUGIN=${GRPC_PLUGIN//&/\\&}
sed \
-e "s|local: protoc-gen-go$|local: $GO_PLUGIN|" \
-e "s|local: protoc-gen-go-grpc$|local: $GRPC_PLUGIN|" \
"$SDK_ROOT/buf.gen.yaml" > "$WORK_DIR/buf.gen.yaml"
BUF_TEMPLATE="$WORK_DIR/buf.gen.yaml"
fi

# Clean previous output before regeneration
find proto -name '*.pb.go' -delete 2>/dev/null || true
find proto -mindepth 1 -type d -empty -delete 2>/dev/null || true

(cd "$REPO_ROOT" && buf generate --template "$SDK_ROOT/buf.gen.yaml")
(cd "$REPO_ROOT" && buf generate --template "$BUF_TEMPLATE")

echo "Proto generation complete."
echo "Generated packages:"
Expand Down Expand Up @@ -162,8 +187,23 @@ if find proto -maxdepth 1 -name '*.proto' -print -quit | grep -q .; then
fi

# Generate to temp directory with adjusted output path
CHECK_TEMPLATE=$(sed 's|out: sdk/go|out: '"$WORK_DIR"'|' buf.gen.yaml)
(cd "$REPO_ROOT" && buf generate --template "$CHECK_TEMPLATE")
BUF_OUTPUT_DIR="$WORK_DIR"
if command -v cygpath &>/dev/null; then
BUF_OUTPUT_DIR=$(cygpath -m "$WORK_DIR")
fi
sed "s|out: sdk/go|out: $BUF_OUTPUT_DIR|" "$SDK_ROOT/buf.gen.yaml" > "$WORK_DIR/buf.gen.yaml"
if command -v cygpath &>/dev/null; then
GO_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go)")
GRPC_PLUGIN=$(cygpath -m "$(command -v protoc-gen-go-grpc)")
GO_PLUGIN=${GO_PLUGIN//&/\\&}
GRPC_PLUGIN=${GRPC_PLUGIN//&/\\&}
sed \
-e "s|local: protoc-gen-go$|local: $GO_PLUGIN|" \
-e "s|local: protoc-gen-go-grpc$|local: $GRPC_PLUGIN|" \
"$WORK_DIR/buf.gen.yaml" > "$WORK_DIR/buf.gen.windows.yaml"
mv "$WORK_DIR/buf.gen.windows.yaml" "$WORK_DIR/buf.gen.yaml"
fi
(cd "$REPO_ROOT" && buf generate --template "$WORK_DIR/buf.gen.yaml")

DIFF_OUTPUT=$(diff -r "$WORK_DIR/proto" "$SDK_ROOT/proto" 2>&1) || true

Expand Down
1 change: 1 addition & 0 deletions tasks/parity.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
["test:e2e-parity"]
description = "Run deterministic schema-v1/schema-v2 parity harness tests"
run = "bash e2e/parity/test.sh"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/run-git-bash.ps1 e2e/parity/test.sh"
hide = true

["e2e:parity:podman"]
Expand Down
2 changes: 1 addition & 1 deletion tasks/rust.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ hide = true
["rust:lockfiles:check"]
description = "Verify all tracked Cargo lockfiles are current"
run = "tasks/scripts/check-cargo-lockfiles.sh"
run_windows = "echo Skipping rust:lockfiles:check: Cargo lockfile validation uses a Unix shell helper."
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/check-cargo-lockfiles.ps1"
hide = true

["rust:lint"]
Expand Down
44 changes: 44 additions & 0 deletions tasks/scripts/check-cargo-lockfiles.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

$ErrorActionPreference = "Stop"

$repoRoot = (& git rev-parse --show-toplevel).Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
throw "Unable to resolve the repository root"
}
Set-Location -LiteralPath $repoRoot

$lockfiles = @(& git ls-files -- ':(glob)**/Cargo.lock')
if ($LASTEXITCODE -ne 0) {
throw "Unable to enumerate tracked Cargo.lock files"
}
if ($lockfiles.Count -eq 0) {
throw "No tracked Cargo.lock files found"
}

$failed = $false
foreach ($lockfile in $lockfiles) {
$lockfileDirectory = Split-Path -Parent $lockfile
$manifest = if ([string]::IsNullOrEmpty($lockfileDirectory)) {
"Cargo.toml"
} else {
Join-Path $lockfileDirectory "Cargo.toml"
}
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
Write-Error "Tracked lockfile $lockfile has no adjacent Cargo.toml"
$failed = $true
continue
}

Write-Output "Checking $lockfile"
& cargo metadata --locked --format-version 1 --manifest-path $manifest | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Validation failed for $lockfile"
$failed = $true
}
}

if ($failed) {
throw "Resolve the reported errors. If a lockfile needs updating, refresh it with Cargo and commit the result."
}
90 changes: 90 additions & 0 deletions tasks/scripts/run-git-bash.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$Script,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Arguments
)

$ErrorActionPreference = "Stop"

$gitCommand = Get-Command git.exe -ErrorAction Stop
$gitRoot = Split-Path -Parent $gitCommand.Source
$bash = $null
# Git can expose cmd/git.exe, mingw64/bin/git.exe, or clangarm64/bin/git.exe.
for ($level = 0; $level -lt 4 -and $gitRoot; $level++) {
$candidate = Join-Path $gitRoot 'bin\bash.exe'
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
$bash = $candidate
break
}
$gitRoot = Split-Path -Parent $gitRoot
}
if (-not $bash) {
throw "Git for Windows bash.exe was not found beside $($gitCommand.Source)"
}

$repoRoot = (& git rev-parse --show-toplevel).Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
throw 'Unable to resolve the repository root'
}
$repoPrefix = [IO.Path]::GetFullPath($repoRoot).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
$shimDirectory = [IO.Path]::GetFullPath((Join-Path $repoRoot ('.git-bash-shim-' + [guid]::NewGuid().ToString('N'))))
if (-not $shimDirectory.StartsWith($repoPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Git Bash shim directory escaped the repository'
}
$shimCreated = $false
$previousPath = $env:PATH
$previousTmpdir = $env:TMPDIR
$python = $env:UV_PYTHON
if ([string]::IsNullOrWhiteSpace($python)) {
$worktreePython = Join-Path $repoRoot ".venv\Scripts\python.exe"
if (Test-Path -LiteralPath $worktreePython -PathType Leaf) {
$python = $worktreePython
}
}

try {
New-Item -ItemType Directory -Path $shimDirectory | Out-Null
$shimCreated = $true
if (-not [string]::IsNullOrWhiteSpace($python)) {
$python = [IO.Path]::GetFullPath($python)
if ($python -notmatch '^(?<drive>[A-Za-z]):\\(?<tail>.*)$') {
throw "Python must use an absolute drive path for Git Bash: $python"
}
$bashPython = "/$($Matches.drive.ToLowerInvariant())/$($Matches.tail.Replace('\', '/'))"
if ($bashPython.Contains("'")) {
throw "Python path cannot contain a single quote: $python"
}
$launcher = "#!/usr/bin/env bash`nexec '$bashPython' `"`$@`"`n"
$utf8WithoutBom = [Text.UTF8Encoding]::new($false)
foreach ($launcherName in @("python", "python3")) {
[IO.File]::WriteAllText(
(Join-Path $shimDirectory $launcherName),
$launcher,
$utf8WithoutBom
)
}
}
$bashTemp = Join-Path $shimDirectory "tmp"
New-Item -ItemType Directory -Path $bashTemp | Out-Null
$env:TMPDIR = $bashTemp.Replace("\", "/")
$env:PATH = "$shimDirectory;$gitRoot\usr\bin;$gitRoot\bin;$env:PATH"
& $bash $Script @Arguments
$exitCode = $LASTEXITCODE
} finally {
$env:PATH = $previousPath
$env:TMPDIR = $previousTmpdir
if ($shimCreated -and (Test-Path -LiteralPath $shimDirectory)) {
$resolvedShim = (Resolve-Path -LiteralPath $shimDirectory).ProviderPath
if (-not $resolvedShim.StartsWith($repoPrefix, [StringComparison]::OrdinalIgnoreCase) -or
((Get-Item -LiteralPath $shimDirectory).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
throw 'Refusing to remove an unexpected Git Bash shim directory'
}
Remove-Item -LiteralPath $shimDirectory -Recurse -Force
}
}

exit $exitCode
40 changes: 40 additions & 0 deletions tasks/scripts/typescript-install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[CmdletBinding()]
param()

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$nodeArchitecture = (& node -p "process.arch").Trim()
if ($LASTEXITCODE -ne 0) {
throw "Unable to determine the Node.js process architecture (exit code $LASTEXITCODE)."
}
if ($nodeArchitecture -notin @('x64', 'arm64')) {
throw "Unsupported Windows Node.js architecture '$nodeArchitecture'."
}

$previousCpu = $env:npm_config_cpu
try {
$env:npm_config_cpu = $nodeArchitecture
& npm ci
if ($LASTEXITCODE -ne 0) {
throw "Unable to install TypeScript SDK dependencies (exit code $LASTEXITCODE)."
}
}
finally {
$env:npm_config_cpu = $previousCpu
}

$nativePackages = @(
"@biomejs/cli-win32-$nodeArchitecture",
"@bufbuild/buf-win32-$nodeArchitecture",
"@rolldown/binding-win32-$nodeArchitecture-msvc"
)
foreach ($package in $nativePackages) {
$manifest = Join-Path 'node_modules' (Join-Path $package 'package.json')
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
throw "The lockfile-pinned native dependency '$package' was not installed."
}
}
Loading
Loading