@@ -1434,6 +1434,101 @@ fn sandbox_should_persist(
14341434 keep || forward. is_some ( )
14351435}
14361436
1437+ fn build_sandbox_resource_limits (
1438+ cpu : Option < & str > ,
1439+ memory : Option < & str > ,
1440+ ) -> Result < Option < prost_types:: Struct > > {
1441+ use prost_types:: { Struct , Value , value:: Kind } ;
1442+
1443+ fn string_value ( value : String ) -> Value {
1444+ Value {
1445+ kind : Some ( Kind :: StringValue ( value) ) ,
1446+ }
1447+ }
1448+
1449+ let mut limits = std:: collections:: BTreeMap :: new ( ) ;
1450+ if let Some ( cpu) = cpu {
1451+ limits. insert ( "cpu" . to_string ( ) , string_value ( validate_cpu_quantity ( cpu) ?) ) ;
1452+ }
1453+ if let Some ( memory) = memory {
1454+ limits. insert (
1455+ "memory" . to_string ( ) ,
1456+ string_value ( validate_memory_quantity ( memory) ?) ,
1457+ ) ;
1458+ }
1459+
1460+ if limits. is_empty ( ) {
1461+ return Ok ( None ) ;
1462+ }
1463+
1464+ let mut fields = std:: collections:: BTreeMap :: new ( ) ;
1465+ fields. insert (
1466+ "limits" . to_string ( ) ,
1467+ Value {
1468+ kind : Some ( Kind :: StructValue ( Struct { fields : limits } ) ) ,
1469+ } ,
1470+ ) ;
1471+ Ok ( Some ( Struct { fields } ) )
1472+ }
1473+
1474+ fn validate_cpu_quantity ( value : & str ) -> Result < String > {
1475+ let value = value. trim ( ) ;
1476+ if value. is_empty ( ) {
1477+ return Err ( miette ! ( "--cpu must not be empty" ) ) ;
1478+ }
1479+
1480+ if let Some ( millicores) = value. strip_suffix ( 'm' ) {
1481+ if millicores. is_empty ( ) || !millicores. bytes ( ) . all ( |b| b. is_ascii_digit ( ) ) {
1482+ return Err ( miette ! (
1483+ "invalid --cpu value '{value}': expected positive cores or millicores, for example 2, 0.5, or 500m"
1484+ ) ) ;
1485+ }
1486+ let millicores = millicores. parse :: < u64 > ( ) . into_diagnostic ( ) ?;
1487+ if millicores == 0 {
1488+ return Err ( miette ! ( "--cpu must be greater than zero" ) ) ;
1489+ }
1490+ return Ok ( value. to_string ( ) ) ;
1491+ }
1492+
1493+ let cores = value. parse :: < f64 > ( ) . map_err ( |_| {
1494+ miette ! (
1495+ "invalid --cpu value '{value}': expected positive cores or millicores, for example 2, 0.5, or 500m"
1496+ )
1497+ } ) ?;
1498+ if !cores. is_finite ( ) || cores <= 0.0 {
1499+ return Err ( miette ! ( "--cpu must be greater than zero" ) ) ;
1500+ }
1501+ Ok ( value. to_string ( ) )
1502+ }
1503+
1504+ fn validate_memory_quantity ( value : & str ) -> Result < String > {
1505+ let value = value. trim ( ) ;
1506+ if value. is_empty ( ) {
1507+ return Err ( miette ! ( "--memory must not be empty" ) ) ;
1508+ }
1509+
1510+ let number_end = value
1511+ . find ( |ch : char | !ch. is_ascii_digit ( ) )
1512+ . unwrap_or ( value. len ( ) ) ;
1513+ let ( number, suffix) = value. split_at ( number_end) ;
1514+ if number. is_empty ( )
1515+ || !matches ! (
1516+ suffix,
1517+ "" | "Ki" | "Mi" | "Gi" | "Ti" | "Pi" | "Ei" | "K" | "M" | "G" | "T" | "P" | "E"
1518+ )
1519+ {
1520+ return Err ( miette ! (
1521+ "invalid --memory value '{value}': expected positive bytes or a quantity such as 512Mi, 4Gi, or 8G"
1522+ ) ) ;
1523+ }
1524+
1525+ let amount = number. parse :: < u128 > ( ) . into_diagnostic ( ) ?;
1526+ if amount == 0 {
1527+ return Err ( miette ! ( "--memory must be greater than zero" ) ) ;
1528+ }
1529+ Ok ( value. to_string ( ) )
1530+ }
1531+
14371532async fn finalize_sandbox_create_session (
14381533 server : & str ,
14391534 sandbox_name : & str ,
@@ -1468,6 +1563,8 @@ pub async fn sandbox_create(
14681563 keep : bool ,
14691564 gpu : bool ,
14701565 gpu_device : Option < & str > ,
1566+ cpu : Option < & str > ,
1567+ memory : Option < & str > ,
14711568 editor : Option < Editor > ,
14721569 providers : & [ String ] ,
14731570 policy : Option < & str > ,
@@ -1530,11 +1627,17 @@ pub async fn sandbox_create(
15301627 . await ?;
15311628
15321629 let policy = load_sandbox_policy ( policy) ?;
1630+ let resource_limits = build_sandbox_resource_limits ( cpu, memory) ?;
15331631
1534- let template = image. map ( |img| SandboxTemplate {
1535- image : img,
1536- ..SandboxTemplate :: default ( )
1537- } ) ;
1632+ let template = if image. is_some ( ) || resource_limits. is_some ( ) {
1633+ Some ( SandboxTemplate {
1634+ image : image. unwrap_or_default ( ) ,
1635+ resources : resource_limits,
1636+ ..SandboxTemplate :: default ( )
1637+ } )
1638+ } else {
1639+ None
1640+ } ;
15381641
15391642 let request = CreateSandboxRequest {
15401643 spec : Some ( SandboxSpec {
@@ -6007,8 +6110,8 @@ fn format_timestamp_ms(ms: i64) -> String {
60076110#[ cfg( test) ]
60086111mod tests {
60096112 use super :: {
6010- TlsOptions , dockerfile_sources_supported_for_gateway , format_endpoint ,
6011- format_gateway_select_header, format_gateway_select_items,
6113+ TlsOptions , build_sandbox_resource_limits , dockerfile_sources_supported_for_gateway ,
6114+ format_endpoint , format_gateway_select_header, format_gateway_select_items,
60126115 format_provider_attachment_table, gateway_add, gateway_auth_label,
60136116 gateway_env_override_warning, gateway_select_with, gateway_type_label, git_sync_files,
60146117 http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
@@ -6211,6 +6314,55 @@ mod tests {
62116314 assert ! ( err. to_string( ) . contains( "unknown setting key" ) ) ;
62126315 }
62136316
6317+ #[ test]
6318+ fn build_sandbox_resource_limits_sets_limits_only ( ) {
6319+ let resources = build_sandbox_resource_limits ( Some ( "500m" ) , Some ( "2Gi" ) )
6320+ . expect ( "resource limits should parse" )
6321+ . expect ( "resource limits should be present" ) ;
6322+
6323+ let limits = resources
6324+ . fields
6325+ . get ( "limits" )
6326+ . and_then ( |value| value. kind . as_ref ( ) )
6327+ . and_then ( |kind| match kind {
6328+ prost_types:: value:: Kind :: StructValue ( inner) => Some ( inner) ,
6329+ _ => None ,
6330+ } )
6331+ . expect ( "limits should be a struct" ) ;
6332+
6333+ assert_eq ! (
6334+ limits
6335+ . fields
6336+ . get( "cpu" )
6337+ . and_then( |value| value. kind. as_ref( ) )
6338+ . and_then( |kind| match kind {
6339+ prost_types:: value:: Kind :: StringValue ( value) => Some ( value. as_str( ) ) ,
6340+ _ => None ,
6341+ } ) ,
6342+ Some ( "500m" )
6343+ ) ;
6344+ assert_eq ! (
6345+ limits
6346+ . fields
6347+ . get( "memory" )
6348+ . and_then( |value| value. kind. as_ref( ) )
6349+ . and_then( |kind| match kind {
6350+ prost_types:: value:: Kind :: StringValue ( value) => Some ( value. as_str( ) ) ,
6351+ _ => None ,
6352+ } ) ,
6353+ Some ( "2Gi" )
6354+ ) ;
6355+ assert ! ( !resources. fields. contains_key( "requests" ) ) ;
6356+ }
6357+
6358+ #[ test]
6359+ fn build_sandbox_resource_limits_rejects_invalid_quantities ( ) {
6360+ assert ! ( build_sandbox_resource_limits( Some ( "0" ) , None ) . is_err( ) ) ;
6361+ assert ! ( build_sandbox_resource_limits( Some ( "half" ) , None ) . is_err( ) ) ;
6362+ assert ! ( build_sandbox_resource_limits( None , Some ( "0Gi" ) ) . is_err( ) ) ;
6363+ assert ! ( build_sandbox_resource_limits( None , Some ( "1.5Gi" ) ) . is_err( ) ) ;
6364+ }
6365+
62146366 #[ test]
62156367 fn inferred_provider_type_returns_type_for_known_command ( ) {
62166368 let result = inferred_provider_type ( & [ "claude" . to_string ( ) , "--help" . to_string ( ) ] ) ;
0 commit comments