Skip to content

Add a core/settings-update ability - #764

Open
jorgefilipecosta wants to merge 53 commits into
developfrom
add/core-manage-settings-ability
Open

jorgefilipecosta wants to merge 53 commits into
developfrom
add/core-manage-settings-ability

Conversation

@jorgefilipecosta

@jorgefilipecosta jorgefilipecosta commented Jun 23, 2026 •

Copy link
Copy Markdown
Member

What?

Adds core/settings-update next to core/settings-get.

Why?

Agents can read the settings exposed to abilities but not change them. This lets them update those settings under the same rules as the REST API.

How?

  • The ability lives in includes/Abilities/Settings/Settings.php, behind the Custom Abilities experiment with core/settings-get. Neither ability registers when no setting is exposed.
  • Input: a map of setting name to its new value, for any setting core/settings-get reads except url and email (siteurl and admin_email), which stay read-only for now: a wrong site URL makes wp-admin unreachable, and wp-admin only changes the admin email after the new address confirms it. As in the settings endpoint, only users who can manage privacy options, network admins on multisite, can change page_for_privacy_policy. For a setting with a registered default (among core settings, language, use_smilies and posts_per_page), null resets the setting to that default. Unknown names, empty input and null for a setting without a default are rejected; the endpoint deletes such a setting, leaving it without a value.
  • It mirrors WP_REST_Settings_Controller::update_item(). Every value is sanitized against its schema before any is written; the REST API does this through each setting's sanitize_callback arg option, while the Abilities API only validates. Settings are written in registration order, objects reject undeclared properties, and null is refused with settings_invalid_stored_value while the stored value fails validation. Unlike the endpoint, which checks stored values while writing and keeps the settings it already wrote, every check runs before any write, so an error leaves every setting unchanged. A value that sanitizing makes invalid is refused with settings_invalid_param, where the endpoint stores it: %ab@x.co in a setting with the email format would be stored as @x.co. null resets a boolean stored as false, which WordPress saves as ''; the endpoint refuses that reset. The endpoint's null only deletes the stored value, while the ability then stores the registered default: core registers its settings, and so their defaults, only in REST and abilities requests, so after the endpoint's { "posts_per_page": null } other requests read false and show one post per page. For the same reason, a value that matches the registered default is stored when no value is, where update_option() stores nothing. A setting with no stored value reads as its registered default, so null can reset it, where the endpoint refuses that null with a 500. The endpoint also skips a privacy policy page change from users who cannot manage privacy options, while the ability refuses the update with settings_cannot_manage_privacy_options. Error codes use settings_ in place of rest_, and the REST-only filters do not run.
  • Output: only the updated settings, as core/settings-get reads them after the update; the endpoint answers with the whole settings object, which core/settings-get still returns. core/settings-get now sanitizes each stored value against its schema and leaves out a value the schema rejects, before or after sanitizing, instead of failing for every setting; the endpoint answers null for such a value. Without that, a setting without a default that the endpoint reset to null, such as default_ping_status, would fail core/settings-get for every setting. A boolean stored as false, which WordPress saves as '', reads as false.
  • Names: as in core (Abilities API: Add a core/settings-get ability wordpress-develop#12141), a setting flagged with show_in_abilities => true takes its name and schema from show_in_rest, and every core setting is flagged with true. Both abilities, and the deprecated core/read-settings alias, now use the settings endpoint's names: title, description, url, email, timezone, language and page_for_privacy_policy replace blogname, blogdescription, siteurl, admin_email, timezone_string, WPLANG and wp_page_for_privacy_policy. url gets the endpoint's uri format, which the abilities client accepts since WordPress 7.1.
  • It is destructive but not idempotent, so it stays on POST: the DELETE method cannot carry null.
  • The tests port the settings endpoint's update tests under their core names. A throwaway harness ran 23 updates through both the endpoint and the ability on WordPress 7.1.2. The results match, apart from the differences above.

Testing Instructions

  1. Enable the Custom Abilities experiment.
  2. Run core/settings-update with { "title": "New name" }, then with { "posts_per_page": null }. The first call returns { "title": "New name" }, the second { "posts_per_page": 10 }. { "url": "https://example.com" } and { "default_ping_status": null } are rejected.
  3. npm run test:php -- --filter 'SettingsTest|Gated_AbilitiesTest|Custom_AbilitiesTest'
  4. npm run test:e2e -- tests/e2e/specs/abilities/core-settings-update.spec.js

Changelog Entry

Added - New core/settings-update ability that mirrors the REST API settings endpoint.
Changed - core/settings-get and its deprecated core/read-settings alias now name settings as the REST API settings endpoint does, for example title instead of blogname and url instead of siteurl.
Changed - The Settings_Get gated ability class is now Settings, so code that removes it with the wpai_gated_abilities filter needs the new class name.

AI disclosure: the latest fixes come from an AI-assisted code review of the core port, and were drafted with AI assistance (Claude Code) and reviewed by me.

Open WordPress Playground Preview

@github-actions

github-actions Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jorgefilipecosta <jorgefilipecosta@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>
Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@codecov

codecov Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.37500% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.83%. Comparing base (19d5670) to head (4f0d1e6).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
includes/Abilities/Settings/Settings.php 99.29% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #764      +/-   ##
=============================================
+ Coverage      81.67%   81.83%   +0.15%     
- Complexity      3103     3115      +12     
=============================================
  Files            129      129              
  Lines          12334    12426      +92     
=============================================
+ Hits           10074    10169      +95     
+ Misses          2260     2257       -3     
Flag Coverage Δ
unit 81.83% <99.37%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jeffpaul jeffpaul added this to the 1.2.0 milestone Jun 23, 2026
@jeffpaul jeffpaul moved this from Triage to Needs review in WordPress AI Roadmap Jun 23, 2026
@jeffpaul
jeffpaul requested review from dkotter and gziolo June 23, 2026 14:59
Comment thread includes/Abilities/Settings/Settings.php Outdated
Comment thread includes/Abilities/Settings/Settings.php
Comment thread includes/Abilities/Settings/Settings.php Outdated
Comment thread includes/Abilities/Settings/Settings.php Outdated
justlevine pushed a commit that referenced this pull request Jun 26, 2026
Bumps the github-actions-updates group with 2 updates:
[actions/checkout](https://github.com/actions/checkout) and
[softprops/action-gh-release](https://github.com/softprops/action-gh-release).

Updates `actions/checkout` from 6.0.3 to 7.0.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1044a6dea927916f2c38ba5aeffbc0a847b1221a"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f0282184c7ce73ab54c7e4ab5a617122602e575f"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d914b262ffc244530a203ab40decab34c3abf34d"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/537c7ef99cef6e5ddb5e7ff5d16d14510503801d"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/130a169078a413d3a5246a393625e8e742f387f6"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/7d09575332117a40b46e5e020664df234cd416f3"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0f9f3aa320cb53abeb534aeb54048075d9697a0e"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f9e715a95fcd1f9253f77dd28f11e88d2d6460c7"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0">compare
view</a></li>
</ul>
</details>
<br />

Updates `softprops/action-gh-release` from 3.0.0 to 3.0.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.1</h2>
<h2>3.0.1</h2>
<ul>
<li>maintenance release with updated dependencies</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>3.0.1</h2>
<ul>
<li>maintenance release with updated dependencies</li>
</ul>
<h2>3.0.0</h2>
<p><code>3.0.0</code> is a major release that moves the action runtime
from Node 20 to Node 24.
Use <code>v3</code> on GitHub-hosted runners and self-hosted fleets that
already support the
Node 24 Actions runtime. If you still need the last Node 20-compatible
line, stay on
<code>v2.6.2</code>.</p>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>Move the action runtime and bundle target to Node 24</li>
<li>Update <code>@types/node</code> to the Node 24 line and allow future
Dependabot updates</li>
<li>Keep the floating major tag on <code>v3</code>; <code>v2</code>
remains pinned to the latest <code>2.x</code> release</li>
</ul>
<h2>2.6.2</h2>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump picomatch from 4.0.3 to 4.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/775">softprops/action-gh-release#775</a></li>
<li>chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/777">softprops/action-gh-release#777</a></li>
<li>chore(deps): bump vite from 8.0.0 to 8.0.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/781">softprops/action-gh-release#781</a></li>
</ul>
<h2>2.6.1</h2>
<p><code>2.6.1</code> is a patch release focused on restoring linked
discussion thread creation when
<code>discussion_category_name</code> is set. It fixes
<code>[#764](https://github.com/softprops/action-gh-release/issues/764)</code>,
where the draft-first publish flow
stopped carrying the discussion category through the final publish
step.</p>
<p>If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.</p>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix: preserve discussion category on publish by <a
href="https://github.com/chenrui333"><code>@​chenrui333</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/765">softprops/action-gh-release#765</a></li>
</ul>
<h2>2.6.0</h2>
<p><code>2.6.0</code> is a minor release centered on
<code>previous_tag</code> support for
<code>generate_release_notes</code>,
which lets workflows pin GitHub's comparison base explicitly instead of
relying on the default range.
It also includes the recent concurrent asset upload recovery fix, a
<code>working_directory</code> docs sync,
a checked-bundle freshness guard for maintainers, and clearer
immutable-prerelease guidance where
GitHub platform behavior imposes constraints on how prerelease asset
uploads can be published.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/softprops/action-gh-release/commit/718ea10b132b3b2eba29c1007bb80653f286566b"><code>718ea10</code></a>
release 3.0.1</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/f1a938b9d84ca9b770d0d8dfeb3e7285fe261e63"><code>f1a938b</code></a>
chore(deps): bump esbuild from 0.28.0 to 0.28.1 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/802">#802</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/0066ead0de7252b4876b36b5357fc3974619d36a"><code>0066ead</code></a>
chore(deps): bump vite from 8.0.14 to 8.0.16 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/806">#806</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/dc643cac6252aaa00c9b0b6c940d489cd7bf6b23"><code>dc643ca</code></a>
chore(deps): bump the npm group with 3 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/805">#805</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/85ee99b6b20742a3823a8a289ee5e6ceab44e8aa"><code>85ee99b</code></a>
chore(deps): bump actions/checkout in the github-actions group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/804">#804</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/9ed3cf9a6863b31f005d951c8d19de20628cf4eb"><code>9ed3cf9</code></a>
chore(deps): bump the npm group with 2 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/800">#800</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/3efcac8951299998593f871640ea8059d6818655"><code>3efcac8</code></a>
chore(deps): bump the npm group with 3 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/798">#798</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/05d6b9164aa74958de40b0179d6a773112fcdc7f"><code>05d6b91</code></a>
chore(deps): bump brace-expansion from 5.0.5 to 5.0.6 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/797">#797</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/403a5240f3837fa857f642062e05aad6bb3391ca"><code>403a524</code></a>
chore(deps): bump <code>@​types/node</code> from 24.12.2 to 24.12.3 in
the npm group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/796">#796</a>)</li>
<li><a
href="https://github.com/softprops/action-gh-release/commit/437e073e786973c6b6af97d9e445c41ae43b1d29"><code>437e073</code></a>
chore(deps): bump the npm group with 4 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/792">#792</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/softprops/action-gh-release/compare/b4309332981a82ec1c5618f44dd2e27cc8bfbfda...718ea10b132b3b2eba29c1007bb80653f286566b">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

<!-- wp-playground-preview:start -->
<a
href="https://playground.wordpress.net?blueprint-url=data:application/json,%7B%22preferredVersions%22%3A%7B%22wp%22%3A%22latest%22%7D%2C%22steps%22%3A%5B%7B%22step%22%3A%22installPlugin%22%2C%22pluginData%22%3A%7B%22resource%22%3A%22url%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2FWordPress%2Fai%2Freleases%2Fdownload%2Fci-artifacts%2Fpr-780-ee5cd01f0ec6e8d122b0d58ced3e253b6eb3938d.zip%22%7D%7D%5D%7D"
target="_blank" rel="noopener noreferrer">
<img
src="https://raw.githubusercontent.com/adamziel/playground-preview/refs/heads/trunk/assets/playground-preview-button.svg"
alt="Open WordPress Playground Preview" width="220" height="57" />
</a>
<!-- wp-playground-preview:end -->

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@jeffpaul jeffpaul modified the milestones: 1.2.0, 1.3.0 Jul 13, 2026
@jeffpaul jeffpaul moved this from Needs review to In progress in WordPress AI Roadmap Jul 13, 2026
@jeffpaul jeffpaul mentioned this pull request Jul 13, 2026
1 task done
@jeffpaul

Copy link
Copy Markdown
Member

I'd like to see #863 land alongside this specific PR (and any others that aren't explicitly read abilities), to help ensure folks enabling these sorts of abilities are doing so knowingly.

@gziolo gziolo mentioned this pull request Jul 17, 2026
6 of 7 tasks
@jeffpaul jeffpaul mentioned this pull request Aug 10, 2026
48 tasks done
@dkotter dkotter modified the milestones: 1.3.0, 1.4.0 Aug 12, 2026
@jeffpaul jeffpaul modified the milestones: 1.4.0, 1.5.0 Sep 17, 2026
@jorgefilipecosta
jorgefilipecosta force-pushed the add/core-manage-settings-ability branch from 68d5383 to 1174887 Compare October 1, 2026 17:00
@jorgefilipecosta
jorgefilipecosta requested a review from a team as a code owner October 1, 2026 17:00
@jorgefilipecosta jorgefilipecosta changed the title Add a core/manage-settings ability Add a core/settings-update ability Oct 1, 2026
@jorgefilipecosta
jorgefilipecosta changed the base branch from develop to update/rename-settings-get-ability October 1, 2026 17:01
* setting; the settings endpoint answers null for it. A setting without a registered
* default that `core/settings-update` reset to null reads this way.
*/
if ( is_wp_error( rest_validate_value_from_schema( $value, $setting['schema'] ) ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this may not matter but I think this will leave out boolean settings that have been turned off, as WordPress often saves those as an empty string instead of false. The validation here will fail for those so those settings will never be returned (previous to this change, they were cast to false)

* The endpoint checks this while writing; checking it here keeps the earlier
* settings in the input from being written when the update fails.
*/
if ( '' === $invalid_stored && is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar thing here. If I want to reset a boolean-type setting to null, this check fails. May be another thing we don't care to support though

* the update, an empty object when none can be read back,
* or a WP_Error.
*/
public function execute_update_settings( $input = array() ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need/want a check here for READ_ONLY_OPTIONS, in case a call skipped the input validation?

'group' => isset( $args['group'] ) && is_string( $args['group'] ) ? $args['group'] : '',
'default' => array_key_exists( 'default', $args ) ? $args['default'] : false,
'schema' => $this->value_schema( $args, $show ),
$settings[ empty( $show['name'] ) ? $option_name : $show['name'] ] = array(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any risk here for someone passing a name that isn't a string? Previously we had a is_string check that would have handled that but that's removed here. This could lead to fatal errors if someone can pass in a non-string value

if ( is_null( $args['value'] ) ) {
delete_option( $args['option_name'] );
} else {
update_option( $args['option_name'], $args['value'] );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if update_option was successful? And if not, report that instead of reporting success?

$options[ $name ] = $args;
}

if ( $invalid_params ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this code ever be triggered? Since we validate params earlier I think this might be dead code

* the new address confirms it.
*
* @since x.x.x
* @var string[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @var string[]
* @var list<string>

WordPress stores false as '', which rest_is_boolean() rejects. Since
3fa0775 validates the stored value before sanitizing it,
core/settings-get left out every boolean setting whose value is false,
including one core/settings-update had just written and a Settings API
checkbox saved unchecked. cast_value() used to read it as false, while
the settings endpoint answers null for it.

Read '' as false for a boolean setting before validating it, and test
that it reads back as false.
WP_REST_Settings_Controller::get_registered_options() runs
rest_default_additional_properties_to_false() on every setting schema,
so objects reject properties they do not declare when the endpoint
reads a value as well as when it writes one. The abilities only did it
for writes: core/settings-get returned a stored object with an
undeclared property whole, where the endpoint answers null, and that
value could not be sent back to core/settings-update.

Close the schema once in value_schema(), so both abilities read and
write against it. The output schemas now declare additionalProperties
false on objects, as the endpoint's schema does. The core class
WP_Abilities_Settings gets the same change, so the two stay identical.
The input schema accepts an object, but execute_get_settings() replaced
anything other than an array with an empty one, so a PHP caller that
passed its filters as an object, such as
(object) array( 'group' => 'reading' ), got every setting back.

Normalize the input with rest_sanitize_object(), as
core/settings-update already does. The core class WP_Abilities_Settings
gets the same change.
The class docblock said core/settings-update writes the same settings
core/settings-get reads, which leaves out siteurl and admin_email, and
Show_In_Abilities named only core/settings-get as a consumer of the
flag. The execute_update_settings() docblock described its checks in
one long sentence; list them in the order they run.
The core class WP_Abilities_Settings switches the core/settings-get
input default to array(), as the other core abilities use. Core sends
that default as {} on the abilities endpoint and, since WordPress 7.1,
through the AI client, and an array cannot be changed in place by a
wp_ability_normalize_input filter the way the shared object can.

The plugin keeps the object, because the WordPress 7.0 AI client sends
ability schemas as is, where array() would go out as []. Mark it with a
Plugin: comment, as the class does for its other differences from core.
The core class WP_Abilities_Settings sets only the public meta flag on
core/settings-get, which WordPress 7.1 also reads as show_in_rest.
WordPress 7.0 does not read public, so the plugin keeps show_in_rest.
Mark it with a Plugin: comment, as the class does for its other
differences from core.
// which cannot carry null.
'idempotent' => false,
),
'show_in_rest' => true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use 'public' => true here, replacing 'show_in_rest' => true once WordPress 7.0 compatibility is no longer needed? On WordPress 7.1+, public also enables REST exposure by default, while show_in_rest alone leaves public false, so the default MCP adapter does not expose this ability. This would align settings-update with settings-get and the other core abilities updated in #1122. If we still need WordPress 7.0 support, we should add public and retain show_in_rest for that compatibility.

jorgefilipecosta and others added 21 commits October 7, 2026 13:53
Port the core change in WordPress/wordpress-develop#12141 (749c2eb430).
When show_in_abilities is true, a setting now takes the name and schema
from show_in_rest; an array is still used as is. Show_In_Abilities flags
every core setting with true, as option.php does, so core/settings-get,
its deprecated core/read-settings alias, and core/settings-update use
the settings endpoint's names: blogname is title, blogdescription is
description, siteurl is url, admin_email is email, timezone_string is
timezone, WPLANG is language, and wp_page_for_privacy_policy is
page_for_privacy_policy. The email format and the comment status enums
now come from show_in_rest, and siteurl gains its uri format, which the
abilities client accepts since WordPress 7.1.

Port core's tests for the REST names and the inherited exposure, use
the new names in the other tests and the e2e specs, and drop the
Show_In_Abilities test for a curated array value, since none is left.

Co-authored-by: Grzegorz Ziolkowski <grzegorz@gziolo.pl>
Port the core change in WordPress/wordpress-develop#12141 (e7e6a2340e),
which matches the other core abilities. Since WordPress 7.1,
wp_prepare_json_schema_for_client() sends an empty object default as {}
on the abilities endpoint and through the AI client, and the execute
callback gets an array when no input is given. Drop the Plugin: comment
that kept the object for WordPress 7.0, so the class matches core again.

Co-authored-by: Grzegorz Ziolkowski <grzegorz@gziolo.pl>
Port the core change in WordPress/wordpress-develop#12141 (30742fc248).
The input schema validates `fields` with rest_is_array(), which also
accepts a comma-separated string, but execute_get_settings() replaced
anything other than an array with an empty one, so a PHP caller that
passed 'title,posts_per_page' got every setting back. REST requests
were not affected, because the run endpoint converts the input to the
schema types first.

Normalize the list with rest_sanitize_array().
Port the core docs change in WordPress/wordpress-develop#12141
(4620fbcaed). An array replaces the show_in_rest name and schema
entirely, so a setting that only sets a name for abilities also drops
its REST schema, such as an email format. Say so where Show_In_Abilities
describes the values it can set.
Port the core test in WordPress/wordpress-develop#12141 (f5cf4af483).
show_in_abilities => true reuses the show_in_rest name and schema, so
guard the opt-in rule next to it: a setting shown in the REST API
without show_in_abilities is not exposed.
… exposed

Port the core docs change in WordPress/wordpress-develop#12141
(89ca6e70f4). The abilities registry can initialize while init is still
running, for example when a plugin uses it at an earlier init priority,
so the docs were wrong to call registering a setting on init reliable.
Say that settings registered after the abilities register are not
exposed, and drop the comments that only repeat that the exposed
settings are computed once.
Port the core change in WordPress/wordpress-develop#12141 (a4e7ea0948).
It does not: it reads values with a plain get_option() call, without
the rest_pre_get_setting filter or the schema default that
/wp/v2/settings passes, and it leaves out a value the endpoint answers
with null. Describe what the ability does instead.
Port the core change in WordPress/wordpress-develop#12141 (f454c10bbf).
PHP turns a numeric array key, such as a setting named 123, into an
integer. The fields enum then listed the integer, so input validation
rejected the name passed as a string, and the strict in_array() check
did not match it either. Cast the names to strings, and test with a
numeric setting name.

core/settings-update builds its answer from the keys of the settings it
wrote, which are integers for such names, so pass them to
core/settings-get as strings too. Without that, the string check would
leave a numeric setting out of the answer.
Port the core change in WordPress/wordpress-develop#12141 (a999491872).
Stored values were only validated before sanitizing, but
rest_sanitize_value_from_schema() can turn a valid value into one the
schema rejects. For example, sanitize_text_field() turns the email
%ab@x.co into @x.co. Output validation then failed the whole call
instead of leaving out that one setting. Validate the sanitized value
too.
The comment above the schema checks in execute_get_settings() had a
sentence about core/settings-update resets, the only difference from
core's method besides the @SInCE lines, and it carried no Plugin:
marker. Drop it.
null deletes the stored value. Among the core settings only language,
use_smilies and posts_per_page register a default, so for the others a
null left the setting without a value: the site title, the date format
or the front page setting read as false, and both abilities then left
the setting out. The description promised that the setting falls back
to its default.

Add null to the type, and to any enum, only for a setting with a
registered default, so the input schema shows which settings can be
reset and the Abilities API rejects a null for the others. The settings
endpoint accepts null for every setting.
rest_sanitize_value_from_schema() can turn a valid value into one the
schema rejects: sanitize_text_field() turns the email %ab@x.co into
@x.co. The ability stored that value, and since core/settings-get now
leaves out such values, the answer was {} with nothing to say why.

Validate the sanitized value too, and refuse it with
settings_invalid_param before any setting is written. The settings
endpoint stores it.
WordPress stores false as '', which the boolean schema rejects. A null
for a boolean turned off in an earlier request was then refused with
settings_invalid_stored_value, although core/settings-get reads the
setting as false, so a client never sees an invalid value to protect.

Read the stored value the way core/settings-get does before checking
it. The settings endpoint refuses that null, since it answers null for
the setting.
A show_in_abilities (or show_in_rest) name that is not a string, such as
an array, was used as an array key, which is a fatal error while the
abilities register, so one bad registration broke every ability.
develop checked the name with is_string(). Use the option name instead,
as for an empty name.

Core's WP_Abilities_Settings, and the settings endpoint, fail the same
way, so this is marked as a plugin difference.
WP_UnitTestCase backs up $wp_actions once and restores it after every
test, so the two tests that set the rest_api_init count no longer save
and restore it themselves.
rest_validate_value_from_schema() ignores an empty enum, so a setting
whose schema has `enum => array()`, such as a list computed when the
setting registers that came out empty, accepts any value through
core/settings-get and the settings endpoint. For a setting with a
default, update_value_schema() appended null to it, which left
`enum => array( null )`, so the ability refused every value but null.

Add null only to an enum that is not empty.
…alue

The comment said the check keeps a client that sends a response back
from deleting values that fail validation. core/settings-get never
answers null, it leaves such values out, so that only holds for answers
from the settings endpoint, whose setting names the abilities now share.
Say so, and note that a repeated null can be refused once the stored
value is gone, since get_option() is passed false as the default.
The input schema alone kept url and email read-only and refused null
for a setting without a default. A wp_ability_validate_input filter
can skip that validation, and then {"url": ...} wrote siteurl before
the output check failed, a null deleted default_ping_status, and an
input with only unknown names answered with every setting, since an
empty `fields` filter means all of them.

Skip read-only settings in the write loop, check each sanitized value
against the input schema rather than the setting's schema, and answer
with no setting when nothing was written.
A null deleted the stored value, as the settings endpoint does, so the
setting fell back to its registered default. That default only applies
in requests that register the setting, and core registers its own
settings only in REST and abilities requests. After
{"posts_per_page": null}, the ability answered 10 while other requests
read false and showed one post per page, and sending 10 again stored
nothing, since update_option() skips a value that matches the
registered default when no value is stored.

Delete the stored value, then store the registered default, so every
request reads it. The delete comes first because sanitize_option()
turns a language that is not installed, such as the en_US default, into
the current value. Also store a value that update_option() skips that
way.
The stored-value check read the value with false as the default, as the
settings endpoint does, so a setting with no stored value failed
validation and null was refused with a 500: language on a site without
a WPLANG row, or posts_per_page after the endpoint's null deleted it.
Since null now stores the default, that refusal blocked the very reset
that repairs such a setting.

Read a missing value as the registered default, as core/settings-get and
the endpoint answer it. A stored value that fails validation is still
refused.
The fallback after update_option() added the value whenever no value
was stored afterwards, including a value update_option() refused on
purpose: a pre_update_option_{$option} filter that turned the update
back was bypassed, and a language that is not installed left a new
en_US row where the settings endpoint stores nothing.

Add the value only when it matches the registered default, the one case
where update_option() stores nothing because no value is stored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

4 participants