Add authenticated host capabilities endpoint - #338
Conversation
✱ 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.
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.
Autofix Details
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.
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
|
Independent narrowing review — round 1 (head
|
328e8a7 to
a4892cd
Compare
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: Missing Stainless SDK mapping
- Added a new
capabilitiesresource instainless.yamlwithget /capabilitiesplusCapabilities*model mappings so SDK generation includes this endpoint and types.
- Added a new
- ✅ Fixed: Rosetta gated on default runtime
- Changed emulation detection to require Apple Silicon macOS with
vzin the supported runtime list rather than requiringvzas the configured default runtime, and updated tests accordingly.
- Changed emulation detection to require Apple Silicon macOS with
Or push these changes by commenting:
@cursor push f2fd2aa197
Preview (f2fd2aa197)
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
@@ -70,7 +70,7 @@
caps = hypervisor.Capabilities{}
}
- emulation := emulationSupported(runtime.GOOS, runtime.GOARCH, defaultRuntime)
+ emulation := emulationSupported(runtime.GOOS, runtime.GOARCH, supported)
networkCaps, err := s.networkCapabilities(ctx)
if err != nil {
@@ -167,8 +167,10 @@
// 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.
-func emulationSupported(goos, goarch string, defaultRuntime hypervisor.Type) bool {
- return defaultRuntime == hypervisor.TypeVZ && goos == "darwin" && goarch == "arm64"
+func emulationSupported(goos, goarch string, supported []string) bool {
+ return goos == "darwin" &&
+ goarch == "arm64" &&
+ slices.Contains(supported, string(hypervisor.TypeVZ))
}
// imagePlatforms returns the image platforms (os/arch) the host can run:
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
@@ -122,11 +122,11 @@
func TestEmulationSupported(t *testing.T) {
t.Parallel()
- require.True(t, emulationSupported("darwin", "arm64", hypervisor.TypeVZ))
- require.False(t, emulationSupported("darwin", "amd64", hypervisor.TypeVZ))
- require.False(t, emulationSupported("darwin", "arm64", hypervisor.TypeCloudHypervisor))
- require.False(t, emulationSupported("linux", "arm64", hypervisor.TypeVZ))
- require.False(t, emulationSupported("linux", "amd64", hypervisor.TypeCloudHypervisor))
+ require.True(t, emulationSupported("darwin", "arm64", []string{"vz"}))
+ require.False(t, emulationSupported("darwin", "arm64", []string{"cloud-hypervisor"}))
+ require.False(t, emulationSupported("darwin", "amd64", []string{"vz"}))
+ require.False(t, emulationSupported("linux", "arm64", []string{"vz"}))
+ require.False(t, emulationSupported("linux", "amd64", []string{"cloud-hypervisor"}))
}
func TestImagePlatforms(t *testing.T) {
diff --git a/stainless.yaml b/stainless.yaml
--- a/stainless.yaml
+++ b/stainless.yaml
@@ -65,6 +65,17 @@
methods:
check: get /health
+ capabilities:
+ models:
+ capabilities: "#/components/schemas/Capabilities"
+ capabilities_server: "#/components/schemas/CapabilitiesServer"
+ capabilities_host: "#/components/schemas/CapabilitiesHost"
+ capabilities_runtime: "#/components/schemas/CapabilitiesRuntime"
+ capabilities_network: "#/components/schemas/CapabilitiesNetwork"
+ capabilities_images: "#/components/schemas/CapabilitiesImages"
+ methods:
+ get: get /capabilities
+
images:
# Configure the models--named types--defined in the resource. Each key in the
# object is the name of the model and the value is either the name of a schema inYou can send follow-ups to the cloud agent here.
-->
✱ stlc build✅ go code · compare
✅ typescript code · compare
Diagnostics: 💡 0 new / 5 total note
Build metadata
This comment is auto-generated by stlc and is kept up to date as you push. |
a4892cd to
bd2c6e8
Compare
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: MicroVM omitted from supported runtimes
- Added
qemu-microvmto LinuxsupportedRuntimes(and updated the unit test expectation) so default runtime capability checks no longer incorrectly zero MicroVM features.
- Added
Or push these changes by commenting:
@cursor push c3e22ab24d
Preview (c3e22ab24d)
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
@@ -160,6 +160,7 @@
string(hypervisor.TypeCloudHypervisor),
string(hypervisor.TypeFirecracker),
string(hypervisor.TypeQEMU),
+ string(hypervisor.TypeQEMUMicroVM),
}
}
}
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
@@ -115,7 +115,7 @@
func TestSupportedRuntimes(t *testing.T) {
t.Parallel()
require.Equal(t,
- []string{"cloud-hypervisor", "firecracker", "qemu"},
+ []string{"cloud-hypervisor", "firecracker", "qemu", "qemu-microvm"},
supportedRuntimes("linux"))
require.Equal(t, []string{"vz"}, supportedRuntimes("darwin"))
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit bd2c6e8. Configure here.
sjmiller609
left a comment
There was a problem hiding this comment.
primary issue: capabilities reported for only one runtime, even though a host can support multiple at once
explore this: how to avoid always updating this code each time capabilities are changed or added?
each hypervisor already registers its
Capabilitiesstruct at init viaRegisterCapabilities. two fixes make the endpoint self-updating: (1) build-tag the linux hypervisor registrations (vz already is darwin-only), so the registry's keys are the host-supported runtime set — delete the hardcodedsupportedRuntimes()switch, and new runtimes appear automatically. (2) move feature-ID derivation onto the struct ascaps.FeatureIDs()inlib/hypervisor, next to the field definitions. adding a capability then touches one package: the struct field, its feature ID, and the openapi schema — the endpoint handler never changes.
nits from AI review
forkappears infeaturesbut has no boolean inruntime— the two surfaces are inconsistent.gatewayis schema-required but can be empty string when no default network resolves.- linux
GuestToGuestEnabledcomment says guests can't reach each other; code returns!n.Isolated. contradictory. imagePlatforms'sif goarch == ""fallback is dead defensive code — delete it.emulationSupported'ssupportedparam is always-true on darwin; simplify to goos/goarch check.NetworkModel()returns bare strings then casts to the oapi enum; return a typed constant.standbydescription says "on this host" while siblings say "default runtime" — align wording.
- Report every runtime available on the host (registry-driven), each with its own feature IDs; retain configured default identity + availability - Platform-gate backend capability registration (linux backends via build-tagged files, vz already darwin-only); qemu-microvm registers only where its board resolves, so it appears automatically on linux/amd64 - Add hypervisor.RegisteredRuntimes deterministic enumeration and Capabilities.FeatureIDs/SupportsStandby/SupportsFork beside the struct - One canonical per-runtime representation (feature IDs, no boolean twins) - Optional network gateway/subnet (never empty-string), typed network.Model contract, truthful guest-to-guest docs - Simplify emulation detection to host os/arch; drop dead imagePlatforms fallback; align OpenAPI descriptions; map CapabilitiesDefaultRuntime in stainless.yaml
|
Thanks Steven — redesigned in f8a1ca5 along the lines you sketched. Point-by-point mapping: Primary: capabilities for only one runtime. The endpoint now reports Self-updating, no per-capability handler edits. Exactly as you proposed:
Nits:
Boundary coverage: |
rgarcia
left a comment
There was a problem hiding this comment.
GPT-5.6 capabilities redesign review — round 1 (f8a1ca5)
Verdict: changes requested. The redesign does resolve Steven's primary shape concern: all registered runtimes are emitted deterministically, feature-ID derivation is beside Capabilities, the handler has no runtime list, the OpenAPI/Stainless/generated surfaces are coherent, and Steven's listed nits are addressed. Two important truthfulness gaps remain:
-
“Available” currently means “registered for this GOOS,” not “this host can launch it.”
qemu/register_linux.goregisters QEMU unconditionally, whilecmd/api/main.go:318-321explicitly permits startup without a QEMU binary and says that runtime will not work./capabilitiesstill includesqemu(andqemu-microvmon amd64), and a configured QEMU default reportsavailable: true; the tests pin that overstatement. Availability/default availability need to incorporate launch prerequisites (or expose a distinct supported-vs-available/readiness model) without reintroducing a handler list. -
The static init-time capability value is not always the effective runtime capability. Cloud Hypervisor registration snapshots
CapabilitiesForVersion(vmm.DefaultVersion)before config is applied, butcloud_hypervisor_default_versioncan select v49.0; the endpoint then advertisesdisk-resizefrom v51.1 even though ordinary v49 launches do not have it. The registry needs a config/runtime-aware capability provider (or equivalent effective resolution), not a value frozen at package init.
Also please make fork explicit in the capability contract rather than defining SupportsFork() as SupportsSnapshot. VMStarter.PrepareFork explicitly allows a snapshot-capable backend to return ErrNotSupported, so the current derivation can advertise fork when the actual fork gate rejects it. That reintroduces the exact boolean/ID drift this redesign is meant to prevent for the next backend.
Checks: complete 95db3fb...f8a1ca5 diff inspected; focused Linux tests + race checks pass; Darwin arm64 API/vz cross-compilation passes; make oapi-generate is drift-free; all real CI, Stainless generation, and current Bugbot pass with no unresolved Bugbot thread.
Address GPT-5.6 review round 1 (three important findings): 1. Runtime availability now reflects launch prerequisites, not just platform registration. The registry accepts a per-runtime LaunchCheck; QEMU (standard and microvm) probes the same system-binary lookup that launches use, since hypeman deliberately starts without it. The endpoint gains a required per-runtime "available" field (supported = listed; available = launchable now), and default_runtime.available uses the same verdict. 2. Capabilities are resolved per registry read instead of being frozen at package init. Cloud Hypervisor resolves from the configured default version (SetDefaultVersion), so pinning v49.0 no longer advertises the v51.1-only disk-resize feature. Covered by a v49/v51 boundary test. 3. Fork support is an explicit Capabilities.SupportsFork field instead of being inferred from SupportsSnapshot, keeping each backend's PrepareFork implementation and its advertised feature aligned. A snapshot-without-fork test pins that snapshots alone never advertise fork; vz gates fork on the same save/restore probe as snapshots. Regenerated lib/oapi via make oapi-generate.
Response to GPT-5.6 capabilities review — round 1 (fixes in
|
GPT-5.6 capabilities redesign review — round 2 (
|
… override, restore public API compatibility - Rosetta emulation reporting now uses the same Virtualization.framework availability probe the vz-shim enforces at launch (LinuxRosettaDirectoryShareAvailability): hosts without Rosetta installed or on macOS < 13 no longer advertise the linux/amd64 image platform or the rosetta-emulation feature. cmd/api already links the framework on macOS, so this adds no build or runtime requirement; off darwin/arm64 the probe is a constant false. - Firecracker registers a LaunchCheck validating the active hypervisor.firecracker_binary_path override with the same executable check the launch path applies, since the override takes precedence over the embedded binaries on every launch. Regression test covers no-override, missing, non-executable, and valid overrides, including per-read re-evaluation. - Public Go source compatibility restored: hypervisor.RegisterCapabilities is back as a deprecated wrapper over RegisterRuntime, and instances.Manager no longer gains DefaultHypervisor \u2014 the capabilities handler type-asserts a narrow defaultHypervisorProvider accessor instead, falling back to the same cloud-hypervisor default lib/instances applies. Reverted the mock widening in lib/builds and lib/instances tests.
Round-2 findings adjudication (fable-repair-pr338-r2)All three round-2 findings were adjudicated as valid and are addressed in c7ed2fe. 1. Rosetta image capability overstated current host availability — valid, fixed. 2. Firecracker availability ignored the configured binary override — valid, fixed. 3. Public Go source incompatibilities — valid, fixed.
Verification: |
GPT-5.6 capabilities redesign review — round 3 (
|
…nch prerequisites Round-3 finding 1: a valid manager wrapper embedding instances.Manager hides the concrete manager's DefaultHypervisor method, so the handler's hardcoded cloud-hypervisor fallback could misreport a configured Firecracker/QEMU default while launches still used it. The fallback is now seeded from config.Hypervisor.Default (the value the wrapped manager was constructed from), normalizing only an empty value to cloud-hypervisor — mirroring lib/instances.NewManagerWithConfigE — with the optional accessor still authoritative when exposed. The opaque-manager test now pins a configured non-default runtime, plus unconfigured normalization and accessor precedence. Round-3 finding 2: QEMU's LaunchCheck only called GetBinaryPath, whose fixed /usr/bin and /usr/local/bin candidates are accepted on a bare os.Stat even if non-executable or broken, and it never verified the host vhost-vsock device — required because every created instance gets a nonzero vsock CID and buildArgs unconditionally attaches a vhost-vsock device (API startup validates KVM only). checkLaunchPrerequisites now validates the resolved binary's execute bit, runs the same --version probe ResolveVersion persists on every cold start, and stats /dev/vhost-vsock; all evaluated per registry read and side-effect-free. Unavailable cases (missing/non-executable/broken/ unparseable binary, missing device) are covered with fake binaries and device paths, and the availability test tracks the full prerequisite check. OpenAPI description updated and regenerated via make oapi-generate.
Round-3 findings adjudication (fable-repair-pr338-r3)Both round-3 findings were adjudicated as valid and are addressed in 610529c. 1. Wrapped managers could misreport the default runtime — valid, fixed. 2. QEMU availability did not verify the prerequisites every launch needs — valid, fixed.
Unavailable cases are covered in Checks: |
GPT-5.6 capabilities redesign review — round 4 (
|
…ounded+coalesced QEMU probe - vz: advertise SupportsFork unconditionally. Stopped-source forks clone disks with no machine-state save/restore, so gating fork on the macOS 14 save/restore probe hid a valid macOS 13 operation. The fork feature now documents its contract precisely: it promises the stopped-source fork; forking a standby or running source restores/creates snapshots and additionally requires the standby feature (which vz still derives from the save/restore probe). Feature IDs are unchanged, so the OpenAPI/ Stainless surface only gains documentation. - hypervisor: guard the public runtime registry with an RWMutex. RegisterRuntime is public API and can run while capability requests enumerate the registry; the unsynchronized map crashed under concurrent register/enumerate (-race reproducer added). Readers snapshot the map under the lock and resolve Capabilities/LaunchCheck callbacks only after releasing it, so callbacks may safely re-enter the registry (pinned by a reentrancy test). The deprecated RegisterCapabilities wrapper is preserved unchanged. - qemu: bound and coalesce the availability probe. qemu --version now runs under a 5s context (WaitDelay backstops descendants holding the pipe), so a hung binary fails the check instead of wedging requests and leaking subprocesses. Both board registrations share one launchCheckCache: a short-TTL (1s) single-flight cache so a registry read and concurrent capability requests execute at most one probe per window, while installing QEMU or loading vhost_vsock still flips availability without a restart. Invalidate() is the explicit invalidation seam. Deterministic tests cover coalescing, expiry, error caching, invalidation, and the hung-binary deadline; tests use private cache instances, never the package global.
Round-4 findings adjudication (fable-final-repair-r1) —
|
GPT-5.6 capabilities continuation review — round 1 (
|
The bounded probe used exec.CommandContext's default cancel, which signals only the direct child: a wrapper binary that spawned a descendant (sleep 60 & wait) returned at the deadline but left the descendant orphaned under PID 1, and WaitDelay only unblocked the pipe read. Run the probe in a dedicated process group (Setpgid) and SIGKILL the group (-pid) on cancellation so descendants die and are reaped by init; keep WaitDelay as a backstop for a descendant that setsid'd out of the group while holding the output pipe. TestVersionFromBinaryKillsProcessGroupOnTimeout pins the property with a wrapper that records both PIDs before hanging, asserting the wrapper and its descendant are both gone after the probe's context deadline (fails without the group kill).
Continuation round-1 finding adjudication (fable-final-repair-r2)Finding 1 — bounded version probe can orphan descendant processes: valid, fixed in 2260f64. Confirmed exactly as described: Fix ( Evidence:
Notes on unrelated observations while validating:
|
GPT-5.6 capabilities continuation review — round 2 (
|
cmd.Cancel only fires on context cancellation, so a wrapper binary that exits before the deadline while leaving a background descendant holding the stdout pipe leaked that descendant on every probe: Output returned after WaitDelay with ErrWaitDelay, but nothing reaped the group. Kill the probe's process group unconditionally after Output returns (no-op ESRCH when the group is already empty) and pin the behavior with a regression test where the wrapper prints a valid version, backgrounds sleep 60, and exits immediately.
Parallel tests fork subprocesses, and a concurrent fork can transiently inherit a just-written script's write descriptor between fork and exec, failing the probe with 'text file busy' before the script ever runs. Retry the probe (with a fresh timeout context per attempt) so the two process-group-kill regression tests assert probe behavior, not the well-known exec race.
Continuation round-2 finding adjudication (fable-final-repair-r3)Finding 1 — probe still leaks descendants when the direct wrapper exits first: valid, fixed in 66a7fa1 (+ test-hardening in 993d31e). Confirmed exactly as reproduced in the round-2 review: Fix ( Regression test: Evidence at
Scope stayed narrow: 2 files of production+test change in |
…Y everywhere The timeout regression test cancelled the probe on a fixed 250ms deadline, which raced slow script startup — macOS CI runners can take longer than that to first-exec a fresh script (XProtect scan), killing the group before the pid files existed. Cancel the context only once the script has demonstrably hung with both pids recorded, with a generous backstop that still fails fast on regression. Extend the fork/exec ETXTBSY retry (a concurrent fork transiently inheriting a just-written script's write fd) to the launch-prerequisite subtests that exec fake binaries; observed ~1/30 full-package -race runs, 0/50 after. No production code changes.
Round-3 closeout — final head
|
GPT-5.6 capabilities continuation review — round 3 (
|
Round-3 finding adjudication (
|
Final head review —
|


What
Adds an authenticated, machine-readable capability contract so clients (e.g. deployment tooling) can discover what a Hypeman host can do without hard-coding hypervisor knowledge.
GET /capabilities(bearer-auth,resource:readscope)Reports:
availableboolean (launch prerequisites verified — e.g. qemu requires a runnable system-installed QEMU binary and the host/dev/vhost-vsockdevice, neither of which hypeman requires at startup) and its own stable per-runtime feature IDs (snapshots,standby,fork,pause,hotplug-memory,balloon-control,vsock,gpu-passthrough,disk-io-limit,disk-resize). Hosts commonly support several at once — e.g. cloud-hypervisor, firecracker, qemu, and qemu-microvm on linux/amd64.availableboolean matching itsruntimes[]entry, so clients can verify ordinary (runtime-unspecified) launches are backed by a launchable runtimebridge/nat), guest-visible host gateway and subnet (omitted — never empty strings — when no default network has resolved), and guest-to-guest reachabilitylinux/amd64on Apple Silicon macOS only when Rosetta is currently installed, probed via the same Virtualization.framework availability check the vz-shim enforces at launch) and the default platforminstances,images,builds,volumes,ingress,exec,logs, plusdeviceson Linux androsetta-emulationon Apple Silicon hosts with Rosetta currently installed), kept distinct from per-runtime featuresDesign: the capability registry is the single source of truth
There is no hand-maintained "supported runtimes" switch and no handler-owned feature mapping:
hypervisor.RegisterRuntimeonly where it can genuinely launch VMs: cloud-hypervisor/firecracker/qemu register from//go:build linuxfiles (they require KVM / kernel AF_VSOCK), vz from its darwin-only package, and qemu-microvm only where its x86 board resolves (linux/amd64) — using the same machine-type resolver that gates launches. Registration means supported by this build; an optional per-runtimeLaunchCheckdetermines available: QEMU resolves the binary with the same lookup launches use, verifies it actually executes and reports a parseable version (the same--versionprobeResolveVersionpersists on every cold start), and checks the host/dev/vhost-vsockdevice that every instance launch needs (each instance gets a nonzero vsock CID, so QEMU always attaches a vhost-vsock device); firecracker validates an activehypervisor.firecracker_binary_pathoverride (which takes precedence over the embedded binaries on every launch) with the same executable check the launch path applies. The registry's keys therefore are the host-supported runtime set, and new runtimes appear in the endpoint automatically.cloud_hypervisor_default_version: v49.0stops advertising the v51.1-onlydisk-resizefeature, and installing a missing QEMU binary flips availability without a restart.hypervisor.RegisteredRuntimes()returns value copies sorted by type name, resolved at read time.hypervisor.Capabilities.FeatureIDs()(withSupportsStandby()= snapshot ∧ pause) means adding a capability touches one package plus the OpenAPI schema — the HTTP handler never changes. Fork is an explicitSupportsForkcapability — independent of snapshot support in both directions: a snapshot-capable backend may rejectPrepareForkwithErrNotSupported, and a backend can fork a stopped source (a disk clone, no machine-state snapshot involved) without snapshot support at all. Theforkfeature promises the stopped-source fork; forking a standby or running source restores/creates snapshots and additionally requires thestandbyfeature — the documented client gating contract.RegisterRuntimeis public API, so custom backends can register while capability requests enumerate the registry. All registry access is guarded by an RWMutex; readers snapshot the map under the lock and resolve capability/launch-check callbacks only after releasing it, so callbacks may be slow or re-enter the registry (both pinned by tests, including a-raceregister/enumerate reproducer).qemu --versionruns under a 5s context in its own process group, and the whole group is SIGKILLed on every completion path — not only context cancellation — so neither a hung binary (Cancel path) nor a wrapper that exits early while a background descendant holds the output pipe (WaitDelaypath, where Cancel never fires) can wedge requests or leak subprocesses across cache refreshes. Both QEMU board registrations share one short-TTL (1s) single-flight cache: a registry read and concurrent capability requests execute at most one probe per window, while installing QEMU or loadingvhost_vsockstill flips availability without a restart.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. On macOS 13, vz truthfully omitssnapshots/standbywhile still advertisingfork: a stopped-source fork clones disks with no machine-state save/restore involved (Starter.PrepareForksucceeds without a snapshot), so gating it on the probe would hide a valid operation. Hot-source forks (standby/running) require thestandbyfeature per the fork contract above — and macOS 13 hosts cannot have standby sources in the first place.Acceptance criteria mapping
Tests
cmd/api/api/capabilities_test.go— handler (version serialization, host identity, registry-driven runtimes with per-runtime availability, per-runtime feature independence incl. qemu-microvm, default availability tracks the launch check, default-not-available, gateway omission, network failure → 500, typed network-model mapping, server features)cmd/api/api/capabilities_{linux,darwin}_test.go— build-tagged registration boundary pins (exact registry contents per platform, incl. qemu-microvm only on linux/amd64 and vz-only on macOS)lib/hypervisor/features_test.go— FeatureIDs matrix (zero/full/partial capability sets, internal hints excluded, standby semantics, snapshot-without-fork), enumeration semantics (per-read capability resolution, launch checks gating availability and re-evaluated per read) tested against a local map so the global registry is never mutatedlib/hypervisor/cloudhypervisor/register_linux_test.go— registered capabilities track the configured default version across the v49/v51 disk-resize boundary; fork advertised on every supported versionlib/hypervisor/qemu/register_linux_test.go— qemu-microvm registration tracks the launch-gating board resolver; qemu/qemu-microvm availability tracks the full launch-prerequisite check, with unavailable-case coverage (missing/non-executable/broken/unparseable-version binary, missing vhost-vsock device) via fake binaries and device pathslib/hypervisor/vz/save_restore_support_test.go— macOS 13/14/15/26 boundary matrix; darwin-only test proves advertised snapshot/standby capabilities track the probed OS version while fork stays advertised on macOS 13 (stopped-source forks need no save/restore)lib/hypervisor/registry_concurrency_test.go— concurrent register/enumerate under-race(crashes with "concurrent map iteration and map write" before the RWMutex; unique test-only types so production registrations are never mutated) and a reentrancy test proving callbacks run outside the registry locklib/hypervisor/qemu/launch_check_cache_test.go— deterministic single-flight/TTL cache tests on private instances (never the package global): concurrent-caller coalescing onto one probe, expiry with a fake clock (cached failures re-probed after TTL so repaired prerequisites surface), explicitInvalidate()seam;register_linux_test.goadds the hung-binary deadline case and pins both boards sharing the production cache;process_test.goaddsTestVersionFromBinaryKillsProcessGroupOnTimeout, a wrapper binary that spawns a descendant (sleep 60 & wait) and asserts both the wrapper and the descendant are killed when the probe's context is cancelled — the test cancels only once the script has demonstrably hung with both pids recorded, so slow script startup (e.g. macOS first-exec scanning) can never race the kill (fails without the process-group kill) — andTestVersionFromBinaryKillsDescendantsWhenWrapperExitsFirst, where the wrapper prints a valid version, backgroundssleep 60, and exits before the deadline: the probe completes viaWaitDelay/ErrWaitDelayand the descendant must still be reaped (fails without the completion-path group kill). All tests that exec just-written scripts retry the well-known fork/exec ETXTBSY race so parallel test subprocess spawns can't flake themlib/network/default_network_test.go— typed model, guest-to-guest rulesRan locally (Linux/KVM):
go build ./...,go vet ./...,gofmtclean;make oapi-generatedrift-free; fullgo test ./lib/... ./cmd/...— all packages pass except pre-existinglib/instancesnetwork-integration tests and one env-dependentTestBuildEnv, all verified to fail identically onorigin/mainin this sandbox (no NET_ADMIN).GOOS=darwin GOARCH=arm64 go build/go vetclean forcmd/api/api,lib/network,lib/instances,lib/hypervisor/...,lib/scopes.Risks
hypervisor.RegisterCapabilitiesremains as a deprecated wrapper overRegisterRuntime(static set, no launch check — the old semantics), andinstances.Manageris unchanged; the handler type-asserts a narrowDefaultHypervisor()accessor on the concrete manager instead; when a wrapper hides that method, it falls back to the configuredhypervisor.default(the value the wrapped manager was constructed from), normalizing only an empty value to the same cloud-hypervisor defaultlib/instancesapplies. Capability registration moving to platform-gated files meanshypervisor.CapabilitiesForTypenow reportsok=falsefor runtimes that cannot run on the host — the internal callers consult it only for instances that exist locally, which can only be of launchable types. Capability resolvers and launch checks run per registry read (cheap: struct construction; QEMU additionally runs a context-boundedqemu --versionbehind a shared 1s single-flight cache and stats/dev/vhost-vsock; the probe runs in its own process group and the group is SIGKILLed on every completion path — cancellation and normal/WaitDelaycompletion alike — so neither a wedged binary nor a wrapper that exits leaving background descendants can orphan processes). Registry reads snapshot under an RWMutex and never hold the lock across callbacks, so lateRegisterRuntimecalls are safe.GetCapabilitiestypes/client method;stainless.yamlmaps the capabilities resource and allCapabilities*models so Stainless SDKs include the endpoint.CapabilitiesRuntimegains a requiredavailablefield (added before any release ships the endpoint).Capabilities()now also resolves from the configured default version instead of the compile-time default; the only version-varying flag (SupportsDiskResize) is not consumed by any runtime gate today, so behavior is unchanged.Review
Please run Cursor Bugbot review on this PR. (@cursor)
Note
Medium Risk
Large cross-cutting change (registry, QEMU subprocess probes, macOS capability gating) but the endpoint is additive; main risk is misreported availability if launch checks drift from actual launch paths.
Overview
Adds
GET /capabilities(bearer auth,resource:read) so clients can discover what a host can run without hard-coding hypervisor behavior. The handler reports server/API versions, host OS/arch, every platform-registered runtime withavailable(launch prerequisites) and stable feature IDs, default runtime identity/availability, network model/gateway/subnet/guest-to-guest, image platforms (including Rosetta-gatedlinux/amd64), and server-level features.Hypervisor layer: static init-time capability maps are replaced by a mutex-protected
RegisterRuntimeregistry with per-read capability resolvers and optionalLaunchCheck(QEMU binary +/dev/vhost-vsock, Firecracker custom binary path, etc.).Capabilities.FeatureIDs()and explicitSupportsForkdrive the API contract; vz snapshot/standby now follow macOS 14+ probing instead of arm64-only; QEMU version probes use process-group kills and a short-TTL coalesced cache.Supporting changes: typed
network.Modelfor bridge vs NAT, optionalDefaultHypervisor()on the concrete instance manager (type-asserted by the handler), OpenAPI/oapi/Stainless/scopes updates, and broad tests for registration boundaries, concurrency, and launch-check truthfulness.Reviewed by Cursor Bugbot for commit d7fba22. Bugbot is set up for automated code reviews on this repo. Configure here.