1616/**
1717 * Resolves and executes WordPress Abilities API function calls from AI models.
1818 *
19+ * This class must be instantiated with the specific abilities that the AI model
20+ * is allowed to execute, ensuring that only explicitly specified abilities can
21+ * be called. This prevents the model from executing arbitrary abilities.
22+ *
1923 * @since 7.0.0
2024 */
2125class WP_AI_Client_Ability_Function_Resolver {
@@ -28,6 +32,35 @@ class WP_AI_Client_Ability_Function_Resolver {
2832 */
2933 private const ABILITY_PREFIX = 'wpab__ ' ;
3034
35+ /**
36+ * Map of allowed ability names for this instance.
37+ *
38+ * Keys are ability name strings, values are `true` for O(1) lookup.
39+ *
40+ * @since 7.0.0
41+ * @var array<string, true>
42+ */
43+ private array $ allowed_abilities ;
44+
45+ /**
46+ * Constructor.
47+ *
48+ * @since 7.0.0
49+ *
50+ * @param WP_Ability|string ...$abilities The abilities that this resolver is allowed to execute.
51+ */
52+ public function __construct ( ...$ abilities ) {
53+ $ this ->allowed_abilities = array ();
54+
55+ foreach ( $ abilities as $ ability ) {
56+ if ( $ ability instanceof WP_Ability ) {
57+ $ this ->allowed_abilities [ $ ability ->get_name () ] = true ;
58+ } elseif ( is_string ( $ ability ) ) {
59+ $ this ->allowed_abilities [ $ ability ] = true ;
60+ }
61+ }
62+ }
63+
3164 /**
3265 * Checks if a function call is an ability call.
3366 *
@@ -36,7 +69,7 @@ class WP_AI_Client_Ability_Function_Resolver {
3669 * @param FunctionCall $call The function call to check.
3770 * @return bool True if the function call is an ability call, false otherwise.
3871 */
39- public static function is_ability_call ( FunctionCall $ call ): bool {
72+ public function is_ability_call ( FunctionCall $ call ): bool {
4073 $ name = $ call ->getName ();
4174 if ( null === $ name ) {
4275 return false ;
@@ -48,16 +81,20 @@ public static function is_ability_call( FunctionCall $call ): bool {
4881 /**
4982 * Executes a WordPress ability from a function call.
5083 *
84+ * Only abilities that were specified in the constructor are allowed to be
85+ * executed. If the ability is not in the allowed list, an error response
86+ * with code `ability_not_allowed` is returned.
87+ *
5188 * @since 7.0.0
5289 *
5390 * @param FunctionCall $call The function call to execute.
5491 * @return FunctionResponse The response from executing the ability.
5592 */
56- public static function execute_ability ( FunctionCall $ call ): FunctionResponse {
93+ public function execute_ability ( FunctionCall $ call ): FunctionResponse {
5794 $ function_name = $ call ->getName () ?? 'unknown ' ;
5895 $ function_id = $ call ->getId () ?? 'unknown ' ;
5996
60- if ( ! self :: is_ability_call ( $ call ) ) {
97+ if ( ! $ this -> is_ability_call ( $ call ) ) {
6198 return new FunctionResponse (
6299 $ function_id ,
63100 $ function_name ,
@@ -69,7 +106,20 @@ public static function execute_ability( FunctionCall $call ): FunctionResponse {
69106 }
70107
71108 $ ability_name = self ::function_name_to_ability_name ( $ function_name );
72- $ ability = wp_get_ability ( $ ability_name );
109+
110+ if ( ! isset ( $ this ->allowed_abilities [ $ ability_name ] ) ) {
111+ return new FunctionResponse (
112+ $ function_id ,
113+ $ function_name ,
114+ array (
115+ /* translators: %s: ability name */
116+ 'error ' => sprintf ( __ ( 'Ability "%s" was not specified in the allowed abilities list. ' ), $ ability_name ),
117+ 'code ' => 'ability_not_allowed ' ,
118+ )
119+ );
120+ }
121+
122+ $ ability = wp_get_ability ( $ ability_name );
73123
74124 if ( ! $ ability instanceof WP_Ability ) {
75125 return new FunctionResponse (
@@ -113,15 +163,17 @@ public static function execute_ability( FunctionCall $call ): FunctionResponse {
113163 * @param Message $message The message to check.
114164 * @return bool True if the message contains ability calls, false otherwise.
115165 */
116- public static function has_ability_calls ( Message $ message ): bool {
117- return null !== array_find (
118- $ message -> getParts (),
119- static function ( MessagePart $ part ): bool {
120- return $ part -> getType ()-> isFunctionCall ()
121- && $ part -> getFunctionCall () instanceof FunctionCall
122- && self :: is_ability_call ( $ part -> getFunctionCall () );
166+ public function has_ability_calls ( Message $ message ): bool {
167+ foreach ( $ message -> getParts () as $ part ) {
168+ if ( $ part -> getType ()-> isFunctionCall () ) {
169+ $ function_call = $ part-> getFunctionCall ();
170+ if ( $ function_call instanceof FunctionCall && $ this -> is_ability_call ( $ function_call ) ) {
171+ return true ;
172+ }
123173 }
124- );
174+ }
175+
176+ return false ;
125177 }
126178
127179 /**
@@ -132,14 +184,14 @@ static function ( MessagePart $part ): bool {
132184 * @param Message $message The message containing function calls.
133185 * @return Message A new message with function responses.
134186 */
135- public static function execute_abilities ( Message $ message ): Message {
187+ public function execute_abilities ( Message $ message ): Message {
136188 $ response_parts = array ();
137189
138190 foreach ( $ message ->getParts () as $ part ) {
139191 if ( $ part ->getType ()->isFunctionCall () ) {
140192 $ function_call = $ part ->getFunctionCall ();
141193 if ( $ function_call instanceof FunctionCall ) {
142- $ function_response = self :: execute_ability ( $ function_call );
194+ $ function_response = $ this -> execute_ability ( $ function_call );
143195 $ response_parts [] = new MessagePart ( $ function_response );
144196 }
145197 }
0 commit comments