Skip to content

feat(rules): the custom_css rule target — SiteAgent 2.20.0 (Aura#576, Plan A) - #135

Merged
BenKalsky merged 13 commits into
mainfrom
feat/custom-css-rules-2-20
Sep 24, 2026
Merged

BenKalsky merged 13 commits into
mainfrom
feat/custom-css-rules-2-20

Conversation

@BenKalsky

Copy link
Copy Markdown
Member

Implements Plan A of the custom_css rule target (Digitizers/Aura#576): Digitizers/Aura docs/superpowers/plans/2026-09-24-custom-css-plan-a-siteagent-2-20.md, spec docs/superpowers/specs/2026-09-24-custom-css-rule-target-design.md. Ships as SiteAgent 2.20.0 — no version bump here; the release PR follows on approval.

What changes

  • Rules (class-aura-worker-rules.php) — new target type custom_css (id = post id, or id-less / * = every page, element and kit).
    • block / warn ignore evidence and over-block: custom_css:<id> matches custom_css:<id> or custom_css:*; id-less matches any custom_css touch; unknown:* matches every live custom_css block/warn.
    • allow needs a touch with precise: true and css_only: true (literal true only), and fails closed at the set level: no unknown:*, no custom_css:*, every custom_css:<id> has its exact twin.
  • Old-fork widening — in enforce() only (where allow never speaks): a tool named elementor-mcp/… from a fork that cannot declare CSS turns page:/post:<id> into custom_css:<id> too, and site:* into custom_css:*. A fork counts as precise only when it is ≥ 1.37.0 and has Elementor_MCP_Rules::css_touches() (Plan B) — a version number alone is not a capability.
  • Elementor's own door (class-elementor-door-governor.php) — every WRITE_TABLE slug is in exactly one of CSS_PRODUCERS / NO_CSS:
    • update-page-settings and manage-elements are precise: CSS is read from named arguments, and any other field makes the call mixed.
    • build-composition and manage-component (site-wide) are conservative.
    • Global-class and tag-default CSS count as design_system (ruling R1).
    • touches_for() appends the CSS declaration to every kind.
  • Drift guard — a fixture of Elementor 4.3.0-beta3 input schemas (all 11 write slugs, verified against source) pins the classification. At run time, a NO_CSS slug or precise producer whose live input schema grew an unhandled CSS-capable path declares conservatively. Unknown schema shapes (tuple items, anyOf/oneOf/allOf, patternProperties, nested arrays) fail closed.
  • /status — css_rules: { fork: "precise" | "widened" | "absent" }, always an object.

Clearing is not CSS: null or an empty-after-trim string declares nothing. Any other non-string value, and a non-array settings, is CSS of unknown shape, so the call gets a custom_css touch with no evidence: block/warn match it and allow does not.

Known limits (not changed here)

  • A call held before the upgrade and retried after it gets a second hold ref, because its touches now include the CSS touch.
  • A human-approved restore can put back a page's custom CSS while a block custom_css rule is live.
  • A <style> tag inside an HTML widget's content is not treated as custom CSS (spec ruling R5, out of scope).
  • A bare {type: object} parameter with no declared properties is treated as closed. Counting it as open would make manage-elements permanently conservative through operations[].interactions[].

Verification

  • vendor/bin/phpunit: OK, 5509 tests (the 10 deprecations were already there before this branch).
  • vendor/bin/phpcs: 0 errors, and no new warnings.
  • New tests: RulesCustomCssMatchTest, RulesOldForkWideningTest, ElementorDoorCssTouchesTest, ElementorDoorCssClassificationTest, StatusCssRulesTest.
  • Every task had its own spec and quality review, then a whole-branch review and one final fix round, re-reviewed with 8/8 findings addressed.

🤖 Generated with Claude Code

BenKalsky and others added 9 commits September 24, 2026 18:23
…recise CSS-only evidence (Aura#576)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ouch (review r1)

An allow admits a call only when every custom_css touch it declared is
precise — a single exact touch riding alongside a conservative, create-time
(custom_css:*) or unknown (unknown:*) declaration no longer buys the whole
call an allow verdict (controller ruling overrides the brief allow branch).
CSS_EXACT_PREFIX is now private const. Adds coverage for the three
over-permit cases, a still-admitted mixed page/post touch, a target with no
id key at all, and enforceable_match() on a CSS allow winner.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…possible CSS for block/warn (Aura#576)

Task 2 of Plan A (SiteAgent 2.20.0): Aura_Worker_Rules::fork_css_state()
reads ELEMENTOR_MCP_VERSION and reports precise/widened/absent; a new
private widen_for_old_fork(), called first in enforce(), synthesises a
custom_css:<id> (or custom_css:* for a site write) touch alongside a
widened fork's own page/post/site touches — never for other tools, never
carrying evidence fields, so no allow rule can be satisfied by it. Test
seam _set_fork_version_for_tests() added for fork_css_state(); wired into
tests/bootstrap.php's sa_reset_state().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…amed arguments, conservative from compositions (Aura#576)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nst its schema, at test and at run time (Aura#576)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t 1.37.0 (final review)

A version number is not a capability. fork_css_state() now returns
'precise' only when ELEMENTOR_MCP_VERSION >= 1.37.0 AND the fork ships
Elementor_MCP_Rules::css_touches(); a loaded fork failing either is
'widened', so a 1.37.0 released without the matcher keeps its page writes
counted as possible CSS instead of silently escaping block custom_css.
The test seam takes an optional capability flag (null = method_exists).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s fail closed (final review)

- update-page-settings `settings` and a manage-elements op's `settings`
  that are non-null and not an array are CSS of unknown shape, never
  css_only (M1).
- css_value(): an empty array clears; every other non-string non-null
  value (false, 0, 0.0, numbers, non-empty arrays, objects) is unknown.
- css_capable_paths(): tuple `items`, anyOf/oneOf/allOf and
  patternProperties count as CSS-capable at their node; nested arrays are
  descended. A bare {type: object} stays closed (controller ruling). The
  4.3 fixture classification is unchanged.
- Test WP_Ability stub gains get_input_schema(), so the production live
  schema read is covered; tests for the manage-elements producer guard,
  a null live schema, and the CSS touch as touches_for() returns it.
- Drop a redundant (string) cast.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and preview docs (final review)

test_widening_never_speaks_for_allow now asks match() directly with the
touch set enforce() builds (a precise control admits, the widened touch
does not) and pairs the allow with a warn under enforce(). The fork
version seam gains @SInCE 2.20.0, and preview_tool() documents why it
does not apply old-fork widening (no elementor-mcp ability reaches it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-24T17:35:11.075052Z 45a552c Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92c07cb338

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +457 to +460
&& isset( $t['precise'], $t['css_only'] )
&& true === $t['precise']
&& true === $t['css_only'] ) {
$set[ self::CSS_EXACT_PREFIX . $id ] = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve conservative duplicate CSS touches

When two declarations name the same CSS target and only one carries exact evidence, normalization retains the shared custom_css:42 key and adds custom_css!exact:42, losing the second declaration's conservative status. Consequently, an allow custom_css:42 rule admits inputs such as one precise CSS-only touch plus one conservative touch for ID 42, despite the intended requirement that every CSS touch be exact. Track inexactness per ID rather than allowing any exact duplicate to certify the normalized target.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 786d4fe — a custom_css id keeps its exact key only if every touch on that id is precise+css_only; exact + conservative on the same id no longer satisfies allow custom_css:<id> (block still matches). Test added.

Comment on lines +2993 to +2995
foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $combinator ) {
if ( isset( $schema[ $combinator ] ) ) {
$out[] = $here;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish unknown schema shapes from handled paths

For a combinator located at an already handled path, this emits only that ordinary path and never examines its branches. For example, if update-page-settings.settings becomes a oneOf whose new branch carries extra_css, the scanner returns only settings; because that path is already in PRODUCER_HANDLED_PATHS, the drift check remains precise, while the runtime parser ignores extra_css and emits no CSS touch. A live custom_css block can therefore be bypassed after exactly the schema evolution this guard is meant to fail closed on; represent unresolved combinators separately or recurse through their branches.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ddad64a — a combinator / patternProperties / tuple now surfaces as <path>{unresolved}, which no handled or exempt list names, so it always turns the slug conservative; branches are not descended. Tests: producer whose settings becomes oneOf, and manage-classes whose exempt operations[].css becomes anyOf, both declare conservatively.

BenKalsky and others added 2 commits September 24, 2026 20:22
…dex r1 on #135)

normalize_touches() minted custom_css!exact:<id> from any one exact touch,
so an exact and a conservative touch on the same id read as exact and
allow custom_css:<id> admitted the call. Exactness is now tracked per id:
one inexact touch on an id removes its exact key. Block/warn unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…or exempt path (Codex r1 on #135)

A combinator, patternProperties or tuple items at a path already on
PRODUCER_HANDLED_PATHS or a NO_CSS exempt list (settings -> oneOf with an
extra_css branch) emitted only the plain path, so the drift check passed
and the new branch was never read. Such nodes now surface as
`<path>{unresolved}`, which no list names, so the write turns
conservative. The 4.3 fixture has none of these shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddad64a9e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +666 to +668
// Whether this site's elementor-mcp can say a write carried CSS (2.20.0,
// Aura spec 2026-09-24 §4.2). An OBJECT on the wire, like `unbound`.
$status['css_rules'] = (object) array( 'fork' => Aura_Worker_Rules::fork_css_state() );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bump the advertised plugin version before release

This release identifies the new behavior as 2.20.0, but the plugin header, AURA_WORKER_VERSION, and readme.txt stable tag all remain at 2.19.3. Consequently, an existing 2.19.3 installation cannot receive this build through the normal Aura self-update path: class-aura-worker-updater.php explicitly rejects a package whose version equals the installed version before installing it. Update all three version declarations so this release can actually be deployed and reported as 2.20.0.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declined for this PR, by procedure: the version bump to 2.20.0 (plugin header, AURA_WORKER_VERSION, readme stable tag, changelog) ships in the separate release PR opened after this feature PR merges — as stated in the PR body. Nothing is deployed from this branch.

$css_only = false;
} else {
$settings = isset( $input['settings'] ) ? $input['settings'] : array();
$found = array_key_exists( 'custom_css', $settings ) ? self::css_value( $settings['custom_css'] ) : 'none';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat unknown open page settings as conservative CSS

When update-page-settings.settings contains a CSS-bearing key other than exactly custom_css, this assigns none and later emits no custom_css touch, so a block custom_css rule is bypassed. Fresh evidence beyond the previously fixed combinator case is that the checked-in 4.3 schema already declares settings.additionalProperties: true; Elementor can begin interpreting a key such as extra_css without changing that schema, while the drift scanner continues to return only the already-handled settings path. CSS-like or otherwise unrecognized keys inside this open container need a conservative touch rather than being treated as no CSS.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 45a552c — in update-page-settings settings and each manage-elements op's settings, any non-empty key matching /css/i other than custom_css is CSS of unknown shape (conservative touch, not css_only). Exceptions: _css_classes (class names) and css_filters* (structured filter controls). *_style keys are deliberately not matched — element settings are full of style controls. Tests added.

…known shape (Codex r2 on #135)

update-page-settings `settings` is additionalProperties: true in the 4.3
schema, so Elementor could start honouring e.g. `extra_css` there with no
schema drift the scanner would see. In page settings and in each
manage-elements op's settings, a non-empty key matching /css/i other than
custom_css now makes the write conservative (no evidence, not css_only).
`_css_classes` and the `css_filters*` control group stay ordinary keys;
`style` is not matched (element settings are full of *_style controls).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@BenKalsky

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 45a552ce5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@BenKalsky
BenKalsky merged commit 9a2b4a8 into main Sep 24, 2026
6 checks passed
@BenKalsky
BenKalsky deleted the feat/custom-css-rules-2-20 branch September 24, 2026 18:56
BenKalsky added a commit that referenced this pull request Sep 24, 2026
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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