Add core/read-content ability#739
Conversation
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
d1db766 to
88df83c
Compare
|
Nice work so far! A few things to iron out:
|
|
For reference, I'm sharing WP CLI commands related to read-only operations on posts:
|
|
@jorgefilipecosta looks like some code review feedback and merge conflicts to clean up to help move this along towards merge and inclusion in the next AI plugin release (to help get some usage testing & feedback before a parallel PR is landed for core in 7.1) |
This is an unrealistic and IMO unwanted end-goal. I think we need to accept that no new core abilities will ship in 7.1, even if they make it into AI@1.1.0 < 3 weeks is not enough time to get actual API design feedback for core. It's not even a full AI Plugin release cycle. By all means if we can get this cleaned up in time for the next plugin release, great, but considering that they're not even behind an Experiment toggle, I wouldn't want y'all rushing it in before you or @dkotter think its viable because of a desire to squeeze it into beta1. |
3a7df4a to
b3748e6
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #739 +/- ##
=============================================
+ Coverage 75.50% 77.07% +1.57%
- Complexity 2086 2281 +195
=============================================
Files 99 100 +1
Lines 8626 9240 +614
=============================================
+ Hits 6513 7122 +609
- Misses 2113 2118 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Hi @gziolo, your feedback was applied. |
peterwilsoncc
left a comment
There was a problem hiding this comment.
I've added a few notes inline.
To avoid burying the lead: I think the key question I have is why you don't include the rendered content so bots can be set up with a read-only account?
|
After a closer inspection, I want to echo the point that @peterwilsoncc raised regarding the data formatting. We need to decide whether we return raw data or rendered/filtered data. This is what I see now:
What is needed will largely depend on the consumer. If they only want to present the data, then the preferred option would be using dates in the site's timezone, title, and content as rendered HTML. If they want to edit content, then maybe fetching raw data would make more sense. There are two ways to go about it:
@justlevine, thank you for the reminder about the timeline for the WP 7.1 release. Let's see how much work is left to iron out all the feedback raised so far. Either way, we want to get some early testing through the AI plugin. All the feedback is appreciated and will help shape this essential ability. |
| 'content_rendered' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The rendered post content. Present when the post type supports the editor. Empty when withheld for a password-protected post.', 'ai' ), | ||
| ), |
There was a problem hiding this comment.
If we return both content_raw and content_rendered, this will fill up the context window with duplicated content.
Suggestion: have a scope input field(similar to REST API)
editwill return the raw content that an agent can modify and update on a new requestreadit will return the "content_rendered". Probably best if we can strip html tags also (or transformed to MD :D [nice to have]) (I have no strong opinion on this one but with stripped tags we will be nice with the context window)
There was a problem hiding this comment.
readit will return the "content_rendered". Probably best if we can strip html tags also (or transformed to MD :D [nice to have]) (I have no strong opinion on this one but with stripped tags we will be nice with the context window)
Warning
Opinionated 😸
I'd be reluctant to add the required code to core given it would add quite a bit of work to each request.
Dries Buytaert wrote up his experience of providing a markdown version of his content for LLMs and finding that the agents simply crawled both versions, so I think it's pretty safe to assume that they're happy with HTML.
There was a problem hiding this comment.
IMO that article has little to do with Markdown vs HTML and is rather about sources of truth/context rot (LLMs.txt is ineffective) and misunderstandings about how agentic discovery works (nothings going to visit your MD endpoints if you don't explicitly encourage it to).
That aside , there are other implications with exposing raw vs rendered content, including security and end-user case. As abilities are meant to be a primitive and not a MCP-specific transport, I think the way to address that in MCP (or upstream in Abilities) is an arg to filter the output.
If in the short term this is a concern, then I'd say keep content_rendered without content_raw, since even if LLMs do prefer markdown to HTML, they'd want the markdown version of the rendered html, not the one fill with custom block markup or unprocessed synced patterns etc. But longer term, IMO the solution belongs in a different layer.
There was a problem hiding this comment.
That aside , there are other implications with exposing raw vs rendered content,
@jasonbahl any chance you have a few minutes to share your thoughts from on over in WPGraphQL-land? With how much influence we took in design language, it would make sense for the output ergonomics to wind up being similar, just maybe a bit flatter. Same considerations, at least 🤔
There was a problem hiding this comment.
In my earlier #739 (comment), I referenced WP CLI as a good example of where good defaults solve the issue you discuss. It could be replicated here by returning the same set: ID, post_title, post_name, post_date, and post_status. All other fields remain accessible, but the consumer needs to explicitly list them in the input using the fields array.
There was a problem hiding this comment.
👋 @justlevine I left some notes here addressing the raw/rendered concerns along with some broader concerns: #739 (review)
There was a problem hiding this comment.
I think the whole discussion missed my point. It's not about HTML vs Markdown, that was just a side note.
The real point: we need both content_raw and content_rendered, but never both at once.
- editing → agent needs
content_raw(block markup, shortcodes) so it can change it and write it back. Rendered HTML is useless here. - reading → agent needs
content_rendered(what a human sees). Raw markup is just noise.
Any single call is one job or the other. So the field it doesn't need is always 100% redundant, the full post body duplicated on every call.
So I'd go with one content field that returns one or the other based on context: raw when editing is needed, rendered when it's read-only. If we want a default, probably raw only.
@jasonbahl this is basically what ?context=edit (REST) and format: RAW (WPGraphQL) already do, force the caller to declare intent. Same idea, just collapsed into a single field instead of two you opt into separately.
One more thing: if we go this way, the ability description needs updating too, so the agent knows when to pick each one (raw when it intends to edit, rendered when it just needs to read).
There was a problem hiding this comment.
The real point: we need both content_raw and content_rendered, but never both at once.
This is not true. Putting aside yet again that abilities aren't meant to serve agents they're meant to be a underlying primitive to serve all last-mile APIs, an agent
- doing migrations,
- building patterns that match existing site designs,
- or frontend generative ui work,
are all cases that would benefit from having both the preprocessed _raw and the full-fat _rendered data beyond the well-established non-AI ones like page builders and headless previews.
But also, worse than that, this would pollute the API with state: you need to know the context in order to be able to accomodate for the data. I've got PostObject.content but no idea whether or not it needs parsing or rendering or data sanitization anymore.
There was a problem hiding this comment.
Yah I agree with @justlevine point I can think some cases where having rendered and non rendered content may be useful for an agent. Currently we don't return content by default, but we can request rendered or raw, or both, same for any other field.
Post types can be unregistered or re-registered with different arguments between the ability being registered and the ability being used, so the permission and execute callbacks now call get_exposed_post_types() each time rather than reusing a set cached at registration.
Matches the REST API user field naming adopted by core/read-users.
has_explicit_edit_fields() bailed on any non-array fields value, but a GET transport delivers list inputs as scalar/CSV strings (the run controller validates but does not sanitize input against the ability schema). It was the only input reader in the class not already coercing such values, so a raw-field request encoded as a string (e.g. fields=content_raw) slipped past the edit-access gate that the array form enforces. Parse fields with wp_parse_list() the same way normalize_fields() does, until core sanitizes ability input.
- Extract the shared scalar/CSV list coercion into parse_list_input(), so has_explicit_edit_fields(), normalize_fields(), and normalize_statuses() no longer each re-implement wp_parse_list() + is_string filtering. - Gate edit-context fields once by dropping them from the requested set when the user cannot edit, making $edit_fields the single source of truth instead of per-field `&& $can_edit` checks that can drift from the list. - Look up requested fields via array_flip()/isset() instead of an in_array() closure run per candidate field per post. - Return an empty string from format_gmt_date() for a post with no resolvable date, instead of a bogus negative-year timestamp. - Honor an explicit `show_in_abilities => false` opt-out on curated settings in mark_setting(), matching mark_post_type() and its own docblock; add a regression test.
On transports that skip schema validation, a malformed `author`, `parent`, or `include` value coerced to a benign default and silently widened the query: `author => 0` dropped the author filter (all authors), an empty `post__in` was ignored by WP_Query (all posts), and a non-integer `parent` became a top-level query. Add a strict `parse_filter_int()` and reject values that cannot be honored with `content_invalid_filter` (400), reusing the existing loud-rejection path for unsupported filter types. Covered by direct execute-callback tests exercising the raw transport, plus a regression test that valid string-encoded values are still honored.
|
@jorgefilipecosta please re-request reviews when you're updates are ready here, thanks! |
Done it is ready for another pass, everything was addressed. |
The core/read-content input schema freezes its post_type enum from the post types exposed when the ability registers on wp_abilities_api_init. Explain that the enum is enforcing (input is validated before the permission/execute callbacks), what the callbacks' call-time re-resolution actually covers (by-id lookups and failing closed on unregistration), and that a late-registered post type is only reachable by post_type/slug/query if the enum is amended via the wp_register_ability_args filter.
…lity A post type registered after wp_abilities_api_init is not part of the ability's contract: it is absent from the input enum and any data returned for it would describe a post type outside the advertised output schema that clients validating ability output do not expect. Drop the earlier claim that such a post type is reachable by id, and keep the guidance that it must reach the enum (early registration or the wp_register_ability_args filter).
|
After discussing with @gziolo I added some documentation specifying the contract a post type to be usable in abilities needs to be registered before abilities are registered (otherwise its schema may have already been passed/stored by some ability consumer/adapter). |
Rename register_get_content()/execute_get_content() to register_read_content()/execute_read_content() so the method names reflect the core/read-content ability's verb instead of "get".
Summary
Part of: #40
Adds the read-only
core/read-contentability to the plugin, mirroring the companion WordPress Core implementation so the two stay in sync. It overrides any core-provided copy.The ability has explicit modes:
id, with optionalpost_typeguard, returns the post directlypost_typeandslugreturns the post directly{ posts, total, total_pages }and supportsstatus,author,parent,include,fields,page, andper_pageDefaults are lean:
id,post_type,status,date,slug, andtitle_rendered. Heavier fields are opt-in viafields; raw fields require edit access, while rendered fields can be requested for readable posts.Companion Core PR: WordPress/wordpress-develop#12195
Security
Uses a coarse capability gate plus per-post read/edit checks. Missing, unreadable, mismatched, and unexposed posts return the same not-found response.
Tests
PHPUnit integration tests cover permissions, fields, single-post modes, query mode, and
include. The E2E spectests/e2e/specs/abilities/core-read-content.spec.jsexercises the client-side ability modes andinclude.Manual testing
Paste this in the browser console on an editor/admin page after enabling AI. It creates two published posts and one draft, then exercises query mode, draft query mode, query
include, single-post byid, and single-post bypost_type+slug. Each ability call logs{ input, output }.