Skip to content

Move the Key Encryption experiment onto the new Secrets API feature plugin #1012

Description

@whyisjake

Summary

The Key Encryption experiment vendors a subset of ericmann/displace-secrets-manager. That repo is now archived and deprecated, superseded by ericmann/secrets-api — the feature plugin proposed for the Secrets API in WordPress 7.2, which adds pluggable platform providers (the main gap raised on the Core proposal).

This came out of the AI team call: pull the new approach into the plugin so the Core merge proposal has a real surface to point at, and tag a release once it lands. I'm happy to take the PR.

What's here today

includes/Vendor/Secrets/ — an SDK subset vendored at commit 49c6aca6beabefc4ed726737d4a88e1baf6869cb, documented in includes/Vendor/Secrets/README.md. Secrets_Bridge stores one secret per connector.

Two things that make this not a drop-in swap

I checked these against the upstream docs and our own code rather than assuming, because the working assumption on the call was that it would be roughly drop-in.

1. Our existing stored keys will not be found automatically.

Secrets_Bridge::secret_key() builds ai/{connector_id}_api_key, so our option rows are _secret_ai/{connector_id}_api_key — namespaced. The new plugin's read-time upgrade deliberately only maps unnamespaced prototype names (docs/migrating-from-displace.md):

Read-time upgrade does not pick those up. The fallback only maps unnamespaced names, so wp_get_secret( 'my-plugin/api_key' ) returns null rather than reaching into a prototype row — the same answer it gives for a secret that was never set.

So a naive swap makes every already-stored connector key read as absent, and users silently re-enter their API keys. Upstream's answer is wp secret migrate-legacy, which we would need to drive ourselves — sites running the AI plugin can't be expected to run a CLI command to keep working.

2. The new API is global functions, so we probably cannot vendor it the way we vendored the old one.

The new plugin exposes wp_get_secret() and friends from src/wp-includes/, shaped to land in Core. secrets-api.php no-ops the entire plugin when the symbol is already defined:

$symbol_taken = function_exists( 'wp_get_secret' );

If we vendored those functions into the AI plugin, we would take the symbol and cause the real feature plugin to disable itself on any site running both. That's worse than the collision the current vendoring was designed to avoid — see "What was intentionally NOT copied" in our vendored README.

Open question this raises

Vendoring the old SDK was justified by not making users install a second plugin. That reasoning doesn't transfer cleanly here. Options, roughly:

  • Depend on the feature plugin when present, keep the existing vendored provider as the fallback. Preserves today's zero-install behavior, costs us two code paths.
  • Vendor the storage layer only, calling it directly and never defining the global functions.
  • Require the feature plugin for the experiment. Cleanest, but changes the install story.

I lean toward the first, but this is the decision worth having opinions on before I write code.

Scope

  • Decide the integration shape (above)
  • Swap Secrets_Bridge onto the new API
  • Migrate existing _secret_ai/* rows so no one loses a stored key
  • Update includes/Vendor/Secrets/README.md, or remove it if we stop vendoring
  • Update docs/experiments/key-encryption.md, including the threat model if the storage owner changes
  • Tag a release once merged, so the Core proposal can point at it

Activity

  1. dkotter commented on Sep 11, 2026

    @dkotter
    Contributor

    Maybe I'm missing something here but can we not do the following to address the two issues raised here:

    1. Add our own migration routine that automatically updates any existing secrets to the new structure. This doesn't need to utilize a WP-CLI command (though we can use that as reference), this can just be code that runs as part of our normal upgrade approach
    2. I've not looked at this new package and how it compares to the old but can we not just directly use the global functions ourselves (wp_get_secret) or add a light wrapper ourselves that checks if those exist and then call them? I don't think we need to add those functions into our own code, we can rely on the package

    So I think with the above, we can replace the current package with the new one, unless I'm missing something else here?

  2. added this to the 1.5.0 milestone on Sep 29, 2026
  3. jeffpaul commented on Sep 29, 2026

    @jeffpaul
    Member

    @whyisjake would be a great thing to toss at whyisjakebot to see how far it gets in a dependency/functionality swap?

  4. self-assigned this
    on Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions