Skip to content

feat: reconcile the owned source set continuously, not at upgrade time - #346

Merged
chubes4 merged 1 commit into
mainfrom
continuous-source-reconcile
Aug 6, 2026
Merged

feat: reconcile the owned source set continuously, not at upgrade time#346
chubes4 merged 1 commit into
mainfrom
continuous-source-reconcile

Conversation

@chubes4

@chubes4 chubes4 commented Aug 6, 2026

Copy link
Copy Markdown
Member

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_completeplus 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_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. 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.

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.
@chubes4
chubes4 merged commit 3d7abfe into main Aug 6, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant