Skip to content

Add a core/settings-update ability - #764

Open
jorgefilipecosta wants to merge 10 commits into
update/rename-settings-get-abilityfrom
add/core-manage-settings-ability
Open

jorgefilipecosta wants to merge 10 commits into
update/rename-settings-get-abilityfrom
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. Based on #1087.

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 siteurl and admin_email, which stay read-only for now: a wrong siteurl makes wp-admin unreachable, and wp-admin only changes admin_email after the new address confirms it. null deletes the stored value, so the setting falls back to its default. Unknown names and empty input are rejected.
  • 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. Error codes use settings_ in place of rest_, and the REST-only filters do not run.
  • Output: the map core/settings-get returns, as the endpoint answers with the whole settings object. core/settings-get now leaves out a value its schema rejects instead of failing for every setting. Without that, resetting a setting that has no default, such as default_ping_status, to null would break both abilities.
  • 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 (unknown or empty input, admin_email, nothing saved when a null is refused) and values core/settings-get already reads differently (an unset array setting reads as [] instead of null).

Testing Instructions

  1. Enable the Custom Abilities experiment.
  2. Run core/settings-update with { "blogname": "New name" }, then with { "posts_per_page": null }. Each call returns the settings, with the new title and then posts_per_page back to 10. { "siteurl": "https://example.com" } is 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.

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: 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 98.92473% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.67%. Comparing base (2734238) to head (19dbe6f).

Files with missing lines Patch % Lines
includes/Abilities/Settings/Settings.php 98.92% 1 Missing ⚠️
Additional details and impacted files
@@                           Coverage Diff                            @@
##             update/rename-settings-get-ability     #764      +/-   ##
========================================================================
+ Coverage                                 81.54%   81.67%   +0.13%     
- Complexity                                 3071     3092      +21     
========================================================================
  Files                                       129      129              
  Lines                                     12254    12341      +87     
========================================================================
+ Hits                                       9992    10080      +88     
+ Misses                                     2262     2261       -1     
Flag Coverage Δ
unit 81.67% <98.92%> (+0.13%) ⬆️

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
Writes the settings core/settings-get reads, the way the REST settings
endpoint updates them: every value is sanitized against its schema
before any is written, the settings are written in the order they were
registered, and null deletes the stored value so the setting falls back
to its default, unless the stored value fails validation. Objects in a
setting's schema reject undeclared properties, as in the endpoint, and
the ability answers with the map core/settings-get returns, as the
endpoint answers with the whole settings object.

Neither settings ability registers when no setting is exposed, and
core/settings-get leaves out a value its schema rejects instead of
failing for every setting. A setting without a registered default reads
that way once it is reset to null.

The ability is gated behind the Custom Abilities experiment with
core/settings-get.
Cover registration, the schemas, writing and sanitizing values, the
cases that fail before anything is written, null resets, and the
invalid stored value that stops an update partway, as it does in the
REST settings endpoint.
Update settings through the client Abilities API, reset the e2e sample
setting to its default with null, and check that an unknown setting is
rejected.
@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
The comments compare the update with the settings endpoint the way the
content abilities compare their writes with the posts endpoint, without
naming the REST API or its controller.
Run core's update tests for the settings endpoint through the ability,
under their core names: arrays, objects and nested objects, an
additionalProperties schema, invalid types, integers and floats, null,
an invalid enum, and an invalid stored value. They replace the tests
that covered the same cases. The filter and privacy policy page tests
are left out: the ability runs no REST filters and does not expose that
setting.
Rename the gated class to Settings, since it now covers both
core/settings-get and core/settings-update.

Restore the settings in the e2e spec with finally, so a failed
assertion does not leave changed values for other specs.

Mention in the core/settings-get description that a setting whose
stored value does not match its schema is left out.
@gziolo

gziolo commented Oct 2, 2026

Copy link
Copy Markdown
Member

