Opened 8 months ago
Last modified 24 hours ago
#64606 assigned enhancement
Add content management abilities
| Reported by: | jorgefilipecosta | Owned by: | jorgefilipecosta |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Abilities API | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description (last modified by )
This tracks the addition of get abilities for post types via the Abilities API, enabling AI agents to retrieve posts and pages. It should introduce a show_in_abilities option on register_post_type() to control which post types expose abilities, enabled by default for post and page initially (then we can expand to menus, media etc).
It should support single-post retrieval by ID and multi-post querying with filters for status, author, etc...
Change History (28)
This ticket was mentioned in PR #11488 on WordPress/wordpress-develop by @ishikaatxecurify.
6 months ago
#4
- Keywords has-patch has-unit-tests added
#5
@
6 months ago
- Component AI → Abilities API
Moving tickets related to the Abilities API to a new sub-component.
#6
@
5 months ago
Removing trunk version as this is not going to be shipped with WP 7.0 but in the next releases.
#7
@
5 months ago
- Keywords abilities removed
Removing abilities and abilities-api custom keywords. This is now indicated by the Abilities API component.
#8
@
5 months ago
- Keywords has-patch, has-unit-tests → has-patch has-unit-tests
- Version trunk
Since this is an enhancement, there's no first version of WordPress this can be reproduced in. Removing trunk version.
This ticket was mentioned in Slack in #core-ai by justlevine. View the logs.
4 months ago
#12
@
4 months ago
To make it easier to find, here's the related AI plugin PR https://github.com/WordPress/ai/pull/739
#13
@
4 months ago
I missed the creation of this ticket while focusing on RTC during the 7.0 release cycle.
@jorgefilipecosta, @gziolo, @justlevine am I able to get some clarity on which is the canonical proposal for inclusion in WordPress Develop?
I'd rather not review any non-starters as it's not helpful for any of us.
#14
@
4 months ago
My understanding is as follows:
- https://github.com/WordPress/ai/pull/739 – @jorgefilipecosta authored the ticket, and it's where the development moved, so the goal is to land it in the AI plugin first, so it gets tested through integrations like MCP adapter or
@wordpress/abilitiesJS package. - https://github.com/WordPress/wordpress-develop/pull/12195 – would be a sync of the code proposed in the AI plugin, to ensure both are shaped as closely as possible to each other and ready to land when it's time
- https://github.com/WordPress/wordpress-develop/pull/11488 – this looks like a community contribution based on the description included in the Trac ticket, but I haven't looked closely at the proposed implementation yet
This ticket was mentioned in PR #10665 on WordPress/wordpress-develop by @jorgefilipecosta.
3 months ago
#16
Part of: https://github.com/WordPress/ai/issues/40 cc: @Jameswlepage
Equivalent core PR of: https://github.com/WordPress/gutenberg/pull/74234 (in Gutenberg).
Inspired by the work on https://github.com/galatanovidiu/mcp-adapter-implementation-example/tree/experiment/layerd-mcp-tools/includes/Abilities by @galatanovidiu.
Ticket: https://core.trac.wordpress.org/ticket/64606
## Core abilities organization
This PR also proposes a logic for how core abilities are organized. In abilities.php, we have two functions: wp_register_core_abilities and wp_register_ability_category (or in the case of Gutenberg, _gutenberg_register_core_abilities and _gutenberg_register_core_ability_categories). These functions then call ability registration functions that are inside the abilities folder. If the ability is simple, it can be registered in just a single internal function, e.g., _wp_register_site_info_ability; for complex abilities, we can register them in a class (like this post management one).
The abilities can be in both Gutenberg and core. Having them in Gutenberg allows us to use them in the workflows functionality that is being worked on by @senadir and allows us to get some testing before core is released.
Gutenberg unregisters the equivalent core ones, so Gutenberg is the source of truth. The same ability can exist in Gutenberg and core, but Gutenberg takes precedence so we can test changes in Gutenberg before releasing in core (similar to what happens with blocks and other WordPress artifacts).
## Core Post management abilities
This PR adds core post management abilities for the WordPress abilities API: core/create-post, core/get-post, core/find-posts, and core/update-post.
It supports nested query support for meta_query, tax_query, and date_query matching WordPress's native WP_Query structure with AND/OR relations. This allows very complex query operations which the REST API does not allow and may be useful for agents to find information.
It uses the permission callback mechanism and tries to correctly check permissions for all cases. The basic permission checking logic first checks the basic and common use case (user can edit, create, or see a post), then we go into specifics in separate functions that are reused for checking status changes (e.g., publish), author changes, and taxonomy assignment permissions.
The class WP_Posts_Abilities_Gutenberg is organized into 6 main areas:
- Ability Registration: The main part that does the
wp_register_abilitycalls. - Initialization: Initializes shared data between the abilities, e.g., schemas.
- Output Formatting: Formats the post object (and taxonomies) for output shared between all abilities.
- Permission Checking: Permission checking logic; some parts are also shared between abilities.
- Query Processing: Utilities to process queries and map the ability input format to the
WP_Queryformat. - Data Processing: Utilities to process input data, sanitize it, map to other formats, etc.
## Missing
The idea of this PR is mainly to get feedback if this is the right direction for the post management abilities. There are some things that are missing:
- Automated testing: Deeply covering all the abilities. If we agree with this approach, I will add the tests to this PR.
- Partial Edits: Not all edits are possible, e.g., it is not possible to mark a post as sticky. We need to decide if marking a post as sticky should be done on the update/create post abilities, following the REST API approach, or if we should have a specific ability for that, following the
wp_corePHP API approach where we have thewp_update_postfunction and alsostick_post/unstick_postfunctions. We can decide on what to do regarding the missing edits as follow-ups, as the PR is already too big. - Pagination: Pagination was not yet implemented; we need to have a general solution for pagination at the abilities API level.
## Test plan
- Open
/wp-admin/edit.php?post_type=post. - Open the browser console the following examples.
const abilitiesAPI = (await import( '@wordpress/abilities' ) );
- [ ] Verify
core/create-postcreates posts with various fields (title, content, status, meta, taxonomies):
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/create-post/run', method: 'POST', data: { input: {post_type: 'post', title: 'Hello World', content: 'My awesome content', 'status': 'publish', meta: {a:23, c:1} } } });
- [ ] Verify
core/get-postretrieves posts by ID with optional taxonomy/meta inclusion:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/get-post/run', method: 'POST', data: { input: {id: 334, include_meta: true, include_taxonomies: true} } })
- [ ] Verify
core/find-postsqueries work with nestedmeta_query,tax_query, anddate_query. - Create posts with the following structure where the title
a-23|c-1represents a post with meta key "a" value of 23 and meta key "c" value of 1. Also adds the correct tags.
- Execute complex find post queries like:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: { post_type: 'post', include_meta: true, meta_query: { queries: [ { key: 'footnotes', compare: 'EXISTS', }, ] } } } }); await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: { post_type: 'post', include_meta: true, meta_query: { relation: 'AND', queries: [ { key: 'a', compare: '=', value: '23' }, { relation: 'OR', queries: [ { key: 'b', compare: '=', value: '1' }, { key: 'c', compare: '=', value: '1' } ] } ] } } } }); await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: { post_type: 'post', include_meta: true, tax_query: { relation: 'AND', queries: [ { taxonomy: 'post_tag', field: 'slug', terms: ['t-23'] }, { relation: 'OR', queries: [ { taxonomy: 'post_tag', field: 'slug', terms: ['c-1'] }, { taxonomy: 'post_tag', field: 'slug', terms: ['b-1'] } ] } ] } } } }); await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: { post_type: 'post', date_query: { relation: 'AND', queries: [ { year: 2025 }, { relation: 'OR', queries: [ { day: 26 }, { month: 11 } ] } ] } } } });
- [ ] Verify
core/update-postmodifies existing posts correctly:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/update-post/run', method: 'POST', data: { input: { id: 327, content: 'Hello' } } });
- [ ] Test permission checks prevent unauthorized access, e.g., users without publish permissions trying to change status to published, etc.
#17
@
3 months ago
- Milestone 7.1 → 7.2
Moved to WordPress 7.2 based on the decision made after evaluating feedback on the merge proposal.
This ticket was mentioned in PR #12195 on WordPress/wordpress-develop by @jorgefilipecosta.
3 months ago
#18
## Summary
Part of: https://github.com/WordPress/ai/issues/40
Ticket https://core.trac.wordpress.org/ticket/64606
Adds the read-only core/read-content ability to WordPress Core, matching the companion plugin implementation in https://github.com/WordPress/ai/pull/739.
A native show_in_abilities post type argument controls exposure and is enabled for post and page. The ability has three explicit modes:
- single post by
id, with an optionalpost_typeguard, returning the post directly - single post by
post_typeandslug, returning the post directly - query mode returning
{ posts, total, total_pages }, withstatus,author,parent,include,fields,page, andper_page
The lean default fields are id, post_type, status, date, slug, and title_rendered. Heavier fields are opt-in through fields; raw fields require edit access, while rendered fields can be requested for readable posts.
## Security
Uses a collection-level capability gate plus per-post read/edit checks. Missing, unreadable, mismatched, and unexposed posts fail closed. Public but non-viewable statuses require edit access, inherited content follows a readable parent, and password-protected rendered fields are withheld from users who cannot edit the post.
## Query behavior
Query results use normal WP_Query date ordering, newest first. include limits results without preserving caller order. An omitted per_page expands to the included ID count up to 100. Out-of-range pages and invalid or unsupported filters return explicit errors instead of silently widening the query. Totals remain in the response body and can exceed returned rows when row-level permissions withhold content.
## Tests
The PHPUnit ability suite mirrors every portable integration test from the plugin PR, covering registration, schemas, permissions, ID/slug/query modes, fields, pagination, filters, ordering, empty projections, rendering, cache priming, and dates. Core-specific REST dispatch tests cover authentication, permissions, retrieval, include ordering, pagination, and out-of-range errors.
#19
@
8 weeks ago
Test Report: Trac #64606
Add content management abilities
Ticket: https://core.trac.wordpress.org/ticket/64606
Status: assigned / enhancement, milestone 7.2, keywords has-patch has-unit-tests
Which of the three PRs I tested, and why
The ticket lists three PRs. Per maintainer clarification in comment #14
(gziolo):
- https://github.com/WordPress/wordpress-develop/pull/10665[#10665] ("Add: WordPress Core Post Management Abilities") — the original draft (create/get/find/update post abilities). Explicitly posted "to get feedback if this is the right direction," with a list of missing pieces (no automated tests, no pagination). Closed, not merged. Superseded.
- https://github.com/WordPress/wordpress-develop/pull/11488[#11488] ("added post and pages abilities") — a community contribution. gziolo: *"I haven't looked closely at the proposed implementation yet."* Not the canonical path. Open, unreviewed.
- https://github.com/WordPress/wordpress-develop/pull/12195[#12195]
("Abilities API: Add a
core/read-contentability") — the canonical one: a sync of the implementation landing first in the companion WordPress/ai plugin (https://github.com/WordPress/ai/pull/739[ai#739]), "to ensure both are shaped as closely as possible... and ready to land when it's time." Open, mergeable, with the most complete test coverage of the three.
I tested #12195.
What it does
Adds a single read-only core/read-content ability with three modes:
- Single post by
id(optionalpost_typeguard). - Single post by
post_type+slug. - Query mode: filter by
status,author,parent,include; returns{ posts, total, total_pages }.
A new show_in_abilities argument on register_post_type() controls
exposure (default false; core sets it true for post and page only).
Lean default fields (id, post_type, status, date, slug,
title_rendered); heavier/raw fields are opt-in via fields and gated by
edit capability. Security model is fail-closed: missing, unreadable,
mismatched, or unexposed posts all reject rather than leak existence.
Note: because this ability is marked readonly in its annotations, the
existing (unrelated to this PR) REST run-controller convention requires
GET, not POST — input travels as a JSON-shaped input query param, not a
JSON body. Worth knowing if you're testing this by hand.
Environment note
The PR branch (add/core-content-ability) is based on a stale trunk
snapshot — diffing it against current trunk pulls in a lot of unrelated
noise (phpstan annotation reverts, a removed _reset_privacy_policy_page_for_post()
that trunk added later, etc.). That's normal drift for a long-open PR, not a
defect, but it will need a rebase before merge, and I didn't rely on the raw
diff to judge scope — I checked out the PR's own commit and reasoned from
that.
Also had to run composer config audit.block-insecure false before
composer install would resolve — Composer's advisory DB has since flagged
exact-pinned dev-tool versions (squizlabs/php_codesniffer 3.13.5,
wp-coding-standards/wpcs 3.3.0) as insecure. Unrelated to this PR; just an
environment note for next time.
Automated tests (PR's own suites)
[cols="3,1,1"]
|===
|Suite |Tests |Result
|tests/phpunit/tests/abilities-api/wpRegisterCoreContentAbility.php
|90 tests / 317 assertions
|PASS
|tests/phpunit/tests/rest-api/wpRestAbilitiesContentController.php
|11 tests / 32 assertions
|PASS
|===
Regression check — wpAbility.php, wpGetAbilities.php, wpRegisterAbility.php,
wpAbilitiesRegistry.php (73 tests / 114 assertions) and the core
WP_Post_Type unit tests (24 tests / 68 assertions), all pass — the new
show_in_abilities post-type argument doesn't disturb existing post-type
behavior.
php -l on all 4 changed/added core files: clean.
Manual verification (live wp-cli site + real REST dispatch)
core/read-contentregisters;post/pagehaveshow_in_abilities=trueby default,attachmentdoes not.- Logged-out request → 401, denied.
- Admin, single post by ID → 200, correct post, lean default fields only
(
title_renderedpresent,content_rawabsent). - Admin, single post by
post_type+slug→ 200, same post. - Admin, query mode (
post_type=post&status=publish) → 200,{posts, total, total_pages}shape; published post included, a sibling draft correctly excluded by the status filter. - Subscriber requesting an edit-context field (
content_raw) on a post they can't edit → 403rest_ability_cannot_execute. - Subscriber reading default (lean) fields of a published post → 200 (can read).
- Subscriber requesting another user's draft by ID → 403, fails closed as documented (no "not found" leak vs. "forbidden" distinction to probe with).
- Edge cases: nonexistent post ID → 403 (fails closed, doesn't reveal
existence); out-of-range
page→ clean 400content_invalid_page_number; unsupported/unknownpost_type→ clean 400 schema-validation error. All match the PR description's stated behavior ("fail closed," "explicit errors instead of silently widening the query").
Conclusion
PR #12195 works as described. Both of its own PHPUnit suites pass (101 tests
/ 349 assertions total), no regressions in the surrounding abilities-api or
post-type code, and independent manual testing over real REST dispatch
confirms every documented behavior: default field leanness, edit-gated raw
fields, fail-closed permission handling on IDs/slugs/query mode, and clean
explicit errors on bad pagination/post-type input. This is the ability the
maintainers intend to land (per comment #14) and it does what it says. Only
non-blocking note is the stale base needing a rebase before merge — nothing
in the ability's own logic needs a fix.
@gziolo commented on PR #12195:
2 weeks ago
#20
In addition to addressing the feedback from @peterwilsoncc, we need to sync the latest fixes applied in the AI plugin. Most importantly:
#21
@
13 days ago
@jorgefilipecosta, would it make sense to split this ticket? I'm confident we can give a try ship core/content-query in WordPress 7.2 because it's been in the AI plugin for a full release cycle. Write abilities are still in development, so they should target WordPress 7.3.
@gziolo commented on PR #12195:
7 days ago
#22
The previous review feedback is addressed, and local verification passed for the focused tests and the broader Abilities API suite on both single-site and multisite.
Two follow-ups remain before approval:
- Please remove
open_worldand its accompanying comment/assertion, since WordPress Core does not support this annotation. - The rendering helpers do not fully restore loop globals when there is no surrounding post. To reproduce:
- Start with an empty global
WP_Queryand no$GLOBALS['post']. - Execute
core/content-queryfor a published post withfields: ['content_rendered']. - Inspect
$GLOBALS['pages']: it still contains the rendered post’s content.
- Start with an empty global
setup_postdata()populates these globals, butwp_reset_postdata()cannot restore them because the main query has no post. The excerpt helper uses the same cleanup pattern. Please preserve and restore the affected globals, including their previously unset state, and add regression coverage.
For awareness, this PR incorporates the author_slug contract from the still-open WordPress/ai#1025. Keep the implementations synchronized as that PR settles.
#23
@
7 days ago
Hi @gziolo I created a separate ticket at https://core.trac.wordpress.org/ticket/66268.
@jorgefilipecosta commented on PR #12195:
6 days ago
#24
Hi @peterwilsoncc, thank you for the reviews, all the feedback was applied 👍
@jorgefilipecosta commented on PR #12195:
5 days ago
#25
LMK if this is no longer the canonical URL as there are a few linked to the ticket.
Hi @peterwilsoncc, yes this is the canonical PR for https://core.trac.wordpress.org/ticket/66268 👍
@ugyensupport commented on PR #12195:
25 hours ago
#26
Thanks for all the iterations here, this is in great shape. A few things I didn't see raised yet:
- Top-level
oneOfin the input schema. Through the MCP adapter this becomes an LLM tool schema, and some providers (Claude, OpenAI strict mode) rejectoneOf/anyOfat the top level. Worth checking against a real client. If it fails, a flat object with the mode checked in code would avoid it. - Registration timing. The
type/statuslists are fixed when the ability registers, so a post type registered later withshow_in_abilities => trueis silently left out. A_doing_it_wrong()(or a note in the docs) would help, plus a warning whenshow_in_abilitiesisn't a boolean ('1',1, arrays). - Lean field requests still call
setup_postdata()(and firethe_post) for every row. It could be skipped when no rendered field orlinkis requested. - Small consistency nits: invalid
page/per_pagesilently fall back while other filters fail closed,author_slugreturns''rather thannullfor a deleted author, and the slug lookup could setorderby/posts_per_pageexplicitly.
Happy to help test any of these.
@ugyensupport commented on PR #12195:
24 hours ago
#27
Test report
Tested PR head ad971412d on PHP 8.5.7 and MySQL 5.7.
Test suites
| Run | Result |
|---|---|
--group 66268 (single site) | ✅ 146 tests, 509 assertions |
--group 66268 (multisite) | ✅ 148 tests, 516 assertions |
--group abilities-api (regression) | ✅ 552 tests, 1593 assertions |
Post type tests (Tests_Post_Types*) | ✅ 48 tests, 149 assertions |
PHPCS on changed src/ files | ✅ 0 errors (the 22 warnings in post.php are pre-existing, same count on base)
|
Follow-up on my earlier comment
I checked the points from my earlier comment with a few throwaway tests:
| Point | Observed | Status |
|---|---|---|
Top-level oneOf | Input and output schemas only have type + oneOf at the top level, no properties. The MCP adapter's SchemaTransformer passes type: object schemas through unchanged, so MCP clients receive {type, oneOf, properties: {}}. | Confirmed |
oneOf error messages | A query for a post type not in the enum fails with: *"input is not a valid Get a single readable post by ID. Reason: id is a required property of input."* The error describes the wrong mode, which is confusing for agents. | New finding |
| Late post type registration | A post type registered with show_in_abilities => true after the ability is rejected with ability_invalid_input, and no notice. | Confirmed |
Non-boolean show_in_abilities | 1, '1', 'true', and array( 'read' ) are all stored as false, with no notice. | Confirmed |
| Lean fields still set up posts | fields: [ 'id', 'slug' ] for 5 rows fired the_post 5 times. | Confirmed |
| Slug lookup resolved twice | Slug mode runs the slug WP_Query twice per call (permission check and execute). | Confirmed |
| Slug query uses defaults | posts_per_page and orderby are unset, so the site's posts_per_page option and the default order apply. | Confirmed |
| Deleted author | author_slug returns "", while missing dates return null. | Confirmed |
Invalid page / per_page | page: "abc" is rejected by schema validation in WP_Ability::execute(). The fallback is only reachable by calling the callback directly. | Not an issue, please disregard |
Verdict
Works as described. All tests pass on single site and multisite, with no regressions. The points above are suggestions, not blockers.
@gziolo commented on PR #12195:
24 hours ago
#28
Top-level oneOf in the input schema.
Confirmed. @jorgefilipecosta is working on enhanced schemas that will address tool calling incompatibility with models.
Registration timing
Post types register on init, so this should be good. We can additional check for extenders to report detected incompatibility when a custom post type that enables show_in_abilities registers after abilities_api_init triggered.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Added get abilities for post types via the Abilities API, enabling AI agents to retrieve posts and pages through core/post-type/{slug}/get.
Trac ticket: https://core.trac.wordpress.org/ticket/64606
## Use of AI Tools