#65583 closed enhancement (wontfix)
Abilities API: support ability registration after the one-shot wp_abilities_api_init window
| Reported by: | extrachill | Owned by: | gziolo |
|---|---|---|---|
| Priority: | normal | Milestone: | |
| Component: | Abilities API | Version: | |
| Severity: | normal | Keywords: | dev-feedback has-patch has-unit-tests |
| Cc: | Focuses: |
Description (last modified by )
Problem
wp_register_ability() and wp_register_ability_category() currently require calls to occur while their dedicated initialization actions are running. Calls made after those one-shot actions trigger _doing_it_wrong() and return null, even when WordPress has fully initialized and the registration occurs before the relevant consumer discovers or uses the ability.
This makes the public wrapper stricter than the underlying registry. WP_Abilities_Registry::register() supports the same registration after initialization, which has led independent consumers to add direct registry fallbacks rather than use the public wrapper consistently.
The practical distinction is between registration that is too early or too late for a consumer:
init → registration → discovery or use
and registration that occurs after a consumer has already created a snapshot:
init → discovery or snapshot → registration
The former can be safe even though it occurs after wp_abilities_api_init. The latter remains the caller's lifecycle responsibility.
Design history
This trade-off was discussed during the original Core merge:
- Aaron Jorbin noted that registration problems are commonly caused by registering too early rather than too late, and recommended
did_action()unless a concrete reason requireddoing_action(). - The original merge used
did_action(), allowing registration after initialization. - PR #10452 deliberately changed the check to
doing_action()based on concerns that arbitrary registration timing could cause less obvious downstream problems.
This ticket revisits that explicit design trade-off. It does not claim that downstream consumers are unable to attach callbacks earlier. The question is whether the dedicated action should be the recommended deterministic registration point or the only supported registration window.
Existing implementation pressure
Shipping implementations have independently added post-hook registration paths through the registry:
These implementations generally resolve abilities dynamically at discovery or execution time. Consumers that create persistent or run-scoped snapshots retain their own concrete lifecycle boundaries, such as server creation, bundle export, or agent tool resolution.
Core also permits wp_unregister_ability() and wp_unregister_ability_category() at any time after registration, so completion of the registration actions does not make the registry immutable.
Proposed behavior
Keep wp_abilities_api_init and wp_abilities_api_categories_init as the recommended deterministic registration hooks, while allowing the public wrappers after init has fired. Continue rejecting genuinely too-early registration before init.
Callers registering afterward are responsible for doing so before relevant discovery, snapshot, or use. This follows the registration model used by blocks: a recommended hook without making that hook the only valid window.
The associated patch adds coverage that:
- abilities can be registered after
wp_abilities_api_initand remain discoverable through the public retrieval functions; - ability categories can be registered after
wp_abilities_api_categories_init; - registration before
initcontinues to fail with_doing_it_wrong().
Change History (10)
This ticket was mentioned in PR #12401 on WordPress/wordpress-develop by @extrachill.
5 weeks ago
#1
- Keywords has-patch has-unit-tests added
#3
@
4 weeks ago
I'm not sure I follow.
- Traditionally in WordPress, you need to register things before a specified hook is triggered.
- That an downstream developer can intentionally circumvent this and manually (re-)initialize the registry sounds like a "WordPress Way"™️ feature, not a bug.
- All the PRs related to MCP Adapter seem like they're straightforwardly solvable by fixing how that plugin does things, not by introducing tech debt or introducing unreliability to the lifecycle.
What am I missing?
This change keeps the pre-init guard, but restores the public wrapper as the supported path after WordPress has booted, instead of requiring consumers to call the registry singleton directly.
(This line is the one that sets of the most alarms for me, and what makes me question the rest of the WHY as a false premise).
#4
@
4 weeks ago
@justlevine To clarify, none of these consumers reinitialize the registry. The workaround calls
WP_Abilities_Registry::get_instance()->register() on the existing singleton because the public wrapper rejects the same registration.
I understand the concern that registration outside the recommended hook could make the lifecycle less predictable. I'd argue that if a plugin has not registered its abilities by the time they are needed, that plugin is _doing_it_wrong().
The pattern I followed is the same one that Gutenberg block registration uses. Is there a specific reason abilities must only be registered during one hook, while blocks can be registered later?
Multiple independent consumers encounter the same registration warning, while others bypass it through the registry method. That suggests the current API is creating needless friction and noisy debug logs without actually preventing late registration. Why not support that flexibility through the public wrapper?
#5
@
3 weeks ago
I reviewed each example cited in this ticket. None appears to demonstrate a need for WordPress Core to support late ability registration:
- WordPress/mcp-adapter#117 is an MCP Adapter hook-composition bug. The adapter attaches its registration callbacks during
rest_api_init, afterwp_abilities_api_inithas fired. WordPress/mcp-adapter#201 addresses this by wiring those callbacks duringMcpAdapter::instance(), beforeinit.
- woocommerce/woocommerce#65272 explicitly identifies the same vendored MCP Adapter bug as its root cause, so it is not an independent example.
- WordPress/mcp-adapter#135 was unrelated to timing. The ability was missing its required category, and the reporter confirmed that adding it fixed registration: confirmation.
- use-novamira/novamira#47 was a plugin bootstrap-order issue. Novamira fixed it locally, with the maintainer confirming the fix works end-to-end in v1.8.1: resolution.
The private late-registration helpers mentioned in the description show that some consumers bypass the public API, but they do not demonstrate that correct early hook composition is insufficient.
All actionable cases above can be addressed at the consumer level by attaching registration callbacks during plugin bootstrap, before the registry is initialized. This is consistent with the intentional lifecycle decision recorded in WordPress/wordpress-develop#10452 and committed in changeset 61130, where doing_action() was deliberately chosen over did_action(). A defined registration window also makes the ordering of related WordPress hooks deterministic, including callbacks that inspect or modify abilities during registration. Allowing registration at arbitrary later points would make the result depend on the request path, the hooks that have already fired, and the callbacks or state available at that moment.
Based on the available evidence, I do not think changing the Core registration contract is justified.
#8
@
3 weeks ago
@gziolo Thanks for taking the time to audit those examples. You're right that they do not establish that correct early hook composition is insufficient, and I've updated the ticket and PR descriptions to remove the misclassified examples and narrow the argument accordingly.
I also traced the Data Machine and Agents API implementations more closely. Their normal plugin bootstrap loads the providers early and attaches callbacks before wp_abilities_api_init; the direct-registry paths are defensive support for late-loaded hosts, not evidence of a current production runtime that cannot register earlier.
I still prefer the more flexible did_action() model. This exact trade-off was debated during the original Core review, where @aaronjorbin argued that registration problems are commonly caused by registering too early rather than too late, and that extenders should be trusted to choose a later action absent a concrete reason otherwise. Safe post-hook registration is possible when it occurs before the relevant discovery, snapshot, or use, and the strict window has caused real integration friction.
However, I have not established an unavoidable production case that justifies reversing the intentional Core contract. Given that, I don't think the available evidence is strong enough to continue pursuing the behavioral change. Feel free to close this ticket and the associated PR.
#9
@
2 weeks ago
- Description modified (diff)
- Milestone Awaiting Review
- Resolution → wontfix
- Status reviewing → closed
@extrachill, I appreciate the follow-up and the additional details you’ve provided that document your use cases and findings.
I will close this one as wontfix based on your previous comment.
@gziolo commented on PR #12401:
2 weeks ago
#10
Closing together with https://core.trac.wordpress.org/ticket/65583.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
## What
Allows Abilities API registration through
wp_register_ability()andwp_register_ability_category()at any point after theinitaction has fired.The
wp_abilities_api_initandwp_abilities_api_categories_inithooks remain the recommended registration points because they make abilities/categories available as soon as the registries initialize. The public wrappers now reject only genuinely too-early registration beforeinit.## Why
The current one-shot registration window is fragile for consumers that initialize after
init, such as REST-initialized adapters, Composer-loaded packages, and headless runtimes that boot WordPress before loading extension code. In those caseswp_register_ability()returnsnulland the ability is absent, with the diagnostic only visible through_doing_it_wrong().Public ecosystem examples of this class of issue:
This change keeps the pre-
initguard, but restores the public wrapper as the supported path after WordPress has booted, instead of requiring consumers to call the registry singleton directly.## Tests
Adds coverage that:
wp_abilities_api_initafterinitand remain discoverable viawp_has_ability(),wp_get_ability(), andwp_get_abilities();wp_abilities_api_categories_initafterinit;initregistration still fails with_doing_it_wrong().Local verification:
php -l src/wp-includes/abilities-api.phpphp -l tests/phpunit/tests/abilities-api/wpRegisterAbility.phpphp -l tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.phpvendor/bin/phpcs --standard=phpcs.xml.dist src/wp-includes/abilities-api.php tests/phpunit/tests/abilities-api/wpRegisterAbility.php tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.phpgit diff --checkFull PHPUnit was not run locally because this checkout does not have
wp-tests-config.phpconfigured.## AI assistance