[Metal] Fall back to the system default device (#24839)

On an M5 Max running macOS 26.6 inside a Codex sandbox, we observed
`MTLCopyAllDevices()` return an empty array in a fresh process even
though `MTLCreateSystemDefaultDevice()` returned a valid device. IREE
snapshots the first enumeration result when creating its Metal driver,
so an initially empty result leaves that driver permanently without
devices.

Instead, when enumeration is empty, we use the default device as a
one-device fallback.

Fixes #24834.

AI assistance disclosure: I used OpenAI Codex substantially to inspect
the implementation, trace the failure, implement the change, and prepare
this PR (but not write this description). I reviewed and understand the
resulting code and analysis.

---------

Signed-off-by: Jeremy Howard <github@jhoward.fastmail.fm>
diff --git a/runtime/src/iree/hal/drivers/metal/CMakeLists.txt b/runtime/src/iree/hal/drivers/metal/CMakeLists.txt
index 11e9914..5cc0c04 100644
--- a/runtime/src/iree/hal/drivers/metal/CMakeLists.txt
+++ b/runtime/src/iree/hal/drivers/metal/CMakeLists.txt
@@ -49,6 +49,7 @@
     iree::hal::utils::resource_set
     iree::schemas::executable_debug_info_c_fbs
     iree::schemas::metal_executable_def_c_fbs
+    "-framework CoreGraphics"
     "-framework Foundation"
     "-framework Metal"
   PUBLIC
diff --git a/runtime/src/iree/hal/drivers/metal/metal_driver.m b/runtime/src/iree/hal/drivers/metal/metal_driver.m
index 4f72aa6..bd05280 100644
--- a/runtime/src/iree/hal/drivers/metal/metal_driver.m
+++ b/runtime/src/iree/hal/drivers/metal/metal_driver.m
@@ -53,11 +53,22 @@
   return (const iree_hal_metal_driver_t*)base_value;
 }
 
-// Returns an retained array of available Metal GPU devices; the caller should release later.
+// Returns a retained array of available Metal GPU devices; the caller should release later.
 static NSArray<id<MTLDevice>>* iree_hal_metal_device_copy() {
 #if defined(IREE_PLATFORM_MACOS)
-  // For macOS, we might have more then one GPU devices.
-  return MTLCopyAllDevices();  // +1
+  // For macOS, we might have more than one GPU device.
+  // Sandboxed processes may be unable to enumerate Metal devices; see
+  // https://github.com/openai/codex/issues/17644.
+  NSArray<id<MTLDevice>>* devices = MTLCopyAllDevices();  // +1
+  if (devices.count == 0) {
+    id<MTLDevice> default_device = MTLCreateSystemDefaultDevice();  // +1
+    if (default_device) {
+      [devices release];                                                // -1
+      devices = [[NSArray alloc] initWithObjects:default_device, nil];  // +1
+      [default_device release];                                         // -1
+    }
+  }
+  return devices;
 #else
   // For other Apple platforms, we only have one GPU device.
   @autoreleasepool {  // Use @autorelasepool to trigger the autorelease carried in NSArray literal.