PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.7
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.7
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / classes / abilities / Ability_Loader.php

Ability_Loader.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.7, at classes/abilities/Ability_Loader.php

234 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability Loader Trait
4 *
5 * @package zip-ai
6 */
7
8 namespace ZipAI\MCP\Classes\Abilities;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use ZipAI\MCP\Classes\Abilities\Abstract_Ability;
16
17 /**
18 * Trait Ability_Loader
19 */
20 trait Ability_Loader {
21
22 public function check_ability_format( $ability ) {
23 $id = $ability->get_id();
24 $canonical_actions = array(
25 // CRUD.
26 'list',
27 'get',
28 'create',
29 'update',
30 'delete',
31 // State.
32 'activate',
33 'deactivate',
34 'restore',
35 // Lifecycle.
36 'install',
37 'uninstall',
38 'upload',
39 // Data.
40 'import',
41 'export',
42 'flush',
43 'replace',
44 // Operations.
45 'check',
46 'clean',
47 'run',
48 'edit',
49 'read',
50 'search',
51 'scan',
52 );
53 $pattern = '/^[a-z0-9-]+\/(' . implode( '|', $canonical_actions ) . ')-[a-z0-9-]+$/';
54
55 if ( ! preg_match( $pattern, $id ) ) {
56 // Check if we are in export mode or debug mode.
57 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
58 trigger_error(
59 sprintf(
60 'Invalid Ability ID format: "%s" in class %s. Expected format: "{namespace}/{action}-{resource}" (e.g., "zipai/update-post_format", "zipai/list-block-patterns"). Action must be one of: %s.',
61 $id,
62 get_class( $ability ),
63 implode( ', ', $canonical_actions )
64 ),
65 E_USER_WARNING
66 );
67 }
68 return false;
69 }
70 return true;
71 }
72
73 /**
74 * Abilities skipped at registration. Hidden from MCP discovery entirely
75 * (LLM cannot see them, internal callers cannot use them either). Use the
76 * `internal` visibility flag in an ability's get_meta() to keep it
77 * registered for backend/server-only callers while hiding from the LLM.
78 *
79 * ExecuteRestRequest stays registered (internal callers like
80 * PageDeliveryService, ParallelPageBuilderService still need it); it is
81 * hidden from the LLM via meta visibility on the Laravel side instead.
82 *
83 * Currently empty — ExecuteAjaxAction and SearchEndpoints were removed
84 * outright (unused by the LLM and every internal caller).
85 */
86 protected static $disabled_abilities = array();
87
88 /**
89 * Build a basename index for disabled abilities.
90 *
91 * Accepts either `ClassName` or `Vendor\Ns\ClassName` entries and
92 * normalizes both to `ClassName` for the file-basename match used by
93 * directory scanning.
94 *
95 * @return array<string, true>
96 */
97 protected function get_disabled_abilities_index() {
98 $index = array();
99
100 foreach ( self::$disabled_abilities as $ability_class ) {
101 if ( ! is_string( $ability_class ) || '' === $ability_class ) {
102 continue;
103 }
104
105 $normalized_class_name = ltrim( $ability_class, '\\' );
106 $last_separator = strrpos( $normalized_class_name, '\\' );
107 $basename = false === $last_separator
108 ? $normalized_class_name
109 : substr( $normalized_class_name, $last_separator + 1 );
110
111 if ( '' !== $basename ) {
112 $index[ $basename ] = true;
113 }
114 }
115
116 return $index;
117 }
118
119 protected function load_abilities_from_dir( $directory, $namespace ) {
120 // Normalize directory path.
121 $directory = trailingslashit( $directory );
122 $disabled_abilities_index = $this->get_disabled_abilities_index();
123
124 // Check if directory exists.
125 if ( ! is_dir( $directory ) ) {
126 return;
127 }
128
129 // Use RecursiveDirectoryIterator for recursive scanning.
130 $directory_iterator = new \RecursiveDirectoryIterator( $directory, \RecursiveDirectoryIterator::SKIP_DOTS );
131 $iterator = new \RecursiveIteratorIterator( $directory_iterator );
132
133 foreach ( $iterator as $file_info ) {
134 if ( $file_info->isDir() || 'php' !== $file_info->getExtension() ) {
135 continue;
136 }
137
138 $file_path = $file_info->getPathname();
139 $class_name = $file_info->getBasename( '.php' );
140
141 // Skip index and handler files.
142 if ( 'index' === $class_name || 'handler' === $class_name ) {
143 continue;
144 }
145
146 if ( isset( $disabled_abilities_index[ $class_name ] ) ) {
147 continue;
148 }
149
150 // Calculate class namespace based on relative path.
151 $relative_path = str_replace( array( $directory, '.php' ), '', $file_path );
152 $path_parts = explode( DIRECTORY_SEPARATOR, $relative_path );
153 array_pop( $path_parts ); // Remove class name from parts.
154
155 // Capitalize each path part to match PSR-4 namespace convention.
156 $path_parts = array_map( 'ucfirst', $path_parts );
157
158 $sub_namespace = ! empty( $path_parts ) ? '\\' . implode( '\\', $path_parts ) : '';
159 $full_class_name = $namespace . $sub_namespace . '\\' . $class_name;
160
161 // Check if class exists.
162 if ( class_exists( $full_class_name ) ) {
163 // Instantiate the ability.
164 $ability = new $full_class_name();
165
166 // Check if it's an instance of Abstract_Ability.
167 if ( $ability instanceof Abstract_Ability ) {
168 // Collect meta data.
169 $meta = array(
170 'tool_type' => $ability->get_tool_type(),
171 'examples' => $ability->get_examples(),
172 'api_endpoint' => $ability->get_api_endpoint(),
173 );
174
175 // Add boost screens if defined.
176 if ( method_exists( $ability, 'get_boost_screens' ) ) {
177 $boost_screens = $ability->get_boost_screens();
178 if ( ! empty( $boost_screens ) ) {
179 $meta['boost_screens'] = $boost_screens;
180 }
181 }
182
183 // Add resource identifier if defined.
184 if ( method_exists( $ability, 'get_resource' ) ) {
185 $resource = $ability->get_resource();
186 if ( ! empty( $resource ) ) {
187 $meta['resource'] = $resource;
188 }
189 }
190
191 // Read-only sub-action allowlist for multiplexed abilities.
192 // Forwarded through meta → MCP tools/list → brain so the
193 // writes-require-approval gate can treat safe sub-actions
194 // (e.g. `action:"list"` on an otherwise-destructive tool)
195 // as reads. Empty for single-purpose abilities.
196 if ( method_exists( $ability, 'get_read_only_actions' ) ) {
197 $read_only_actions = $ability->get_read_only_actions();
198 if ( is_array( $read_only_actions ) && ! empty( $read_only_actions ) ) {
199 $meta['read_only_actions'] = array_values( $read_only_actions );
200 }
201 }
202
203 // Merge with any class-specific meta.
204 if ( method_exists( $ability, 'get_meta_data' ) ) {
205 $meta = array_merge( $meta, $ability->get_meta_data() );
206 }
207
208 // Add constraints if the method exists.
209 if ( method_exists( $ability, 'get_constraints' ) ) {
210 $meta['constraints'] = $ability->get_constraints();
211 }
212
213 // Validate ID format.
214 $this->check_ability_format( $ability );
215
216 // Register directly with wp_register_ability (no lazy loading).
217 wp_register_ability(
218 $ability->get_id(),
219 array(
220 'label' => $ability->get_label(),
221 'description' => $ability->get_description(),
222 'category' => $ability->get_category(),
223 'input_schema' => $ability->get_final_input_schema(),
224 'execute_callback' => array( $ability, 'handle_execute' ),
225 'permission_callback' => array( $ability, 'check_permission' ),
226 'meta' => $meta,
227 )
228 );
229 }
230 }
231 }
232 }
233 }
234