feat: add macOS support via Virtualization.framework (vz)#82
feat: add macOS support via Virtualization.framework (vz)#82
Conversation
Move Linux-specific resource detection (CPU, memory, disk, network) and device management (discovery, mdev, vfio) into _linux.go files. Add stub _darwin.go files that return empty/unsupported results for macOS. This is a pure refactoring with no functional changes on Linux. Prepares the codebase for macOS support where these Linux-specific features (cgroups, sysfs, VFIO) are not available. Co-authored-by: Cursor <cursoragent@cursor.com>
Move Linux bridge/TAP networking into bridge_linux.go. Add bridge_darwin.go stub since macOS vz uses built-in NAT networking. Extract shared IP allocation logic to ip.go. Move VMM binary detection (cloud-hypervisor, qemu paths) into binaries_linux.go. Add binaries_darwin.go that returns empty paths since vz is in-process. No functional changes on Linux. Co-authored-by: Cursor <cursoragent@cursor.com>
Move ingress binary embedding into platform-specific files. Update build tags on architecture-specific files to also include OS constraint. Replace checkKVMAccess() with platform-agnostic checkHypervisorAccess(): - Linux: checks /dev/kvm access (existing behavior) - macOS: verifies ARM64 arch and Virtualization.framework availability No functional changes on Linux. Co-authored-by: Cursor <cursoragent@cursor.com>
…ions Add GetVsockDialer() to instance manager interface. This abstraction handles the difference between: - Linux: socket-based vsock (AF_VSOCK or Unix socket proxy) - macOS vz: in-process vsock via VirtualMachine object Update API handlers (exec, cp, instances) and build manager to use GetVsockDialer() instead of directly creating vsock connections. Add DialVsock() method to VsockDialer interface for explicit dialing. Co-authored-by: Cursor <cursoragent@cursor.com>
Implement Hypervisor and VMStarter interfaces using github.com/Code-Hex/vz/v3 library for Apple's Virtualization.framework. Key differences from Linux hypervisors: - In-process: VMs run within hypeman process (no separate PID) - NAT networking: Uses vz built-in NAT (192.168.64.0/24) - Direct vsock: Connects via VirtualMachine object, not socket files - Snapshot support: Available on macOS 14+ ARM64 Registers vz starter on macOS via init() in hypervisor_darwin.go. Linux hypervisor_linux.go is a no-op placeholder. Co-authored-by: Cursor <cursoragent@cursor.com>
Guest init changes: - Add hvc0 serial console support (vz uses hvc0, not ttyS0) - Prioritize /dev/hvc0 for console output in logger and mount Binary embedding: - Add darwin-specific embed files for cross-compiled linux/arm64 binaries - Guest init and agent binaries are embedded when building on macOS OCI image handling: - Add vmPlatform() to return linux/arm64 for VM images regardless of host - Fixes image pull on macOS which would otherwise request darwin/arm64 Instance lifecycle: - Track active hypervisors for vz (needed for in-process VM references) - Handle vz-specific cleanup in delete (no PID to kill) - Support vz in instance queries Co-authored-by: Cursor <cursoragent@cursor.com>
Build system: - Add macOS targets to Makefile (build-darwin, run, sign) - Add .air.darwin.toml for live reload on macOS - Add vz.entitlements for Virtualization.framework code signing - Add .env.darwin.example with macOS-specific configuration Documentation: - Update DEVELOPMENT.md with macOS setup instructions - Update README.md to mention macOS support - Update lib/hypervisor/README.md with vz implementation details - Update lib/instances/README.md for multi-hypervisor support - Update lib/network/README.md with platform comparison Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Changes the vz hypervisor from in-process to subprocess model, allowing VMs to survive hypeman restarts. Mirrors the cloud-hypervisor architecture. Key changes: - Add cmd/vz-shim binary that hosts vz VMs in a subprocess - Shim exposes HTTP API on Unix socket for VM control (matching CH pattern) - Shim exposes vsock proxy on separate Unix socket using CH protocol - Update vz starter to spawn shim subprocess instead of in-process VM - Add vz.Client implementing Hypervisor interface via HTTP to shim - Update VsockDialer to use Unix socket proxy instead of in-process VM - Add hypervisor.ClientFactory for uniform hypervisor client creation - Remove activeHypervisors tracking (no longer needed) - Simplify vsock_darwin.go (vz now uses same socket pattern as other hypervisors) - Update Makefile to build and sign vz-shim binary Co-authored-by: Cursor <cursoragent@cursor.com>
…ation Add snapshot save/restore infrastructure to vz-shim: - Snapshot endpoint in shim server (vm.snapshot) - RestoreVM implementation in starter (loads config from metadata.json) - Snapshot method in client (adapts directory path to file path) Document Virtualization.framework limitation: - Linux guest VMs cannot be reliably saved/restored - Only macOS guests support this functionality - This is an undocumented Apple limitation confirmed by Tart and UTM projects - References: Tart #1177, #796; UTM #6654 The infrastructure is in place for potential future macOS guest support while correctly disabling snapshot capability for Linux guests. Also improves MAC address handling and error logging in vm.go. Co-authored-by: Cursor <cursoragent@cursor.com>
- Makefile: prepend e2fsprogs sbin to PATH for dev-darwin and run-darwin targets so mkfs.ext4 is found without requiring shell profile changes - manager.go: don't cache failed image builds, clean up failed build directory to allow retries after fixing the underlying issue
|
Bugbot Autofix prepared fixes for 3 of the 3 bugs found in the latest run.
Or push these changes by commenting: Preview (6f89d8c359)diff --git a/lib/builds/manager.go b/lib/builds/manager.go
--- a/lib/builds/manager.go
+++ b/lib/builds/manager.go
@@ -608,62 +608,6 @@
}
}
-// dialBuilderVsock connects to a builder VM's vsock socket using Cloud Hypervisor's handshake
-func (m *manager) dialBuilderVsock(vsockSocketPath string) (net.Conn, error) {
- // Connect to the Cloud Hypervisor vsock Unix socket
- conn, err := net.DialTimeout("unix", vsockSocketPath, 5*time.Second)
- if err != nil {
- return nil, fmt.Errorf("dial vsock socket %s: %w", vsockSocketPath, err)
- }
-
- // Set deadline for handshake
- if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
- conn.Close()
- return nil, fmt.Errorf("set handshake deadline: %w", err)
- }
-
- // Perform Cloud Hypervisor vsock handshake
- // Format: "CONNECT <port>\n" -> "OK <port>\n"
- handshakeCmd := fmt.Sprintf("CONNECT %d\n", BuildAgentVsockPort)
- if _, err := conn.Write([]byte(handshakeCmd)); err != nil {
- conn.Close()
- return nil, fmt.Errorf("send vsock handshake: %w", err)
- }
-
- // Read handshake response
- reader := bufio.NewReader(conn)
- response, err := reader.ReadString('\n')
- if err != nil {
- conn.Close()
- return nil, fmt.Errorf("read vsock handshake response: %w", err)
- }
-
- // Clear deadline after successful handshake
- if err := conn.SetDeadline(time.Time{}); err != nil {
- conn.Close()
- return nil, fmt.Errorf("clear deadline: %w", err)
- }
-
- response = strings.TrimSpace(response)
- if !strings.HasPrefix(response, "OK ") {
- conn.Close()
- return nil, fmt.Errorf("vsock handshake failed: %s", response)
- }
-
- return &bufferedConn{Conn: conn, reader: reader}, nil
-}
-
-// bufferedConn wraps a net.Conn with a bufio.Reader to ensure any buffered
-// data from the handshake is properly drained before reading from the connection
-type bufferedConn struct {
- net.Conn
- reader *bufio.Reader
-}
-
-func (c *bufferedConn) Read(p []byte) (int, error) {
- return c.reader.Read(p)
-}
-
// updateStatus updates the build status
func (m *manager) updateStatus(id string, status string, err error) {
meta, readErr := readMetadata(m.paths, id)
diff --git a/lib/hypervisor/vz/starter.go b/lib/hypervisor/vz/starter.go
--- a/lib/hypervisor/vz/starter.go
+++ b/lib/hypervisor/vz/starter.go
@@ -11,6 +11,7 @@
"os"
"os/exec"
"path/filepath"
+ "syscall"
"time"
"github.com/kernel/hypeman/lib/hypervisor"
@@ -134,10 +135,15 @@
}
// Spawn the shim process
- cmd := exec.CommandContext(ctx, shimPath, "-config", string(configJSON))
+ // Use Command (not CommandContext) so process survives parent context cancellation
+ cmd := exec.Command(shimPath, "-config", string(configJSON))
cmd.Stdout = nil // Shim logs to file
cmd.Stderr = nil
cmd.Stdin = nil
+ // Detach from parent process group
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Setpgid: true,
+ }
if err := cmd.Start(); err != nil {
return 0, nil, fmt.Errorf("start vz-shim: %w", err)
@@ -330,10 +336,15 @@
}
// Spawn the shim process
- cmd := exec.CommandContext(ctx, shimPath, "-config", string(configJSON))
+ // Use Command (not CommandContext) so process survives parent context cancellation
+ cmd := exec.Command(shimPath, "-config", string(configJSON))
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Stdin = nil
+ // Detach from parent process group
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Setpgid: true,
+ }
if err := cmd.Start(); err != nil {
return 0, nil, fmt.Errorf("start vz-shim: %w", err)
diff --git a/lib/instances/vsock_linux.go b/lib/instances/vsock.go
--- a/lib/instances/vsock_linux.go
+++ b/lib/instances/vsock.go
@@ -1,5 +1,3 @@
-//go:build linux
-
package instances
import (
diff --git a/lib/instances/vsock_darwin.go b/lib/instances/vsock_darwin.go
deleted file mode 100644
--- a/lib/instances/vsock_darwin.go
+++ /dev/null
@@ -1,20 +1,0 @@
-//go:build darwin
-
-package instances
-
-import (
- "context"
-
- "github.com/kernel/hypeman/lib/hypervisor"
-)
-
-// GetVsockDialer returns a VsockDialer for the specified instance.
-// On macOS, all hypervisors (including vz via shim) use socket-based vsock.
-func (m *manager) GetVsockDialer(ctx context.Context, instanceID string) (hypervisor.VsockDialer, error) {
- inst, err := m.GetInstance(ctx, instanceID)
- if err != nil {
- return nil, err
- }
-
- return hypervisor.NewVsockDialer(hypervisor.Type(inst.HypervisorType), inst.VsockSocket, inst.VsockCID)
-}
\ No newline at end of file |
Add -registry-push flag to gen-jwt that adds repo_access claims to the JWT, enabling push permissions for specific repositories. This is needed to push the builder image to Hypeman's internal registry during local development. Also add documentation for the builder image setup workflow in DEVELOPMENT.md, covering the full process from building the image to configuring BUILDER_IMAGE.
Instead of collecting all logs and sending them at the end of a build, stream log lines incrementally from the builder agent to the manager via vsock "log" messages. The manager appends each log line to the build log file immediately, enabling real-time log streaming to clients via the SSE endpoint. Changes: - Add streamingLogWriter in builder agent with channel-based streaming - Add markClosed() mechanism to prevent panic from writes after channel close - Handle "log" message type in manager's waitForResult - Remove redundant final log save since logs are now streamed incrementally
- Remove unused datasize import (CI failure) - Fix network devices overwritten in loop: collect all devices then set once - Use exec.Command instead of CommandContext so vz-shim survives context cancel - Map unknown VM states to StateShutdown instead of StateRunning - Add stale unix socket cleanup before net.Listen in vz-shim - Clarify Intel Mac rejection is intentional (kernel panics, no nested virt) - Merge identical vsock_darwin.go/vsock_linux.go into single vsock.go - Remove dead dialBuilderVsock code and bufferedConn type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Bugbot Autofix prepared fixes for 3 of the 3 bugs found in the latest run.
Or push these changes by commenting: Preview (d65c8be753)diff --git a/lib/builds/builder_agent/main.go b/lib/builds/builder_agent/main.go
--- a/lib/builds/builder_agent/main.go
+++ b/lib/builds/builder_agent/main.go
@@ -129,12 +129,13 @@
w.buffer.Write(p)
w.mu.Unlock()
- // Check if channel is closed before attempting to send
+ // Hold read lock during both the closed check AND the channel send.
+ // This prevents a TOCTOU race where markClosed() + close(logChan) could
+ // execute between reading closed=false and the send, which would panic.
+ // markClosed() acquires a write lock, so it blocks while we hold the read lock,
+ // and close(logChan) only runs after markClosed() completes.
w.closedMu.RLock()
- isClosed := w.closed
- w.closedMu.RUnlock()
-
- if !isClosed {
+ if !w.closed {
// Send to channel for streaming (non-blocking)
line := string(p)
select {
@@ -143,6 +144,7 @@
// Channel full, drop the log line for streaming but it's still in buffer
}
}
+ w.closedMu.RUnlock()
// Also write to stdout for local debugging
os.Stdout.Write(p)
diff --git a/lib/builds/manager.go b/lib/builds/manager.go
--- a/lib/builds/manager.go
+++ b/lib/builds/manager.go
@@ -331,9 +331,14 @@
return
}
- // Note: Logs are now streamed via vsock "log" messages and written incrementally
- // in waitForResult, so we no longer need to save them here.
- // The result.Logs field is kept for backward compatibility but is redundant.
+ // Logs are streamed incrementally via vsock "log" messages in waitForResult,
+ // but the streaming channel may drop lines when full. Overwrite the log file
+ // with the complete logs from result.Logs to ensure no lines are lost.
+ if result.Logs != "" {
+ if err := writeLog(m.paths, id, []byte(result.Logs)); err != nil {
+ m.logger.Warn("failed to save build logs", "id", id, "error", err)
+ }
+ }
if !result.Success {
m.logger.Error("build failed", "id", id, "error", result.Error, "duration", duration)
diff --git a/lib/builds/storage.go b/lib/builds/storage.go
--- a/lib/builds/storage.go
+++ b/lib/builds/storage.go
@@ -171,6 +171,21 @@
return os.MkdirAll(logsDir, 0755)
}
+// writeLog overwrites the build log file with the given data.
+// Used to write the complete log record after build completion.
+func writeLog(p *paths.Paths, id string, data []byte) error {
+ if err := ensureLogsDir(p, id); err != nil {
+ return err
+ }
+
+ logPath := p.BuildLog(id)
+ if err := os.WriteFile(logPath, data, 0644); err != nil {
+ return fmt.Errorf("write log: %w", err)
+ }
+
+ return nil
+}
+
// appendLog appends log data to the build log file
func appendLog(p *paths.Paths, id string, data []byte) error {
if err := ensureLogsDir(p, id); err != nil {
diff --git a/lib/network/bridge_darwin.go b/lib/network/bridge_darwin.go
--- a/lib/network/bridge_darwin.go
+++ b/lib/network/bridge_darwin.go
@@ -4,7 +4,6 @@
import (
"context"
- "fmt"
"github.com/kernel/hypeman/lib/logger"
)
@@ -76,23 +75,3 @@
//
// The CreateAllocation and ReleaseAllocation methods in allocate.go
// will need platform-specific handling for the TAP-related calls.
-
-// macOSNetworkConfig holds macOS-specific network configuration
-type macOSNetworkConfig struct {
- UseNAT bool // Always true for macOS
-}
-
-// GetMacOSNetworkConfig returns the macOS network configuration
-func GetMacOSNetworkConfig() *macOSNetworkConfig {
- return &macOSNetworkConfig{
- UseNAT: true,
- }
-}
-
-// IsMacOS returns true on macOS builds
-func IsMacOS() bool {
- return true
-}
-
-// ErrRateLimitNotSupported indicates rate limiting is not supported on macOS
-var ErrRateLimitNotSupported = fmt.Errorf("network rate limiting is not supported on macOS") |
- Hold closedMu RLock through channel send in streamingLogWriter to prevent panic from sending on a closed channel - Remove unused macOSNetworkConfig, GetMacOSNetworkConfig, IsMacOS, and ErrRateLimitNotSupported from bridge_darwin.go (vz uses framework-level NAT, rate limiting will never apply) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tmp dir fallback constructed the same path as the initial check, making it unreachable dead code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: Preview (c33dcf6521)diff --git a/lib/hypervisor/vz/starter.go b/lib/hypervisor/vz/starter.go
--- a/lib/hypervisor/vz/starter.go
+++ b/lib/hypervisor/vz/starter.go
@@ -175,10 +175,10 @@
if _, err := os.Stat(shimPath); err == nil {
return shimPath, nil
}
- // Also check parent's tmp dir (for air hot-reload development)
- // When running ./tmp/main, check ./tmp/vz-shim
+ // Also check the parent directory (for air hot-reload development)
+ // When running ./tmp/main, check ./vz-shim
if filepath.Base(exeDir) == "tmp" {
- shimPath = filepath.Join(exeDir, "vz-shim")
+ shimPath = filepath.Join(filepath.Dir(exeDir), "vz-shim")
if _, err := os.Stat(shimPath); err == nil {
return shimPath, nil
} |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
✱ Stainless preview buildsThis PR will update the Edit this comment to update it. It will appear in the SDK's changelogs. ✅ hypeman-typescript studio · code · diff
✅ hypeman-go studio · code · diff
⏳ These are partial results; builds are still running. This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
- install.sh: OS branching for macOS (launchd, codesign, Docker socket auto-detection, arm64 check, ~/Library paths) - uninstall.sh: macOS support (launchctl, vz-shim cleanup) - Makefile: platform-aware build/test targets (Darwin vs Linux) - Build manager: auto-build builder image on startup via background goroutine with atomic readiness gate and DOCKER_SOCKET config - CI: test-darwin job on self-hosted macOS ARM64 runner with per-run DATA_DIR isolation; e2e-install job for install/uninstall cycle - e2e-install-test.sh: platform-agnostic install → verify → uninstall - DEVELOPMENT.md: document DOCKER_SOCKET and builder auto-build Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lock Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…discovery - Add //go:build linux to vm_metrics and vmm test files - Change t.Fatal to t.Skip for /dev/kvm checks across all test files - Skip TestNetworkResource_Allocated on non-Linux (no rate limiting) - test-darwin uses go list to discover only compilable packages - Use separate concurrency groups per CI job Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: Preview (273e3e446a)diff --git a/lib/hypervisor/vz/starter.go b/lib/hypervisor/vz/starter.go
--- a/lib/hypervisor/vz/starter.go
+++ b/lib/hypervisor/vz/starter.go
@@ -393,7 +393,6 @@
// containsImageName checks if a path contains the image name components.
func containsImageName(path, imageName string) bool {
// Extract just the image name without tag (e.g., "alpine" from "alpine:3.20")
- parts := filepath.SplitList(imageName)
name := imageName
if idx := len(name) - 1; idx >= 0 {
for i := len(name) - 1; i >= 0; i-- {
@@ -403,7 +402,6 @@
}
}
}
- _ = parts
return filepath.Base(filepath.Dir(filepath.Dir(path))) == name ||
filepath.Base(filepath.Dir(path)) == name
} |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Bugbot Autofix prepared fixes for 2 of the 2 bugs found in the latest run.
Or push these changes by commenting: Preview (279d227209)diff --git a/lib/builds/manager.go b/lib/builds/manager.go
--- a/lib/builds/manager.go
+++ b/lib/builds/manager.go
@@ -15,6 +15,9 @@
"sync/atomic"
"time"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/layout"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/nrednav/cuid2"
"github.com/kernel/hypeman/lib/images"
"github.com/kernel/hypeman/lib/instances"
@@ -232,7 +235,103 @@
return
}
- m.logger.Info("builder image built successfully", "image", builderImage)
+ m.logger.Info("builder image built successfully, pushing to registry", "image", builderImage)
+
+ // Export image from Docker to a temporary tarball
+ tmpFile, err := os.CreateTemp("", "hypeman-builder-*.tar")
+ if err != nil {
+ m.logger.Warn("failed to create temp file for builder image export", "error", err)
+ return
+ }
+ tmpPath := tmpFile.Name()
+ tmpFile.Close()
+ defer os.Remove(tmpPath)
+
+ saveCmd := exec.CommandContext(ctx, "docker", "save", "-o", tmpPath, builderImage)
+ saveCmd.Env = append(os.Environ(), fmt.Sprintf("DOCKER_HOST=unix://%s", dockerSocket))
+ if saveOutput, saveErr := saveCmd.CombinedOutput(); saveErr != nil {
+ m.logger.Warn("failed to export builder image from Docker",
+ "error", saveErr,
+ "output", string(saveOutput))
+ return
+ }
+
+ // Load image from Docker save tarball
+ img, err := tarball.ImageFromPath(tmpPath, nil)
+ if err != nil {
+ m.logger.Warn("failed to load builder image from tarball", "error", err)
+ return
+ }
+
+ // Get image digest
+ digestHash, err := img.Digest()
+ if err != nil {
+ m.logger.Warn("failed to get builder image digest", "error", err)
+ return
+ }
+
+ // Write to OCI layout cache so the image manager can find it
+ cacheDir := m.paths.SystemOCICache()
+ if err := os.MkdirAll(cacheDir, 0755); err != nil {
+ m.logger.Warn("failed to create OCI cache directory", "error", err)
+ return
+ }
+
+ ociLayout, err := layout.FromPath(cacheDir)
+ if err != nil {
+ ociLayout, err = layout.Write(cacheDir, empty.Index)
+ if err != nil {
+ m.logger.Warn("failed to create OCI layout", "error", err)
+ return
+ }
+ }
+
+ if err := ociLayout.AppendImage(img, layout.WithAnnotations(map[string]string{
+ "org.opencontainers.image.ref.name": digestHash.Hex,
+ })); err != nil {
+ m.logger.Warn("failed to write builder image to OCI layout", "error", err)
+ return
+ }
+
+ // Import into image manager to trigger conversion to disk format
+ parts := strings.SplitN(builderImage, ":", 2)
+ imageName := parts[0]
+ imageTag := "latest"
+ if len(parts) == 2 {
+ imageTag = parts[1]
+ }
+ repo := fmt.Sprintf("%s/%s", registryHost, imageName)
+
+ if _, err := m.imageManager.ImportLocalImage(ctx, repo, imageTag, digestHash.String()); err != nil {
+ m.logger.Warn("failed to import builder image", "error", err)
+ return
+ }
+
+ m.logger.Info("builder image imported, waiting for conversion", "image", imageRef, "digest", digestHash.String())
+
+ // Wait for image conversion to complete before signaling ready
+ for attempt := 0; attempt < 120; attempt++ {
+ select {
+ case <-ctx.Done():
+ m.logger.Warn("context cancelled while waiting for builder image conversion")
+ return
+ default:
+ }
+
+ if imgInfo, err := m.imageManager.GetImage(ctx, imageRef); err == nil {
+ switch imgInfo.Status {
+ case images.StatusReady:
+ m.logger.Info("builder image ready", "image", imageRef)
+ return
+ case images.StatusFailed:
+ m.logger.Warn("builder image conversion failed", "image", imageRef)
+ return
+ }
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ m.logger.Warn("timed out waiting for builder image conversion", "image", imageRef)
}
// CreateBuild starts a new build job
diff --git a/lib/instances/query.go b/lib/instances/query.go
--- a/lib/instances/query.go
+++ b/lib/instances/query.go
@@ -31,7 +31,7 @@
return stateResult{State: StateStopped}
}
- // 3. Socket exists - query hypervisor for actual state
+ // Socket exists - query hypervisor for actual state
hv, err := m.getHypervisor(stored.SocketPath, stored.HypervisorType)
if err != nil {
// Failed to create client - this is unexpected if socket exists |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
||
| // Wait for one direction to close | ||
| <-done | ||
| } |
There was a problem hiding this comment.
Vsock proxy exits after single direction closes
Medium Severity
The vsock proxy in handleVsockConnection waits for only one direction to close (<-done), then returns — closing both connections via defer. The second goroutine may still be copying data in the other direction. This can cause premature truncation of data still in flight (e.g., a response being written back after the client closes its write side). Waiting for both directions to finish would ensure complete data transfer.
|
Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.
Or push these changes by commenting: Preview (1d8b16b503)diff --git a/cmd/vz-shim/server.go b/cmd/vz-shim/server.go
--- a/cmd/vz-shim/server.go
+++ b/cmd/vz-shim/server.go
@@ -300,8 +300,9 @@
done <- struct{}{}
}()
- // Wait for one direction to close
+ // Wait for both directions to close
<-done
+ <-done
}
func copyData(dst, src net.Conn) { |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The find_release_with_artifact function returns 1 when no darwin CLI artifact exists, which under set -e kills the script before the empty-check can handle it gracefully. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
launchd doesn't perform shell expansion, so ~ in DATA_DIR causes "mkdir ~: read-only file system" when the service starts. Also fix JWT_SECRET sed pattern to match the darwin template's default value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The uninstall script needs sudo on macOS when binaries were installed to /usr/local/bin with elevated privileges. Also ensures the tilde expansion fix is included. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The service requires Caddy for the ingress manager, and on macOS it's not embedded - it must be in PATH. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
launchd doesn't inherit the user's PATH, so Homebrew-installed binaries like caddy aren't found. Add standard Homebrew paths to the plist's EnvironmentVariables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous check used /v1/instances with a fake Bearer token which would be rejected, and / which returns 404. Use the unauthenticated /health endpoint instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The busybox image at queue position 0 can finish quickly, causing alpine to transition from pending to pulling before the second CreateImage call. Accept pulling as a valid idempotent status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>



Summary
Adds macOS support to hypeman using Apple's Virtualization.framework via the Code-Hex/vz library.
Key changes:
_linux.gofiles with stub_darwin.gocounterpartslib/hypervisor/vz/package implementingHypervisorandVMStarterinterfacescmd/vz-shim/binary that hosts VMs in a separate process, allowing VMs to survive hypeman restarts (mirrors cloud-hypervisor architecture)CONNECT {port}\n→OK {port}\n) over Unix sockethypervisor.NewClient()for uniform hypervisor client creation across all VMM typesArchitecture (vz-shim subprocess model):
Platform comparison:
Snapshot limitation
Virtualization.framework does not support save/restore for Linux guest VMs - only macOS guests work. This is an undocumented Apple limitation confirmed by other projects:
"You can only suspend macOS VMs"The snapshot infrastructure is implemented in vz-shim for potential future macOS guest support, but the capability is correctly disabled for Linux guests.
vz-shim API:
The shim exposes a Cloud Hypervisor-compatible HTTP API on Unix socket:
GET /api/v1/vm.info- VM state and configurationPUT /api/v1/vm.pause- Pause VMPUT /api/v1/vm.resume- Resume VMPUT /api/v1/vm.shutdown- Graceful shutdownPUT /api/v1/vm.power-button- ACPI power buttonPUT /api/v1/vm.snapshot- Save VM state (infrastructure for macOS guests)GET /api/v1/vmm.ping- Health checkPUT /api/v1/vmm.shutdown- Terminate shimVsock proxy uses same text-based handshake as Cloud Hypervisor:
CONNECT {port}\nOK {port}\nCI Considerations
Current CI uses self-hosted Linux runners with KVM. For macOS:
macos-14) support Apple Silicon but may lack virtualization entitlementsmacos-14to verify compilation, defer VM tests to self-hostedExample addition to test.yml:
Test plan
hypeman resources- verified resource detectionhypeman pull- verified linux/arm64 image pullhypeman run- verified VM creation and boothypeman exec- verified command execution in VMhypeman ps- verified instance listinghypeman build- verified Dockerfile→VM image buildhypeman ingress- verified external access to VM serviceshypeman rm- verified instance cleanupNote
High Risk
Adds a new hypervisor backend and macOS-specific build/runtime paths (codesigning, shim process, networking/vsock), which affects core VM lifecycle and build execution across platforms.
Overview
Adds experimental macOS (Apple Silicon) support by introducing a new
vzhypervisor backend using Virtualization.framework, implemented as avz-shimsubprocess with a Cloud Hypervisor–compatible control API and vsock proxy.Refactors hypervisor integration to be more pluggable (
hypervisor.NewClient+ client factories, newinstances.Manager.GetVsockDialer) and routes exec/cp/stat and build-manager vsock connections through the instance manager to support multiple hypervisors.Updates platform-specific behavior across the stack: macOS stubs for Linux-only networking/GPU passthrough, Linux-only embedded Caddy binaries, Linux-targeted OCI image pulling regardless of host OS, sector-aligned ext4 disk sizing, and macOS-focused build/test/dev tooling (codesigning targets,
.air.darwin.toml,.env.darwin.example, macOS CI jobs, and docs).Written by Cursor Bugbot for commit 67a3923. This will update automatically on new commits. Configure here.