feat(snapshots): post-meta snapshot + restore kind (P0.2 primitive)#47
Conversation
Adds `Aura_Worker_Snapshots::snapshot_meta( $post_id, $keys )` and a `meta` restore kind. This is the enabling primitive for bringing Elementor page/kit writes under SiteAgent governance (P0.2): `_elementor_data`, `_elementor_page_settings`, and the kit-scoped globals repositories all persist as single-valued post-meta, which `snapshot_post` (post_content only) could not capture. Each requested key is captured with an existence flag so restore re-deletes a key that was absent at capture time rather than resurrecting an empty value; values are re-slashed on restore so they round-trip exactly (get_post_meta returns unslashed, meta writers expect slashed input). Targets single-valued meta keys — the shape every Elementor storage key uses. Test bootstrap gains metadata_exists / delete_post_meta / wp_slash stubs. Full suite green (491), 6 new SnapshotsTest cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 567875142d
ℹ️ 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".
| } | ||
| // Meta writers expect slashed input (WP unslashes before | ||
| // storing); get_post_meta returned unslashed, so re-slash. | ||
| update_post_meta( $post_id, $key, wp_slash( $info['value'] ) ); |
There was a problem hiding this comment.
Return an error when meta restoration fails
When restoring an existing meta key, this ignores update_post_meta()'s result and the surrounding restore() path still returns success: true; in the inspected REST restore flow that becomes a 200 response. If the metadata write fails (for example a database error or a pre_update_post_metadata filter veto) the Elementor value remains clobbered while governance records a successful rollback, so please verify the write or compare the stored value after a false return (since WordPress also returns false when unchanged) before reporting success.
Useful? React with 👍 / 👎.
…ound-1 P2) The `meta` restore case ignored `update_post_meta()`'s result, so a failed rollback write (DB error, filter veto) still returned success:true — governance would record a restore that never happened while the value stays clobbered. update_post_meta returns false both on real failure AND on unchanged-value (no-op), so disambiguate by reading the value back: only a mismatch is a failed restore. Two regression tests cover both branches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3cb1ad5cd
ℹ️ 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".
| if ( empty( $info['existed'] ) ) { | ||
| // The key was absent when captured — remove it, don't | ||
| // resurrect an empty value. | ||
| delete_post_meta( $post_id, $key ); |
There was a problem hiding this comment.
Fail closed when absent meta deletion fails
When restoring a snapshot for a meta key that was absent at capture time but was added later, this path ignores delete_post_meta()'s result and immediately reports success. If the delete is vetoed by a metadata filter or fails at the database layer, the rollback leaves the newly-added Elementor meta in place while governance records a successful restore; the update branch below already does a read-back check, but this absent-key branch needs the same fail-closed treatment.
Useful? React with 👍 / 👎.
…round-2 P2) The absent-at-capture restore branch deleted the meta key but ignored the result, so a vetoed/failed delete left the newly-added Elementor meta in place while governance recorded a successful rollback. Mirror the update branch's read-back: delete_post_meta returns false on both failure and nothing-to-delete, so verify via metadata_exists — a key still present means the delete failed. Regression test forces a delete veto via a new delete_post_meta_return stub hook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cba3521e5b
ℹ️ 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".
| if ( ! get_post( $post_id ) ) { | ||
| return array( 'success' => false, 'error' => 'Post not found: ' . $post_id ); | ||
| } |
There was a problem hiding this comment.
Reject revision IDs before meta snapshots
When callers pass a revision or autosave ID, this check succeeds because revisions are posts, but the snapshot is captured with metadata_exists()/get_post_meta() against the revision ID while restore later uses update_post_meta()/delete_post_meta(), which WordPress redirects to the parent post. In that scenario an absent revision key can delete the parent's Elementor meta, or revision meta can overwrite the parent, while restore reports success; normalize with wp_is_post_revision() or reject revisions before capturing.
Useful? React with 👍 / 👎.
| if ( ! is_array( $captured ) ) { | ||
| return array( 'success' => false, 'error' => 'Snapshot payload corrupt.' ); | ||
| } | ||
| $post_id = (int) $record['target']; |
There was a problem hiding this comment.
Fail restores when the target post is gone
If the page/kit is deleted after the snapshot is taken, restore still trusts the saved ID and proceeds to update_post_meta(), which can add orphaned wp_postmeta rows for a non-existent object and return success even though the Elementor page was not restored. Re-check get_post( $post_id ) before the meta restore loop and fail closed when the target no longer exists.
Useful? React with 👍 / 👎.
…(Codex round-3 P2 x2) Two fail-closed hardenings for snapshot_meta / meta restore: - Reject revision/autosave post ids at snapshot time (wp_is_post_revision). get_post_meta reads a revision's own meta, but update/delete_post_meta can affect the parent, so a snapshot taken against a revision could clobber or wipe the parent page's Elementor data. Elementor data lives on the parent. - Re-check get_post() before the meta restore loop. If the page/kit was deleted after capture, writing meta would create orphaned wp_postmeta rows and falsely report success. Adds wp_is_post_revision stub; two regression tests. Full suite green (496). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
…ve page edits (v1.17.0) (#15) * feat(governance): SiteAgent capture-before-write bridge for destructive page edits (v1.17.0) Adds Elementor_MCP_Governance — when SiteAgent's snapshot engine (\Aura_Worker_Snapshots) is installed alongside the plugin, every destructive ability that targets a page (any tool carrying a post_id) is transparently wrapped in elementor_mcp_register_ability(): the page's Elementor state (_elementor_data + _elementor_page_settings) is snapshotted BEFORE the write and rolled back if the write returns WP_Error or throws. - Soft dependency: is_active() is class_exists(\Aura_Worker_Snapshots); with no SiteAgent present nothing is wrapped and behaviour is unchanged. - Fail closed: if the pre-write snapshot can't be captured, the write is refused rather than made blind. - Governance seams: elementor_mcp_governance_write / _rolled_back actions expose the rollback point to the gateway. No mutation of tool return payloads. - Scope (this plank): page-targeting writes only. Kit/repository writes (no post_id) pass through; approval grants + post-write render checks are follow-ups. Depends on SiteAgent snapshot_meta primitive (Digitizers/SiteAgent#47). Test bootstrap gains an Aura_Worker_Snapshots stub; 8 GovernanceFunctionalTest cases. Full suite green (641). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(governance): govern all write-capable page tools + normalize post_id (Codex round-1 P1+P2) P1: the wrap gate keyed off destructive=true, but many page mutators (update-element, batch-update, import-template, atomic add/update) save _elementor_data while annotated destructive=false — they bypassed snapshot/ rollback entirely. Gate on readonly=false instead (write-capable superset); the run-time post_id check still decides whether there's a page to snapshot. Abilities with no annotations stay unwrapped (unclassifiable). P2: run_governed used (int) on post_id, so a negative id failed the `<= 0` skip yet the handlers absint() before saving — mutating abs(id) with no snapshot. Use absint() to match the handlers and close the bypass. Also make PAGE_META_KEYS explicitly public. +3 regression tests (non-destructive page writer wrapped, readonly not wrapped, negative id normalized). Suite 644. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(governance): explicit page-writer allowlist + surface failed rollbacks (Codex round-2 P1+P2x2) The readonly=false gate was too broad. Three fixes: P1: govern only an explicit allowlist of page-structure writers (GOVERNED_TOOLS) instead of any readonly=false + post_id tool. post_id + readonly=false also matches tools that write OTHER state (set-template-conditions -> _elementor_conditions, SEO meta) — a page-data snapshot would roll back the wrong keys. The allowlist is exactly the tools that write _elementor_data / _elementor_page_settings. P2 (preview): apply-flag a11y/SEO generators (fix-color-contrast, generate-meta-tags, etc.) are excluded by the allowlist, so a preview call (no apply) no longer trips a false snapshot/deny. Their governed-on-apply handling is a follow-up plank. P2 (rollback): a failed restore() was ignored while returning only the original write error, hiding a possible partial write. Now a failed rollback returns the more-severe governance_rollback_failed (both the WP_Error and throw paths) and fires elementor_mcp_governance_rollback_failed. +5 governance tests (allowlist governs regardless of destructive flag, non-page-data writer not governed, both rollback-failure paths). Suite 646. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(governance): snapshot at the page-write chokepoint, drop the allowlist (Codex round-3 P2) The explicit allowlist was incomplete (missed add-widget/update-widget, apply-template, add-stock-image, interactions, apply-global-class, add-custom- css/js) and fragile — "writes page data" is a runtime property, not knowable from a tool name. New design, robust by construction: the ability wrapper arms a governed run for the target post; the actual write site (Elementor_MCP_Data::save_page_data / save_page_settings) calls Governance::before_page_write(), which captures the snapshot lazily on the first real page-data write. Consequences: - every save_page_data() caller is covered — no list to maintain; - tools that write other state (template conditions, SEO meta) or preview-only calls (apply=false) never reach the write site, so are never snapshotted — this fixes the round-2 wrong-key and preview findings structurally; - the tool boundary is preserved for rollback and the future grant plank. Gate returns to readonly=false (write-capable); before_page_write fails closed if the snapshot can't be captured. save_page_data/save_page_settings return the gate WP_Error so the write is refused. Rollback-failure surfacing retained. Suite 648 (15 governance tests, incl. "no page write → no snapshot" and "snapshot captured once per run"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds post-meta capture/restore to
Aura_Worker_Snapshots:snapshot_meta( int $post_id, string|array $keys )→ newmetasnapshot kindrestore()gains ametacaseWhy
Enabling primitive for P0.2 — bringing Elementor page/kit writes under SiteAgent governance. Elementor stores everything as single-valued post-meta (
_elementor_data,_elementor_page_settings, kit-scoped globals repositories). The existingsnapshot_postcapturespost_contentonly, so it cannot roll back an Elementor edit. This closes that gap so the fork's mutating tools can snapshot-before-write.Behavior
optionkind'sexistedhandling).get_post_metareturns unslashed; meta writers expect slashed) so the value round-trips byte-exact.Tests
6 new
SnapshotsTestcases (roundtrip, absent→absent, multi-key, invalid post, empty keys). Bootstrap gainsmetadata_exists/delete_post_meta/wp_slashstubs. Full suite green: 491 tests.🤖 Generated with Claude Code