@@ -13,7 +13,7 @@ use agent_spec::spec::{
1313 TaskLifecycle ,
1414} ;
1515use agent_spec:: {
16- AgentDesiredState , AgentSpec , JobType , Resource , Task , discover, discover_file,
16+ AgentDesiredState , AgentSpec , JobType , Resource , SessionDriver , Task , discover, discover_file,
1717 discover_strict,
1818} ;
1919
@@ -390,6 +390,7 @@ agent "cos" {
390390 Some ( "Silber.cos" )
391391 ) ;
392392 assert ! ( spec. delivery. is_none( ) ) ;
393+ assert ! ( spec. session_driver. is_none( ) ) ;
393394 assert ! ( spec. has_delivery_transport( ) ) ;
394395}
395396
@@ -480,6 +481,165 @@ fn deliver_rejects_unknown_duplicate_mixed_and_malformed_declarations() {
480481 ) ;
481482 }
482483}
484+ #[ test]
485+ fn session_driver_is_closed_ownership_for_an_opaque_launch ( ) {
486+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
487+ for driver in [ "claude" , "codex" , "pi" , "opencode" , "omp" ] {
488+ write (
489+ tmp. path ( ) ,
490+ & format ! ( "agents/h/{driver}/agent.kdl" ) ,
491+ & format ! (
492+ r#"agent "{driver}" {{ host "h"; argv "axe" "agent" "launch"; session-driver "{driver}" }}"#
493+ ) ,
494+ ) ;
495+ }
496+
497+ let found = discover ( tmp. path ( ) ) ;
498+ assert ! ( found. errors. is_empty( ) , "{:?}" , found. errors) ;
499+ for ( name, expected) in [
500+ ( "claude" , SessionDriver :: Claude ) ,
501+ ( "codex" , SessionDriver :: Codex ) ,
502+ ( "pi" , SessionDriver :: Pi ) ,
503+ ( "opencode" , SessionDriver :: OpenCode ) ,
504+ ( "omp" , SessionDriver :: Omp ) ,
505+ ] {
506+ let spec = find ( & found. specs , name) ;
507+ assert_eq ! ( spec. session_driver, Some ( expected) ) ;
508+ assert_eq ! ( spec. session_driver. unwrap( ) . as_str( ) , name) ;
509+ assert ! ( spec. driver. is_none( ) ) ;
510+ assert ! ( spec. delivery. is_none( ) ) ;
511+ assert_eq ! ( spec. tasks. len( ) , 1 ) ;
512+ assert_eq ! ( argv( & spec. tasks[ 0 ] ) , [ "axe" , "agent" , "launch" ] ) ;
513+ assert ! ( !spec. tasks[ 0 ] . derived) ;
514+ }
515+ }
516+
517+ #[ test]
518+ fn session_driver_rejects_unknown_duplicate_malformed_and_conflicting_declarations ( ) {
519+ for ( name, declaration, expected) in [
520+ (
521+ "unknown" ,
522+ r#"agent "worker" { argv "axe"; session-driver "cursor" }"# ,
523+ "unsupported `session-driver` value 'cursor'" ,
524+ ) ,
525+ (
526+ "duplicate" ,
527+ r#"agent "worker" { argv "axe"; session-driver "claude"; session-driver "codex" }"# ,
528+ "declares `session-driver` more than once" ,
529+ ) ,
530+ (
531+ "missing" ,
532+ r#"agent "worker" { argv "axe"; session-driver }"# ,
533+ "must contain exactly one positional string" ,
534+ ) ,
535+ (
536+ "non-string" ,
537+ r#"agent "worker" { argv "axe"; session-driver #true }"# ,
538+ "value must be a string" ,
539+ ) ,
540+ (
541+ "property" ,
542+ r#"agent "worker" { argv "axe"; session-driver "claude" mode="owner" }"# ,
543+ "must contain exactly one positional string" ,
544+ ) ,
545+ (
546+ "children" ,
547+ r#"agent "worker" { argv "axe"; session-driver "claude" { prompt "ignored" } }"# ,
548+ "must contain exactly one positional string" ,
549+ ) ,
550+ (
551+ "ding" ,
552+ r#"agent "worker" { argv "axe"; session-driver "claude"; ding }"# ,
553+ "declares both `session-driver` and `ding`" ,
554+ ) ,
555+ (
556+ "deliver" ,
557+ r#"agent "worker" { argv "axe"; session-driver "claude"; deliver "mcp" }"# ,
558+ "declares both `session-driver` and `deliver`" ,
559+ ) ,
560+ (
561+ "driver" ,
562+ r#"agent "worker" { session-driver "claude"; claude { prompt "go" } }"# ,
563+ "declares both `session-driver` and a typed driver" ,
564+ ) ,
565+ ] {
566+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
567+ write (
568+ tmp. path ( ) ,
569+ & format ! ( "agents/h/{name}/agent.kdl" ) ,
570+ declaration,
571+ ) ;
572+ let found = discover ( tmp. path ( ) ) ;
573+ assert ! ( found. specs. is_empty( ) , "{name}: {:?}" , found. specs) ;
574+ assert_eq ! ( found. errors. len( ) , 1 , "{name}: {:?}" , found. errors) ;
575+ assert ! (
576+ found. errors[ 0 ] . message. contains( expected) ,
577+ "{name}: expected {expected:?}, got {:?}" ,
578+ found. errors[ 0 ]
579+ ) ;
580+ }
581+ }
582+
583+ #[ test]
584+ fn session_driver_lowers_from_toml_and_json_and_rejects_null ( ) {
585+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
586+ write (
587+ tmp. path ( ) ,
588+ "agents/h/toml/agent.toml" ,
589+ "identity = \" toml\" \n host = \" h\" \n argv = [\" axe\" ]\n session_driver = \" codex\" \n " ,
590+ ) ;
591+ write (
592+ tmp. path ( ) ,
593+ "agents/h/json/agent.json" ,
594+ r#"{"identity":"json","host":"h","argv":["axe"],"session_driver":"pi"}"# ,
595+ ) ;
596+ write (
597+ tmp. path ( ) ,
598+ "agents/h/null/agent.json" ,
599+ r#"{"identity":"null","host":"h","argv":["axe"],"session_driver":null}"# ,
600+ ) ;
601+
602+ let found = discover ( tmp. path ( ) ) ;
603+ assert_eq ! ( find( & found. specs, "toml" ) . session_driver, Some ( SessionDriver :: Codex ) ) ;
604+ assert_eq ! ( find( & found. specs, "json" ) . session_driver, Some ( SessionDriver :: Pi ) ) ;
605+ assert_eq ! ( found. errors. len( ) , 1 , "{:?}" , found. errors) ;
606+ assert ! (
607+ found. errors[ 0 ]
608+ . message
609+ . contains( "field `session_driver` must not be null" ) ,
610+ "{:?}" ,
611+ found. errors[ 0 ]
612+ ) ;
613+ }
614+
615+ #[ test]
616+ fn typed_driver_blocks_reject_legacy_ding ( ) {
617+ for ( name, driver) in [
618+ ( "claude" , r#"claude { prompt "go" }"# ) ,
619+ ( "codex" , r#"codex { prompt "go" }"# ) ,
620+ ( "pi" , r#"pi { prompt "go" }"# ) ,
621+ ( "opencode" , r#"opencode { prompt "go" }"# ) ,
622+ ( "omp" , r#"omp { prompt "go" }"# ) ,
623+ ] {
624+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
625+ write (
626+ tmp. path ( ) ,
627+ & format ! ( "agents/h/{name}/agent.kdl" ) ,
628+ & format ! ( r#"agent "{name}" {{ ding; {driver} }}"# ) ,
629+ ) ;
630+ let found = discover ( tmp. path ( ) ) ;
631+ assert ! ( found. specs. is_empty( ) , "{name}: {:?}" , found. specs) ;
632+ assert_eq ! ( found. errors. len( ) , 1 , "{name}: {:?}" , found. errors) ;
633+ assert ! (
634+ found. errors[ 0 ]
635+ . message
636+ . contains( "declares both `ding` and a typed driver" ) ,
637+ "{name}: {:?}" ,
638+ found. errors[ 0 ]
639+ ) ;
640+ }
641+ }
642+
483643
484644#[ test]
485645fn compact_adopt_only_lifecycle_lowers_to_the_generated_agent_task ( ) {
0 commit comments