@@ -482,6 +482,8 @@ pub struct ImportAgentCompanionPetPackageRequest {
482482 pub path : String ,
483483 #[ serde( default ) ]
484484 pub expected_fingerprint : Option < String > ,
485+ #[ serde( default ) ]
486+ pub builtin_id : Option < String > ,
485487}
486488
487489#[ derive( Debug , Deserialize ) ]
@@ -497,13 +499,16 @@ pub use openbitfun_services_core::pet_packages::PetPackage as AgentCompanionPetP
497499pub struct ListAgentCompanionPetsRequest {
498500 #[ serde( default ) ]
499501 pub include_external : bool ,
502+ #[ serde( default ) ]
503+ pub builtin_import_version : Option < u32 > ,
500504}
501505
502506#[ derive( Debug , Serialize ) ]
503507#[ serde( rename_all = "camelCase" ) ]
504508pub struct ListAgentCompanionPetsResponse {
505509 pub pets : Vec < AgentCompanionPetPackageDto > ,
506510 pub import_operations_version : u32 ,
511+ pub builtin_import_version : u32 ,
507512 #[ serde( skip_serializing_if = "Option::is_none" ) ]
508513 pub external : Option < openbitfun_services_core:: pet_packages:: PetCatalog > ,
509514}
@@ -2525,11 +2530,57 @@ mod pet_package_tests {
25252530 let old: ImportAgentCompanionPetPackageRequest =
25262531 serde_json:: from_value ( serde_json:: json!( { "path" : "legacy-pet" } ) ) . unwrap ( ) ;
25272532 assert ! ( old. expected_fingerprint. is_none( ) ) ;
2533+ assert ! ( old. builtin_id. is_none( ) ) ;
25282534 let request: ListAgentCompanionPetsRequest =
25292535 serde_json:: from_value ( serde_json:: json!( { } ) ) . unwrap ( ) ;
25302536 assert ! ( !request. include_external) ;
2537+ assert ! ( request. builtin_import_version. is_none( ) ) ;
2538+ }
2539+
2540+ #[ test]
2541+ fn bundled_pet_request_requires_review ( ) {
2542+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
2543+ let request =
2544+ serde_json:: from_value ( serde_json:: json!( { "path" : "app.asar" , "builtinId" : "codex" } ) )
2545+ . unwrap ( ) ;
2546+ assert ! ( import_pet_request( temp. path( ) , request)
2547+ . unwrap_err( )
2548+ . contains( "reviewed fingerprint" ) ) ;
25312549 }
25322550
2551+ #[ test]
2552+ #[ ignore = "Imports installed Codex pets into an isolated temporary directory" ]
2553+ fn installed_builtin_pets_can_be_previewed_and_imported ( ) {
2554+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
2555+ let sources =
2556+ openbitfun_core:: external_sources:: external_builtin_pet_sources ( "codex" , None ) ;
2557+ assert ! ( sources. diagnostics. is_empty( ) , "{:?}" , sources. diagnostics) ;
2558+ assert ! ( !sources. pets. is_empty( ) ) ;
2559+ for source in sources. pets {
2560+ let identity = format ! ( "codex:builtin:{}" , source. id) ;
2561+ let candidate = openbitfun_services_core:: pet_packages:: candidate_from_bytes (
2562+ temp. path ( ) ,
2563+ & identity,
2564+ & source. archive_path ,
2565+ & source. manifest ,
2566+ source. image ,
2567+ )
2568+ . unwrap ( ) ;
2569+ assert ! ( candidate
2570+ . preview_data_url
2571+ . starts_with( "data:image/png;base64," ) ) ;
2572+ let request = serde_json:: from_value ( serde_json:: json!( {
2573+ "path" : source. archive_path, "builtinId" : source. id, "expectedFingerprint" : candidate. fingerprint,
2574+ } ) ) . unwrap ( ) ;
2575+ let imported = import_pet_request ( temp. path ( ) , request) . unwrap ( ) ;
2576+ assert ! ( Path :: new( & imported. spritesheet_path) . is_file( ) ) ;
2577+ assert_eq ! (
2578+ imported. sprite_version_number,
2579+ candidate. pet. sprite_version_number
2580+ ) ;
2581+ println ! ( "Validated bundled pet: {}" , imported. display_name) ;
2582+ }
2583+ }
25332584 #[ test]
25342585 fn pet_manifest_versions_are_validated_and_exported ( ) {
25352586 for version in [ None , Some ( 1 ) , Some ( 2 ) ] {
@@ -2656,14 +2707,43 @@ pub async fn list_agent_companion_pets(
26562707 state : State < ' _ , AppState > ,
26572708 request : Option < ListAgentCompanionPetsRequest > ,
26582709) -> Result < ListAgentCompanionPetsResponse , String > {
2710+ let include_builtins = request
2711+ . as_ref ( )
2712+ . is_some_and ( |r| r. builtin_import_version == Some ( 1 ) ) ;
26592713 let mut response = list_agent_companion_pets_impl ( & state) . await ?;
26602714 if request. is_some_and ( |r| r. include_external ) {
26612715 let source_root = openbitfun_core:: external_sources:: external_pet_source_root ( "codex" )
26622716 . ok_or ( "Pet source is unavailable" ) ?;
26632717 let installed_root = companion_user_packages_dir ( & state) ;
26642718 response. external = Some (
26652719 tokio:: task:: spawn_blocking ( move || {
2666- openbitfun_services_core:: pet_packages:: catalog ( & source_root, & installed_root)
2720+ let mut catalog =
2721+ openbitfun_services_core:: pet_packages:: catalog ( & source_root, & installed_root) ;
2722+ if !include_builtins {
2723+ return catalog;
2724+ }
2725+ let bundled =
2726+ openbitfun_core:: external_sources:: external_builtin_pet_sources ( "codex" , None ) ;
2727+ catalog. diagnostics . extend ( bundled. diagnostics ) ;
2728+ for source in bundled. pets {
2729+ let identity = format ! ( "codex:builtin:{}" , source. id) ;
2730+ match openbitfun_services_core:: pet_packages:: candidate_from_bytes (
2731+ & installed_root,
2732+ & identity,
2733+ & source. archive_path ,
2734+ & source. manifest ,
2735+ source. image ,
2736+ ) {
2737+ Ok ( mut candidate) => {
2738+ candidate. builtin_id = Some ( source. id ) ;
2739+ catalog. candidates . push ( candidate) ;
2740+ }
2741+ Err ( error) => catalog
2742+ . diagnostics
2743+ . push ( format ! ( "Codex built-in pet {}: {error}" , source. id) ) ,
2744+ }
2745+ }
2746+ catalog
26672747 } )
26682748 . await
26692749 . map_err ( |e| e. to_string ( ) ) ?,
@@ -2679,6 +2759,7 @@ pub(crate) async fn list_agent_companion_pets_impl(
26792759 Ok ( ListAgentCompanionPetsResponse {
26802760 pets,
26812761 import_operations_version : 1 ,
2762+ builtin_import_version : 1 ,
26822763 external : None ,
26832764 } )
26842765}
@@ -2689,15 +2770,42 @@ pub async fn import_agent_companion_pet_package(
26892770 request : ImportAgentCompanionPetPackageRequest ,
26902771) -> Result < AgentCompanionPetPackageDto , String > {
26912772 let root = companion_user_packages_dir ( & state) ;
2692- tokio:: task:: spawn_blocking ( move || {
2693- openbitfun_services_core:: pet_packages:: import (
2773+ tokio:: task:: spawn_blocking ( move || import_pet_request ( & root, request) )
2774+ . await
2775+ . map_err ( |e| e. to_string ( ) ) ?
2776+ }
2777+
2778+ fn import_pet_request (
2779+ root : & Path ,
2780+ request : ImportAgentCompanionPetPackageRequest ,
2781+ ) -> Result < AgentCompanionPetPackageDto , String > {
2782+ if let Some ( id) = request. builtin_id {
2783+ let expected = request
2784+ . expected_fingerprint
2785+ . as_deref ( )
2786+ . ok_or ( "Built-in pet import requires a reviewed fingerprint" ) ?;
2787+ let mut catalog =
2788+ openbitfun_core:: external_sources:: external_builtin_pet_sources ( "codex" , Some ( & id) ) ;
2789+ let source = catalog. pets . pop ( ) . ok_or_else ( || {
2790+ if catalog. diagnostics . is_empty ( ) {
2791+ "Built-in pet is unavailable" . to_string ( )
2792+ } else {
2793+ catalog. diagnostics . join ( "; " )
2794+ }
2795+ } ) ?;
2796+ return openbitfun_services_core:: pet_packages:: import_bytes (
26942797 & root,
2695- Path :: new ( & request. path ) ,
2696- request. expected_fingerprint . as_deref ( ) ,
2697- )
2698- } )
2699- . await
2700- . map_err ( |e| e. to_string ( ) ) ?
2798+ & format ! ( "codex:builtin:{id}" ) ,
2799+ & source. manifest ,
2800+ source. image ,
2801+ expected,
2802+ ) ;
2803+ }
2804+ openbitfun_services_core:: pet_packages:: import (
2805+ & root,
2806+ Path :: new ( & request. path ) ,
2807+ request. expected_fingerprint . as_deref ( ) ,
2808+ )
27012809}
27022810
27032811pub ( crate ) async fn import_agent_companion_pet_package_impl (
0 commit comments