PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 All 37 releases
← All changes | includes/ai-copilot/abilities.php +58 -10 1.0.0 → 1.1.13 View file →
@@ -104,8 +104,53 @@
104 104 return trim( (string) $slug, '_' );
105 105 }
106 106
107 107 /**
108 + * Maps abilities to model-facing tool names that are unique within one
109 + * request.
110 + *
111 + * Stripping the namespace means two plugins can land on the same name
112 + * (`desktop-mode/get-site-context` and `acme/get-site-context`), and
113 + * providers reject the whole request when tool names repeat ("Tool names
114 + * must be unique"). OpenStation's own abilities claim their bare names
115 + * first, because the system prompt and progress labels refer to those.
116 + * Any later ability whose bare name is taken keeps its namespace
117 + * (`acme_get_site_context`) so it stays callable; one that still
118 + * collides is dropped.
119 + *
120 + * @param string[] $ability_names Fully-namespaced ability names.
121 + * @return array<string,string> Tool name => ability name, own abilities first.
122 + */
123 +function openstation_ai_unique_ability_tool_names( array $ability_names ) {
124 + $own = array();
125 + $others = array();
126 + foreach ( $ability_names as $ability_name ) {
127 + $ability_name = (string) $ability_name;
128 + if ( 0 === strpos( $ability_name, 'desktop-mode/' ) ) {
129 + $own[] = $ability_name;
130 + } else {
131 + $others[] = $ability_name;
132 + }
133 + }
134 +
135 + $map = array();
136 + foreach ( array_merge( $own, $others ) as $ability_name ) {
137 + $candidates = array(
138 + openstation_ai_ability_tool_name( $ability_name ),
139 + openstation_ai_ability_tool_name( str_replace( '/', '_', $ability_name ) ),
140 + );
141 + foreach ( $candidates as $tool_name ) {
142 + if ( '' !== $tool_name && ! isset( $map[ $tool_name ] ) ) {
143 + $map[ $tool_name ] = $ability_name;
144 + break;
145 + }
146 + }
147 + }
148 +
149 + return $map;
150 +}
151 +
152 +/**
108 153 * Permission callback: any logged-in user who can read the site.
109 154 *
110 155 * Mirrors the read-only search/navigation tools, which were ungated beyond the
111 156 * Copilot's own logged-in requirement.
@@ -152,9 +197,9 @@
152 197 'required' => array( 'query', 'offset' ),
153 198 'properties' => array(
154 199 'query' => array(
155 200 'type' => 'string',
156 - 'description' => 'Keyword search terms matched against the title and content (WordPress native search). Distil the user\'s request to the essential nouns — e.g. for "that post I wrote about making paella" pass "paella". Avoid stop-words and full sentences.',
201 + 'description' => 'Keyword search terms matched against the title and content (WordPress native search). Distil the user\'s request to the essential nouns — e.g. for "that post I wrote about making paella" pass "paella". Avoid stop-words and full sentences. An empty string lists everything, newest first — only for questions about who, how many or the latest, never to look for a topic.',
157 202 ),
158 203 'offset' => array(
159 204 'type' => 'integer',
160 205 'description' => 'Zero-based starting position. Use 0 for the first batch, 10 for the second, and so on.',
@@ -208,9 +253,9 @@
208 253 wp_register_ability(
209 254 'desktop-mode/search-posts',
210 255 array(
211 256 'label' => __( 'Search posts', 'desktop-mode' ),
212 - 'description' => 'Keyword-searches published WordPress blog posts by title and content (WordPress native search). Use this when the user is looking for content they or someone else wrote as a post or article. Pass the key search terms as `query`. Returns up to 10 matching posts with their title, a content excerpt, date, and URLs. If has_more is true, call again with the next offset.',
257 + 'description' => 'Keyword-searches published WordPress blog posts by title and content (WordPress native search). Use this when the user is looking for content they or someone else wrote as a post or article: pass the key search terms as `query`. Only for listing or counting questions ("how many posts do I have?") pass an empty `query`, which lists the newest posts (`total` is the full count). Returns up to 10 matching posts with their title, a content excerpt, date, and URLs. If has_more is true, call again with the next offset.',
213 258 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
214 259 'input_schema' => $query_offset_input,
215 260 'output_schema' => $search_output,
216 261 'execute_callback' => static function ( $input ) {
@@ -240,9 +285,9 @@
240 285 wp_register_ability(
241 286 'desktop-mode/search-comments',
242 287 array(
243 288 'label' => __( 'Search comments', 'desktop-mode' ),
244 - 'description' => 'Keyword-searches approved WordPress comments across ALL posts by their text (WordPress native search). Use this when the user remembers something a reader said but does not know which post it was on. Pass the distinctive words from the comment as `query`. Returns up to 10 matching comments with an excerpt, parent post title, and URLs. If has_more is true, call again with the next offset.',
289 + 'description' => 'Keyword-searches approved WordPress comments by their text (WordPress native search), across all posts the requesting user is allowed to read — comments on private, draft, or password-protected posts the user cannot access are excluded. Use this when the user remembers something a reader said but does not know which post it was on: pass the distinctive words from the comment as `query`. Also use it, with an empty `query`, for questions about the comments themselves — who has commented, the latest comments, how many there are (`total` is the full count). Returns up to 10 matching comments with the commenter\'s name (`author_name`), an excerpt, parent post title, and URLs. If has_more is true, call again with the next offset.',
245 290 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
246 291 'input_schema' => $query_offset_input,
247 292 'output_schema' => $search_output,
248 293 'execute_callback' => static function ( $input ) {
@@ -256,9 +301,9 @@
256 301 wp_register_ability(
257 302 'desktop-mode/search-comments-by-post',
258 303 array(
259 304 'label' => __( 'Search comments on a post', 'desktop-mode' ),
260 - 'description' => 'Keyword-searches approved comments on a SPECIFIC post by its WordPress ID. Use this when you have already identified a post (via search-posts) and the user\'s query also mentions something a reader said on that post — e.g. "I remember a comment on my Málaga post asking about the Alcazaba at night." Call search-posts first to find the post ID, then call this tool with that ID and the distinctive words as `query`. Much more precise than search-comments when the parent post is known. If has_more is true, call again with the next offset.',
305 + 'description' => 'Keyword-searches approved comments on a SPECIFIC post by its WordPress ID (the post must be readable by the requesting user; an unreadable or nonexistent post returns an empty result). Use this when the user describes both a post and something a reader said on it: identify the post with search-posts, then pass its ID and the distinctive words of the comment as `query`. More precise than search-comments whenever the parent post is known. Each match carries the commenter\'s name (`author_name`) and an excerpt. If has_more is true, call again with the next offset.',
261 306 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
262 307 'input_schema' => array(
263 308 'type' => 'object',
264 309 'additionalProperties' => false,
@@ -324,9 +369,9 @@
324 369 wp_register_ability(
325 370 'desktop-mode/search-wporg-plugins',
326 371 array(
327 372 'label' => __( 'Search WordPress.org plugins', 'desktop-mode' ),
328 - 'description' => 'Searches the official WordPress.org plugin directory. Use this when the user asks for a plugin recommendation — e.g. "is there a plugin for SEO?", "find me a backup plugin", "a caching plugin", "form builder". Returns up to 10 plugins with name, description, rating, active install count, and an admin URL that opens the plugin-info / install screen directly.',
373 + 'description' => 'Searches the official WordPress.org plugin directory. Use this when the user asks for a plugin recommendation — e.g. "is there a plugin for SEO?", "find me a backup plugin", "a caching plugin", "form builder". Returns up to 10 plugins with name, description, rating (0-100) and stars (0-5), active install count, and an admin URL that opens the plugin-info / install screen directly.',
329 374 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
330 375 'input_schema' => array(
331 376 'type' => 'object',
332 377 'additionalProperties' => false,
@@ -341,9 +386,9 @@
341 386 'output_schema' => openstation_ai_ability_output_schema(
342 387 array(
343 388 'results' => array(
344 389 'type' => 'array',
345 - 'description' => 'Matching plugins with name, description, rating, installs, and admin URL.',
390 + 'description' => 'Matching plugins with name, description, rating (0-100), stars (0-5), installs, and admin URL.',
346 391 ),
347 392 'count' => array( 'type' => 'integer' ),
348 393 )
349 394 ),
@@ -399,11 +444,14 @@
399 444 /**
400 445 * Registers the comment-spam analysis ability.
401 446 *
402 447 * Not offered to the model during a search turn (see
403 - * {@see openstation_ai_search_ability_names()}); the moderation pipeline
404 - * resolves and executes it directly ({@see openstation_ai_analyze_comment_now()}
405 - * runs through it). Exposed in the abilities catalog for observability + reuse.
448 + * {@see openstation_ai_search_ability_names()}) — a search turn can be driven
449 + * by attacker-controlled comment text, and this one spends provider tokens.
450 + * It is an on-demand ability: a caller with `moderate_comments` runs it for one
451 + * comment and gets the verdict back. Automatic scoring on comment save was
452 + * removed (`docs/migration-comments-ai-scoring.md`), so nothing in the plugin
453 + * invokes it on its own.
406 454 *
407 455 * @return void
408 456 */
409 457 function openstation_ai_register_comment_analysis_ability() {
@@ -410,9 +458,9 @@
410 458 wp_register_ability(
411 459 'desktop-mode/analyze-comment',
412 460 array(
413 461 'label' => __( 'Analyze comment for spam', 'desktop-mode' ),
414 - 'description' => 'Runs the AI spam/harm analysis for a single comment and returns its structured verdict ({ topic, ai_summary, harmful, spam }). Used by comment moderation to score incoming comments.',
462 + 'description' => 'Runs the AI spam/harm analysis for a single comment and returns its structured verdict ({ topic, ai_summary, harmful, spam }). On-demand only: nothing in the plugin runs it automatically.',
415 463 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
416 464 'input_schema' => array(
417 465 'type' => 'object',
418 466 'additionalProperties' => false,