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
20 changes: 19 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ jobs:
test -f "$HYPEMAN_WINDOWS_OVMF_VARS"
test -r /ci/windows/base.raw
test -r /ci/windows/persona.qcow2
test -r /ci/windows/persona-agent.qcow2
qemu-img info --output=json /ci/windows/persona.qcow2 \
| jq -e '.format == "qcow2" and .["backing-filename-format"] == "raw"' >/dev/null

Expand Down Expand Up @@ -153,6 +154,23 @@ jobs:
done
exit 1

- name: Test Windows guest control
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_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.
- name: Login to Docker Hub
Expand Down Expand Up @@ -195,7 +213,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
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
20 changes: 20 additions & 0 deletions docs/windows-guest-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Windows guest agent

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I feel like this should not be in docs but maybe in a section in the appropriate lib/**/README.md as it's more about developer level concerns, not user facing docs


Windows personas include `hypeman-guest-agent.exe` as the automatic `HypemanGuestAgent` LocalSystem service and the signed virtio-win VioSock driver. The agent listens on virtio-vsock port 2222 and implements the existing guest gRPC protocol.

The Windows build supports:

- command execution in the LocalSystem service session
- command execution in the active interactive desktop session
- ConPTY allocation and terminal resize events
- file copy, path stat, and graceful shutdown

Exec requests use `session: "system"` by default. `session: "desktop"` obtains the token for the active Windows session and returns an error when no interactive user is logged in.

Build the service with:

```sh
make build-windows-guest-agent
```

The generated executable, Windows driver packages, credentials, and prepared persona disks are release inputs. They 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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

new dependency is ok 👍

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