Add authenticated host capabilities endpoint - #338
Conversation
Add an authenticated GET /capabilities endpoint that reports
machine-readable host capabilities so clients can gate behavior without
selecting or naming a concrete hypervisor:
- server build version and API contract version (from the embedded spec)
- host OS/architecture
- effective default-runtime features (snapshot, standby, pause, memory
hotplug, balloon, vsock, GPU passthrough, disk I/O limit, disk resize)
- guest networking model (bridge/NAT), guest-visible host gateway and
subnet, and guest-to-guest reachability
- supported image platforms (including Rosetta-emulated linux/amd64 on
Apple Silicon macOS) and the default platform
- stable feature IDs derived from the effective default runtime
Runtime-derived capabilities no longer overstate macOS support: vz
snapshot/standby is now gated on the probed macOS version (Apple Silicon
+ macOS 14+) instead of a static arm64 check, and a failed probe reports
no support rather than guessing.
Make diagnostics secret-safe:
- GET /instances and GET /instances/{id} redact env values by default
(keys preserved, values replaced with "[redacted]"); include_env=true
opts back into plaintext. Env updates that round-trip the sentinel
never overwrite real values.
- Instance metadata.json, guest config disks, and build config/metadata
files are written 0600; legacy 0644 instance metadata is tightened at
startup; overly-permissive server config files trigger a startup
warning.
Health and resources endpoints are unchanged.
✱ Stainless preview builds for hypemanThis PR will update the Edit this comment to update it. It will appear in the SDK's changelogs. ✅ hypeman-openapi studio · code · diff
✅ hypeman-typescript studio · code · diff
✅ hypeman-go studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Build config perms not enforced
- writeBuildConfig now explicitly chmods config.json to 0600 after every write so rewrites of legacy 0644 files tighten permissions.
- ✅ Fixed: Capabilities ignore platform support
- GetCapabilities now zeros runtime-derived booleans/features when the default runtime is not in the host-supported runtime list, preventing unsupported runtime overreporting.
Or push these changes by commenting:
@cursor push 2ff8424299
Preview (2ff8424299)
diff --git a/cmd/api/api/capabilities.go b/cmd/api/api/capabilities.go
--- a/cmd/api/api/capabilities.go
+++ b/cmd/api/api/capabilities.go
@@ -55,12 +55,14 @@
if s.InstanceManager != nil {
defaultRuntime = s.InstanceManager.DefaultHypervisor()
}
- caps, capsKnown := hypervisor.CapabilitiesForType(defaultRuntime)
+ supported := supportedRuntimes(runtime.GOOS)
+ caps, capsKnown := capabilitiesForDefaultRuntime(defaultRuntime, supported)
if !capsKnown {
- // The configured default runtime is not available on this platform;
+ // The configured default runtime is not usable on this host;
// report zeroed features rather than guessing.
- log.WarnContext(ctx, "default runtime has no registered capabilities on this host",
- "runtime", string(defaultRuntime))
+ log.WarnContext(ctx, "default runtime has no usable capabilities on this host",
+ "runtime", string(defaultRuntime),
+ "supported", supported)
}
emulation := emulationSupported(runtime.GOOS, runtime.GOARCH, defaultRuntime)
@@ -85,7 +87,7 @@
},
Runtime: oapi.CapabilitiesRuntime{
Default: string(defaultRuntime),
- Supported: supportedRuntimes(runtime.GOOS),
+ Supported: supported,
Snapshot: caps.SupportsSnapshot,
Standby: standbySupported(caps),
Pause: caps.SupportsPause,
@@ -157,6 +159,23 @@
}
}
+func capabilitiesForDefaultRuntime(defaultRuntime hypervisor.Type, supported []string) (hypervisor.Capabilities, bool) {
+ if !runtimeSupported(defaultRuntime, supported) {
+ return hypervisor.Capabilities{}, false
+ }
+ return hypervisor.CapabilitiesForType(defaultRuntime)
+}
+
+func runtimeSupported(defaultRuntime hypervisor.Type, supported []string) bool {
+ defaultRuntimeName := string(defaultRuntime)
+ for _, runtimeName := range supported {
+ if runtimeName == defaultRuntimeName {
+ return true
+ }
+ }
+ return false
+}
+
// emulationSupported reports whether the host can boot images built for the
// other CPU architecture. This mirrors the create-path rule for attaching
// the Rosetta share: vz on Apple Silicon macOS.
diff --git a/cmd/api/api/capabilities_test.go b/cmd/api/api/capabilities_test.go
--- a/cmd/api/api/capabilities_test.go
+++ b/cmd/api/api/capabilities_test.go
@@ -111,6 +111,19 @@
require.Equal(t, []string{"vz"}, supportedRuntimes("darwin"))
}
+func TestRuntimeSupported(t *testing.T) {
+ t.Parallel()
+ require.True(t, runtimeSupported(hypervisor.TypeVZ, supportedRuntimes("darwin")))
+ require.False(t, runtimeSupported(hypervisor.TypeCloudHypervisor, supportedRuntimes("darwin")))
+}
+
+func TestCapabilitiesForDefaultRuntime_IgnoresUnsupportedRuntime(t *testing.T) {
+ t.Parallel()
+ caps, ok := capabilitiesForDefaultRuntime(hypervisor.TypeCloudHypervisor, supportedRuntimes("darwin"))
+ require.False(t, ok)
+ require.Equal(t, hypervisor.Capabilities{}, caps)
+}
+
func TestEmulationSupported(t *testing.T) {
t.Parallel()
require.True(t, emulationSupported("darwin", "arm64", hypervisor.TypeVZ))
diff --git a/lib/builds/storage.go b/lib/builds/storage.go
--- a/lib/builds/storage.go
+++ b/lib/builds/storage.go
@@ -237,6 +237,9 @@
if err := os.WriteFile(configPath, data, 0600); err != nil {
return fmt.Errorf("write build config: %w", err)
}
+ if err := os.Chmod(configPath, 0600); err != nil {
+ return fmt.Errorf("chmod build config: %w", err)
+ }
return nil
}
diff --git a/lib/builds/storage_test.go b/lib/builds/storage_test.go
--- a/lib/builds/storage_test.go
+++ b/lib/builds/storage_test.go
@@ -36,3 +36,34 @@
loaded.Tags["team"] = "mutated"
require.Equal(t, "backend", build.Tags["team"])
}
+
+func TestWriteBuildConfig_UsesOwnerOnlyPermissions(t *testing.T) {
+ tempDir := t.TempDir()
+ p := paths.New(tempDir)
+ id := "build-config-1"
+
+ cfg := &BuildConfig{RegistryToken: "secret-token"}
+ require.NoError(t, writeBuildConfig(p, id, cfg))
+
+ info, err := os.Stat(p.BuildConfig(id))
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0600), info.Mode().Perm())
+}
+
+func TestWriteBuildConfig_TightensLegacyPermissions(t *testing.T) {
+ tempDir := t.TempDir()
+ p := paths.New(tempDir)
+ id := "build-config-legacy"
+
+ require.NoError(t, os.MkdirAll(p.BuildDir(id), 0755))
+ configPath := p.BuildConfig(id)
+ require.NoError(t, os.WriteFile(configPath, []byte(`{"registry_token":"old-token"}`), 0644))
+ require.NoError(t, os.Chmod(configPath, 0644))
+
+ cfg := &BuildConfig{RegistryToken: "new-token"}
+ require.NoError(t, writeBuildConfig(p, id, cfg))
+
+ info, err := os.Stat(configPath)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0600), info.Mode().Perm())
+}You can send follow-ups to the cloud agent here.
If the configured default runtime is not usable on the host platform (e.g. cloud-hypervisor configured on macOS), report zeroed capability booleans and base-only feature IDs instead of advertising a runtime that cannot launch there. Also make the world-readable config file test robust to a JWT_SECRET environment override in CI.
os.WriteFile does not change permissions of an existing file, so token refresh rewrites of build config.json (which carries the registry push token) would leave legacy 0644 files world-readable. Chmod after every write and tighten legacy build config/metadata files at manager startup. Addresses Cursor Bugbot feedback on PR #338.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Sentinel env still counts as update
- The update path now strips redaction sentinels from
req.Envbefore validation and control-flow checks, and a regression test confirms sentinel-only env maps no longer trigger env-update restrictions on stopped instances.
- The update path now strips redaction sentinels from
Or push these changes by commenting:
@cursor push 40e329ae56
Preview (40e329ae56)
diff --git a/lib/instances/update.go b/lib/instances/update.go
--- a/lib/instances/update.go
+++ b/lib/instances/update.go
@@ -49,6 +49,7 @@
}
req.RestartPolicy = normalizedRestartPolicy
}
+ req.Env = mergeEnvUpdate(nil, req.Env)
if err := validateUpdateInstanceRequest(meta, req); err != nil {
return nil, err
diff --git a/lib/instances/update_test.go b/lib/instances/update_test.go
--- a/lib/instances/update_test.go
+++ b/lib/instances/update_test.go
@@ -9,6 +9,7 @@
"github.com/kernel/hypeman/lib/autostandby"
"github.com/kernel/hypeman/lib/egressproxy"
"github.com/kernel/hypeman/lib/healthcheck"
+ "github.com/kernel/hypeman/lib/redact"
snapshotstore "github.com/kernel/hypeman/lib/snapshot"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -401,3 +402,56 @@
t.Fatal("timed out waiting for lifecycle update event")
}
}
+
+func TestManagerUpdateInstanceIgnoresSentinelOnlyEnvUpdateOnStoppedInstance(t *testing.T) {
+ t.Parallel()
+
+ manager, _ := setupTestManager(t)
+ id := "inst-update-sentinel-noop"
+ require.NoError(t, manager.ensureDirectories(id))
+ meta := &metadata{
+ StoredMetadata: StoredMetadata{
+ Id: id,
+ Name: id,
+ CreatedAt: time.Now(),
+ DataDir: manager.paths.InstanceDir(id),
+ SocketPath: manager.paths.InstanceSocket(id, "cloud-hypervisor.sock"),
+ NetworkEgress: &NetworkEgressPolicy{
+ Enabled: true,
+ },
+ Credentials: map[string]CredentialPolicy{
+ "OUTBOUND_OPENAI_KEY": {
+ Source: CredentialSource{Env: "OUTBOUND_OPENAI_KEY"},
+ },
+ },
+ Env: map[string]string{
+ "OUTBOUND_OPENAI_KEY": "real-secret",
+ },
+ AutoStandby: &autostandby.Policy{
+ Enabled: false,
+ IdleTimeout: "5m0s",
+ },
+ },
+ }
+ require.NoError(t, manager.saveMetadata(meta))
+
+ updated, err := manager.UpdateInstance(context.Background(), id, UpdateInstanceRequest{
+ Env: map[string]string{
+ "OUTBOUND_OPENAI_KEY": redact.Sentinel,
+ },
+ AutoStandby: &autostandby.Policy{
+ Enabled: true,
+ IdleTimeout: "10m",
+ },
+ })
+ require.NoError(t, err)
+ require.NotNil(t, updated)
+ require.NotNil(t, updated.AutoStandby)
+ assert.True(t, updated.AutoStandby.Enabled)
+ assert.Equal(t, "10m0s", updated.AutoStandby.IdleTimeout)
+
+ saved, err := manager.loadMetadata(id)
+ require.NoError(t, err)
+ require.NotNil(t, saved)
+ assert.Equal(t, "real-secret", saved.Env["OUTBOUND_OPENAI_KEY"])
+}You can send follow-ups to the cloud agent here.
Strip redaction sentinels at the top of updateInstance so a read-modify-write that round-trips only redacted env values does not count as an env mutation: it no longer requires the instance to be running and no longer routes through the egress-proxy credential path. Addresses Cursor Bugbot feedback on PR #338.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Legacy config disks stay world-readable
- Added a startup permission sweep for existing
config.ext4files and wired it into manager initialization so legacy disks are tightened to 0600 immediately after upgrade.
- Added a startup permission sweep for existing
Or push these changes by commenting:
@cursor push cf40507f85
Preview (cf40507f85)
diff --git a/lib/instances/manager.go b/lib/instances/manager.go
--- a/lib/instances/manager.go
+++ b/lib/instances/manager.go
@@ -311,6 +311,9 @@
// Restrict permissions on metadata written by older versions (may be 0644
// and contains env values / credential bindings).
m.tightenMetadataPermissions()
+ // Restrict permissions on guest config disks written by older versions
+ // (may be 0644 and embed config.json with env values).
+ m.tightenConfigDiskPermissions()
return m, nil
}
diff --git a/lib/instances/metadata_permissions_test.go b/lib/instances/metadata_permissions_test.go
--- a/lib/instances/metadata_permissions_test.go
+++ b/lib/instances/metadata_permissions_test.go
@@ -86,6 +86,26 @@
"legacy 0644 metadata must be tightened to 0600 at startup")
}
+// TestManagerTightensLegacyConfigDiskPermissions proves the startup sweep
+// upgrades config disks written by older versions (mode 0644) to 0600.
+func TestManagerTightensLegacyConfigDiskPermissions(t *testing.T) {
+ t.Parallel()
+ dataDir := t.TempDir()
+ p := paths.New(dataDir)
+ id := "inst-config-disk-legacy"
+
+ // Simulate a legacy config disk written with world-readable permissions.
+ require.NoError(t, os.MkdirAll(p.InstanceDir(id), 0755))
+ require.NoError(t, os.WriteFile(p.InstanceConfigDisk(id), []byte("ext4-bytes-placeholder"), 0644))
+
+ newPermTestManager(t, dataDir)
+
+ info, err := os.Stat(p.InstanceConfigDisk(id))
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0600), info.Mode().Perm(),
+ "legacy 0644 config disks must be tightened to 0600 at startup")
+}
+
// TestMergeEnvUpdateSkipsRedactionSentinel proves a redacted read response
// round-tripped into an env update cannot clobber real secret values.
func TestMergeEnvUpdateSkipsRedactionSentinel(t *testing.T) {
diff --git a/lib/instances/storage.go b/lib/instances/storage.go
--- a/lib/instances/storage.go
+++ b/lib/instances/storage.go
@@ -154,6 +154,33 @@
}
}
+// tightenConfigDiskPermissions restricts existing guest config disk files to
+// owner-only access. Disks written before restrictive permissions were
+// introduced may be mode 0644; they embed config.json with environment values.
+// Best-effort: individual failures are logged, not fatal.
+func (m *manager) tightenConfigDiskPermissions() {
+ log := logger.FromContext(context.Background())
+ entries, err := os.ReadDir(m.paths.GuestsDir())
+ if err != nil {
+ return // no guests directory yet
+ }
+ for _, entry := range entries {
+ if !entry.IsDir() {
+ continue
+ }
+ configDiskPath := m.paths.InstanceConfigDisk(entry.Name())
+ info, err := os.Stat(configDiskPath)
+ if err != nil {
+ continue
+ }
+ if info.Mode().Perm() != 0600 {
+ if err := os.Chmod(configDiskPath, 0600); err != nil {
+ log.Warn("failed to tighten instance config disk permissions", "path", configDiskPath, "error", err)
+ }
+ }
+ }
+}
+
// createOverlayDisk creates a sparse overlay disk for the instance
func (m *manager) createOverlayDisk(id string, sizeBytes int64) error {
overlayPath := m.paths.InstanceOverlay(id)You can send follow-ups to the cloud agent here.
Legacy config.ext4 files embed env values and could remain 0644 after upgrade until the instance is recreated. Extend the startup permission sweep to cover them alongside metadata.json. Addresses Cursor Bugbot feedback on PR #338.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Wrong macOS capabilities gateway
- DefaultNetwork now always resolves live NAT network state on macOS so /capabilities reports the guest-visible gateway and subnet instead of cached Linux config values.
Or push these changes by commenting:
@cursor push 511c951880
Preview (511c951880)
diff --git a/lib/network/default_network_test.go b/lib/network/default_network_test.go
--- a/lib/network/default_network_test.go
+++ b/lib/network/default_network_test.go
@@ -9,10 +9,9 @@
"github.com/stretchr/testify/require"
)
-// TestDefaultNetworkPrefersInitializedNetwork proves DefaultNetwork returns
-// the effective default network established at Initialize time, including the
-// guest-visible gateway, without touching host kernel state.
-func TestDefaultNetworkPrefersInitializedNetwork(t *testing.T) {
+// TestDefaultNetworkReturnsGuestVisibleValues proves DefaultNetwork reports
+// guest-visible gateway/subnet details for the active host networking model.
+func TestDefaultNetworkReturnsGuestVisibleValues(t *testing.T) {
t.Parallel()
cfg := &config.Config{}
m := NewManager(paths.New(t.TempDir()), cfg, nil).(*manager)
@@ -28,14 +27,23 @@
nw, err := m.DefaultNetwork(context.Background())
require.NoError(t, err)
- require.Equal(t, "10.100.0.1", nw.Gateway)
- require.Equal(t, "10.100.0.0/16", nw.Subnet)
+ if NetworkModel() == "nat" {
+ require.Equal(t, "192.168.64.1", nw.Gateway)
+ require.Equal(t, "192.168.64.0/24", nw.Subnet)
+ } else {
+ require.Equal(t, "10.100.0.1", nw.Gateway)
+ require.Equal(t, "10.100.0.0/16", nw.Subnet)
+ }
// Mutating the returned copy must not affect the cached network.
nw.Gateway = " mutated "
again, err := m.DefaultNetwork(context.Background())
require.NoError(t, err)
- require.Equal(t, "10.100.0.1", again.Gateway)
+ if NetworkModel() == "nat" {
+ require.Equal(t, "192.168.64.1", again.Gateway)
+ } else {
+ require.Equal(t, "10.100.0.1", again.Gateway)
+ }
}
func TestGuestToGuestEnabled(t *testing.T) {
diff --git a/lib/network/manager.go b/lib/network/manager.go
--- a/lib/network/manager.go
+++ b/lib/network/manager.go
@@ -167,10 +167,13 @@
m.defaultNetwork = cloneNetwork(network)
}
-// DefaultNetwork returns the effective default network. It prefers the
-// network established during Initialize and falls back to querying live
-// host state (kernel bridge on Linux, the vz NAT stub on macOS).
+// DefaultNetwork returns the effective default network. Linux hosts prefer
+// the network established during Initialize, while macOS hosts always query
+// live NAT state because config subnet/gateway settings are not guest-visible.
func (m *manager) DefaultNetwork(ctx context.Context) (*Network, error) {
+ if NetworkModel() == "nat" {
+ return m.getDefaultNetwork(ctx)
+ }
if network := m.cachedDefaultNetwork(); network != nil {
return network, nil
}You can send follow-ups to the cloud agent here.
On macOS the configured subnet/gateway are ignored (guests use vz NAT), so the Initialize-cached network is never guest-visible. DefaultNetwork now prefers live host state per platform: the config-mirroring cache on Linux, the vz NAT stub (192.168.64.1, 192.168.64.0/24) on macOS. This is reporting-only; allocation behavior is unchanged. Addresses Cursor Bugbot feedback on PR #338.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Build metadata perms not forced
- Added an explicit
os.Chmod(tempPath, 0600)inwriteMetadataso pre-existing temp files cannot retain broader permissions before rename.
- Added an explicit
Or push these changes by commenting:
@cursor push 7afceaf739
Preview (7afceaf739)
diff --git a/lib/builds/storage.go b/lib/builds/storage.go
--- a/lib/builds/storage.go
+++ b/lib/builds/storage.go
@@ -64,6 +64,10 @@
if err := os.WriteFile(tempPath, data, 0600); err != nil {
return fmt.Errorf("write temp metadata: %w", err)
}
+ // WriteFile does not change permissions of an existing file.
+ if err := os.Chmod(tempPath, 0600); err != nil {
+ return fmt.Errorf("chmod temp metadata: %w", err)
+ }
finalPath := p.BuildMetadata(meta.ID)
if err := os.Rename(tempPath, finalPath); err != nil {You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 0f6f74e. Configure here.
A leftover 0644 temp file from an older write would otherwise keep its permissions through WriteFile and be renamed into place world-readable. Addresses Cursor Bugbot feedback on PR #338.
Independent review — round 1 (head
|
Remove the secret-storage and diagnostics hardening from this change so the
PR contains only the authenticated GET /capabilities endpoint and what it
needs to be truthful:
- Drop include_env query params and default instance-response env redaction
- Drop lib/redact and redaction-sentinel PATCH semantics
- Drop metadata/config-disk/build permission tightening and startup sweeps
- Drop server-config permission warnings and access-log canary tests
GET /instances and GET /instances/{id} response behavior and generated SDK
signatures are byte-identical to main. The hardening work is split out into
a follow-up ticket.
Independent narrowing review — round 1 (head
|


What
Adds an authenticated, machine-readable capability contract so clients (e.g. deployment tooling) can discover what a Hypeman host can do without selecting or naming a concrete hypervisor.
GET /capabilities(bearer-auth,resource:readscope)Reports:
bridge/nat), guest-visible host gateway, subnet, and guest-to-guest reachability. On macOS the vz NAT stub (guest gateway192.168.64.1) is authoritative; the configured subnet/gateway is not guest-visible there.linux/amd64on Apple Silicon macOS) and the default platformIf the configured default runtime cannot run on the host platform, runtime features are reported zeroed rather than overstated.
Runtime-derived macOS capabilities
vz snapshot/standby support was a static
runtime.GOARCH == "arm64"check, which overstates support on macOS 13 (VZ save/restore requires macOS 14+). It is now probed at runtime (kern.osproductversion), and a failed probe reports no support rather than guessing. This gates both the new endpoint and the existing standby code path, which reads the sameCapabilities().Backward compatibility
GET /instances,GET /instances/{id},/health, and/resourcesresponse behavior and generated SDK signatures are unchanged relative tomain. Theopenapi.yamldiff is purely additive: the new/capabilitiespath andCapabilities*schemas.Split out for later (not in this PR)
Secret-storage and diagnostics hardening originally drafted here was removed and tabled as a follow-up ticket:
include_envquery params and default env redaction in instance responses,lib/redactand redaction-sentinel PATCH semantics, owner-only permissions for instance metadata / guest config disks / build config + legacy startup sweeps, server-config permission warnings, and access-log redaction canary tests. This PR intentionally makes no permission or redaction changes.Acceptance criteria mapping
Tests
New coverage:
cmd/api/api/capabilities_test.go— handler (version serialization, host identity, effective defaults, network model/gateway, feature IDs), plus pure Linux/macOS boundary tests for supported runtimes, emulation, image platforms, standby semanticslib/hypervisor/vz/save_restore_support_test.go— macOS 13/14/15/26 boundary matrix (runs on any GOOS); darwin-only test proves advertised capabilities track the probed OS versionlib/network/default_network_test.go— effective gateway per networking model, guest-to-guest rulesRan locally (Linux/KVM):
go build ./...,go vet ./...,gofmtclean; fullgo test ./...— all packages pass except 6 pre-existing snapshot/fork integration tests inlib/instancesthat fail identically onmainon this host (environmental), plus one env-dependentTestBuildEnvcase that also fails onmainunder a foreignTERMand passes with a clean env.GOOS=darwin GOARCH=arm64 go build ./cmd/api/api/ ./lib/network/ ./lib/instances/ ./lib/scopes/cross-compiles clean.Risks
GetCapabilitiestypes/client method — Stainless SDKs regenerate fromopenapi.yaml.vzsysctl probe,model_darwin.go) can't be run on this Linux dev box; the probe logic is platform-neutral and unit-tested on Linux, and the darwin packages cross-compile clean.Review
Please run Cursor Bugbot review on this PR. (@cursor)
Note
Low Risk
Mostly additive API and capability reporting; the vz snapshot change narrows advertised support on older macOS but aligns with actual Virtualization.framework behavior.
Overview
Adds an authenticated
GET /capabilitiesendpoint so clients can discover what a host supports without picking a hypervisor by name.The handler reports server/API versions, host OS/arch, effective default runtime booleans and a platform
supportedruntime list, guest networking (model, gateway, subnet, guest-to-guest), image platforms (including Rosetta when applicable), and a stablefeaturesID list. If the configured default runtime cannot run on the host (e.g. cloud-hypervisor on macOS), runtime flags are zeroed instead of overstated.Networking:
NetworkManager.DefaultNetworkreturns the guest-visible default network—Linux uses the initialized bridge cache; macOS uses live vz NAT (192.168.64.1) rather than config-only subnet/gateway. Platform helpers addNetworkModel,GuestToGuestEnabled, and bridge vs NAT behavior.macOS vz: Snapshot/standby is no longer inferred from arm64 alone; it requires darwin/arm64 and macOS 14+ via
kern.osproductversion, with failed probes reporting no snapshot support.Plumbing:
instances.Manager.DefaultHypervisor()exposes the launch default; OpenAPI and generatedoapiclient include the new route;GET /capabilitiesmaps toresource:readscope.Reviewed by Cursor Bugbot for commit 328e8a7. Bugbot is set up for automated code reviews on this repo. Configure here.