Abilities API: add execution lifecycle filters to WP_Ability methods#11731
Abilities API: add execution lifecycle filters to WP_Ability methods#11731gziolo wants to merge 11 commits into
Conversation
|
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 Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
f59694c to
9b2aa9a
Compare
|
This is super useful for any substrate that mediates ability execution on behalf of agents. Sharing four concrete use cases from that perspective so the filter shapes can be validated against them:
The "schema validation remains the final integrity gate" framing matches what a substrate wants downstream: filters can transform but can't bypass the contract. One small thing I'd double-check: when the agent runtime hooks cc / FYI: this would be consumed by |
|
Thanks for the feedback. I'm glad the four shapes map cleanly to a substrate's needs. Keeping the short-circuit return as |
| */ | ||
| $pre_execute_sentinel = new stdClass(); | ||
| $pre = apply_filters( 'wp_pre_execute_ability', $pre_execute_sentinel, $this->name, $input, $this ); | ||
| if ( $pre !== $pre_execute_sentinel ) { |
There was a problem hiding this comment.
Clever. I was thinking at first this wouldn't be good since it doesn't match filters like pre_option which work by using false as the sentinel value. But since the ability can return any value at all, then this wouldn't work.
There was a problem hiding this comment.
I discovered the same pattern used in Customizer where similarly null is a valid output.
We still might need to iterate on it based in feedback shared in WordPress/ai#477 (reply in thread) by @ibrahimhajjaj:
The stdClass sentinel is fine in isolation but hard to compose. A filter callback can't hold a reference to the per-call sentinel, so all we have is instanceof stdClass. If two plugins both want "pass through, no opinion" they can't tell each other's sentinels apart from core's. Worth a line in the docblock maybe.
I'm considering a formal WP_Filter_Sentinel class to made it first-class concept removing the ambiguity. That would be similar to WP_Error on the naming level, but very specialized.
There was a problem hiding this comment.
I discovered the same pattern used in Customizer where similarly
nullis a valid output.
Oh, yeah:
wordpress-develop/src/wp-includes/class-wp-customize-setting.php
Lines 338 to 345 in 7938204
I added that in e158ff2 😊
Introduces a new filter inside `WP_Ability::normalize_input()` that fires after the method's built-in default-value handling, allowing plugins to transform input — for example, prompt enrichment or parameter defaulting beyond what JSON Schema handles. Returning a `WP_Error` halts execution. `WP_Ability::execute()` now short-circuits when `normalize_input()` returns a `WP_Error`, so a halt from the filter propagates as the execute result without reaching `validate_input()`, `check_permissions()`, or the `wp_before_execute_ability` action. Props gziolo. Fixes #64989.
Introduces a new filter inside `WP_Ability::check_permissions()` that fires after the registered `permission_callback` returns. Plugins can use it to enforce additional authorization rules — transport-level permission layering for protocol adapters, multi-factor gates, or temporary elevation for batch operations. Because the filter lives inside the method, overrides apply consistently across `execute()`, REST API permission checks, and WP-CLI call sites. Filters can return `true`, `false`, or a `WP_Error`. Props gziolo. Fixes #64989.
Introduces a short-circuit filter at the top of `WP_Ability::execute()`, modeled on `rest_pre_dispatch`. Returning a non-null value bypasses the entire pipeline — `normalize_input()`, `validate_input()`, `check_permissions()`, the registered `execute_callback`, output validation, and the `wp_before_execute_ability` / `wp_after_execute_ability` actions are all skipped, and the filter's return value is returned to the caller as-is. Useful for cached responses, rate limiting, maintenance mode, and test mocking. Callers that short-circuit are responsible for input integrity since input validation does not run. Props gziolo. Fixes #64989.
Introduces a new filter inside `WP_Ability::do_execute()` that fires after the registered `execute_callback` returns. Plugins can use it to transform the result — response formatting, stripping internal metadata, content safety filtering, response enrichment — or to recover from an execution failure by returning a successful value in place of a `WP_Error`. Because `do_execute()` is invoked from `WP_Ability::execute()` between `check_permissions()` and `validate_output()`, the transformed value is still validated against `output_schema` before the `wp_after_execute_ability` action fires. Placing the filter inside `do_execute()` keeps each "result" filter consistent with `wp_ability_normalize_input` and `wp_ability_permission_result`, which sit inside the methods whose return values they filter. Props gziolo. Fixes #64989.
Use a unique stdClass sentinel for wp_pre_execute_ability, mirroring the Customizer symbol pattern, so returning null can intentionally short-circuit execution.
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Adds a docblock note to the `wp_ability_permission_result` filter clarifying that non-bool and non-WP_Error return values are coerced to `false`, plus a test asserting the coercion contract so `check_permissions()` honors its documented `bool|WP_Error` return type. Follow-up to df03035. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
09cd6ba to
1cbaab6
Compare
Replaces the bare `stdClass` instance used as the `wp_pre_execute_ability` filter default with a new `WP_Filter_Sentinel` final class. The previous sentinel was indistinguishable from any other `stdClass` a filter callback might return, making it hard for cooperating plugins to tell core's "no opinion" marker from an unrelated `stdClass` short-circuit value. `WP_Filter_Sentinel` is a general-purpose primitive added alongside `WP_Error`. Identity comparison (`===`) against the original instance remains the detection mechanism, and the existing pass-through contract (return `$pre` unchanged) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds four new filters to
WP_Abilityto give plugins hook points across the executionlifecycle. Today the only execution-phase hooks are observation-only actions
(
wp_before_execute_ability,wp_after_execute_ability); plugins that need totransform input, modify output, override permission decisions, or short-circuit
execution have no place to do that in core, and have built parallel hook systems
on top.
This PR closes that gap by introducing four filters that live inside their
owning
WP_Abilitymethods (so they apply consistently acrossexecute()anddirect callers, including REST permission checks), plus one orchestration-level
filter at the top of
execute()for short-circuiting.Filters added
wp_pre_execute_abilityexecute()WP_Filter_Sentinelinstance as the default sonull,false, and arbitrary objects are valid short-circuit results.wp_ability_normalize_inputnormalize_input()WP_Errorhalts execution.wp_ability_permission_resultcheck_permissions()permission_callbackresult. Applies consistently acrossexecute()and directcheck_permissions()callers.wp_ability_execute_resultdo_execute()WP_Error.Pipeline ordering inside
execute()Schema validation remains the final integrity gate:
wp_ability_normalize_inputfires before
validate_input(), andwp_ability_execute_resultfires beforevalidate_output(). Filters cannot bypass schema validation except byshort-circuiting via
wp_pre_execute_ability, where the caller takesresponsibility for the returned value's shape.
WP_Filter_SentinelIntroduces a small, reusable marker class (
src/wp-includes/class-wp-filter-sentinel.php)loaded alongside
WP_Hookinwp-includes/plugin.php. Each instance is unique byidentity, so callers can pass one as a filter's default value and compare returns
with
===to detect "no filter modified this" — even when valid replacement valuesinclude
null,false, or arbitrary objects. Used bywp_pre_execute_ability,and available for any future filter that needs the same pass-through detection.
REST behavior
WP_REST_Abilities_V1_Run_Controller::check_ability_permissions()now propagatesWP_Errorresults fromnormalize_input()directly instead of feeding them intovalidate_input(). When the filter does not set its own status, the controllerdefaults to 400; filter-set statuses (e.g., 422, 429) are preserved.
Test plan
npm run env:composer -- format— cleannpm run env:composer -- lint— cleannpm run env:composer -- compat— cleannpm run typecheck:php— 0 errorsNew tests cover:
WP_Errorrecovery, result transform), with filter args verified via args-as-guards on the transformed value rather than side-effect captures.wp_pre_execute_ability: pipeline configured to fail (permission denial) so the short-circuit value coming through cleanly proves the bypass.nullshort-circuit preserved by theWP_Filter_Sentinelpattern.WP_Filter_Sentineldefault.wp_ability_execute_resultruns before output validation and beforewp_after_execute_ability.WP_Errorpropagation fromwp_ability_normalize_inputhalts execution.wp_ability_permission_resultfiring whencheck_permissions()is called directly.Trac ticket
https://core.trac.wordpress.org/ticket/64989
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code, Codex
Model(s): Claude Opus 4.7 (1M context)
Used for: Planning, implementation, unit tests, code review, commit messages, and PR description.