Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4ab69ee
Add Windows hypervisor primitives
sjmiller609 Aug 19, 2026
a5f3c40
Make UEFI profile test architecture-aware
sjmiller609 Aug 20, 2026
54fab7a
Support Debian Secure Boot firmware paths
sjmiller609 Aug 20, 2026
1ae68d6
Allow loaded hosts to start Windows devices
sjmiller609 Aug 20, 2026
9a4cafa
Isolate the Windows hypervisor CI gate
sjmiller609 Aug 20, 2026
53c6be8
Retry the isolated Windows config gate
sjmiller609 Aug 20, 2026
32eb71a
Address Windows hypervisor review feedback
sjmiller609 Aug 20, 2026
6b63aca
Rewrite Windows state paths for QEMU forks
sjmiller609 Aug 20, 2026
0083143
Add OCI Windows machine images
sjmiller609 Aug 19, 2026
6584b31
Harden Windows image materialization
sjmiller609 Aug 19, 2026
1d87481
Keep machine image tests portable
sjmiller609 Aug 20, 2026
1fd0c5f
Isolate the Windows images CI gate
sjmiller609 Aug 20, 2026
dd0654d
Build embedded agent before Windows gates
sjmiller609 Aug 20, 2026
31b5a61
Use image naming for Windows artifacts
sjmiller609 Aug 20, 2026
0dc80f4
Protect pending Windows image dependencies
sjmiller609 Aug 21, 2026
7c5df66
Add Windows guest control
sjmiller609 Aug 20, 2026
28d52d2
Keep guest agent portable on Unix hosts
sjmiller609 Aug 20, 2026
d5ade27
Terminate Windows exec process trees
sjmiller609 Aug 20, 2026
282ae1c
Stabilize Windows process cleanup gate
sjmiller609 Aug 20, 2026
739d3ca
Terminate Windows exec jobs synchronously
sjmiller609 Aug 20, 2026
de91d07
Isolate the Windows guest-control CI gate
sjmiller609 Aug 20, 2026
0886125
Document Windows guest control design
sjmiller609 Aug 20, 2026
95cf15f
Launch Windows commands on the interactive desktop
sjmiller609 Aug 21, 2026
89800a9
Probe Windows guest-agent readiness directly
sjmiller609 Aug 21, 2026
f36f3e2
Keep Windows system execution path unchanged
sjmiller609 Aug 21, 2026
d7fc903
Probe Windows readiness with PowerShell
sjmiller609 Aug 21, 2026
04e39c1
Use instance readiness for Windows gate
sjmiller609 Aug 21, 2026
c6ed97d
Isolate Windows desktop process launch
sjmiller609 Aug 21, 2026
a0a4c9a
Stabilize Windows guest control coverage
sjmiller609 Aug 22, 2026
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
61 changes: 57 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,76 @@ jobs:
! command -v mkfs.ext4 &> /dev/null || \
! command -v iptables &> /dev/null || \
! command -v qemu-system-x86_64 &> /dev/null || \
! qemu-system-x86_64 --version >/dev/null 2>&1; then
! qemu-system-x86_64 --version >/dev/null 2>&1 || \
! command -v qemu-img &> /dev/null || \
! command -v swtpm &> /dev/null || \
! test -d /usr/share/OVMF; then
apt_update_with_retry
timeout 300s sudo apt-get install -y erofs-utils e2fsprogs iptables qemu-system-x86 qemu-utils
timeout 300s sudo apt-get install -y erofs-utils e2fsprogs iptables ovmf qemu-system-x86 qemu-utils swtpm
fi
if test -f /usr/share/OVMF/OVMF_CODE_4M.secboot.fd && test -f /usr/share/OVMF/OVMF_VARS_4M.ms.fd; then
ovmf_code=/usr/share/OVMF/OVMF_CODE_4M.secboot.fd
ovmf_vars=/usr/share/OVMF/OVMF_VARS_4M.ms.fd
elif test -f /usr/share/OVMF/OVMF_CODE.secboot.fd && test -f /usr/share/OVMF/OVMF_VARS.ms.fd; then
ovmf_code=/usr/share/OVMF/OVMF_CODE.secboot.fd
ovmf_vars=/usr/share/OVMF/OVMF_VARS.ms.fd
else
echo "Secure Boot OVMF firmware with Microsoft-enrolled variables is unavailable" >&2
exit 1
fi
echo "HYPEMAN_WINDOWS_OVMF_CODE=$ovmf_code" >> "$GITHUB_ENV"
echo "HYPEMAN_WINDOWS_OVMF_VARS=$ovmf_vars" >> "$GITHUB_ENV"
go mod download

