@@ -837,6 +837,11 @@ impl ComputeRuntime {
837837 self . driver_info . features . contains ( & i32:: from ( feature) )
838838 }
839839
840+ #[ must_use]
841+ pub ( crate ) fn uses_gateway_managed_lifecycle ( & self ) -> bool {
842+ self . supports_feature ( ComputeDriverFeature :: GatewayManagedLifecycle )
843+ }
844+
840845 #[ must_use]
841846 pub ( crate ) fn preserves_unspecified_process_identity ( & self ) -> bool {
842847 self . supports_feature ( ComputeDriverFeature :: PreserveUnspecifiedProcessIdentity )
@@ -2055,14 +2060,10 @@ impl ComputeRuntime {
20552060 /// persisted lifecycle intent.
20562061 ///
20572062 /// An explicit sandbox stop persists `Stopped`; gateway shutdown does not.
2058- /// Docker, Podman, and VM compute is stopped through the same public driver
2059- /// RPC and restarted from the retained running-intent phase on gateway
2060- /// startup. Kubernetes compute remains cluster-owned and is excluded.
2063+ /// Drivers opt into stop/start ownership through their public capability
2064+ /// snapshot. Kubernetes and legacy extension drivers omit that capability.
20612065 async fn stop_persisted_sandboxes_on_shutdown ( & self ) -> Result < ( ) , String > {
2062- if !matches ! (
2063- self . driver_kind( ) ,
2064- Some ( ComputeDriverKind :: Docker | ComputeDriverKind :: Podman | ComputeDriverKind :: Vm )
2065- ) {
2066+ if !self . uses_gateway_managed_lifecycle ( ) {
20662067 return Ok ( ( ) ) ;
20672068 }
20682069
@@ -2158,7 +2159,7 @@ impl ComputeRuntime {
21582159
21592160 /// Reconcile running intent for local compute after a gateway restart.
21602161 ///
2161- /// Drivers opt into this sweep through their startup capability snapshot.
2162+ /// Drivers opt into this sweep through their lifecycle capability snapshot.
21622163 /// `StartSandbox` is idempotent, so call it for every persisted phase that
21632164 /// requires running compute. Stable stopped, deleting, and error states are
21642165 /// deliberately left alone.
@@ -2167,7 +2168,7 @@ impl ComputeRuntime {
21672168 /// so the watch loop sees the post-start state on its first poll.
21682169 pub async fn start_persisted_sandboxes ( & self ) -> Result < ( ) , String > {
21692170 self . recover_persisted_lifecycle_transitions ( ) . await ?;
2170- if !self . supports_feature ( ComputeDriverFeature :: GatewayStartReconciliation ) {
2171+ if !self . uses_gateway_managed_lifecycle ( ) {
21712172 return Ok ( ( ) ) ;
21722173 }
21732174
@@ -4075,11 +4076,11 @@ pub async fn new_test_runtime_with_driver(
40754076) -> ComputeRuntime {
40764077 let features = match driver_name. parse :: < ComputeDriverKind > ( ) . ok ( ) {
40774078 Some ( ComputeDriverKind :: Docker | ComputeDriverKind :: Podman ) => vec ! [
4078- i32 :: from( ComputeDriverFeature :: GatewayStartReconciliation ) ,
4079+ i32 :: from( ComputeDriverFeature :: GatewayManagedLifecycle ) ,
40794080 i32 :: from( ComputeDriverFeature :: PreserveUnspecifiedProcessIdentity ) ,
40804081 ] ,
40814082 Some ( ComputeDriverKind :: Vm ) => {
4082- vec ! [ i32 :: from( ComputeDriverFeature :: GatewayStartReconciliation ) ]
4083+ vec ! [ i32 :: from( ComputeDriverFeature :: GatewayManagedLifecycle ) ]
40834084 }
40844085 _ => Vec :: new ( ) ,
40854086 } ;
@@ -8177,7 +8178,12 @@ mod tests {
81778178 #[ tokio:: test]
81788179 async fn shutdown_stops_running_intent_without_changing_persisted_phase ( ) {
81798180 let driver = ControlledDriver :: new ( ) ;
8180- let runtime = test_runtime_for_driver ( driver. clone ( ) , "docker" ) . await ;
8181+ let runtime = test_runtime_for_driver_with_features (
8182+ driver. clone ( ) ,
8183+ "extension" ,
8184+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
8185+ )
8186+ . await ;
81818187
81828188 for ( id, name, phase) in [
81838189 ( "sb-unspecified" , "unspecified" , SandboxPhase :: Unspecified ) ,
@@ -8236,7 +8242,12 @@ mod tests {
82368242 async fn shutdown_stop_sweep_continues_after_driver_errors ( ) {
82378243 let driver = ControlledDriver :: new ( ) ;
82388244 driver. set_stop_outcome ( ControlledLifecycleOutcome :: Error ( "runtime angry" ) ) ;
8239- let runtime = test_runtime_for_driver ( driver. clone ( ) , "podman" ) . await ;
8245+ let runtime = test_runtime_for_driver_with_features (
8246+ driver. clone ( ) ,
8247+ "extension" ,
8248+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
8249+ )
8250+ . await ;
82408251 for ( id, name) in [ ( "sb-1" , "one" ) , ( "sb-2" , "two" ) ] {
82418252 runtime
82428253 . store
@@ -8258,7 +8269,12 @@ mod tests {
82588269 async fn shutdown_stop_sweep_uses_bounded_concurrency ( ) {
82598270 let driver = ControlledDriver :: new ( ) ;
82608271 driver. block_stop ( ) ;
8261- let runtime = test_runtime_for_driver ( driver. clone ( ) , "docker" ) . await ;
8272+ let runtime = test_runtime_for_driver_with_features (
8273+ driver. clone ( ) ,
8274+ "extension" ,
8275+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
8276+ )
8277+ . await ;
82628278 let sandbox_count = SHUTDOWN_STOP_CONCURRENCY + 1 ;
82638279 for index in 0 ..sandbox_count {
82648280 runtime
@@ -8296,16 +8312,15 @@ mod tests {
82968312 }
82978313
82988314 #[ tokio:: test]
8299- async fn shutdown_stop_sweep_runs_for_each_local_driver_only ( ) {
8300- for ( driver_name, expected_calls) in [
8301- ( "docker" , 1 ) ,
8302- ( "podman" , 1 ) ,
8303- ( "vm" , 1 ) ,
8304- ( "kubernetes" , 0 ) ,
8305- ( "extension" , 0 ) ,
8306- ] {
8315+ async fn shutdown_stop_sweep_runs_for_any_capable_driver ( ) {
8316+ for driver_name in [ "docker" , "podman" , "vm" , "extension" ] {
83078317 let driver = ControlledDriver :: new ( ) ;
8308- let runtime = test_runtime_for_driver ( driver. clone ( ) , driver_name) . await ;
8318+ let runtime = test_runtime_for_driver_with_features (
8319+ driver. clone ( ) ,
8320+ driver_name,
8321+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
8322+ )
8323+ . await ;
83098324 runtime
83108325 . store
83118326 . put_message ( & sandbox_record ( "sb-1" , "sandbox" , SandboxPhase :: Ready ) )
@@ -8319,19 +8334,43 @@ mod tests {
83198334
83208335 assert_eq ! (
83218336 driver. stop_calls( ) ,
8322- expected_calls ,
8337+ 1 ,
83238338 "unexpected shutdown behavior for {driver_name}"
83248339 ) ;
83258340 }
83268341 }
83278342
8343+ #[ tokio:: test]
8344+ async fn shutdown_stop_sweep_skips_drivers_without_feature ( ) {
8345+ for driver_name in [ "kubernetes" , "docker" , "extension" ] {
8346+ let driver = ControlledDriver :: new ( ) ;
8347+ let runtime = test_runtime_for_driver ( driver. clone ( ) , driver_name) . await ;
8348+ runtime
8349+ . store
8350+ . put_message ( & sandbox_record ( "sb-1" , "sandbox" , SandboxPhase :: Ready ) )
8351+ . await
8352+ . unwrap ( ) ;
8353+
8354+ runtime
8355+ . stop_persisted_sandboxes_on_shutdown ( )
8356+ . await
8357+ . unwrap ( ) ;
8358+
8359+ assert_eq ! (
8360+ driver. stop_calls( ) ,
8361+ 0 ,
8362+ "{driver_name} should retain operator-owned lifecycle"
8363+ ) ;
8364+ }
8365+ }
8366+
83288367 #[ tokio:: test]
83298368 async fn start_persisted_sandboxes_starts_running_phases ( ) {
83308369 let driver = ControlledDriver :: new ( ) ;
83318370 let runtime = test_runtime_for_driver_with_features (
83328371 driver. clone ( ) ,
83338372 "extension" ,
8334- vec ! [ ComputeDriverFeature :: GatewayStartReconciliation ] ,
8373+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
83358374 )
83368375 . await ;
83378376
@@ -8375,7 +8414,7 @@ mod tests {
83758414 let runtime = test_runtime_for_driver_with_features (
83768415 driver,
83778416 "extension" ,
8378- vec ! [ ComputeDriverFeature :: GatewayStartReconciliation ] ,
8417+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
83798418 )
83808419 . await ;
83818420
@@ -8410,7 +8449,7 @@ mod tests {
84108449 let runtime = test_runtime_for_driver_with_features (
84118450 driver,
84128451 "extension" ,
8413- vec ! [ ComputeDriverFeature :: GatewayStartReconciliation ] ,
8452+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
84148453 )
84158454 . await ;
84168455
@@ -8445,7 +8484,7 @@ mod tests {
84458484 let runtime = test_runtime_for_driver_with_features (
84468485 driver. clone ( ) ,
84478486 driver_name,
8448- vec ! [ ComputeDriverFeature :: GatewayStartReconciliation ] ,
8487+ vec ! [ ComputeDriverFeature :: GatewayManagedLifecycle ] ,
84498488 )
84508489 . await ;
84518490 let sandbox = sandbox_record ( "sb-1" , "local" , SandboxPhase :: Ready ) ;
@@ -8691,7 +8730,7 @@ mod tests {
86918730 . with_driver_name ( "fake-remote-driver" )
86928731 . with_default_image ( "openshell/sandbox:remote" )
86938732 . with_features ( [
8694- ComputeDriverFeature :: GatewayStartReconciliation ,
8733+ ComputeDriverFeature :: GatewayManagedLifecycle ,
86958734 ComputeDriverFeature :: PreserveUnspecifiedProcessIdentity ,
86968735 ] )
86978736 . with_gateway_listener_requirement (
@@ -8767,6 +8806,17 @@ mod tests {
87678806 assert ! ( driver_config. fields. contains_key( "pool" ) ) ;
87688807 assert ! ( !driver_config. fields. contains_key( "network_mode" ) ) ;
87698808
8809+ driver. clear_calls ( ) ;
8810+ runtime
8811+ . stop_persisted_sandboxes_on_shutdown ( )
8812+ . await
8813+ . unwrap ( ) ;
8814+ assert ! ( matches!(
8815+ driver. calls( ) . as_slice( ) ,
8816+ [ FakeComputeDriverCall :: StopSandbox { sandbox_id, sandbox_name } ]
8817+ if sandbox_id == "sb-uds" && sandbox_name == "uds-sandbox"
8818+ ) ) ;
8819+
87708820 driver. clear_calls ( ) ;
87718821 runtime. start_persisted_sandboxes ( ) . await . unwrap ( ) ;
87728822 assert ! ( matches!(
0 commit comments