*/ private $settings; /** @param array $settings */ public function __construct( array $settings = [] ) { $this->settings = $settings ? $settings : WTeam_Executor_Settings::get_settings(); } /** @return array> */ public static function definitions() { $definitions = [ 'config_get_debug' => [ 'kind' => 'read', 'rule' => 'backend', 'handler' => 'execute_config_get_debug', 'label' => __( 'wp-config debug read', 'wteam-executor' ), 'description' => 'Read only whitelisted non-secret WordPress debug/environment constants from wp-config.php.', 'arguments' => new stdClass(), 'guidance' => 'Use to inspect only safe wp-config.php constants. Never request or expose the full configuration file.', ], 'file_read' => [ 'kind' => 'read', 'rule' => 'file_reads', 'handler' => 'execute_file_read', 'label' => __( 'source/log read', 'wteam-executor' ), 'description' => 'Read a bounded safe WordPress text/log excerpt. wp-config.php is available only as a redacted configuration profile.', 'arguments' => [ 'path' => 'portable path', 'mode' => 'head|tail|grep|config|lines|tail_lines', 'start_line' => 'for lines mode', 'max_lines' => '1..500', 'grep' => 'string' ], 'guidance' => 'Use bounded source/log evidence. Use tail for logs. Diagnostic reads never modify the target file.', ], 'database_read' => [ 'kind' => 'read', 'rule' => 'database_reads', 'handler' => 'execute_database_read', 'label' => __( 'database read', 'wteam-executor' ), 'description' => 'Run bounded structured table_list, table_schema, select_rows or count_rows operations against allowed tables in the active WordPress site database.', 'arguments' => [ 'operation' => 'table_list|table_schema|select_rows|count_rows', 'table' => 'string', 'columns' => 'array', 'where' => 'array<0..1 {column,operator,value}>', 'order_by' => 'string', 'order' => 'ASC|DESC', 'limit' => 'integer' ], 'guidance' => 'Use only structured bounded reads. Never supply SQL. Discover tables with table_list and inspect columns with table_schema. Use count_rows when only an exact number of matching rows is needed; it returns one count and no row values. Use select_rows for explicit columns and at most one narrow filter. Never request password hashes, API keys, tokens, cookies, card data or unrelated personal data.', ], ]; return apply_filters( 'wteam_executor_tool_definitions', $definitions ); } /** @return array */ public static function tool_names() { return array_keys( self::definitions() ); } /** @return array */ public static function user_facing_replacements() { $result = []; foreach ( self::definitions() as $name => $definition ) { $result[ $name ] = isset( $definition['label'] ) ? (string) $definition['label'] : $name; } return $result; } /** @return array> */ public function capabilities() { $capabilities = []; foreach ( self::definitions() as $name => $definition ) { $required_controls = $this->required_controls_for_definition( $definition ); $capabilities[] = [ 'name' => $name, 'kind' => isset( $definition['kind'] ) ? (string) $definition['kind'] : 'read', 'enabled' => $this->is_tool_enabled( $name ), 'guidance' => isset( $definition['guidance'] ) ? (string) $definition['guidance'] : ( isset( $definition['description'] ) ? (string) $definition['description'] : '' ), 'arguments' => isset( $definition['arguments'] ) ? $definition['arguments'] : new stdClass(), 'required_controls' => $required_controls, ]; } return $capabilities; } /** * Return the exact user-visible controls required by a backend capability. * * @param array $definition Tool definition. * @return array> */ private function required_controls_for_definition( array $definition ) { $controls = [ [ 'label' => __( 'Enable server-side assistant tools', 'wteam-executor' ), 'enabled' => ! empty( $this->settings['assistant_backend_tools_enabled'] ), ], ]; $rule = isset( $definition['rule'] ) ? (string) $definition['rule'] : 'backend'; if ( 'file_reads' === $rule ) { $controls[] = [ 'label' => __( 'Enable file and log reads', 'wteam-executor' ), 'enabled' => ! empty( $this->settings['file_reads_enabled'] ) ]; } elseif ( 'database_reads' === $rule ) { $controls[] = [ 'label' => __( 'Enable database reads', 'wteam-executor' ), 'enabled' => ! empty( $this->settings['database_reads_enabled'] ) ]; } return apply_filters( 'wteam_executor_backend_tool_controls', $controls, $definition, $this->settings ); } /** @param string $name @return bool */ private function is_tool_enabled( $name ) { $definitions = self::definitions(); if ( empty( $definitions[ $name ] ) || empty( $this->settings['assistant_backend_tools_enabled'] ) ) { return false; } $definition = $definitions[ $name ]; if ( isset( $definition['enabled_callback'] ) && is_callable( $definition['enabled_callback'] ) ) { return (bool) call_user_func( $definition['enabled_callback'], $this->settings, $definition ); } $rule = isset( $definition['rule'] ) ? (string) $definition['rule'] : 'backend'; if ( 'backend' === $rule ) { return true; } if ( 'file_reads' === $rule ) { return ! empty( $this->settings['file_reads_enabled'] ); } if ( 'database_reads' === $rule ) { return ( new WTeam_Executor_Data_Policy( $this->settings ) )->reads_enabled(); } return false; } /** * @param string $name * @param array $arguments * @return array|WP_Error */ public function execute( $name, array $arguments ) { $name = sanitize_key( (string) $name ); $definitions = self::definitions(); if ( empty( $definitions[ $name ] ) ) { return new WP_Error( 'wteam_executor_unknown_tool', __( 'The assistant requested an unknown backend tool.', 'wteam-executor' ) ); } if ( ! $this->is_tool_enabled( $name ) ) { return new WP_Error( 'wteam_executor_backend_tool_disabled', __( 'This backend capability is disabled by the current Executor settings.', 'wteam-executor' ) ); } if ( isset( $definitions[ $name ]['callback'] ) && is_callable( $definitions[ $name ]['callback'] ) ) { return call_user_func( $definitions[ $name ]['callback'], $arguments, $this->settings ); } $handler = isset( $definitions[ $name ]['handler'] ) ? (string) $definitions[ $name ]['handler'] : ''; if ( '' === $handler || ! method_exists( $this, $handler ) ) { return new WP_Error( 'wteam_executor_tool_handler_missing', __( 'The backend capability is registered without a valid handler.', 'wteam-executor' ) ); } return $this->{$handler}( $arguments ); } /** @return array|WP_Error */ private function execute_config_get_debug( array $arguments ) { unset( $arguments ); return ( new WTeam_Executor_Config_Reader() )->get_debug_state(); } /** @return array|WP_Error */ private function execute_file_read( array $arguments ) { $mode = isset( $arguments['mode'] ) ? sanitize_key( (string) $arguments['mode'] ) : 'head'; $reader = new WTeam_Executor_File_Reader(); $result = $reader->read( [ 'path' => isset( $arguments['path'] ) ? (string) $arguments['path'] : '', 'mode' => $mode, 'start_line' => isset( $arguments['start_line'] ) ? max( 1, (int) $arguments['start_line'] ) : 1, 'max_lines' => isset( $arguments['max_lines'] ) ? max( 1, min( 500, (int) $arguments['max_lines'] ) ) : ( in_array( $mode, [ 'lines', 'tail_lines' ], true ) ? 160 : 120 ), 'grep' => isset( $arguments['grep'] ) ? (string) $arguments['grep'] : '', ] ); return is_array( $result ) ? $result : new WP_Error( 'wteam_executor_file_read_failed', __( 'The assistant file reader returned an invalid result.', 'wteam-executor' ) ); } /** @return array|WP_Error */ private function execute_database_read( array $arguments ) { $policy = new WTeam_Executor_Data_Policy( $this->settings ); $reader = new WTeam_Executor_Data_Reader( $policy ); return $reader->read( [ 'type' => 'database', 'operation' => isset( $arguments['operation'] ) ? sanitize_key( (string) $arguments['operation'] ) : 'table_list', 'table' => isset( $arguments['table'] ) ? (string) $arguments['table'] : '', 'columns' => isset( $arguments['columns'] ) && is_array( $arguments['columns'] ) ? $arguments['columns'] : [], 'where' => isset( $arguments['where'] ) && is_array( $arguments['where'] ) ? $arguments['where'] : [], 'order_by' => isset( $arguments['order_by'] ) ? (string) $arguments['order_by'] : '', 'order' => isset( $arguments['order'] ) ? (string) $arguments['order'] : 'ASC', 'limit' => isset( $arguments['limit'] ) ? (int) $arguments['limit'] : $policy->max_read_rows(), ] ); } }