security = $security; $this->discovery = $discovery; } /** * Execute a tool call from the AI * * @param string $tool_name Name of the tool (generated from route) * @param array $params Parameters for the tool * @return array Result with 'success', 'data' or 'error' */ public function execute_tool($tool_name, array $params = array()) { // Check execution limit if ($this->execution_count >= self::MAX_EXECUTIONS) { return $this->error_response( 'EXECUTION_LIMIT_REACHED', 'Maximum number of tool executions reached in this request' ); } $this->execution_count++; // Validate the tool exists and is whitelisted $route_meta = $this->security->validate_tool($tool_name); if (!$route_meta) { return $this->error_response( 'TOOL_NOT_ALLOWED', "Tool '{$tool_name}' is not available or not whitelisted" ); } $route = $route_meta['route']; $method = $route_meta['method']; // Log execution attempt $this->log_execution('attempt', $tool_name, array( 'route' => $route, 'method' => $method, 'params' => $params, )); // Execute the route return $this->execute_route($route, $method, $params); } /** * Execute a REST API route internally * * @param string $route Route path * @param string $method HTTP method * @param array $params Parameters * @return array Result */ public function execute_route($route, $method, array $params = array()) { // Security check 1: Is route whitelisted? if (!$this->security->is_route_whitelisted($route, $method)) { // Try to match parameterized route $matched_route = $this->match_parameterized_route($route, $method); if (!$matched_route || !$this->security->is_route_whitelisted($matched_route, $method)) { return $this->error_response( 'ROUTE_NOT_WHITELISTED', "Route '{$method} {$route}' is not whitelisted for AI agent access" ); } } // Security check 2: Is route forbidden for this HTTP method? if ($this->discovery->is_forbidden_route($route, $method)) { return $this->error_response( 'ROUTE_FORBIDDEN', "Route '{$route}' is forbidden for security reasons" ); } // Build the request $request = $this->build_request($route, $method, $params); // Execute via REST API try { $response = rest_do_request($request); return $this->process_response($response); } catch (Exception $e) { $this->log_execution('error', $route, array( 'method' => $method, 'error' => $e->getMessage(), )); return $this->error_response( 'EXECUTION_FAILED', 'Failed to execute request: ' . $e->getMessage() ); } } /** * Build a WP_REST_Request object * * @param string $route Route path * @param string $method HTTP method * @param array $params Parameters * @return WP_REST_Request */ private function build_request($route, $method, array $params) { $request = new WP_REST_Request($method, $route); // Separate URL params from body params $url_params = array(); $body_params = array(); // Extract URL parameters from route pattern $url_param_names = $this->extract_url_param_names($route); foreach ($params as $key => $value) { if (in_array($key, $url_param_names)) { $url_params[$key] = $value; } else { $body_params[$key] = $value; } } // Set URL parameters foreach ($url_params as $key => $value) { $request->set_url_params(array($key => $value)); } // Set query or body parameters based on method if (in_array($method, array('GET', 'HEAD', 'DELETE'))) { $request->set_query_params($body_params); } else { $request->set_body_params($body_params); } // Set content type for POST/PUT/PATCH if (in_array($method, array('POST', 'PUT', 'PATCH'))) { $request->set_header('Content-Type', 'application/json'); } return $request; } /** * Extract URL parameter names from route pattern * * @param string $route * @return array */ private function extract_url_param_names($route) { $names = array(); if (preg_match_all('/\(\?P<([^>]+)>[^)]+\)/', $route, $matches)) { $names = $matches[1]; } return $names; } /** * Match a concrete path to a parameterized route pattern * * @param string $path Concrete path (e.g., /wp/v2/posts/123) * @param string $method HTTP method * @return string|null Matched route pattern or null */ private function match_parameterized_route($path, $method) { $routes = $this->security->get_enabled_routes(); foreach ($routes as $route) { if ($route['method'] !== strtoupper($method)) { continue; } // Convert route pattern to regex for matching $pattern = '#^' . $route['route'] . '$#'; if (preg_match($pattern, $path)) { return $route['route']; } } return null; } /** * Process the REST API response * * @param WP_REST_Response $response * @return array */ private function process_response($response) { $data = $response->get_data(); $status = $response->get_status(); $headers = $response->get_headers(); // Check for errors if ($status >= 400) { $error_message = 'Request failed'; $error_code = 'REQUEST_FAILED'; if (is_wp_error($data)) { $error_message = $data->get_error_message(); $error_code = $data->get_error_code(); } elseif (isset($data['message'])) { $error_message = $data['message']; $error_code = isset($data['code']) ? $data['code'] : 'API_ERROR'; } $this->log_execution('api_error', 'response', array( 'status' => $status, 'error' => $error_message, )); return array( 'success' => false, 'error' => array( 'code' => $error_code, 'message' => $error_message, 'status' => $status, ), ); } // Log success $this->log_execution('success', 'response', array( 'status' => $status, 'data_type' => gettype($data), )); // Truncate large responses for AI context $processed_data = $this->truncate_response_data($data); return array( 'success' => true, 'data' => $processed_data, 'meta' => array( 'status' => $status, 'total_results' => $this->count_results($data), ), ); } /** * Truncate large response data to prevent token overflow * * @param mixed $data * @param int $max_items Maximum items in arrays * @return mixed */ private function truncate_response_data($data, $max_items = 10) { if (is_array($data)) { // Check if it's a list of items if (isset($data[0]) && is_array($data[0])) { $total = count($data); $data = array_slice($data, 0, $max_items); // Add truncation notice if ($total > $max_items) { $data[] = array( '_truncated' => true, '_message' => sprintf('Showing %d of %d total results', $max_items, $total), ); } } // Recursively process nested arrays foreach ($data as $key => $value) { if (is_array($value)) { // Remove potentially large/unnecessary fields $fields_to_remove = array( 'content', 'rendered', 'raw', 'guid', '_links', 'yoast_head', 'yoast_head_json', ); foreach ($fields_to_remove as $field) { if (isset($value[$field]) && is_string($value[$field]) && strlen($value[$field]) > 500) { $value[$field] = substr($value[$field], 0, 500) . '... [truncated]'; } } $data[$key] = $value; } } } return $data; } /** * Count results in response data * * @param mixed $data * @return int|null */ private function count_results($data) { if (is_array($data) && isset($data[0])) { return count($data); } return null; } /** * Create error response * * @param string $code * @param string $message * @return array */ private function error_response($code, $message) { return array( 'success' => false, 'error' => array( 'code' => $code, 'message' => $message, ), ); } /** * Log execution for debugging * * @param string $type * @param string $context * @param array $data */ private function log_execution($type, $context, array $data = array()) { $this->execution_log[] = array( 'type' => $type, 'context' => $context, 'data' => $data, 'timestamp' => microtime(true), ); } /** * Get execution log * * @return array */ public function get_execution_log() { return $this->execution_log; } /** * Reset execution counter (for new request) */ public function reset_execution_count() { $this->execution_count = 0; $this->execution_log = array(); } /** * Get current execution count * * @return int */ public function get_execution_count() { return $this->execution_count; } /** * Execute multiple tool calls in sequence * * @param array $tool_calls Array of ['tool_name' => ..., 'params' => ...] * @return array Results for each tool call */ public function execute_tool_calls(array $tool_calls) { $results = array(); foreach ($tool_calls as $index => $call) { if (!isset($call['tool_name'])) { $results[$index] = $this->error_response( 'INVALID_TOOL_CALL', 'Tool call missing tool_name' ); continue; } $params = isset($call['params']) ? $call['params'] : array(); $results[$index] = $this->execute_tool($call['tool_name'], $params); } return $results; } }