Make WordPress Core

Opened 5 weeks ago

Closed 2 weeks ago

Last modified 2 weeks ago

#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 gziolo)

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 required doing_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_init and remain discoverable through the public retrieval functions;
  • ability categories can be registered after wp_abilities_api_categories_init;
  • registration before init continues to fail with _doing_it_wrong().

Patch: WordPress/wordpress-develop#12401

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

## What

Allows Abilities API registration through wp_register_ability() and wp_register_ability_category() at any point after the init action has fired.

The wp_abilities_api_init and wp_abilities_api_categories_init hooks 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 before init.

## 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 cases wp_register_ability() returns null and the ability is absent, with the diagnostic only visible through _doing_it_wrong().

Public ecosystem examples of this class of issue:

  • WordPress/mcp-adapter#117
  • WordPress/mcp-adapter#135
  • woocommerce/woocommerce#65272
  • use-novamira/novamira#47

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.

## Tests

Adds coverage that:

  • abilities can be registered outside wp_abilities_api_init after init and remain discoverable via wp_has_ability(), wp_get_ability(), and wp_get_abilities();
  • ability categories can be registered outside wp_abilities_api_categories_init after init;
  • pre-init registration still fails with _doing_it_wrong().

Local verification:

  • php -l src/wp-includes/abilities-api.php
  • php -l tests/phpunit/tests/abilities-api/wpRegisterAbility.php
  • php -l tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.php
  • vendor/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.php
  • git diff --check

Full PHPUnit was not run locally because this checkout does not have wp-tests-config.php configured.

## AI assistance

  • AI assistance: Yes
  • Tool(s): opencode — GPT 5.5 (OpenAI)
  • Used for: Drafted the patch, tests, and PR text from the public Trac ticket and source-code investigation; the submitter reviewed and remains responsible for the change.

#2 @gziolo
4 weeks ago

  • Description modified (diff)
  • Owner set to gziolo
  • Status newreviewing

#3 @justlevine
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 @extrachill
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 @gziolo
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, after wp_abilities_api_init has fired. WordPress/mcp-adapter#201 addresses this by wiring those callbacks during McpAdapter::instance(), before init.

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.

#6 @extrachill
3 weeks ago

  • Description modified (diff)

#7 @extrachill
3 weeks ago

  • Description modified (diff)

#8 @extrachill
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 @gziolo
2 weeks ago

  • Description modified (diff)
  • Milestone Awaiting Review
  • Resolutionwontfix
  • Status reviewingclosed

@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.

Note: See TracTickets for help on using tickets.