- name: Verify Linux test toolchain
run: |
set -euo pipefail
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
for bin in mkfs.erofs mkfs.ext4 iptables qemu-system-x86_64; do
for bin in mkfs.erofs mkfs.ext4 iptables qemu-img qemu-system-x86_64 swtpm; do
if ! sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin' >/dev/null"; then
echo "missing required binary under sudo PATH: $bin"
exit 1
fi
sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin'"
done
test -f "$HYPEMAN_WINDOWS_OVMF_CODE"
test -f "$HYPEMAN_WINDOWS_OVMF_VARS"
test -r /ci/windows/image-agent.qcow2

- name: Test Windows hypervisor primitives
run: |
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
for attempt in 1 2 3; do
if sudo env \
"PATH=$TEST_PATH" \
"CI=true" \
"HYPEMAN_RUN_WINDOWS_CONFIG_INTEGRATION=1" \
"HYPEMAN_WINDOWS_OVMF_CODE=$HYPEMAN_WINDOWS_OVMF_CODE" \
"HYPEMAN_WINDOWS_OVMF_VARS=$HYPEMAN_WINDOWS_OVMF_VARS" \
go test -count=1 -run '^TestWindowsConfigIntegration$' -timeout 2m ./lib/hypervisor/qemu; then
exit 0
fi
test "$attempt" = 3 || sleep 5
done
exit 1

- name: Test Windows guest control
run: |
make build-embedded
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
for attempt in 1 2 3; do
if sudo env \
"PATH=$TEST_PATH" \
"CI=true" \
"HYPEMAN_RUN_WINDOWS_GUEST_CONTROL_INTEGRATION=1" \
"HYPEMAN_WINDOWS_OVMF_CODE=$HYPEMAN_WINDOWS_OVMF_CODE" \
"HYPEMAN_WINDOWS_OVMF_VARS=$HYPEMAN_WINDOWS_OVMF_VARS" \
go test -count=1 -run '^TestWindowsGuestAgentIntegration$' -timeout 2m ./lib/instances; then
exit 0
fi
test "$attempt" = 3 || sleep 5
done
exit 1

