Skip to content

feat: launch Windows runner in interactive desktop session (WARP-849)#77

Open
PrashantRaj18198 wants to merge 2 commits into
mainfrom
prashant-warp-849
Open

feat: launch Windows runner in interactive desktop session (WARP-849)#77
PrashantRaj18198 wants to merge 2 commits into
mainfrom
prashant-warp-849

Conversation

@PrashantRaj18198
Copy link
Copy Markdown
Collaborator

@PrashantRaj18198 PrashantRaj18198 commented May 21, 2026

Summary

  • Adds a Windows-only code path that launches the GitHub runner into the active interactive desktop session (Session 1, `winsta0\default`) via `WTSQueryUserToken` + `CreateProcessAsUser`, instead of inheriting the agent's Session 0 service context.
  • Fixes WARP-849: Windows jobs were running in Session 0 with no window station, blocking any UI/GUI workload (Selenium, WinAppDriver, MAUI/WinUI).
  • Adds `prashant-warp-849` to the `release-testing` workflow trigger so the new binary is built and synced to R2 for end-to-end validation.

What changed

  • `pkg/manager/manager_windows_github_cri.go` — gated with `//go:build !windows`. Becomes a thin stub on linux/darwin (the package must cross-compile for goreleaser); `StartRunner` returns "not implemented on this OS" if ever invoked off-Windows (it isn't — this provider is only selected on Windows runners).
  • `pkg/manager/manager_windows_github_cri_windows.go` — new, `//go:build windows`. Owns the full Windows manager. Launch flow:
    1. `WTSEnumerateSessions` → pick first `WTSActive` (RDP-attached works too), fall back to `WTSGetActiveConsoleSessionId`.
    2. `WTSQueryUserToken(sessionID)` → `DuplicateTokenEx` to a primary token.
    3. `CreateEnvironmentBlock` for that token, merged with caller env (overlay wins, case-insensitive keys).
    4. Three anonymous pipes (`CreatePipe` + `SetHandleInformation`) for stdout/stderr/stdin with proper inheritance flags.
    5. `CreateProcessAsUser` with `STARTUPINFOW.lpDesktop = "winsta0\default"`, `STARTF_USESTDHANDLES`, `CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW`. `lpApplicationName = NULL` so Windows PATH-resolves `powershell.exe` against the user's env.
    6. Context cancellation triggers `TerminateProcess`; handle-close ordering avoids racing the cancel watcher.
  • All Win32 symbols used are already in the vendored `golang.org/x/sys/windows` — no new dependencies, no `go.mod`/`go.sum` changes.

Prereq (not in this PR)

This change is the agent half. It assumes a real interactive logon exists in Session 1. Today that's flaky because Sysprep strips the `DefaultPassword` from the Winlogon registry. Companion AMI/cloud-init work needed:

  • A.1 Replace bare Winlogon registry writes in `patches/common/images/windows/scripts/warpbuild/010_user_runneradmin.vm.ps1` with Sysinternals `Autologon.exe runneradmin . ` (stores password as LSA secret, survives Sysprep), OR
  • A.2 Add a post-Sysprep EC2Launch v2 task that re-stamps the Winlogon keys.

Without A.1 or A.2, `WTSQueryUserToken` returns no token and `StartRunner` errors out — loud failure instead of silent Session-0 fallback.

Test plan

  • `GOOS=windows GOARCH=amd64 go build ./...` passes (verified locally).
  • `GOOS=linux GOARCH=amd64 go build ./...` and `GOOS=darwin go build ./...` pass (verified locally).
  • `release-testing` workflow uploads the new binary under `warpbuild-agentd/prashant-warp-849/` in R2.
  • Land AMI prereq (A.1 or A.2), bake new Windows image pointing the cloud-init template at the testing R2 path.
  • Re-run `benchmarks/.github/workflows/windows-interactive-session.yaml` on `warp-windows-latest-x64-16x` — all five assertions pass (`SessionId != 0`, `WindowStation = WinSta0`, `Desktop = default`, `explorer.exe` running, WinForms window opens).

🤖 Generated with Claude Code


Note

Medium Risk
Moderate risk: replaces Windows runner process-launching with CreateProcessAsUser/session-token logic and custom stdio piping, which can fail in environments without an active interactive logon and impacts job execution behavior on Windows.

Overview
Enables Windows UI/GUI workloads by changing the github_windows_cri provider to start the runner inside the active interactive session (winsta0\default) instead of the service Session 0 context, using WTS session enumeration, user token duplication, environment block creation/merge, and CreateProcessAsUser with pipe-forwarded stdout/stderr.

Splits the manager into OS-specific files: a !windows stub that returns a clear "Windows only" error for cross-builds/tests, and a new windows implementation containing the full start logic and cleanup/cancellation handling.

Also updates release-testing workflow triggers to include the prashant-warp-849 branch so artifacts can be built and uploaded to R2 for validation.

Reviewed by Cursor Bugbot for commit ada3ebd. Configure here.

PrashantRaj18198 and others added 2 commits May 21, 2026 14:20
Windows jobs were running in Session 0 (the service session) with no
window station, blocking UI/GUI workloads (Selenium, WinAppDriver,
MAUI/WinUI). The agent now launches the runner into the active
interactive desktop (Session 1, winsta0\default) via WTSQueryUserToken
+ CreateProcessAsUser, mirroring how GitHub-hosted runners work.

Prereq (tracked separately): the AMI/cloud-init must ensure a real
interactive logon exists in session 1 (Sysinternals Autologon to
survive Sysprep, or a post-Sysprep EC2Launch re-stamp of the Winlogon
keys). Without that, StartRunner now fails loudly instead of silently
running in Session 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So the WARP-849 agent change ships to R2 and is picked up by the
testing AMI/cloud-init for end-to-end verification of the interactive
session launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented May 21, 2026

WARP-849

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ada3ebd. Configure here.

stdoutW.Close()
stderrW.Close()
stdinR.Close()
_ = windows.DestroyEnvironmentBlock(envBlockPtr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong free on merged env block

High Severity

After merging overlay variables, buildEnvironmentBlock returns a pointer into a Go-allocated UTF-16 slice, but startInteractiveProcess still calls DestroyEnvironmentBlock on that pointer. That API is only valid for blocks from CreateEnvironmentBlock. Normal startup always uses a non-empty overlay (e.g. WARPBUILD_GH_JIT_TOKEN), so this path runs on every launch.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ada3ebd. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant