feat: reconcile the owned source set continuously, not at upgrade time - #346
Merged
Conversation
Derivation ran only during upgrade.sh. That is enough for a plugin that already exists and useless for one the agent is about to write: it creates the directory, and nothing recomputes anything until somebody SSHes in and runs an upgrade. "Build me a booking plugin" ended in a support ticket, which is the thing managed hosting exists to remove. An mu-plugin now listens to WordPress's own plugin and theme lifecycle hooks. A plugin the agent SCAFFOLDS fires none of them — no install, no activation, just a directory appearing — so there is also an hourly sweep. An inventory fingerprint makes the usual call a single option read, which is what lets the hooks be broad. WHY NOT INVERT THE PERMISSIONS Allowing wp-content/plugins/** and denying each known third-party would make creation frictionless in one step, with no reconcile needed. It would also turn "can the agent edit the payment gateway" from a question about the allow list into a question about the deny list, where a stale or incomplete entry means yes. Every safety property here rests on deny-by-default, and one saved step is not worth trading it on a site taking card payments. Creation goes through WP-CLI, which the agent already has, and the reconcile grants edit rights immediately after. WHY THE DERIVATION MOVED INTO PHP It began in lib/owned-source-discovery.sh. Keeping it there and adding a PHP copy for the reactive path would be two implementations of the same safety rule, free to drift — the exact failure #336 and #337 removed from the capture path. PHP wins the tie because it is the only one of the two that can run from a WordPress hook, so the shell now delegates and there is one answer. A test asserts the installer does not re-derive. A BUG THE TESTS CAUGHT update_themes is a SEPARATE transient from update_plugins, with its own freshness. A fresh plugin signal alongside a missing theme signal left every bundled theme unrecognised, so twentytwentyfive derived as the site's, became editable, and would have been captured. Same fail-open danger as the plugin case reached through a different door. It is gated independently now. The only reason it did not ship is that the test asserted the bundled theme was excluded rather than assuming it. The manifest directory is also handed to www-data. PHP writes it, setup created it root-owned, and the reactive path would have updated the option while capture went on reading a stale file — the drift the manifest exists to prevent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last friction in #336: an agent can now create a plugin and have it become editable and captured with no operator step.
The gap
Derivation ran only during
upgrade.sh— fine for a plugin that already exists, useless for one the agent is about to write. It creates the directory, and nothing recomputes until somebody SSHes in. "Build me a booking plugin" ended in a support ticket.The design fork, stated
There were two shapes. Invert the permissions — allow
wp-content/plugins/**, deny each known third-party — so a new directory is writable instantly and no reconcile is needed. Or keep deny-by-default and reconcile reactively.I took the second. Inverting turns "can the agent edit the payment gateway" from a question about the allow list into a question about the deny list, where a stale or incomplete entry means yes. Every safety property in this project rests on deny-by-default; one saved step isn't worth trading it on a site taking card payments.
Creation goes through WP-CLI, which the agent already has. The reconcile grants edit rights immediately after.
How it stays current
An mu-plugin hooks
activated_plugin,deactivated_plugin,deleted_plugin,switch_theme,deleted_theme,upgrader_process_complete— plus an hourly sweep, because a plugin the agent scaffolds fires none of those. An inventory fingerprint makes the usual call a single option read, which is what lets the hooks be broad.One implementation, not two
Derivation began in
lib/owned-source-discovery.sh. Keeping it there and adding a PHP copy would be two implementations of the same safety rule, free to drift — the exact failure #336 and #337 removed from the capture path.PHP wins the tie because it's the only one that can run from a WordPress hook. The shell now delegates, and a test asserts the installer doesn't re-derive.
A bug the tests caught
update_themesis a separate transient fromupdate_plugins, with its own freshness. A fresh plugin signal alongside a missing theme signal left every bundled theme unrecognised — sotwentytwentyfivederived as the site's, became editable, and would have been captured.Same fail-open danger as the plugin case, reached through a different door. Gated independently now. The only reason it didn't ship is that the test asserted the bundled theme was excluded rather than assuming it.
Also
The manifest directory is handed to
www-data. PHP writes it; setup created it root-owned, so the reactive path would have updated the option while capture kept reading a stale file — precisely the drift the manifest exists to prevent.Since this now runs unattended on a live site, every fail-closed property is asserted directly: missing, empty, and stale signals each defer; an empty derived set defers rather than opening
wp-content; and a missing signal cannot open WooCommerce is its own assertion rather than an implication.