Skip to content

Add authenticated host capabilities endpoint - #338

Open
rgarcia wants to merge 9 commits into
mainfrom
oss/host-capabilities-safe-diagnostics
Open

Add authenticated host capabilities endpoint#338
rgarcia wants to merge 9 commits into
mainfrom
oss/host-capabilities-safe-diagnostics

Conversation

@rgarcia

@rgarcia rgarcia commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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:read scope)

Reports:

  • server: build version (git revision) and API contract version (from the embedded OpenAPI document)
  • host: OS and architecture
  • runtime: effective default runtime identifier, platform-supported runtime list, and boolean features (snapshot, standby, pause, hotplug memory, balloon control, vsock, GPU passthrough, disk I/O limit, disk resize)
  • network: model (bridge/nat), guest-visible host gateway, subnet, and guest-to-guest reachability. On macOS the vz NAT stub (guest gateway 192.168.64.1) is authoritative; the configured subnet/gateway is not guest-visible there.
  • images: runnable image platforms (incl. Rosetta-emulated linux/amd64 on Apple Silicon macOS) and the default platform
  • features: stable feature IDs derived from the effective default runtime and host platform

If 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 same Capabilities().

Backward compatibility

GET /instances, GET /instances/{id}, /health, and /resources response behavior and generated SDK signatures are unchanged relative to main. The openapi.yaml diff is purely additive: the new /capabilities path and Capabilities* 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_env query params and default env redaction in instance responses, lib/redact and 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

  • Authenticated machine-readable capability endpoint: version, host OS/arch, effective default-runtime features, network model/gateway, image platforms, stable feature IDs
  • Runtime-derived capabilities do not overstate macOS snapshot/standby support
  • Existing health, resource, and instance API contracts remain backward compatible
  • Tests cover Linux/macOS capability boundaries, effective defaults, version serialization, and gateway reporting
  • Generic to Hypeman — no downstream-product logic, no installer work

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 semantics
  • lib/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 version
  • lib/network/default_network_test.go — effective gateway per networking model, guest-to-guest rules

Ran locally (Linux/KVM): go build ./..., go vet ./..., gofmt clean; full go test ./... — all packages pass except 6 pre-existing snapshot/fork integration tests in lib/instances that fail identically on main on this host (environmental), plus one env-dependent TestBuildEnv case that also fails on main under a foreign TERM and passes with a clean env. GOOS=darwin GOARCH=arm64 go build ./cmd/api/api/ ./lib/network/ ./lib/instances/ ./lib/scopes/ cross-compiles clean.

