Skip to content

feat: derive the owned source set from site state - #340

Merged
chubes4 merged 1 commit into
mainfrom
derive-owned-sources
Aug 6, 2026
Merged

feat: derive the owned source set from site state#340
chubes4 merged 1 commit into
mainfrom
derive-owned-sources

Conversation

@chubes4

@chubes4 chubes4 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Closes the declaration half of #336, without making declaration an action.

The idea

Declaring what a site owns was an operator action — --owned-source per component, over SSH, repeated in the capture config. A non-technical owner asking for a new plugin had to file a support ticket, which is the thing managed hosting exists to remove.

But ownership isn't a judgement call. It's a fact already on the box:

owned = installed − known to wp.org − installed by wp-coding-agents − explicitly excluded

Verified against h44lacrosse.com's live state:

DERIVED                                    RECORDED (declared by hand)
  wp-content/plugins/data-machine-business   wp-content/themes/h44-lacrosse-theme
  wp-content/plugins/h44-core                wp-content/plugins/h44-core
  wp-content/plugins/h44-forms               wp-content/plugins/h44-forms
  wp-content/themes/h44-lacrosse-theme

Exactly the hand-declared set, plus one vendor plugin. With --not-owned data-machine-business the two are identical.

The failure mode this is built around

The wp.org signal is a transient. Fresh install, cache flush, wp.org unreachable — it's absent. Read absence as "nothing belongs to wp.org" and every plugin classifies as the site's, handing the agent write access to WooCommerce and a Stripe payment gateway. Automation whose whole purpose is to be safer than a human would be the thing that opened the payment plugin.

So ownership is inferred only from the presence of evidence. Missing, empty, or stale (>2d) produces no derivation and the caller keeps the last recorded set.

Verified live by forcing the freshness gate to zero:

=== FAIL-CLOSED: signal forced stale (the dangerous path) ===
  refused to derive (exit 1) — caller keeps the recorded set
  output was: []

Why not Update URI

WP 5.8+ defines exactly the right header — a plugin declaring an external update source is by definition not the site's, and it would generalise past wp.org to premium plugins. It's unusable: measured across every plugin on that box, including WooCommerce, the Stripe gateway and Akismet, not one sets it.

Hence --not-owned <slug>, which also keeps licensed vendor code out of the operator's repository.

Precedence

explicit flags > derived > recorded. Derivation must beat the recorded set or a newly created plugin never becomes editable and this changes nothing; it must lose to explicit flags, which are the operator deciding deliberately. Pinned by a test that strips comments before checking call order — my comments name the functions in a different order than the code calls them.

Coverage

tests/owned-source-discovery.sh, wired into CI. Every "missing evidence" case asserted separately, plus the catastrophe asserted directly rather than implied: a missing signal cannot open WooCommerce.

One test bug worth noting: my first version used substring matching and couldn't distinguish plugins/data-machine from plugins/data-machine-business — the exact pair this file exists to tell apart. Slug assertions are now exact-line.

Declaring what a site owns was an operator action: --owned-source per
component, over SSH, repeated in the capture configuration. A non-technical
owner asking for a new plugin therefore had to file a support ticket, which
is the thing managed hosting exists to remove.

Ownership is not a judgement call. It is a fact already on the box:

  owned = installed − known to wp.org − installed by wp-coding-agents
                    − explicitly excluded

Measured against h44lacrosse.com, that produces exactly the three
components previously declared by hand, plus one vendor plugin the
exclusion list covers. A plugin the agent creates is now editable and
captured with no operator step and no agent self-declaration.

THE FAILURE MODE THIS IS BUILT AROUND

The wp.org signal is a transient. On a fresh install, after a cache flush,
or when wp.org is unreachable it is absent — and if absence were read as
"no plugin is known to wp.org", every plugin would classify as the site's.
On h44lacrosse.com that hands a coding agent write access to WooCommerce
and a Stripe payment gateway, produced by automation whose entire purpose
is to be safer than an operator doing it by hand.

So ownership is inferred only from the PRESENCE of evidence. A missing,
empty, or stale signal produces no derivation and the caller keeps the last
recorded set. Verified against the live site: forcing the freshness gate to
zero makes it refuse with empty output rather than derive.

The Update URI header (WP 5.8+) would have generalised this past wp.org to
premium and vendor plugins, and is unusable: measured across every plugin
on that box, including WooCommerce, the Stripe gateway and Akismet, not one
sets it. Hence --not-owned, which is also what keeps licensed vendor code
out of the operator's repository.

Precedence is explicit flags > derived > recorded. Derivation must beat the
recorded set or a newly created plugin would never become editable and this
would change nothing; it must lose to explicit flags, which are the
operator deciding deliberately.
@chubes4
chubes4 merged commit 057424e into main Aug 6, 2026
43 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