-
Notifications
You must be signed in to change notification settings - Fork 55
Add initial core abilities for WordPress 6.9 #108
Changes from 7 commits
cfff125
1957efb
299ecc9
7f928ef
19f665a
033f653
dbf97f7
28b6040
93e85df
53d37fb
6dced81
4dce755
00fa5c1
afd83c9
766490b
8f09665
b7b69a1
73a2092
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| <?php | ||
| /** | ||
| * Core Abilities registration. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Abilities_API | ||
| * @since 0.3.0 | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| /** | ||
| * Registers the default core abilities that ship with the Abilities API. | ||
| * | ||
| * @since 0.3.0 | ||
| */ | ||
| // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core. | ||
| class WP_Core_Abilities { | ||
| /** | ||
| * Category slugs for core abilities. | ||
| * | ||
| * @since 0.3.0 | ||
| */ | ||
| public const CATEGORY_SITE = 'site'; | ||
| public const CATEGORY_USER = 'user'; | ||
|
Jameswlepage marked this conversation as resolved.
|
||
| /** | ||
| * Registers the core abilities categories. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function register_category(): void { | ||
| // Site-related capabilities | ||
| wp_register_ability_category( | ||
| self::CATEGORY_SITE, | ||
| array( | ||
| 'label' => __( 'Site' ), | ||
| 'description' => __( 'Abilities that retrieve or modify site information and settings.' ), | ||
| ) | ||
| ); | ||
|
|
||
| // User-related capabilities | ||
| wp_register_ability_category( | ||
| self::CATEGORY_USER, | ||
| array( | ||
| 'label' => __( 'User' ), | ||
| 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), | ||
| ) | ||
| ); | ||
|
Jameswlepage marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /** | ||
| * Registers the default core abilities. | ||
| * | ||
| * @since 0.3.0 | ||
|
Jameswlepage marked this conversation as resolved.
Outdated
|
||
| * | ||
| * @return void | ||
| */ | ||
| public static function register(): void { | ||
| self::register_get_site_info(); | ||
| self::register_get_current_user_info(); | ||
| self::register_get_environment_info(); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the `wp/get-site-info` ability. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| protected static function register_get_site_info(): void { | ||
| $fields = array( | ||
| 'name', | ||
| 'description', | ||
| 'url', | ||
| 'wpurl', | ||
| 'admin_email', | ||
| 'charset', | ||
| 'language', | ||
| 'version', | ||
| ); | ||
|
|
||
| wp_register_ability( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be duplicating the site settings REST endpoint. What is the value of exposing this here when there is already a way to expose this information.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great Q! If you're asking from a more philosophical POV ("why do Abilities even exist"), then the answer is similar to "What is the value of having REST endpoints when there's already a php function and ajax?". The tl;dr goal of the Abilities API is to create a sort of "Generic API" abstraction + registry, the idea being that instead of needed to reregister/expose the same functionality to various last-mile APIs (REST, admin-ajax, graphql, PHP in theme/plugins, MCP/A2A, Command Palette, and whatever future tech integrations that come up with). It's standardized and reliable, like hooks but for functionality, so all anybody needs is a generic adapter and it'l work without any domain knowledge of the ability itself. In fact, I'd hope and expect to see the internals of many of our existing REST endpoints updated to internally use the Abilities API in 7.0 and beyond. If you're asking from a more practical POV (e.g. regarding the shape of the input/output schemas), I think we're open to evolution here. I believe we want something that it generic to make sense in multiple context, and not just a single one of either REST, MCP, or Command Palette. |
||
| 'wp/get-site-info', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should our prefix be
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m biased and prefer
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If Gutenberg uses core as a prefix, we probably should follow that convention because it's the more modern implementation on WordPress. If anybody has strong feelings, let me know. Will roll it back to core, but not super opinionated here. |
||
| array( | ||
| 'label' => __( 'Get Site Information' ), | ||
| 'description' => __( 'Returns a single site information field configured in WordPress (e.g., site name, URL, version) for display or diagnostics.' ), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly to #108 (comment), I'm wondering if we'd get more mileage out of this if it returned an entire array of blog_info / site health, and instead used a The obvious use case is LLMs/REST where you don't want a bunch of round trips for basic data, but I think it's helpful beyond that. Semantically, it lets '-info' align better with the other two abilities. Also, a shaped output_schema with the actual properties and matching types is better for both LLMs, traditional tools, and humans to reason with vs an Thoughts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. It was the first ability I wrote, so missing consistency and not self documenting which I think is a big unlock. Kind of 'layered' but I think that's OK in this situation.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, if we make this change, then REST would require a weird empty GET /run?input # All fields This could be a nonbreaking improvement in the future and imo shouldn't block
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the problem is that when you provide empty |
||
| 'category' => self::CATEGORY_SITE, | ||
| 'input_schema' => array( | ||
| 'type' => 'object', | ||
|
gziolo marked this conversation as resolved.
|
||
| 'properties' => array( | ||
| 'field' => array( | ||
| 'type' => 'string', | ||
| 'enum' => $fields, | ||
| 'description' => __( 'The site information field to retrieve.' ), | ||
| ), | ||
| ), | ||
|
Jameswlepage marked this conversation as resolved.
Outdated
|
||
| 'required' => array( 'field' ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'required' => array( 'field', 'value' ), | ||
| 'properties' => array( | ||
| 'field' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The requested site information field.' ), | ||
| ), | ||
| 'value' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The string value of the requested site information field.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => static function ( $input = array() ): array { | ||
| $field = $input['field']; | ||
| $value = get_bloginfo( $field ); | ||
|
|
||
| return array( | ||
| 'field' => $field, | ||
| 'value' => $value, | ||
| ); | ||
| }, | ||
| 'permission_callback' => static function (): bool { | ||
| return current_user_can( 'manage_options' ); | ||
| }, | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => true, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the `wp/get-current-user-info` ability. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| protected static function register_get_current_user_info(): void { | ||
| wp_register_ability( | ||
| 'wp/get-current-user-info', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if it makes sense to generalize this to I don't think we need both, and implementing it this way will make it easier to handle the "agent user" pattern that @gziolo brought up in #108 (comment)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it’s perfectly fine to start with supporting only the current user because it has simple permission check model. There isn’t anything sensitive about asking for your info. If any, we could drop the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However I guess you are comparing it to the site info where such filtering by field is possible. I left my comment earlier, the downside of the shape proposed for site info is that the input and output schema describes more the shape of the response rather what individual fields represent. It’s much richer for the user as every field has description explaining their role.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed it - good optionality now, but I like starting as direct/upfront/atomic as possible. |
||
| array( | ||
| 'label' => __( 'Get Current User Information' ), | ||
| 'description' => __( 'Returns basic profile details for the current authenticated user to support personalization, auditing, and access-aware behavior.' ), | ||
| 'category' => self::CATEGORY_USER, | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'required' => array( 'id', 'display_name', 'user_nicename', 'user_login', 'roles', 'locale' ), | ||
| 'properties' => array( | ||
| 'id' => array( | ||
| 'type' => 'integer', | ||
| 'description' => __( 'The user ID.' ), | ||
| ), | ||
| 'display_name' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The display name of the user.' ), | ||
| ), | ||
| 'user_nicename' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The URL-friendly name for the user.' ), | ||
| ), | ||
| 'user_login' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The login username for the user.' ), | ||
| ), | ||
| 'roles' => array( | ||
| 'type' => 'array', | ||
| 'description' => __( 'The roles assigned to the user.' ), | ||
| 'items' => array( | ||
| 'type' => 'string', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Roles is an array, not a string. See: https://core.trac.wordpress.org/browser/trunk/src/wp-includes/class-wp-user.php#L82
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @aaronjorbin, what we have seems correct, roles is of type array with each item being a string. Feel free to correct me if I'm missing anything. |
||
| ), | ||
| ), | ||
| 'locale' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The locale string for the user, such as en_US.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => static function (): array { | ||
| $current_user = wp_get_current_user(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to be interesting to see how it plays out in practice with AI clients that use some access tokens. It might happen that this will report back as an agent user 😄
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related: #108 (comment) Switching to |
||
|
|
||
| return array( | ||
| 'id' => $current_user->ID, | ||
| 'display_name' => $current_user->display_name, | ||
| 'user_nicename' => $current_user->user_nicename, | ||
| 'user_login' => $current_user->user_login, | ||
| 'roles' => $current_user->roles, | ||
| 'locale' => get_user_locale( $current_user ), | ||
| ); | ||
| }, | ||
| 'permission_callback' => static function (): bool { | ||
| return is_user_logged_in(); | ||
| }, | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => false, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the `wp/get-environment-info` ability. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| protected static function register_get_environment_info(): void { | ||
| wp_register_ability( | ||
| 'wp/get-environment-info', | ||
| array( | ||
| 'label' => __( 'Get Environment Info' ), | ||
| 'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version).' ), | ||
| 'category' => self::CATEGORY_SITE, | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'required' => array( 'environment', 'php_version', 'db_server_info', 'wp_version' ), | ||
| 'properties' => array( | ||
| 'environment' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site\'s runtime environment classification (e.g., production, staging, development).' ), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use examples here, too.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can only be one of four values. I think it's valueable to make that clearer.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ), | ||
| 'php_version' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The PHP runtime version executing WordPress.' ), | ||
| ), | ||
| 'db_server_info' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The database server vendor and version string reported by the driver (e.g., “8.0.34”, “10.11.6-MariaDB”).' ), | ||
|
Jameswlepage marked this conversation as resolved.
Outdated
|
||
| ), | ||
| 'wp_version' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The WordPress core version running on this site.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => static function (): array { | ||
| global $wpdb; | ||
|
|
||
| $env = wp_get_environment_type(); | ||
| $php_version = phpversion(); | ||
| $db_server_info = ''; | ||
| if ( isset( $wpdb ) && is_object( $wpdb ) && method_exists( $wpdb, 'db_server_info' ) ) { | ||
| $db_server_info = $wpdb->db_server_info() ?? ''; | ||
| } | ||
| $wp_version = get_bloginfo( 'version' ); | ||
|
|
||
| return array( | ||
| 'environment' => $env, | ||
| 'php_version' => $php_version, | ||
| 'db_server_info' => $db_server_info, | ||
| 'wp_version' => $wp_version, | ||
| ); | ||
| }, | ||
| 'permission_callback' => static function (): bool { | ||
| return current_user_can( 'manage_options' ); | ||
| }, | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => true, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.