Risks

  • Additive endpoint only; no existing behavior changes.
  • The generated Go client gains GetCapabilities types/client method — Stainless SDKs regenerate from openapi.yaml.
  • macOS-only code paths (vz sysctl 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.
  • vz capabilities are computed once at process start (init registration), same as before; the OS version floor does not change at runtime.

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 /capabilities endpoint 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 supported runtime list, guest networking (model, gateway, subnet, guest-to-guest), image platforms (including Rosetta when applicable), and a stable features ID 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.DefaultNetwork returns 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 add NetworkModel, 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 generated oapi client include the new route; GET /capabilities maps to resource:read scope.

Reviewed by Cursor Bugbot for commit 328e8a7. Bugbot is set up for automated code reviews on this repo. Configure here.

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.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

✱ Stainless preview builds for hypeman

This PR will update the hypeman SDKs with the following commit message.

feat: Add host capabilities endpoint and secret-safe diagnostics

Edit this comment to update it. It will appear in the SDK's changelogs.

hypeman-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /capabilities`
hypeman-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ❗) → test ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /capabilities`
hypeman-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ✅test ✅

go get github.com/stainless-sdks/hypeman-go@1a7b83c08d26cea2ee9e97a19c959de911ebb196
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /capabilities`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-08-04 01:21:02 UTC

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 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.

Create PR

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.

Comment thread lib/builds/storage.go Outdated
Comment thread cmd/api/api/capabilities.go
rgarcia added 2 commits August 3, 2026 13:31
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 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.Env before validation and control-flow checks, and a regression test confirms sentinel-only env maps no longer trigger env-update restrictions on stopped instances.

Create PR

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.

Comment thread lib/instances/update.go
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 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.ext4 files and wired it into manager initialization so legacy disks are tightened to 0600 immediately after upgrade.

Create PR

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.

Comment thread lib/instances/configdisk.go Outdated
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 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.

Create PR

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.

Comment thread lib/network/manager.go
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using high effort and found 1 potential issue.

Fix All in Cursor

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) in writeMetadata so pre-existing temp files cannot retain broader permissions before rename.

Create PR

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.

Comment thread lib/builds/storage.go Outdated
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.
@rgarcia

rgarcia commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Independent review — round 1 (head 63ca479)

Verdict: satisfied — no blocking or important findings.

Checked against ticket 02 (capabilities endpoint + safe diagnostics) and the planning spec:

  • GET /capabilities is bearer-authenticated (security: bearerAuth in the spec, resource:read scope in lib/scopes), reports server/API version, host OS/arch, effective default-runtime feature booleans, network model + guest-visible gateway/subnet, image platforms (incl. Rosetta emulation), and stable feature IDs — no hypervisor selection required by clients.
  • ✅ macOS snapshot/standby is now runtime-derived (kern.osproductversion, macOS 14+ floor, failed probe ⇒ unsupported), replacing the arch-only check; boundary matrix tests run on any GOOS and a darwin-only test tracks the probed version. capabilities() feeds both the endpoint and the existing standby path.
  • /health (still unauthenticated) and /resources are unchanged and pinned by backward-compat tests.
  • ✅ Env redaction on list/get with include_env=true opt-in, sentinel-safe read-modify-write (withoutRedactionSentinels strips sentinels before validation and merge), Authorization-header canary test on the access logger.
  • ✅ 0600 on instance metadata, config disks, build config/metadata, with startup sweeps for legacy 0644 files; operator config files only warn. Bugbot's five earlier findings (build-config chmod, unusable-default-runtime overstatement, sentinel control-flow, legacy config-disk sweep, macOS NAT gateway, build-metadata temp chmod) were each fixed in follow-up commits and all threads are resolved; its check passed on the latest head.
  • ✅ CI green (test, test-darwin, e2e-install, semgrep, Stainless preview). Change is generic to Hypeman — no downstream-product logic or installer work.

Minor, non-blocking nits (no action required for this round):

  1. Lifecycle mutation responses (create/start/stop/standby/restore/fork/update) still echo plaintext env; only list/get redact. Since include_env=true needs no extra scope this isn't a privilege boundary, but redacting there too (with the same opt-in) would make the default uniform.
  2. An env-only PATCH whose values are all sentinels now returns 400 ("request must include env, …") rather than a no-op. That matches empty-PATCH behavior, but a naive redacted read-modify-write client will see an error; worth a note in the API docs if it bites anyone.

Structured verdict for the workflow: approved.

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.
@rgarcia rgarcia changed the title Add host capabilities endpoint and secret-safe diagnostics Add authenticated host capabilities endpoint Aug 3, 2026
@rgarcia

rgarcia commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Independent narrowing review — round 1 (head 7632089)

Verdict: approved — scope is narrow, no blocking or important findings.

Scope verification (origin/main..7632089, 18 files):

  • ✅ The final diff contains only the capability endpoint and its truthful prerequisites: GET /capabilities (handler, scope mapping, OpenAPI schema + regenerated lib/oapi), runtime-derived vz snapshot/standby gating (kern.osproductversion probe, macOS 14+ floor, failed probe → unsupported), and guest-visible network reporting (DefaultNetwork with preferCachedDefaultNetwork() so the macOS vz NAT stub is authoritative).
  • ✅ Grepped the full diff for include_env, redaction, sentinels, canaries, chmod/permission changes — none present. All tabled hardening (ticket 05) is fully removed: lib/builds/storage.go, lib/instances/update.go, lib/instances/configdisk.go, lib/redact no longer appear in the diff.
  • GET /instances / GET /instances/{id} untouched; the lib/oapi/oapi.go diff has zero non-generated deletions and no instance-type changes — purely additive Capabilities* types + re-encoded embedded spec. Backward-compat tests pin /health and /resources.
  • ✅ Interface additions (instances.Manager.DefaultHypervisor, network.Manager.DefaultNetwork) are minimal accessors; test doubles updated accordingly.

Correctness spot-checks:

  • supportedRuntimes("darwin") == ["vz"] is correct as a runnability floor: qemu/cloud-hypervisor are registered in darwin platformStarters but hypeman's qemu config is accel=kvm-only, so listing only vz avoids overstating.
  • Zeroing runtime features when the configured default runtime can't run on the host (with a warning log) is the right "don't overstate" behavior; standby correctly requires snapshot and pause, and this now also gates the existing standby path via the shared Capabilities().
  • DefaultNetwork returns a clone (mutation-safe, verified by test); Linux falls back to live bridge state when the cache is unset; macOS always queries the NAT stub (192.168.64.1), fixing the earlier Bugbot-flagged wrong-gateway issue.
  • vz version-parse boundary matrix (13/14/14.x/15/26, empty, unparsable) runs on any GOOS; darwin-only test proves advertised caps track the stubbed probe.

CI / Bugbot at head 7632089: test, test-darwin, e2e-install, preview, scan, Socket — all pass. Cursor Bugbot: completed, no issues found. All six earlier Bugbot findings targeted code removed by the narrowing or were fixed (platform-support gating, macOS gateway) and do not apply to the final diff.

Minor (non-blocking):

  • The Stainless preview comment's SDK changelog message still reads "Add host capabilities endpoint and secret-safe diagnostics" — stale from the pre-narrowed scope. Worth editing that comment before merge so SDK changelogs don't advertise removed functionality.

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