Thanks for addressing my earlier feedback. I tested both abilities on my local site, and they behave the same as /wp/v2/settings for all the cases I tried: normal updates, null resetting to the default, invalid values, unknown settings, empty input, and permissions.

I pushed a commit with a few small fixes:

  • Renamed the gated class from Settings_Get to Settings, since it now covers both abilities.
  • The e2e tests now restore the settings in finally, so a failed assertion doesn't affect other tests.
  • The core/settings-get description now says that a setting with an invalid stored value is left out of the result.

@gziolo

gziolo commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

When core/settings-update returns settings_invalid_stored_value, the settings registered before the failing one are already saved. Here's a small change (might need some enhancements) that runs every check before any write, so an error always means nothing was saved:

@@ execute_update_settings()
 		$options        = array();
 		$invalid_params = array();
+		$invalid_stored = '';
 		foreach ( (array) $this->exposed_settings as $name => $setting ) {
@@
-			// The endpoint's sanitize callback keeps null as is.
-			if ( ! is_null( $args['value'] ) ) {
+			if ( is_null( $args['value'] ) ) {
+				/*
+				 * As in the settings endpoint, a stored value that does not pass validation
+				 * cannot be updated to null. The endpoint returns such values as null, so this
+				 * keeps a client that sends a response back from deleting them by mistake.
+				 * 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'] ) ) ) {
+					$invalid_stored = $name;
+				}
+			} else {
+				// The endpoint's sanitize callback keeps null as is, and sanitizes anything else.
 				$args['value'] = rest_sanitize_value_from_schema( $args['value'], $args['schema'], $name );
 			}
@@
-		foreach ( $options as $name => $args ) {
+		if ( '' !== $invalid_stored ) {
+			return new WP_Error(
+				'settings_invalid_stored_value',
+				/* translators: %s: Property name. */
+				sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.', 'ai' ), $invalid_stored ),
+				array( 'status' => 500 )
+			);
+		}
+
+		foreach ( $options as $args ) {
 			/*
 			 * A null value for an option would have the same effect as
@@
 			if ( is_null( $args['value'] ) ) {
-				/*
-				 * As in the settings endpoint, a stored value that does not pass validation
-				 * cannot be updated to null. The endpoint returns such values as null, so this
-				 * keeps a client that sends a response back from deleting them by mistake.
-				 */
-				if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
-					return new WP_Error(
-						'settings_invalid_stored_value',
-						/* translators: %s: Property name. */
-						sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.', 'ai' ), $name ),
-						array( 'status' => 500 )
-					);
-				}
-
 				delete_option( $args['option_name'] );

Invalid parameters (400) are still reported first, same as now. The stored value error (500) comes next, and only then do we write.

This changes test_core_settings_update_writes_in_registration_order, which asserts the partial save on purpose. It would become test_core_settings_update_refused_null_writes_no_setting, with the last assertion flipped to 'Original Name'. The downside is that no test covers the write order anymore. I think that's fine, because the order is hard to observe once partial saves are gone.

This is an intentional difference from /wp/v2/settings, so it would be good to mention it in the PR description.

Happy to push it if you agree.

@gziolo

gziolo commented Oct 2, 2026

Copy link
Copy Markdown
Member

Should we opt out sensitive settings from core/settings-update? I mean these two:

  • siteurl: a wrong value can make the site unreachable.
  • admin_email: wp-admin asks for email confirmation before changing it, but the ability skips that step.

They could stay readable with core/get-site-info. The REST API isn't this strict and allows both, but agents make mistakes more easily, so I'd rather be careful here.

@jorgefilipecosta

jorgefilipecosta commented Oct 2, 2026 •

Copy link
Copy Markdown
Member Author

Should we opt out sensitive settings from core/settings-update? I mean these two:

  • siteurl: a wrong value can make the site unreachable.
  • admin_email: wp-admin asks for email confirmation before changing it, but the ability skips that step.

They could stay readable with core/get-site-info. The REST API isn't this strict and allows both, but agents make mistakes more easily, so I'd rather be careful here.

Digging deeper into I applied the suggest admin_email for example on normal UI requires an email confirmation, If we support agent changes I guess we should also include that confirmation, changing site url is also risky. I left both settings out for now we can change that later.

@jorgefilipecosta

Copy link
Copy Markdown
Member Author

When core/settings-update returns settings_invalid_stored_value, the settings registered before the failing one are already saved. Here's a small change (might need some enhancements) that runs every check before any write, so an error always means nothing was saved:

@@ execute_update_settings()
 		$options        = array();
 		$invalid_params = array();
+		$invalid_stored = '';
 		foreach ( (array) $this->exposed_settings as $name => $setting ) {
@@
-			// The endpoint's sanitize callback keeps null as is.
-			if ( ! is_null( $args['value'] ) ) {
+			if ( is_null( $args['value'] ) ) {
+				/*
+				 * As in the settings endpoint, a stored value that does not pass validation
+				 * cannot be updated to null. The endpoint returns such values as null, so this
+				 * keeps a client that sends a response back from deleting them by mistake.
+				 * 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'] ) ) ) {
+					$invalid_stored = $name;
+				}
+			} else {
+				// The endpoint's sanitize callback keeps null as is, and sanitizes anything else.
 				$args['value'] = rest_sanitize_value_from_schema( $args['value'], $args['schema'], $name );
 			}
@@
-		foreach ( $options as $name => $args ) {
+		if ( '' !== $invalid_stored ) {
+			return new WP_Error(
+				'settings_invalid_stored_value',
+				/* translators: %s: Property name. */
+				sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.', 'ai' ), $invalid_stored ),
+				array( 'status' => 500 )
+			);
+		}
+
+		foreach ( $options as $args ) {
 			/*
 			 * A null value for an option would have the same effect as
@@
 			if ( is_null( $args['value'] ) ) {
-				/*
-				 * As in the settings endpoint, a stored value that does not pass validation
-				 * cannot be updated to null. The endpoint returns such values as null, so this
-				 * keeps a client that sends a response back from deleting them by mistake.
-				 */
-				if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
-					return new WP_Error(
-						'settings_invalid_stored_value',
-						/* translators: %s: Property name. */
-						sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.', 'ai' ), $name ),
-						array( 'status' => 500 )
-					);
-				}
-
 				delete_option( $args['option_name'] );

Invalid parameters (400) are still reported first, same as now. The stored value error (500) comes next, and only then do we write.

This changes test_core_settings_update_writes_in_registration_order, which asserts the partial save on purpose. It would become test_core_settings_update_refused_null_writes_no_setting, with the last assertion flipped to 'Original Name'. The downside is that no test covers the write order anymore. I think that's fine, because the order is hard to observe once partial saves are gone.

This is an intentional difference from /wp/v2/settings, so it would be good to mention it in the PR description.

Happy to push it if you agree.

Hi @gziolo feel free to push any changes.

jorgefilipecosta and others added 4 commits October 2, 2026 17:46
Check the stored value behind every null before writing any setting, so
an invalid stored value fails the update without saving the settings
registered before it. The settings endpoint checks it while writing and
keeps those earlier writes.

Co-authored-by: Grzegorz Ziolkowski <grzegorz@gziolo.pl>
Now that a failed update writes nothing, the write order no longer shows
in the stored values, so watch the updated_option action instead.
The value it reads can be the fallback default, not only a stored one: a
setting without a registered default that was reset to null is left out
too.
Both stay readable through core/settings-get. A wrong siteurl makes
wp-admin unreachable, and wp-admin only changes admin_email once the new
address confirms it, while the ability would change it at once without
telling the old address.
@jorgefilipecosta

Copy link
Copy Markdown
Member Author

Hi @gziolo I applied all the feedback.

@jeffpaul
jeffpaul requested a review from gziolo October 4, 2026 14:54
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