# Slash-command runs are maintainer-approved and need authenticated pulls
# for images that are not covered by the prewarm cache.
Expand Down Expand Up @@ -139,7 +192,7 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: |
for attempt in 1 2 3; do
if make build; then
if make build && make build-windows-guest-agent; then
exit 0
fi
if [ "$attempt" -lt 3 ]; then
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cloud-hypervisor
cloud-hypervisor/**
lib/system/exec_agent/exec-agent
lib/system/guest_agent/guest-agent
lib/system/guest_agent/hypeman-guest-agent.exe
lib/system/init/init
lib/hypervisor/vz/vz-shim/vz-shim

Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ lib/system/guest_agent/guest-agent: lib/system/guest_agent/*.go
@echo "Building guest-agent for Linux..."
cd lib/system/guest_agent && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o guest-agent .

lib/system/guest_agent/hypeman-guest-agent.exe: lib/system/guest_agent/*.go
@echo "Building guest-agent for Windows..."
cd lib/system/guest_agent && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o hypeman-guest-agent.exe .

build-windows-guest-agent: lib/system/guest_agent/hypeman-guest-agent.exe

# Build init binary (runs as PID 1 in guest VM) for embedding
# Cross-compile for Linux since it runs inside the VM
lib/system/init/init: lib/system/init/*.go
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ hypeman logs --source vmm my-app

# View Hypeman operational logs
hypeman logs --source hypeman my-app

# View software TPM logs for a TPM-backed QEMU guest
hypeman logs --source swtpm my-app
```

For all available commands, run `hypeman --help`.
Expand Down
20 changes: 18 additions & 2 deletions cmd/api/api/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -40,6 +41,7 @@ type ExecRequest struct {
WaitForAgent int32 `json:"wait_for_agent,omitempty"` // seconds to wait for guest agent to be ready
Rows uint32 `json:"rows,omitempty"` // Initial terminal rows (0 = default)
Cols uint32 `json:"cols,omitempty"` // Initial terminal cols (0 = default)
Session string `json:"session,omitempty"` // system (default) or desktop (Windows)
}

// ResizeMessage represents a window resize control message
Expand Down Expand Up @@ -106,9 +108,22 @@ func (s *ApiService) ExecHandler(w http.ResponseWriter, r *http.Request) {
return
}

// Default command if not specified
session := guest.ExecSession_EXEC_SESSION_SYSTEM
switch strings.ToLower(execReq.Session) {
case "", "system":
case "desktop":
session = guest.ExecSession_EXEC_SESSION_DESKTOP
default:
ws.WriteMessage(websocket.TextMessage, []byte(`{"error":"session must be system or desktop"}`))
return
}

if len(execReq.Command) == 0 {
execReq.Command = []string{"/bin/sh"}
if strings.HasPrefix(inst.Platform, "windows/") {
execReq.Command = []string{"cmd.exe"}
} else {
execReq.Command = []string{"/bin/sh"}
}
}

// Get JWT subject for audit logging (if available)
Expand Down Expand Up @@ -170,6 +185,7 @@ func (s *ApiService) ExecHandler(w http.ResponseWriter, r *http.Request) {
WaitForAgent: time.Duration(execReq.WaitForAgent) * time.Second,
Rows: execReq.Rows,
Cols: execReq.Cols,
Session: session,
ResizeChan: resizeChan,
})

Expand Down
2 changes: 2 additions & 0 deletions cmd/api/api/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,8 @@ func (s *ApiService) GetInstanceLogs(ctx context.Context, request oapi.GetInstan
source = instances.LogSourceVMM
case oapi.Hypeman:
source = instances.LogSourceHypeman
case oapi.Swtpm:
source = instances.LogSourceSWTPM
}
}

Expand Down
19 changes: 19 additions & 0 deletions docs/windows-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Windows machine images

Hypeman accepts Windows desktop disks as OCI images for `windows/amd64`. Ordinary Windows container images are not bootable and are rejected.

A machine image uses these OCI config labels:

| Label | Base | Image |
|---|---|---|
| `io.hypeman.machine-image.version` | `1` | `1` |
| `io.hypeman.machine-image.kind` | `windows-base` | `windows-image` |
| `io.hypeman.machine-image.disk-path` | relative path to the source disk | relative path to a qcow2 delta |
| `io.hypeman.machine-image.disk-format` | `raw`, `qcow2`, `vhd`, or `vhdx` | `qcow2` |
| `io.hypeman.machine-image.base` | omitted | digest-pinned base reference |
| `io.hypeman.machine-image.tpm` | `2.0` | `2.0` |
| `io.hypeman.machine-image.secure-boot` | `required` | `required` |

The base must be pulled before its dependent Windows images. A base cannot be deleted while any cached image references its digest. Instance references are not tracked by the image cache, matching existing Linux behavior: do not delete a base while a dependent Windows instance exists.

Windows installation media, activation material, credentials, and generated disks belong in private registries and must not be committed to this repository.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.4
require (
al.essio.dev/pkg/shellescape v1.6.0
github.com/Code-Hex/vz/v3 v3.7.1
github.com/aymanbagabas/go-pty v0.2.2
Comment thread
sjmiller609 marked this conversation as resolved.
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/creack/pty v1.1.24
github.com/cyphar/filepath-securejoin v0.6.1
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/aymanbagabas/go-pty v0.2.2 h1:YZREB4eSj+1xdbbItIokX0ekjjeifgJOA+ZvxU4/WM8=
github.com/aymanbagabas/go-pty v0.2.2/go.mod h1:gfvlwH+0U66BCwxJREjJaAOEs9H1OFf3YFjI9WSiZ04=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
Expand Down Expand Up @@ -125,6 +127,8 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGh
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d h1:nP8SfQJqruIVSWYJTuYc37jLHEY1Z0fF+zKSrs3K/C8=
github.com/hugelgupf/vmtest v0.0.0-20240307030256-5d9f3d34a58d/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
Expand Down Expand Up @@ -264,6 +268,8 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/u-root/gobusybox/src v0.0.0-20250101170133-2e884e4509c7 h1:dtiVT4SeBUc/vHtwI2HjDZN+FCKTstQBxugIxJEGo9g=
github.com/u-root/gobusybox/src v0.0.0-20250101170133-2e884e4509c7/go.mod h1:PW3wGFCHjdHxAhra5FKvcARbCGqGfentYuPKmuhv8DY=
github.com/u-root/u-root v0.15.0 h1:8JXfjAA/Vs8EXfZUA2ftvoHbiYYLdaU8umJ461aq+Jw=
github.com/u-root/u-root v0.15.0/go.mod h1:/0Qr7qJeDwWxoKku2xKQ4Szc+SwBE3g9VE8jNiamsmc=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
Expand Down Expand Up @@ -356,6 +362,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
2 changes: 2 additions & 0 deletions lib/guest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ type ExecOptions struct {
WaitForAgent time.Duration // Max time to wait for agent to be ready (0 = no wait, fail immediately)
Rows uint32 // Initial terminal rows (0 = default 24)
Cols uint32 // Initial terminal cols (0 = default 80)
Session ExecSession // SYSTEM service session or active Windows desktop session
ResizeChan <-chan *WindowSize // Optional: channel to receive resize events (pointer to avoid copying mutex)
}

Expand Down Expand Up @@ -477,6 +478,7 @@ func execIntoInstanceOnce(ctx context.Context, dialer hypervisor.VsockDialer, op
TimeoutSeconds: opts.Timeout,
Rows: opts.Rows,
Cols: opts.Cols,
Session: opts.Session,
},
},
}); err != nil {
Expand Down
Loading
Loading