@@ -1070,3 +1070,138 @@ async fn replay_fixture_blocks_a2a_discover_egress_host_mismatches() {
10701070
10711071 server. abort ( ) ;
10721072}
1073+
1074+ #[ tokio:: test]
1075+ #[ serial]
1076+ async fn replay_fixture_denies_web_fetch_loopback_when_allow_private_false ( ) {
1077+ // Ensure this test doesn't depend on external approval-mode env.
1078+ let _mode = EnvVarGuard :: set ( "LOOPFORGE_APPROVAL_MODE" , "off" ) ;
1079+ let _allow = EnvVarGuard :: set ( "LOOPFORGE_APPROVAL_ALLOW" , "" ) ;
1080+
1081+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
1082+ "fixtures/replay/session_ssrf_web_fetch_loopback_denied.json"
1083+ ) ) ;
1084+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
1085+
1086+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
1087+ let ( agent, _paths, workspace_root) = fixture_agent (
1088+ & tmp,
1089+ server. base_url . clone ( ) ,
1090+ rexos:: security:: SecurityConfig :: default ( ) ,
1091+ ) ;
1092+
1093+ let session_id = "s-replay-ssrf-web-fetch" ;
1094+ agent
1095+ . set_session_allowed_tools ( session_id, vec ! [ "web_fetch" . to_string( ) ] )
1096+ . unwrap ( ) ;
1097+
1098+ let err = agent
1099+ . run_session (
1100+ workspace_root,
1101+ session_id,
1102+ None ,
1103+ "fetch loopback" ,
1104+ TaskKind :: Coding ,
1105+ )
1106+ . await
1107+ . unwrap_err ( ) ;
1108+ let err_text = err. to_string ( ) ;
1109+ assert ! (
1110+ err_text. contains( "loopback/private address" ) ,
1111+ "expected loopback/private deny, got: {err_text}"
1112+ ) ;
1113+ assert ! (
1114+ err_text. contains( "web_fetch" ) ,
1115+ "expected tool name in error, got: {err_text}"
1116+ ) ;
1117+
1118+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
1119+ assert_eq ! ( requests. len( ) , 1 , "expected one chat completions call" ) ;
1120+ assert_eq ! (
1121+ compact_request( & requests[ 0 ] ) ,
1122+ json!( {
1123+ "model" : "fixture-model" ,
1124+ "temperature" : 0.0 ,
1125+ "tools" : [ {
1126+ "name" : "web_fetch" ,
1127+ "type" : "function" ,
1128+ "param_type" : "object" ,
1129+ "required" : [ "url" ] ,
1130+ "properties" : [ "allow_private" , "max_bytes" , "timeout_ms" , "url" ] ,
1131+ "additional_properties" : false ,
1132+ } ] ,
1133+ "message_roles" : [ "user" ] ,
1134+ "assistant_tool_calls" : [ ] ,
1135+ "tool_messages" : [ ] ,
1136+ } )
1137+ ) ;
1138+
1139+ server. abort ( ) ;
1140+ }
1141+
1142+ #[ tokio:: test]
1143+ #[ serial]
1144+ async fn replay_fixture_denies_a2a_discover_loopback_when_allow_private_false ( ) {
1145+ let _mode = EnvVarGuard :: set ( "LOOPFORGE_APPROVAL_MODE" , "off" ) ;
1146+ let _allow = EnvVarGuard :: set ( "LOOPFORGE_APPROVAL_ALLOW" , "" ) ;
1147+
1148+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
1149+ "fixtures/replay/session_ssrf_a2a_discover_loopback_denied.json"
1150+ ) ) ;
1151+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
1152+
1153+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
1154+ let ( agent, _paths, workspace_root) = fixture_agent (
1155+ & tmp,
1156+ server. base_url . clone ( ) ,
1157+ rexos:: security:: SecurityConfig :: default ( ) ,
1158+ ) ;
1159+
1160+ let session_id = "s-replay-ssrf-a2a-discover" ;
1161+ agent
1162+ . set_session_allowed_tools ( session_id, vec ! [ "a2a_discover" . to_string( ) ] )
1163+ . unwrap ( ) ;
1164+
1165+ let err = agent
1166+ . run_session (
1167+ workspace_root,
1168+ session_id,
1169+ None ,
1170+ "discover loopback" ,
1171+ TaskKind :: Coding ,
1172+ )
1173+ . await
1174+ . unwrap_err ( ) ;
1175+ let err_text = err. to_string ( ) ;
1176+ assert ! (
1177+ err_text. contains( "loopback/private address" ) ,
1178+ "expected loopback/private deny, got: {err_text}"
1179+ ) ;
1180+ assert ! (
1181+ err_text. contains( "a2a_discover" ) ,
1182+ "expected tool name in error, got: {err_text}"
1183+ ) ;
1184+
1185+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
1186+ assert_eq ! ( requests. len( ) , 1 , "expected one chat completions call" ) ;
1187+ assert_eq ! (
1188+ compact_request( & requests[ 0 ] ) ,
1189+ json!( {
1190+ "model" : "fixture-model" ,
1191+ "temperature" : 0.0 ,
1192+ "tools" : [ {
1193+ "name" : "a2a_discover" ,
1194+ "type" : "function" ,
1195+ "param_type" : "object" ,
1196+ "required" : [ "url" ] ,
1197+ "properties" : [ "allow_private" , "url" ] ,
1198+ "additional_properties" : false ,
1199+ } ] ,
1200+ "message_roles" : [ "user" ] ,
1201+ "assistant_tool_calls" : [ ] ,
1202+ "tool_messages" : [ ] ,
1203+ } )
1204+ ) ;
1205+
1206+ server. abort ( ) ;
1207+ }
0 commit comments